From 46f5ea15a2498c5e140b6cfbc87425c9abc005fd Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Thu, 16 May 2024 07:44:40 -0400 Subject: [PATCH] Python: Introduce Pydantic settings (#6193) ### Motivation and Context SK Python is tightly coupled to the use of a `.env` file to read all secrets, keys, endpoints, and more. This doesn't scale well for users who wish to be able to use environment variables with their SK Applications. By introducing Pydantic Settings, it is possible to use both environment variables as well as have a fall-back to a `.env` file (via a `env_file_path` parameter), if desired. By introducing Pydantic Settings, we are removing the requirement to have to create Text/Embedding/Chat completion objects with an `api_key` or other previously required information (in the case of AzureChatCompletion that means an `endpoint`, an `api_key`, a `deployment_name`, and an `api_version`). When the AI connector is created, the Pydantic settings are loaded either via env vars or the fall-back `.env` file, and that means the user can create a chat completion object like: ```python chat_completion = OpenAIChatCompletion(service_id="test") ``` or, to optionally override the `ai_model_id` env var: ```python chat_completion = OpenAIChatCompletion(service_id="test", ai_model_id="gpt-4-1106") ``` Note: we have left the ability to specific an `api_key`/`org_id` for `OpenAIChatCompletion` or a `deployment_name`, `endpoint`, `base_url`, and `api_version` for `AzureChatCompletion` as before, but if your settings are configured to use env vars/.env file then there is no need to pass this information. ### Description The PR introduces the use of Pydantic settings and removes the use of the python-dotenv library. - Closes #1779 - Updates notebooks, samples, code and tests to remove the explicit config of api_key or other previous .env files values. - Adds new unit test config using monkeypatch to simulate env variables for testing - All unit and integration tests passing ### Contribution Checklist - [X] The code builds clean without any errors or warnings - [X] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [X] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- .../workflows/python-integration-tests.yml | 68 +- python/.env.example | 7 +- python/DEV_SETUP.md | 22 +- python/README.md | 35 +- python/poetry.lock | 24 +- python/pyproject.toml | 2 +- ...ython_code_interpreter_function_calling.py | 7 +- .../chat_gpt_api_function_calling.py | 3 - .../chat_completion/azure_chat_gpt_api.py | 3 +- .../samples/concepts/chat_completion/chat.py | 6 +- .../concepts/chat_completion/chat_gpt_api.py | 6 +- .../chat_completion/openai_logit_bias.py | 16 +- python/samples/concepts/grounding/grounded.py | 9 +- .../samples/concepts/logging/setup_logging.py | 7 +- .../memory/azure_cognitive_search_memory.py | 19 +- .../memory/google_palm_chat_with_memory.py | 6 +- python/samples/concepts/memory/memory.py | 9 +- .../azure_chat_gpt_with_data_api.py | 14 +- ...chat_gpt_with_data_api_function_calling.py | 12 +- ...re_chat_gpt_with_data_api_vector_search.py | 13 +- ...penai_function_calling_stepwise_planner.py | 3 +- ...penai_function_calling_stepwise_planner.py | 3 - .../concepts/planners/sequential_planner.py | 6 +- .../plugins/azure_key_vault_settings.py | 26 + .../plugins/azure_python_code_interpreter.py | 10 +- .../plugins/google_palm_chat_with_plugin.py | 4 +- ...nai_function_calling_with_custom_plugin.py | 12 +- .../plugins/openai_plugin_azure_key_vault.py | 7 +- .../concepts/plugins/plugins_from_dir.py | 7 +- .../azure_chat_gpt_api_handlebars.py | 3 +- .../azure_chat_gpt_api_jinja2.py | 3 +- .../prompt_templates/configuring_prompts.py | 8 +- .../prompt_templates/load_yaml_prompt.py | 4 - .../prompt_templates/template_language.py | 4 +- .../rag/rag_with_text_memory_plugin.py | 9 +- .../samples/concepts/rag/self-critique_rag.py | 22 +- .../concepts/search/bing_plugin_examples.py | 9 +- .../concepts/search/bing_search_plugin.py | 13 +- .../concepts/search/google_search_plugin.py | 6 +- .../google_palm_text_completion.py | 10 +- .../demos/booking_restaurant/README.md | 12 +- .../booking_sample_settings.py | 45 + .../booking_restaurant/restaurant_booking.py | 25 +- .../getting_started/00-getting-started.ipynb | 34 +- .../01-basic-loading-the-kernel.ipynb | 54 +- .../02-running-prompts-from-file.ipynb | 8 +- .../03-prompt-function-inline.ipynb | 38 +- .../04-kernel-arguments-chat.ipynb | 10 +- .../05-using-the-planner.ipynb | 22 +- .../06-memory-and-embeddings.ipynb | 19 +- .../08-native-function-inline.ipynb | 23 +- .../09-groundedness-checking.ipynb | 9 +- .../10-multiple-results-per-prompt.ipynb | 19 +- .../11-streaming-completions.ipynb | 17 +- .../weaviate-persistent-memory.ipynb | 1008 ++++++++--------- .../services/gp_chat_completion.py | 43 +- .../services/gp_text_completion.py | 33 +- .../google_palm/services/gp_text_embedding.py | 40 +- .../settings/google_palm_settings.py | 46 + .../connectors/ai/open_ai/const.py | 2 +- .../open_ai/services/azure_chat_completion.py | 228 ++-- .../open_ai/services/azure_text_completion.py | 186 ++- .../open_ai/services/azure_text_embedding.py | 133 ++- .../services/open_ai_chat_completion.py | 105 +- .../services/open_ai_text_completion.py | 105 +- .../services/open_ai_text_embedding.py | 55 +- .../settings/azure_open_ai_settings.py | 79 ++ .../ai/open_ai/settings/open_ai_settings.py | 49 + .../connectors/memory/astradb/__init__.py | 3 +- .../memory/astradb/astradb_memory_store.py | 33 +- .../memory/astradb/astradb_settings.py | 28 + .../memory/azure_cognitive_search/__init__.py | 3 +- .../azure_ai_search_settings.py | 33 + .../azure_cognitive_search_memory_store.py | 48 +- .../memory/azure_cosmosdb/__init__.py | 3 +- .../azure_cosmos_db_memory_store.py | 30 +- .../azure_cosmosdb/azure_cosmosdb_settings.py | 20 + .../connectors/memory/memory_settings_base.py | 21 + .../memory/mongodb_atlas/__init__.py | 5 +- .../mongodb_atlas_memory_store.py | 32 +- .../mongodb_atlas/mongodb_atlas_settings.py | 19 + .../connectors/memory/pinecone/__init__.py | 5 +- .../memory/pinecone/pinecone_memory_store.py | 20 +- .../memory/pinecone/pinecone_settings.py | 19 + .../connectors/memory/postgres/__init__.py | 3 +- .../memory/postgres/postgres_memory_store.py | 22 +- .../memory/postgres/postgres_settings.py | 19 + .../connectors/memory/redis/__init__.py | 3 +- .../memory/redis/redis_memory_store.py | 21 +- .../connectors/memory/redis/redis_settings.py | 19 + .../connectors/memory/weaviate/__init__.py | 3 +- .../memory/weaviate/weaviate_memory_store.py | 72 +- .../memory/weaviate/weaviate_settings.py | 28 + .../search_engine/bing_connector.py | 28 +- .../search_engine/bing_connector_settings.py | 36 + .../sessions_python_tool/README.md | 2 +- .../sessions_python_plugin.py | 18 +- .../sessions_python_settings.py | 30 +- python/semantic_kernel/exceptions/__init__.py | 1 + .../exceptions/memory_connector_exceptions.py | 23 + python/semantic_kernel/utils/settings.py | 377 ------ python/tests/conftest.py | 175 ++- .../tests/integration/completions/conftest.py | 5 +- .../test_azure_oai_chat_service.py | 83 +- .../test_azure_oai_chat_service_extensions.py | 18 +- .../test_azure_oai_text_service.py | 52 +- .../test_conversation_summary_plugin.py | 25 +- .../completions/test_gp_chat_service.py | 6 +- .../completions/test_oai_chat_service.py | 78 +- .../completions/test_oai_text_service.py | 42 +- .../connectors/memory/test_astradb.py | 22 +- .../memory/test_azure_cognitive_search.py | 4 +- .../connectors/memory/test_mongodb_atlas.py | 22 +- .../connectors/memory/test_pinecone.py | 16 +- .../connectors/memory/test_postgres.py | 16 +- .../connectors/memory/test_redis.py | 15 +- .../memory/test_weaviate_memory_store.py | 14 +- .../test_azure_oai_embedding_service.py | 53 +- .../embeddings/test_gp_embedding_service.py | 9 +- .../embeddings/test_oai_embedding_service.py | 19 +- ...t_int_function_calling_stepwise_planner.py | 4 +- .../test_sequential_plan_parser.py | 6 +- .../test_sequential_planner.py | 24 +- .../services/test_palm_chat_completion.py | 31 +- .../services/test_palm_text_completion.py | 39 +- .../services/test_palm_text_embedding.py | 28 +- .../services/test_azure_chat_completion.py | 284 ++--- .../services/test_azure_text_completion.py | 143 +-- .../services/test_azure_text_embedding.py | 141 +-- .../services/test_openai_chat_completion.py | 79 +- .../services/test_openai_text_completion.py | 83 +- .../services/test_openai_text_embedding.py | 4 +- .../test_sessions_python_plugin.py | 64 +- .../test_kernel_function_from_method.py | 4 +- .../test_kernel_function_from_prompt.py | 24 +- ...est_azure_cognitive_search_memory_store.py | 2 +- 136 files changed, 2594 insertions(+), 2971 deletions(-) create mode 100644 python/samples/concepts/plugins/azure_key_vault_settings.py create mode 100644 python/samples/demos/booking_restaurant/booking_sample_settings.py create mode 100644 python/semantic_kernel/connectors/ai/google_palm/settings/google_palm_settings.py create mode 100644 python/semantic_kernel/connectors/ai/open_ai/settings/azure_open_ai_settings.py create mode 100644 python/semantic_kernel/connectors/ai/open_ai/settings/open_ai_settings.py create mode 100644 python/semantic_kernel/connectors/memory/astradb/astradb_settings.py create mode 100644 python/semantic_kernel/connectors/memory/azure_cognitive_search/azure_ai_search_settings.py create mode 100644 python/semantic_kernel/connectors/memory/azure_cosmosdb/azure_cosmosdb_settings.py create mode 100644 python/semantic_kernel/connectors/memory/memory_settings_base.py create mode 100644 python/semantic_kernel/connectors/memory/mongodb_atlas/mongodb_atlas_settings.py create mode 100644 python/semantic_kernel/connectors/memory/pinecone/pinecone_settings.py create mode 100644 python/semantic_kernel/connectors/memory/postgres/postgres_settings.py create mode 100644 python/semantic_kernel/connectors/memory/redis/redis_settings.py create mode 100644 python/semantic_kernel/connectors/memory/weaviate/weaviate_settings.py create mode 100644 python/semantic_kernel/connectors/search_engine/bing_connector_settings.py create mode 100644 python/semantic_kernel/exceptions/memory_connector_exceptions.py delete mode 100644 python/semantic_kernel/utils/settings.py diff --git a/.github/workflows/python-integration-tests.yml b/.github/workflows/python-integration-tests.yml index 856c01d156..b02fc8eae1 100644 --- a/.github/workflows/python-integration-tests.yml +++ b/.github/workflows/python-integration-tests.yml @@ -76,25 +76,21 @@ jobs: env: # Set Azure credentials secret as an input HNSWLIB_NO_NATIVE: 1 Python_Integration_Tests: Python_Integration_Tests - AzureOpenAI__Label: azure-text-davinci-003 - AzureOpenAIEmbedding__Label: azure-text-embedding-ada-002 - AzureOpenAI__DeploymentName: ${{ vars.AZUREOPENAI__DEPLOYMENTNAME }} - AzureOpenAI__Text__DeploymentName: ${{ vars.AZUREOPENAI__TEXT__DEPLOYMENTNAME }} - AzureOpenAIChat__DeploymentName: ${{ vars.AZUREOPENAI__CHAT__DEPLOYMENTNAME }} - AzureOpenAIEmbeddings__DeploymentName: ${{ vars.AZUREOPENAIEMBEDDINGS__DEPLOYMENTNAME2 }} - AzureOpenAIEmbeddings_EastUS__DeploymentName: ${{ vars.AZUREOPENAIEMBEDDINGS_EASTUS__DEPLOYMENTNAME}} - AzureOpenAI__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }} - AzureOpenAI_EastUS__Endpoint: ${{ secrets.AZUREOPENAI_EASTUS__ENDPOINT }} - AzureOpenAI_EastUS__ApiKey: ${{ secrets.AZUREOPENAI_EASTUS__APIKEY }} - AzureOpenAIEmbeddings__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }} - AzureOpenAI__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }} - AzureOpenAIEmbeddings__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }} - Bing__ApiKey: ${{ secrets.BING__APIKEY }} - OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }} - Pinecone__ApiKey: ${{ secrets.PINECONE__APIKEY }} - Postgres__Connectionstr: ${{secrets.POSTGRES__CONNECTIONSTR}} - AZURE_COGNITIVE_SEARCH_ADMIN_KEY: ${{secrets.AZURE_COGNITIVE_SEARCH_ADMIN_KEY}} - AZURE_COGNITIVE_SEARCH_ENDPOINT: ${{secrets.AZURE_COGNITIVE_SEARCH_ENDPOINT}} + AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }} # azure-text-embedding-ada-002 + AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_CHAT_DEPLOYMENT_NAME }} + AZURE_OPENAI_TEXT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_TEXT_DEPLOYMENT_NAME }} + AZURE_OPENAI_API_VERSION: ${{ vars.AZURE_OPENAI_API_VERSION }} + AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} + AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} + BING_API_KEY: ${{ secrets.BING_API_KEY }} + OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI_CHAT_MODEL_ID }} + OPENAI_TEXT_MODEL_ID: ${{ vars.OPENAI_TEXT_MODEL_ID }} + OPENAI_EMBEDDING_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + PINECONE_API_KEY: ${{ secrets.PINECONE__APIKEY }} + POSTGRES_CONNECTION_STRING: ${{secrets.POSTGRES__CONNECTIONSTR}} + AZURE_AI_SEARCH_API_KEY: ${{secrets.AZURE_AI_SEARCH_API_KEY}} + AZURE_AI_SEARCH_ENDPOINT: ${{secrets.AZURE_AI_SEARCH_ENDPOINT}} MONGODB_ATLAS_CONNECTION_STRING: ${{secrets.MONGODB_ATLAS_CONNECTION_STRING}} run: | if ${{ matrix.os == 'ubuntu-latest' }}; then @@ -142,25 +138,21 @@ jobs: env: # Set Azure credentials secret as an input HNSWLIB_NO_NATIVE: 1 Python_Integration_Tests: Python_Integration_Tests - AzureOpenAI__Label: azure-text-davinci-003 - AzureOpenAIEmbedding__Label: azure-text-embedding-ada-002 - AzureOpenAI__DeploymentName: ${{ vars.AZUREOPENAI__DEPLOYMENTNAME }} - AzureOpenAI__Text__DeploymentName: ${{ vars.AZUREOPENAI__TEXT__DEPLOYMENTNAME }} - AzureOpenAIChat__DeploymentName: ${{ vars.AZUREOPENAI__CHAT__DEPLOYMENTNAME }} - AzureOpenAIEmbeddings__DeploymentName: ${{ vars.AZUREOPENAIEMBEDDINGS__DEPLOYMENTNAME2 }} - AzureOpenAIEmbeddings_EastUS__DeploymentName: ${{ vars.AZUREOPENAIEMBEDDINGS_EASTUS__DEPLOYMENTNAME}} - AzureOpenAI__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }} - AzureOpenAIEmbeddings__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }} - AzureOpenAI__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }} - AzureOpenAI_EastUS__Endpoint: ${{ secrets.AZUREOPENAI_EASTUS__ENDPOINT }} - AzureOpenAI_EastUS__ApiKey: ${{ secrets.AZUREOPENAI_EASTUS__APIKEY }} - AzureOpenAIEmbeddings__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }} - Bing__ApiKey: ${{ secrets.BING__APIKEY }} - OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }} - Pinecone__ApiKey: ${{ secrets.PINECONE__APIKEY }} - Postgres__Connectionstr: ${{secrets.POSTGRES__CONNECTIONSTR}} - AZURE_COGNITIVE_SEARCH_ADMIN_KEY: ${{secrets.AZURE_COGNITIVE_SEARCH_ADMIN_KEY}} - AZURE_COGNITIVE_SEARCH_ENDPOINT: ${{secrets.AZURE_COGNITIVE_SEARCH_ENDPOINT}} + AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }} # azure-text-embedding-ada-002 + AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_CHAT_DEPLOYMENT_NAME }} + AZURE_OPENAI_TEXT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_TEXT_DEPLOYMENT_NAME }} + AZURE_OPENAI_API_VERSION: ${{ vars.AZURE_OPENAI_API_VERSION }} + AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} + AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} + BING_API_KEY: ${{ secrets.BING_API_KEY }} + OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI_CHAT_MODEL_ID }} + OPENAI_TEXT_MODEL_ID: ${{ vars.OPENAI_TEXT_MODEL_ID }} + OPENAI_EMBEDDING_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + PINECONE_API_KEY: ${{ secrets.PINECONE__APIKEY }} + POSTGRES_CONNECTION_STRING: ${{secrets.POSTGRES__CONNECTIONSTR}} + AZURE_AI_SEARCH_API_KEY: ${{secrets.AZURE_AI_SEARCH_API_KEY}} + AZURE_AI_SEARCH_ENDPOINT: ${{secrets.AZURE_AI_SEARCH_ENDPOINT}} MONGODB_ATLAS_CONNECTION_STRING: ${{secrets.MONGODB_ATLAS_CONNECTION_STRING}} run: | if ${{ matrix.os == 'ubuntu-latest' }}; then diff --git a/python/.env.example b/python/.env.example index b7154cdb70..d6a0e18dff 100644 --- a/python/.env.example +++ b/python/.env.example @@ -46,4 +46,9 @@ ASTRADB_APP_TOKEN="" ASTRADB_ID="" ASTRADB_REGION="" ASTRADB_KEYSPACE="" -ACA_POOL_MANAGEMENT_ENDPOINT="" \ No newline at end of file +ACA_POOL_MANAGEMENT_ENDPOINT="" +BOOKING_SAMPLE_CLIENT_ID="" +BOOKING_SAMPLE_TENANT_ID="" +BOOKING_SAMPLE_CLIENT_SECRET="" +BOOKING_SAMPLE_BUSINESS_ID="" +BOOKING_SAMPLE_SERVICE_ID="" \ No newline at end of file diff --git a/python/DEV_SETUP.md b/python/DEV_SETUP.md index 126fd62d2b..76cbcb8987 100644 --- a/python/DEV_SETUP.md +++ b/python/DEV_SETUP.md @@ -10,17 +10,31 @@ Make sure you have an [OpenAI API Key](https://platform.openai.com) or [Azure OpenAI service key](https://learn.microsoft.com/azure/cognitive-services/openai/quickstart?pivots=rest-api) -Copy those keys into a `.env` file (see the `.env.example` file): +There are two methods to manage keys, secrets, and endpoints: -```bash +1. Store them in environment variables. SK Python leverages pydantic settings to load keys, secrets, and endpoints. This means that there is a first attempt to load them from environment variables. The `.env` file naming applies to how the names should be stored as environment variables. + +2. If you'd like to use the `.env` file, you will need to configure the `.env` file with the following keys into a `.env` file (see the `.env.example` file): + +``` OPENAI_API_KEY="" OPENAI_ORG_ID="" -AZURE_OPENAI_DEPLOYMENT_NAME="" +AZURE_OPENAI_CHAT_DEPLOYMENT_NAME="" +AZURE_OPENAI_TEXT_DEPLOYMENT_NAME="" +AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME="" AZURE_OPENAI_ENDPOINT="" AZURE_OPENAI_API_KEY="" ``` -We suggest adding a copy of the `.env` file under these folders: +You will then configure the Text/ChatCompletion class with the keyword argument `env_file_path`: + +```python +chat_completion = OpenAIChatCompletion(service_id="test", env_file_path=) +``` + +This optional `env_file_path` parameter will allow pydantic settings to use the `.env` file as a fallback to read the settings. + +If using the second method, we suggest adding a copy of the `.env` file under these folders: - [python/tests](tests) - [./samples/getting_started](./samples/getting_started). diff --git a/python/README.md b/python/README.md index 57e55c290e..db821e29dd 100644 --- a/python/README.md +++ b/python/README.md @@ -20,16 +20,30 @@ Make sure you have an [OpenAI API Key](https://platform.openai.com) or [Azure OpenAI service key](https://learn.microsoft.com/azure/cognitive-services/openai/quickstart?pivots=rest-api) -Copy those keys into a `.env` file (see the `.env.example` file): +There are two methods to manage keys, secrets, and endpoints: + +1. Store them in environment variables. SK Python leverages pydantic settings to load keys, secrets, and endpoints. This means that there is a first attempt to load them from environment variables. The `.env` file naming applies to how the names should be stored as environment variables. + +2. If you'd like to use the `.env` file, you will need to configure the `.env` file with the following keys in the file (see the `.env.example` file): ``` OPENAI_API_KEY="" OPENAI_ORG_ID="" -AZURE_OPENAI_DEPLOYMENT_NAME="" +AZURE_OPENAI_CHAT_DEPLOYMENT_NAME="" +AZURE_OPENAI_TEXT_DEPLOYMENT_NAME="" +AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME="" AZURE_OPENAI_ENDPOINT="" AZURE_OPENAI_API_KEY="" ``` +You will then configure the Text/ChatCompletion class with the keyword argument `env_file_path`: + +```python +chat_completion = OpenAIChatCompletion(service_id="test", env_file_path=) +``` + +This optional `env_file_path` parameter will allow pydantic settings to use the `.env` file as a fallback to read the settings. + # Running a prompt ```python @@ -37,30 +51,21 @@ import asyncio from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, AzureChatCompletion from semantic_kernel.prompt_template import PromptTemplateConfig -from semantic_kernel.utils.settings import openai_settings_from_dot_env, azure_openai_settings_from_dot_env kernel = Kernel() # Prepare OpenAI service using credentials stored in the `.env` file -api_key, org_id = openai_settings_from_dot_env() service_id="chat-gpt" kernel.add_service( OpenAIChatCompletion( service_id=service_id, - ai_model_id="gpt-3.5-turbo", - api_key=api_key, - org_id=org_id ) ) # Alternative using Azure: -# deployment, api_key, endpoint = azure_openai_settings_from_dot_env() # kernel.add_service( # AzureChatCompletion( # service_id=service_id, -# deployment_name=deployment, -# endpoint=endpoint, -# api_key=api_key # ) # ) @@ -112,10 +117,10 @@ if __name__ == "__main__": ```python # Create a reusable function summarize function summarize = kernel.add_function( - function_name="tldr_function", - plugin_name="tldr_plugin", - prompt="{{$input}}\n\nOne line TLDR with the fewest words.", - prompt_template_settings=req_settings, + function_name="tldr_function", + plugin_name="tldr_plugin", + prompt="{{$input}}\n\nOne line TLDR with the fewest words.", + prompt_template_settings=req_settings, ) # Summarize the laws of thermodynamics diff --git a/python/poetry.lock b/python/poetry.lock index 3a2e4bb21e..5d3a489d6c 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -4430,6 +4430,25 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pydantic-settings" +version = "2.2.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, + {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, +] + +[package.dependencies] +pydantic = ">=2.3.0" +python-dotenv = ">=0.21.0" + +[package.extras] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + [[package]] name = "pygments" version = "2.17.2" @@ -4759,7 +4778,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -6831,4 +6849,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = "^3.10,<3.13" -content-hash = "947f0d69d4a2086ff91e5b4eebf2349ea11049579e05645a04a20cce15fd6e08" +content-hash = "8f37912da67cd7728e5b3555e5286fa4fe7a2faf63b240d26b6ae6360c3d2d7f" diff --git a/python/pyproject.toml b/python/pyproject.toml index 07ddcc700e..c4716ec24c 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -24,11 +24,11 @@ grpcio = [ { version = ">=1.60.0", python = ">=3.12" } ] openai = ">=1.0" -python-dotenv = "^1.0.1" regex = "^2023.6.3" openapi_core = ">=0.18,<0.20" prance = "^23.6.21.0" pydantic = "^2" +pydantic-settings = "^2.2.1" motor = "^3.3.2" defusedxml = "^0.7.1" pybars4 = "^0.9.13" diff --git a/python/samples/concepts/auto_function_calling/azure_python_code_interpreter_function_calling.py b/python/samples/concepts/auto_function_calling/azure_python_code_interpreter_function_calling.py index 8280faeea2..baae3b2f05 100644 --- a/python/samples/concepts/auto_function_calling/azure_python_code_interpreter_function_calling.py +++ b/python/samples/concepts/auto_function_calling/azure_python_code_interpreter_function_calling.py @@ -20,10 +20,6 @@ from semantic_kernel.core_plugins.time_plugin import TimePlugin from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.kernel import Kernel -from semantic_kernel.utils.settings import ( - azure_container_apps_settings_from_dot_env_as_dict, - azure_openai_settings_from_dot_env_as_dict, -) auth_token: AccessToken | None = None @@ -56,12 +52,11 @@ kernel = Kernel() service_id = "sessions-tool" chat_service = AzureChatCompletion( - service_id=service_id, **azure_openai_settings_from_dot_env_as_dict(include_api_version=True) + service_id=service_id, ) kernel.add_service(chat_service) sessions_tool = SessionsPythonTool( - **azure_container_apps_settings_from_dot_env_as_dict(), auth_callback=auth_callback, ) diff --git a/python/samples/concepts/auto_function_calling/chat_gpt_api_function_calling.py b/python/samples/concepts/auto_function_calling/chat_gpt_api_function_calling.py index 81e6f37bef..6c0f44a9c2 100644 --- a/python/samples/concepts/auto_function_calling/chat_gpt_api_function_calling.py +++ b/python/samples/concepts/auto_function_calling/chat_gpt_api_function_calling.py @@ -17,7 +17,6 @@ from semantic_kernel.contents.function_call_content import FunctionCallContent from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent from semantic_kernel.core_plugins import MathPlugin, TimePlugin from semantic_kernel.functions import KernelArguments -from semantic_kernel.utils.settings import openai_settings_from_dot_env if TYPE_CHECKING: from semantic_kernel.functions import KernelFunction @@ -38,12 +37,10 @@ you will return a full answer to me as soon as possible. kernel = Kernel() # Note: the underlying gpt-35/gpt-4 model version needs to be at least version 0613 to support tools. -api_key, org_id = openai_settings_from_dot_env() kernel.add_service( OpenAIChatCompletion( service_id="chat", ai_model_id="gpt-3.5-turbo-1106", - api_key=api_key, ), ) diff --git a/python/samples/concepts/chat_completion/azure_chat_gpt_api.py b/python/samples/concepts/chat_completion/azure_chat_gpt_api.py index 46acdbe54f..8771d135bb 100644 --- a/python/samples/concepts/chat_completion/azure_chat_gpt_api.py +++ b/python/samples/concepts/chat_completion/azure_chat_gpt_api.py @@ -7,7 +7,6 @@ from semantic_kernel import Kernel from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBehavior from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion from semantic_kernel.contents import ChatHistory -from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env_as_dict logging.basicConfig(level=logging.WARNING) @@ -24,7 +23,7 @@ kernel = Kernel() service_id = "chat-gpt" chat_service = AzureChatCompletion( - service_id=service_id, **azure_openai_settings_from_dot_env_as_dict(include_api_version=True) + service_id=service_id, ) kernel.add_service(chat_service) diff --git a/python/samples/concepts/chat_completion/chat.py b/python/samples/concepts/chat_completion/chat.py index 21911b9298..1c51702cc8 100644 --- a/python/samples/concepts/chat_completion/chat.py +++ b/python/samples/concepts/chat_completion/chat.py @@ -6,7 +6,6 @@ from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion from semantic_kernel.contents import ChatHistory from semantic_kernel.prompt_template import InputVariable, PromptTemplateConfig -from semantic_kernel.utils.settings import openai_settings_from_dot_env prompt = """ ChatBot can have a conversation with you about any topic. @@ -21,11 +20,8 @@ ChatBot:> kernel = Kernel() -api_key, org_id = openai_settings_from_dot_env() service_id = "chat" -kernel.add_service( - OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo-1106", api_key=api_key, org_id=org_id) -) +kernel.add_service(OpenAIChatCompletion(service_id=service_id)) settings = kernel.get_prompt_execution_settings_from_service_id(service_id) settings.max_tokens = 2000 diff --git a/python/samples/concepts/chat_completion/chat_gpt_api.py b/python/samples/concepts/chat_completion/chat_gpt_api.py index a229935095..cb231a4d03 100644 --- a/python/samples/concepts/chat_completion/chat_gpt_api.py +++ b/python/samples/concepts/chat_completion/chat_gpt_api.py @@ -6,7 +6,6 @@ from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion from semantic_kernel.contents import ChatHistory from semantic_kernel.functions import KernelArguments -from semantic_kernel.utils.settings import openai_settings_from_dot_env system_message = """ You are a chat bot. Your name is Mosscap and @@ -19,11 +18,8 @@ flowery prose. kernel = Kernel() -api_key, org_id = openai_settings_from_dot_env() service_id = "chat-gpt" -kernel.add_service( - OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo", api_key=api_key, org_id=org_id) -) +kernel.add_service(OpenAIChatCompletion(service_id=service_id)) settings = kernel.get_prompt_execution_settings_from_service_id(service_id) settings.max_tokens = 2000 diff --git a/python/samples/concepts/chat_completion/openai_logit_bias.py b/python/samples/concepts/chat_completion/openai_logit_bias.py index eb9f4d3901..0d2a7480a4 100644 --- a/python/samples/concepts/chat_completion/openai_logit_bias.py +++ b/python/samples/concepts/chat_completion/openai_logit_bias.py @@ -9,7 +9,6 @@ from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAITe from semantic_kernel.contents import ChatHistory from semantic_kernel.functions import KernelArguments from semantic_kernel.prompt_template import InputVariable, PromptTemplateConfig -from semantic_kernel.utils.settings import openai_settings_from_dot_env """ Logit bias enables prioritizing certain tokens within a given output. @@ -31,10 +30,11 @@ def _prepare_input_chat(chat: ChatHistory): return "".join([f"{msg.role}: {msg.content}\n" for msg in chat]) -async def chat_request_example(kernel: Kernel, api_key, org_id): +async def chat_request_example(kernel: Kernel): service_id = "chat_service" openai_chat_completion = OpenAIChatCompletion( - service_id=service_id, ai_model_id="gpt-3.5-turbo", api_key=api_key, org_id=org_id + service_id=service_id, + ai_model_id="gpt-3.5-turbo", ) kernel.add_service(openai_chat_completion) @@ -111,10 +111,11 @@ async def chat_request_example(kernel: Kernel, api_key, org_id): return chat, banned_words -async def text_complete_request_example(kernel: Kernel, api_key, org_id): +async def text_complete_request_example(kernel: Kernel): service_id = "text_service" openai_text_completion = OpenAITextCompletion( - service_id=service_id, ai_model_id="gpt-3.5-turbo-instruct", api_key=api_key, org_id=org_id + service_id=service_id, + ai_model_id="gpt-3.5-turbo-instruct", ) kernel.add_service(openai_text_completion) @@ -210,18 +211,17 @@ def _format_output(chat, banned_words) -> None: async def main() -> None: kernel = Kernel() - api_key, org_id = openai_settings_from_dot_env() print("Chat completion example:") print("------------------------") - chat, banned_words = await chat_request_example(kernel, api_key, org_id) + chat, banned_words = await chat_request_example(kernel) _format_output(chat, banned_words) print("------------------------") print("\nText completion example:") print("------------------------") - chat, banned_words = await text_complete_request_example(kernel, api_key, org_id) + chat, banned_words = await text_complete_request_example(kernel) _format_output(chat, banned_words) return diff --git a/python/samples/concepts/grounding/grounded.py b/python/samples/concepts/grounding/grounded.py index ed89c161d2..73ee6e117d 100644 --- a/python/samples/concepts/grounding/grounded.py +++ b/python/samples/concepts/grounding/grounded.py @@ -5,7 +5,6 @@ from samples.utils import Colors from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, OpenAIChatCompletion from semantic_kernel.functions import KernelArguments -from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env, openai_settings_from_dot_env def get_grounding_text(): @@ -56,22 +55,16 @@ def setup(use_azure: bool = False, plugin_name: str = "GroundingPlugin"): # Configure AI service used by the kernel if use_azure: - deployment, api_key, endpoint = azure_openai_settings_from_dot_env() service_id = "chat_completion" kernel.add_service( AzureChatCompletion( service_id=service_id, - deployment_name=deployment, - endpoint=endpoint, - api_key=api_key, - api_version="2023-12-01-preview", ), ) else: - api_key, org_id = openai_settings_from_dot_env() service_id = "chat-gpt" kernel.add_service( - OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo", api_key=api_key, org_id=org_id), + OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo"), ) # note: using plugins from the samples folder diff --git a/python/samples/concepts/logging/setup_logging.py b/python/samples/concepts/logging/setup_logging.py index f3d2eb4c7c..3b189ad867 100644 --- a/python/samples/concepts/logging/setup_logging.py +++ b/python/samples/concepts/logging/setup_logging.py @@ -6,7 +6,6 @@ import os from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion from semantic_kernel.utils.logging import setup_logging -from semantic_kernel.utils.settings import openai_settings_from_dot_env async def main(): @@ -17,12 +16,8 @@ async def main(): kernel = Kernel() - api_key, org_id = openai_settings_from_dot_env() - service_id = "chat-gpt" - kernel.add_service( - OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo", api_key=api_key, org_id=org_id) - ) + kernel.add_service(OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo")) plugins_directory = os.path.join(__file__, "../../../../../prompt_template_samples/") plugin = kernel.add_plugin(parent_directory=plugins_directory, plugin_name="FunPlugin") diff --git a/python/samples/concepts/memory/azure_cognitive_search_memory.py b/python/samples/concepts/memory/azure_cognitive_search_memory.py index adc9699d87..0580125185 100644 --- a/python/samples/concepts/memory/azure_cognitive_search_memory.py +++ b/python/samples/concepts/memory/azure_cognitive_search_memory.py @@ -2,11 +2,10 @@ import asyncio -from dotenv import dotenv_values - 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 @@ -44,12 +43,8 @@ async def search_acs_memory_questions(memory: SemanticTextMemory) -> None: async def main() -> None: kernel = Kernel() - config = dotenv_values(".env") + azure_ai_search_settings = AzureAISearchSettings() - AZURE_COGNITIVE_SEARCH_ENDPOINT = config["AZURE_COGNITIVE_SEARCH_ENDPOINT"] - AZURE_COGNITIVE_SEARCH_ADMIN_KEY = config["AZURE_COGNITIVE_SEARCH_ADMIN_KEY"] - AZURE_OPENAI_API_KEY = config["AZURE_OPENAI_API_KEY"] - AZURE_OPENAI_ENDPOINT = config["AZURE_OPENAI_ENDPOINT"] vector_size = 1536 # Setting up OpenAI services for text completion and text embedding @@ -57,24 +52,20 @@ async def main() -> None: kernel.add_service( AzureTextCompletion( service_id=text_complete_service_id, - deployment_name="text-embedding-ada-002", - endpoint=AZURE_OPENAI_ENDPOINT, - api_key=AZURE_OPENAI_API_KEY, ), ) embedding_service_id = "ada" embedding_gen = AzureTextEmbedding( service_id=embedding_service_id, - deployment_name="text-embedding-ada-002", - endpoint=AZURE_OPENAI_ENDPOINT, - api_key=AZURE_OPENAI_API_KEY, ) kernel.add_service( embedding_gen, ) acs_connector = AzureCognitiveSearchMemoryStore( - vector_size, AZURE_COGNITIVE_SEARCH_ENDPOINT, AZURE_COGNITIVE_SEARCH_ADMIN_KEY + 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) diff --git a/python/samples/concepts/memory/google_palm_chat_with_memory.py b/python/samples/concepts/memory/google_palm_chat_with_memory.py index eedc9214c8..0599826353 100644 --- a/python/samples/concepts/memory/google_palm_chat_with_memory.py +++ b/python/samples/concepts/memory/google_palm_chat_with_memory.py @@ -8,7 +8,6 @@ from semantic_kernel.core_plugins import TextMemoryPlugin from semantic_kernel.functions import KernelFunction from semantic_kernel.memory import SemanticTextMemory, VolatileMemoryStore from semantic_kernel.prompt_template import PromptTemplateConfig -from semantic_kernel.utils.settings import google_palm_settings_from_dot_env collection_id = "generic" @@ -82,12 +81,11 @@ async def chat(kernel: Kernel, chat_func: KernelFunction) -> bool: async def main() -> None: kernel = Kernel() - apikey = google_palm_settings_from_dot_env() model_id = "models/embedding-gecko-001" - palm_text_embed = sk_gp.GooglePalmTextEmbedding(model_id, apikey) + palm_text_embed = sk_gp.GooglePalmTextEmbedding(model_id) kernel.add_service(palm_text_embed) chat_service_id = "models/chat-bison-001" - palm_chat_completion = sk_gp.GooglePalmChatCompletion(chat_service_id, apikey) + palm_chat_completion = sk_gp.GooglePalmChatCompletion(chat_service_id) kernel.add_service(palm_chat_completion) memory = SemanticTextMemory(storage=VolatileMemoryStore(), embeddings_generator=palm_text_embed) diff --git a/python/samples/concepts/memory/memory.py b/python/samples/concepts/memory/memory.py index 01b570f5e4..980c36f7af 100644 --- a/python/samples/concepts/memory/memory.py +++ b/python/samples/concepts/memory/memory.py @@ -8,7 +8,6 @@ from semantic_kernel.core_plugins import TextMemoryPlugin from semantic_kernel.functions import KernelFunction from semantic_kernel.memory import SemanticTextMemory, VolatileMemoryStore from semantic_kernel.prompt_template import PromptTemplateConfig -from semantic_kernel.utils.settings import openai_settings_from_dot_env collection_id = "generic" @@ -83,13 +82,11 @@ async def chat(kernel: Kernel, chat_func: KernelFunction) -> bool: async def main() -> None: kernel = Kernel() - api_key, org_id = openai_settings_from_dot_env() service_id = "chat-gpt" - kernel.add_service( - OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo", api_key=api_key, org_id=org_id) - ) + kernel.add_service(OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo")) embedding_gen = OpenAITextEmbedding( - service_id="ada", ai_model_id="text-embedding-ada-002", api_key=api_key, org_id=org_id + service_id="ada", + ai_model_id="text-embedding-ada-002", ) kernel.add_service(embedding_gen) 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 94e5b81076..92a6d0c6ec 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 @@ -10,28 +10,20 @@ from semantic_kernel.connectors.ai.open_ai import ( AzureChatPromptExecutionSettings, ExtraBody, ) +from semantic_kernel.connectors.memory.azure_cognitive_search.azure_ai_search_settings import AzureAISearchSettings from semantic_kernel.contents import ChatHistory from semantic_kernel.functions import KernelArguments from semantic_kernel.prompt_template import InputVariable, PromptTemplateConfig -from semantic_kernel.utils.settings import ( - azure_aisearch_settings_from_dot_env_as_dict, - azure_openai_settings_from_dot_env_as_dict, -) kernel = Kernel() logging.basicConfig(level=logging.INFO) -# Load Azure OpenAI Settings -aoai_settings = azure_openai_settings_from_dot_env_as_dict(include_api_version=True) - # For example, AI Search index may contain the following document: # Emily and David, two passionate scientists, met during a research expedition to Antarctica. # Bonded by their love for the natural world and shared curiosity, they uncovered a # groundbreaking phenomenon in glaciology that could potentially reshape our understanding of climate change. -azure_ai_search_settings = azure_aisearch_settings_from_dot_env_as_dict() - # Depending on the index that you use, you might need to enable the below # and adapt it so that it accurately reflects your index. @@ -43,15 +35,15 @@ azure_ai_search_settings = azure_aisearch_settings_from_dot_env_as_dict() # } # Create the data source settings +azure_ai_search_settings = AzureAISearchSettings.create() -az_source = AzureAISearchDataSource(parameters=azure_ai_search_settings) +az_source = AzureAISearchDataSource(parameters=azure_ai_search_settings.model_dump()) 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", - **aoai_settings, ) kernel.add_service(chat_service) diff --git a/python/samples/concepts/on_your_data/azure_chat_gpt_with_data_api_function_calling.py b/python/samples/concepts/on_your_data/azure_chat_gpt_with_data_api_function_calling.py index f5d8ff8ee0..55cfa5a495 100644 --- a/python/samples/concepts/on_your_data/azure_chat_gpt_with_data_api_function_calling.py +++ b/python/samples/concepts/on_your_data/azure_chat_gpt_with_data_api_function_calling.py @@ -12,6 +12,7 @@ from semantic_kernel.connectors.ai.open_ai import ( AzureChatPromptExecutionSettings, ExtraBody, ) +from semantic_kernel.connectors.memory.azure_cognitive_search.azure_ai_search_settings import AzureAISearchSettings from semantic_kernel.contents import ChatHistory from semantic_kernel.core_plugins import TimePlugin from semantic_kernel.functions import KernelArguments @@ -25,12 +26,9 @@ logging.basicConfig(level=logging.DEBUG) kernel = sk.Kernel() -# Load Azure OpenAI Settings -deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env(include_deployment=True) - # Create the data source settings -azure_ai_search_settings = sk.azure_aisearch_settings_from_dot_env_as_dict() -az_source = AzureAISearchDataSource(parameters=azure_ai_search_settings) +azure_ai_search_settings = AzureAISearchSettings() +az_source = AzureAISearchDataSource(parameters=azure_ai_search_settings.model_dump()) extra = ExtraBody(data_sources=[az_source]) req_settings = AzureChatPromptExecutionSettings(service_id="chat-gpt", extra_body=extra, tool_choice="auto") @@ -42,10 +40,6 @@ req_settings = AzureChatPromptExecutionSettings(service_id="chat-gpt", extra_bod chat_service = AzureChatCompletion( service_id="chat-gpt", - deployment_name=deployment, - api_key=api_key, - endpoint=endpoint, - api_version="2024-02-15-preview", ) kernel.add_service( chat_service, diff --git a/python/samples/concepts/on_your_data/azure_chat_gpt_with_data_api_vector_search.py b/python/samples/concepts/on_your_data/azure_chat_gpt_with_data_api_vector_search.py index 2f823d572c..9e0cf43643 100644 --- a/python/samples/concepts/on_your_data/azure_chat_gpt_with_data_api_vector_search.py +++ b/python/samples/concepts/on_your_data/azure_chat_gpt_with_data_api_vector_search.py @@ -9,28 +9,22 @@ from semantic_kernel.connectors.ai.open_ai import ( AzureChatPromptExecutionSettings, ExtraBody, ) +from semantic_kernel.connectors.memory.azure_cognitive_search.azure_ai_search_settings import AzureAISearchSettings from semantic_kernel.contents import ChatHistory from semantic_kernel.functions import KernelArguments from semantic_kernel.kernel import Kernel from semantic_kernel.prompt_template import InputVariable, PromptTemplateConfig -from semantic_kernel.utils.settings import ( - azure_aisearch_settings_from_dot_env_as_dict, - azure_openai_settings_from_dot_env_as_dict, -) kernel = Kernel() logging.basicConfig(level=logging.DEBUG) -# Load Azure OpenAI Settings -aoai_settings = azure_openai_settings_from_dot_env_as_dict(include_api_version=True) - # For example, AI Search index may contain the following document: # Emily and David, two passionate scientists, met during a research expedition to Antarctica. # Bonded by their love for the natural world and shared curiosity, they uncovered a # groundbreaking phenomenon in glaciology that could potentially reshape our understanding of climate change. -azure_ai_search_settings = azure_aisearch_settings_from_dot_env_as_dict() +azure_ai_search_settings = AzureAISearchSettings() # This example index has fields "title", "chunk", and "vector". # Add fields mapping to the settings. @@ -48,7 +42,7 @@ azure_ai_search_settings["embeddingDependency"] = { azure_ai_search_settings["query_type"] = "vector" # Create the data source settings -az_source = AzureAISearchDataSource(parameters=azure_ai_search_settings) +az_source = AzureAISearchDataSource(parameters=azure_ai_search_settings.model_dump()) extra = ExtraBody(data_sources=[az_source]) service_id = "chat-gpt" req_settings = AzureChatPromptExecutionSettings(service_id=service_id, extra_body=extra) @@ -56,7 +50,6 @@ req_settings = AzureChatPromptExecutionSettings(service_id=service_id, extra_bod # When using data, use the 2024-02-15-preview API version. chat_service = AzureChatCompletion( service_id="chat-gpt", - **aoai_settings, ) kernel.add_service(chat_service) diff --git a/python/samples/concepts/planners/azure_openai_function_calling_stepwise_planner.py b/python/samples/concepts/planners/azure_openai_function_calling_stepwise_planner.py index 66cd1d55b2..dbc19b2faa 100644 --- a/python/samples/concepts/planners/azure_openai_function_calling_stepwise_planner.py +++ b/python/samples/concepts/planners/azure_openai_function_calling_stepwise_planner.py @@ -7,7 +7,6 @@ from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion from semantic_kernel.core_plugins import MathPlugin, TimePlugin from semantic_kernel.planners import FunctionCallingStepwisePlanner, FunctionCallingStepwisePlannerOptions -from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env_as_dict async def main(): @@ -16,7 +15,7 @@ async def main(): service_id = "planner" kernel.add_service( AzureChatCompletion( - service_id=service_id, **azure_openai_settings_from_dot_env_as_dict(include_api_version=True) + service_id=service_id, ), ) diff --git a/python/samples/concepts/planners/openai_function_calling_stepwise_planner.py b/python/samples/concepts/planners/openai_function_calling_stepwise_planner.py index 4a5d07e788..88e994dfda 100644 --- a/python/samples/concepts/planners/openai_function_calling_stepwise_planner.py +++ b/python/samples/concepts/planners/openai_function_calling_stepwise_planner.py @@ -7,19 +7,16 @@ from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion from semantic_kernel.core_plugins import MathPlugin, TimePlugin from semantic_kernel.planners import FunctionCallingStepwisePlanner, FunctionCallingStepwisePlannerOptions -from semantic_kernel.utils.settings import openai_settings_from_dot_env async def main(): kernel = Kernel() service_id = "planner" - api_key, _ = openai_settings_from_dot_env() kernel.add_service( OpenAIChatCompletion( service_id=service_id, ai_model_id="gpt-3.5-turbo-1106", - api_key=api_key, ), ) diff --git a/python/samples/concepts/planners/sequential_planner.py b/python/samples/concepts/planners/sequential_planner.py index 385a7fd432..3715daab9c 100644 --- a/python/samples/concepts/planners/sequential_planner.py +++ b/python/samples/concepts/planners/sequential_planner.py @@ -6,17 +6,13 @@ from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion from semantic_kernel.core_plugins import MathPlugin, TextPlugin, TimePlugin from semantic_kernel.planners import SequentialPlanner -from semantic_kernel.utils.settings import openai_settings_from_dot_env async def main(): kernel = Kernel() - api_key, org_id = openai_settings_from_dot_env() service_id = "gpt-3.5" - kernel.add_service( - OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo", api_key=api_key, org_id=org_id) - ) + kernel.add_service(OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo")) kernel.add_plugins({"math": MathPlugin(), "time": TimePlugin(), "text": TextPlugin()}) # create an instance of sequential planner. diff --git a/python/samples/concepts/plugins/azure_key_vault_settings.py b/python/samples/concepts/plugins/azure_key_vault_settings.py new file mode 100644 index 0000000000..c23135afe3 --- /dev/null +++ b/python/samples/concepts/plugins/azure_key_vault_settings.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft. All rights reserved. + +from pydantic import SecretStr + +from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings +from semantic_kernel.kernel_pydantic import HttpsUrl + + +class AzureKeyVaultSettings(BaseModelSettings): + """Azure Key Vault model settings + + Optional: + - vault_url: HttpsUrl - Azure Key Vault URL + (Env var AZURE_KEY_VAULT_VAULT_URL) + - client_id: str - Azure Key Vault client ID + (Env var AZURE_KEY_VAULT_CLIENT_ID) + - client_secret: SecretStr - Azure Key Vault client secret + (Env var AZURE_KEY_VAULT_CLIENT_SECRET) + """ + + endpoint: HttpsUrl + client_id: str + client_secret: SecretStr + + class Config(BaseModelSettings.Config): + env_prefix = "AZURE_KEY_VAULT_" diff --git a/python/samples/concepts/plugins/azure_python_code_interpreter.py b/python/samples/concepts/plugins/azure_python_code_interpreter.py index 6c773afe93..ae276297bd 100644 --- a/python/samples/concepts/plugins/azure_python_code_interpreter.py +++ b/python/samples/concepts/plugins/azure_python_code_interpreter.py @@ -13,10 +13,6 @@ from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin im ) from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException from semantic_kernel.kernel import Kernel -from semantic_kernel.utils.settings import ( - azure_container_apps_settings_from_dot_env_as_dict, - azure_openai_settings_from_dot_env_as_dict, -) auth_token: AccessToken | None = None @@ -50,13 +46,11 @@ async def main(): service_id = "python-code-interpreter" chat_service = AzureChatCompletion( - service_id=service_id, **azure_openai_settings_from_dot_env_as_dict(include_api_version=True) + service_id=service_id, ) kernel.add_service(chat_service) - python_code_interpreter = SessionsPythonTool( - **azure_container_apps_settings_from_dot_env_as_dict(), auth_callback=auth_callback - ) + python_code_interpreter = SessionsPythonTool(auth_callback=auth_callback) sessions_tool = kernel.add_plugin(python_code_interpreter, "PythonCodeInterpreter") diff --git a/python/samples/concepts/plugins/google_palm_chat_with_plugin.py b/python/samples/concepts/plugins/google_palm_chat_with_plugin.py index a1c97db51b..648f384eaf 100644 --- a/python/samples/concepts/plugins/google_palm_chat_with_plugin.py +++ b/python/samples/concepts/plugins/google_palm_chat_with_plugin.py @@ -6,7 +6,6 @@ from semantic_kernel import Kernel from semantic_kernel.connectors.ai.google_palm import GooglePalmChatCompletion from semantic_kernel.contents import ChatHistory from semantic_kernel.prompt_template import InputVariable, PromptTemplateConfig -from semantic_kernel.utils.settings import google_palm_settings_from_dot_env """ System messages prime the assistant with different personalities or behaviors. @@ -30,9 +29,8 @@ Bartholomew "Blackbeard" Thorne. """ kernel = Kernel() -api_key = google_palm_settings_from_dot_env() service_id = "models/chat-bison-001" -palm_chat_completion = GooglePalmChatCompletion(service_id, api_key) +palm_chat_completion = GooglePalmChatCompletion(service_id) kernel.add_service(palm_chat_completion) req_settings = kernel.get_prompt_execution_settings_from_service_id(service_id=service_id) diff --git a/python/samples/concepts/plugins/openai_function_calling_with_custom_plugin.py b/python/samples/concepts/plugins/openai_function_calling_with_custom_plugin.py index c364e8e6bd..cef76ce689 100644 --- a/python/samples/concepts/plugins/openai_function_calling_with_custom_plugin.py +++ b/python/samples/concepts/plugins/openai_function_calling_with_custom_plugin.py @@ -3,12 +3,7 @@ from __future__ import annotations import asyncio -import sys - -if sys.version_info >= (3, 9): - from typing import Annotated -else: - from typing_extensions import Annotated +from typing import Annotated from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBehavior from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, OpenAIChatCompletion @@ -21,7 +16,6 @@ from semantic_kernel.core_plugins.time_plugin import TimePlugin from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.functions.kernel_function_decorator import kernel_function from semantic_kernel.kernel import Kernel -from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env_as_dict, openai_settings_from_dot_env class WeatherPlugin: @@ -55,14 +49,12 @@ async def main(): if use_azure_openai: # Please make sure your AzureOpenAI Deployment allows for function calling ai_service = AzureChatCompletion( - service_id=service_id, **azure_openai_settings_from_dot_env_as_dict(include_api_version=True) + service_id=service_id, ) else: - api_key, _ = openai_settings_from_dot_env() ai_service = OpenAIChatCompletion( service_id=service_id, ai_model_id="gpt-3.5-turbo-1106", - api_key=api_key, ) kernel.add_service(ai_service) diff --git a/python/samples/concepts/plugins/openai_plugin_azure_key_vault.py b/python/samples/concepts/plugins/openai_plugin_azure_key_vault.py index fe8a7f5083..877c39960a 100644 --- a/python/samples/concepts/plugins/openai_plugin_azure_key_vault.py +++ b/python/samples/concepts/plugins/openai_plugin_azure_key_vault.py @@ -7,12 +7,12 @@ from typing import Dict, Optional import httpx from aiohttp import ClientSession +from azure_key_vault_settings import AzureKeyVaultSettings from semantic_kernel import Kernel from semantic_kernel.connectors.openai_plugin import OpenAIAuthenticationType, OpenAIFunctionExecutionParameters from semantic_kernel.functions import KernelPlugin from semantic_kernel.functions.kernel_arguments import KernelArguments -from semantic_kernel.utils.settings import azure_key_vault_settings_from_dot_env async def add_secret_to_key_vault(kernel: Kernel, plugin: KernelPlugin): @@ -125,7 +125,10 @@ async def main(): # 4. Replace your tenant ID with the "TENANT_ID" placeholder in # python/samples/kernel-syntax-examples/resources/akv-openai.json - endpoint, client_id, client_secret = azure_key_vault_settings_from_dot_env() + azure_keyvault_settings = AzureKeyVaultSettings.create() + client_id = azure_keyvault_settings.client_id + client_secret = azure_keyvault_settings.client_secret.get_secret_value() + endpoint = azure_keyvault_settings.endpoint authentication_provider = OpenAIAuthenticationProvider( { diff --git a/python/samples/concepts/plugins/plugins_from_dir.py b/python/samples/concepts/plugins/plugins_from_dir.py index 93fca9467f..621820709a 100644 --- a/python/samples/concepts/plugins/plugins_from_dir.py +++ b/python/samples/concepts/plugins/plugins_from_dir.py @@ -6,7 +6,6 @@ import os from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import AzureTextCompletion, OpenAITextCompletion from semantic_kernel.functions import KernelArguments -from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env, openai_settings_from_dot_env async def main(): @@ -18,14 +17,12 @@ async def main(): # Configure AI service used by the kernel if useAzureOpenAI: - deployment_name, api_key, endpoint = azure_openai_settings_from_dot_env() kernel.add_service( - AzureTextCompletion(service_id=service_id, deployment_name=model, api_key=api_key, endpoint=endpoint), + AzureTextCompletion(service_id=service_id), ) else: - api_key, org_id = openai_settings_from_dot_env() kernel.add_service( - OpenAITextCompletion(service_id=service_id, ai_model_id=model, api_key=api_key, org_id=org_id), + OpenAITextCompletion(service_id=service_id, ai_model_id=model), ) # note: using plugins from the samples folder diff --git a/python/samples/concepts/prompt_templates/azure_chat_gpt_api_handlebars.py b/python/samples/concepts/prompt_templates/azure_chat_gpt_api_handlebars.py index 1c4e824e0e..14c7382411 100644 --- a/python/samples/concepts/prompt_templates/azure_chat_gpt_api_handlebars.py +++ b/python/samples/concepts/prompt_templates/azure_chat_gpt_api_handlebars.py @@ -7,7 +7,6 @@ from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion from semantic_kernel.contents import ChatHistory from semantic_kernel.functions import KernelArguments -from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env_as_dict logging.basicConfig(level=logging.WARNING) @@ -24,7 +23,7 @@ kernel = Kernel() service_id = "chat-gpt" chat_service = AzureChatCompletion( - service_id=service_id, **azure_openai_settings_from_dot_env_as_dict(include_api_version=True) + service_id=service_id, ) kernel.add_service(chat_service) diff --git a/python/samples/concepts/prompt_templates/azure_chat_gpt_api_jinja2.py b/python/samples/concepts/prompt_templates/azure_chat_gpt_api_jinja2.py index 13c9f5fc79..3ad656c853 100644 --- a/python/samples/concepts/prompt_templates/azure_chat_gpt_api_jinja2.py +++ b/python/samples/concepts/prompt_templates/azure_chat_gpt_api_jinja2.py @@ -7,7 +7,6 @@ from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion from semantic_kernel.contents import ChatHistory from semantic_kernel.functions import KernelArguments -from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env_as_dict logging.basicConfig(level=logging.WARNING) @@ -24,7 +23,7 @@ kernel = Kernel() service_id = "chat-gpt" chat_service = AzureChatCompletion( - service_id=service_id, **azure_openai_settings_from_dot_env_as_dict(include_api_version=True) + service_id=service_id, ) kernel.add_service(chat_service) diff --git a/python/samples/concepts/prompt_templates/configuring_prompts.py b/python/samples/concepts/prompt_templates/configuring_prompts.py index 63538c7d5b..3e15101273 100644 --- a/python/samples/concepts/prompt_templates/configuring_prompts.py +++ b/python/samples/concepts/prompt_templates/configuring_prompts.py @@ -7,7 +7,6 @@ from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAICh from semantic_kernel.contents import ChatHistory from semantic_kernel.functions import KernelArguments from semantic_kernel.prompt_template import InputVariable, PromptTemplateConfig -from semantic_kernel.utils.settings import openai_settings_from_dot_env async def main(): @@ -17,18 +16,17 @@ async def main(): model = "gpt-35-turbo" if useAzureOpenAI else "gpt-3.5-turbo-1106" service_id = model - api_key, org_id = openai_settings_from_dot_env() kernel.add_service( - OpenAIChatCompletion(service_id=service_id, ai_model_id=model, api_key=api_key, org_id=org_id), + OpenAIChatCompletion(service_id=service_id, ai_model_id=model), ) template = """ Previous information from chat: {{$chat_history}} - + User: {{$request}} - Assistant: + Assistant: """ print("--- Rendered Prompt ---") diff --git a/python/samples/concepts/prompt_templates/load_yaml_prompt.py b/python/samples/concepts/prompt_templates/load_yaml_prompt.py index 2ef6432b0d..b721fbc183 100644 --- a/python/samples/concepts/prompt_templates/load_yaml_prompt.py +++ b/python/samples/concepts/prompt_templates/load_yaml_prompt.py @@ -6,19 +6,15 @@ import os from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion from semantic_kernel.contents import ChatHistory -from semantic_kernel.utils.settings import openai_settings_from_dot_env async def main(): kernel = Kernel() - api_key, _ = openai_settings_from_dot_env() - service_id = "default" chat_service = OpenAIChatCompletion( ai_model_id="gpt-4-0613", service_id=service_id, - api_key=api_key, ) kernel.add_service(chat_service) diff --git a/python/samples/concepts/prompt_templates/template_language.py b/python/samples/concepts/prompt_templates/template_language.py index 2b3599bcaa..fb733357d5 100644 --- a/python/samples/concepts/prompt_templates/template_language.py +++ b/python/samples/concepts/prompt_templates/template_language.py @@ -6,7 +6,6 @@ from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAIChatPromptExecutionSettings from semantic_kernel.core_plugins import TimePlugin from semantic_kernel.prompt_template import KernelPromptTemplate, PromptTemplateConfig -from semantic_kernel.utils.settings import openai_settings_from_dot_env async def main(): @@ -16,9 +15,8 @@ async def main(): model = "gpt-35-turbo" if useAzureOpenAI else "gpt-3.5-turbo-1106" service_id = model - api_key, org_id = openai_settings_from_dot_env() kernel.add_service( - OpenAIChatCompletion(service_id=service_id, ai_model_id=model, api_key=api_key, org_id=org_id), + OpenAIChatCompletion(service_id=service_id, ai_model_id=model), ) kernel.add_plugin(TimePlugin(), "time") diff --git a/python/samples/concepts/rag/rag_with_text_memory_plugin.py b/python/samples/concepts/rag/rag_with_text_memory_plugin.py index e0bf67aef9..8fefc17c09 100644 --- a/python/samples/concepts/rag/rag_with_text_memory_plugin.py +++ b/python/samples/concepts/rag/rag_with_text_memory_plugin.py @@ -5,19 +5,16 @@ from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAITextEmbedding from semantic_kernel.core_plugins import TextMemoryPlugin from semantic_kernel.memory import SemanticTextMemory, VolatileMemoryStore -from semantic_kernel.utils.settings import openai_settings_from_dot_env async def main(): kernel = Kernel() - api_key, org_id = openai_settings_from_dot_env() service_id = "default" - kernel.add_service( - OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo", api_key=api_key, org_id=org_id) - ) + kernel.add_service(OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo")) embedding_gen = OpenAITextEmbedding( - service_id="ada", ai_model_id="text-embedding-ada-002", api_key=api_key, org_id=org_id + service_id="ada", + ai_model_id="text-embedding-ada-002", ) kernel.add_service(embedding_gen) diff --git a/python/samples/concepts/rag/self-critique_rag.py b/python/samples/concepts/rag/self-critique_rag.py index c125e2981c..be1aec5261 100644 --- a/python/samples/concepts/rag/self-critique_rag.py +++ b/python/samples/concepts/rag/self-critique_rag.py @@ -2,11 +2,10 @@ import asyncio -from dotenv import dotenv_values - from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, AzureTextEmbedding -from semantic_kernel.connectors.memory import AzureCognitiveSearchMemoryStore +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.contents import ChatHistory from semantic_kernel.core_plugins import TextMemoryPlugin from semantic_kernel.memory import SemanticTextMemory @@ -30,35 +29,26 @@ async def populate_memory(memory: SemanticTextMemory) -> None: async def main() -> None: kernel = Kernel() - config = dotenv_values(".env") - - AZURE_COGNITIVE_SEARCH_ENDPOINT = config["AZURE_AISEARCH_URL"] - AZURE_COGNITIVE_SEARCH_ADMIN_KEY = config["AZURE_AISEARCH_API_KEY"] - AZURE_OPENAI_API_KEY = config["AZURE_OPENAI_API_KEY"] - AZURE_OPENAI_ENDPOINT = config["AZURE_OPENAI_ENDPOINT"] + azure_ai_search_settings = AzureAISearchSettings() vector_size = 1536 # Setting up OpenAI services for text completion and text embedding kernel.add_service( AzureChatCompletion( service_id="dv", - deployment_name="gpt-35-turbo", - endpoint=AZURE_OPENAI_ENDPOINT, - api_key=AZURE_OPENAI_API_KEY, ), ) embedding_gen = AzureTextEmbedding( service_id="ada", - deployment_name="text-embedding-ada-002", - endpoint=AZURE_OPENAI_ENDPOINT, - api_key=AZURE_OPENAI_API_KEY, ) kernel.add_service( embedding_gen, ) acs_connector = AzureCognitiveSearchMemoryStore( - vector_size, AZURE_COGNITIVE_SEARCH_ENDPOINT, AZURE_COGNITIVE_SEARCH_ADMIN_KEY + 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) diff --git a/python/samples/concepts/search/bing_plugin_examples.py b/python/samples/concepts/search/bing_plugin_examples.py index 7443df6244..6482a3a6d7 100644 --- a/python/samples/concepts/search/bing_plugin_examples.py +++ b/python/samples/concepts/search/bing_plugin_examples.py @@ -8,7 +8,6 @@ from semantic_kernel.connectors.search_engine import BingConnector from semantic_kernel.core_plugins import WebSearchEnginePlugin from semantic_kernel.functions import KernelArguments from semantic_kernel.prompt_template import KernelPromptTemplate, PromptTemplateConfig -from semantic_kernel.utils.settings import bing_search_settings_from_dot_env, openai_settings_from_dot_env async def example1(kernel: Kernel, search_plugin_name: str): @@ -101,15 +100,11 @@ async def main(): model = "gpt-3.5-turbo-1106" service_id = model - api_key, org_id = openai_settings_from_dot_env() kernel.add_service( - OpenAIChatCompletion(service_id=service_id, ai_model_id=model, api_key=api_key, org_id=org_id), + OpenAIChatCompletion(service_id=service_id, ai_model_id=model), ) - bing_api_key = bing_search_settings_from_dot_env() - assert bing_api_key is not None - - bing_connector = BingConnector(api_key=bing_api_key) + bing_connector = BingConnector() bing = WebSearchEnginePlugin(bing_connector) kernel.add_plugin(bing, "bing") diff --git a/python/samples/concepts/search/bing_search_plugin.py b/python/samples/concepts/search/bing_search_plugin.py index 3f2a185f4a..f93b181c02 100644 --- a/python/samples/concepts/search/bing_search_plugin.py +++ b/python/samples/concepts/search/bing_search_plugin.py @@ -1,33 +1,22 @@ # Copyright (c) Microsoft. All rights reserved. -import os - -from dotenv import load_dotenv from semantic_kernel import Kernel from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion from semantic_kernel.connectors.search_engine import BingConnector from semantic_kernel.core_plugins import WebSearchEnginePlugin from semantic_kernel.prompt_template import PromptTemplateConfig -from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env - -load_dotenv() async def main(): kernel = Kernel() - deployment, key, endpoint, api_version = azure_openai_settings_from_dot_env(include_api_version=True) service_id = "chat-gpt" kernel.add_service( AzureChatCompletion( service_id=service_id, - deployment_name=deployment, - api_key=key, - endpoint=endpoint, - api_version=api_version, ), ) - connector = BingConnector(api_key=os.getenv("BING_API_KEY")) + connector = BingConnector() web_plugin = kernel.add_plugin(WebSearchEnginePlugin(connector), "WebSearch") print("---------------- Question 1 -----------------\n") diff --git a/python/samples/concepts/search/google_search_plugin.py b/python/samples/concepts/search/google_search_plugin.py index b77227d9e8..0c24f34238 100644 --- a/python/samples/concepts/search/google_search_plugin.py +++ b/python/samples/concepts/search/google_search_plugin.py @@ -10,17 +10,13 @@ from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion from semantic_kernel.connectors.search_engine import GoogleConnector from semantic_kernel.core_plugins import WebSearchEnginePlugin from semantic_kernel.functions import KernelArguments -from semantic_kernel.utils.settings import openai_settings_from_dot_env load_dotenv() async def main(): kernel = Kernel() - api_key, org_id = openai_settings_from_dot_env() - kernel.add_service( - OpenAIChatCompletion(service_id="chat-gpt", ai_model_id="gpt-3.5-turbo", api_key=api_key, org_id=org_id) - ) + kernel.add_service(OpenAIChatCompletion(service_id="chat-gpt", ai_model_id="gpt-3.5-turbo")) """ Instantiate a Google Connector diff --git a/python/samples/concepts/text_generation/google_palm_text_completion.py b/python/samples/concepts/text_generation/google_palm_text_completion.py index 282b1cad3c..0c14c32a7d 100644 --- a/python/samples/concepts/text_generation/google_palm_text_completion.py +++ b/python/samples/concepts/text_generation/google_palm_text_completion.py @@ -4,14 +4,13 @@ import asyncio from semantic_kernel.connectors.ai.google_palm import GooglePalmTextCompletion, GooglePalmTextPromptExecutionSettings from semantic_kernel.kernel import Kernel -from semantic_kernel.utils.settings import google_palm_settings_from_dot_env -async def text_completion_example_complete(kernel, api_key, user_mssg, settings): +async def text_completion_example_complete(kernel, user_mssg, settings): """ Complete a text prompt using the Google PaLM model and print the results. """ - palm_text_completion = GooglePalmTextCompletion("models/text-bison-001", api_key) + palm_text_completion = GooglePalmTextCompletion("models/text-bison-001") kernel.add_service(palm_text_completion) answer = await palm_text_completion.complete(user_mssg, settings) return answer @@ -19,7 +18,6 @@ async def text_completion_example_complete(kernel, api_key, user_mssg, settings) async def main() -> None: kernel = Kernel() - apikey = google_palm_settings_from_dot_env() settings = GooglePalmTextPromptExecutionSettings() user_mssg1 = ( @@ -29,13 +27,13 @@ async def main() -> None: "boxes have 98 coins in total. How many coins are there in each box? " "Think about it step by step, and show your work." ) - response = await text_completion_example_complete(kernel, apikey, user_mssg1, settings) + response = await text_completion_example_complete(kernel, user_mssg1, settings) print(f"User:> {user_mssg1}\n\nChatBot:> {response}\n") # Use temperature to influence the variance of the responses settings.number_of_responses = 3 settings.temperature = 1 user_mssg2 = "I need a concise answer. A common method for traversing a binary tree is" - response = await text_completion_example_complete(kernel, apikey, user_mssg2, settings) + response = await text_completion_example_complete(kernel, user_mssg2, settings) print(f"User:> {user_mssg2}\n\nChatBot:> {response}") return diff --git a/python/samples/demos/booking_restaurant/README.md b/python/samples/demos/booking_restaurant/README.md index 88e31608df..37dd9ca2e2 100644 --- a/python/samples/demos/booking_restaurant/README.md +++ b/python/samples/demos/booking_restaurant/README.md @@ -35,11 +35,21 @@ This sample uses function calling capable models and has been tested with the fo ## Configuring the sample -Please make sure your .env file contains the following: +Please make sure either your environment variables or your .env file contains the following: - "BOOKING_SAMPLE_CLIENT_ID" - "BOOKING_SAMPLE_TENANT_ID" - "BOOKING_SAMPLE_CLIENT_SECRET" +- "BOOKING_SAMPLE_BUSINESS_ID" +- "BOOKING_SAMPLE_SERVICE_ID" + +If wanting to use the `.env` file, you must pass the `env_file_path` parameter with a valid path: + +```python +booking_sample_settings = BookingSampleSettings(env_file_path=env_file_path) +``` + +This will tell Pydantic settings to also load the `.env` file instead of just trying to load environment variables. ### Create an App Registration in Azure Active Directory diff --git a/python/samples/demos/booking_restaurant/booking_sample_settings.py b/python/samples/demos/booking_restaurant/booking_sample_settings.py new file mode 100644 index 0000000000..04f9795411 --- /dev/null +++ b/python/samples/demos/booking_restaurant/booking_sample_settings.py @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft. All rights reserved. + +from pydantic import SecretStr +from pydantic_settings import BaseSettings + + +class BookingSampleSettings(BaseSettings): + """Restaurant Booking Sample settings + + The settings are first loaded from environment variables with the prefix 'BOOKING_'. If the + environment variables are not found, the settings can be loaded from a .env file with the + encoding 'utf-8'. If the settings are not found in the .env file, the settings are ignored; + however, validation will fail alerting that the settings are missing. + + Required settings for prefix 'BOOKING_' are: + - client_id = The App Registration Client ID (Env var BOOKING_CLIENT_ID) + - tenant_id = The App Registration Tenant ID (Env var BOOKING_TENANT_ID) + - client_secret = The App Registration Client Secret (Env var BOOKING_CLIENT_SECRET) + - business_id = The sample booking service ID (Env var BOOKING_BUSINESS_ID) + - service_id = The sample booking service ID (Env var BOOKING_SERVICE_ID) + + For more information on these required settings, please see the sample's README.md file. + """ + + env_file_path: str | None = None + client_id: str + tenant_id: str + client_secret: SecretStr + business_id: str + service_id: str + + class Config: + env_prefix = "BOOKING_" + env_file = None + env_file_encoding = "utf-8" + extra = "ignore" + case_sensitive = False + + @classmethod + def create(cls, **kwargs): + 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/samples/demos/booking_restaurant/restaurant_booking.py b/python/samples/demos/booking_restaurant/restaurant_booking.py index 684907166e..153b9ddab7 100644 --- a/python/samples/demos/booking_restaurant/restaurant_booking.py +++ b/python/samples/demos/booking_restaurant/restaurant_booking.py @@ -3,9 +3,10 @@ import asyncio from azure.identity import ClientSecretCredential +from booking_sample_settings import BookingSampleSettings from bookings_plugin.bookings_plugin import BookingsPlugin -from dotenv import dotenv_values from msgraph import GraphServiceClient +from pydantic import ValidationError from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import ( @@ -13,26 +14,30 @@ from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_pro ) from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion from semantic_kernel.contents.chat_history import ChatHistory +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.kernel import Kernel -from semantic_kernel.utils.settings import booking_sample_settings_from_dot_env_as_dict, openai_settings_from_dot_env kernel = Kernel() service_id = "open_ai" -api_key, _ = openai_settings_from_dot_env() -ai_service = OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo-1106", api_key=api_key) +ai_service = OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo-1106") kernel.add_service(ai_service) -client_secret_credential = ClientSecretCredential(**booking_sample_settings_from_dot_env_as_dict()) +try: + booking_sample_settings = BookingSampleSettings.create() +except ValidationError as e: + raise ServiceInitializationError("Failed to initialize the booking sample settings.") from e + +tenant_id = booking_sample_settings.tenant_id +client_id = booking_sample_settings.client_id +client_secret = booking_sample_settings.client_secret +client_secret_credential = ClientSecretCredential(tenant_id=tenant_id, client_id=client_id, client_secret=client_secret) graph_client = GraphServiceClient(credentials=client_secret_credential, scopes=["https://graph.microsoft.com/.default"]) -config = dotenv_values(".env") -booking_business_id = config.get("BOOKING_SAMPLE_BUSINESS_ID") -assert booking_business_id, "BOOKING_SAMPLE_BUSINESS_ID is not set in .env file" -booking_service_id = config.get("BOOKING_SAMPLE_SERVICE_ID") -assert booking_service_id, "BOOKING_SAMPLE_SERVICE_ID is not set in .env file" +booking_business_id = booking_sample_settings.business_id +booking_service_id = booking_sample_settings.service_id bookings_plugin = BookingsPlugin( graph_client=graph_client, diff --git a/python/samples/getting_started/00-getting-started.ipynb b/python/samples/getting_started/00-getting-started.ipynb index e0b19a8c47..34839d98c7 100644 --- a/python/samples/getting_started/00-getting-started.ipynb +++ b/python/samples/getting_started/00-getting-started.ipynb @@ -46,7 +46,7 @@ "from services import Service\n", "\n", "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", - "selectedService = Service.OpenAI" + "selectedService = Service.AzureOpenAI" ] }, { @@ -56,23 +56,39 @@ "source": [ "## Option 1: using OpenAI\n", "\n", - "**Step 2**: Add your [OpenAI Key](https://openai.com/product/) key to a `.env` file in the same folder (org Id only if you have multiple orgs):\n", + "**Step 2**: Add your [OpenAI Key](https://openai.com/product/) key to either your environment variables or to the `.env` file in the same folder (org Id only if you have multiple orgs):\n", "\n", "```\n", "OPENAI_API_KEY=\"sk-...\"\n", "OPENAI_ORG_ID=\"\"\n", "```\n", + "The environment variables names should match the names used in the `.env` file, as shown above.\n", + "\n", + "If using the `.env` file, please configure the `env_file_path` parameter with a valid path when creating the ChatCompletion class:\n", + "\n", + "```\n", + "chat_completion = OpenAIChatCompletion(service_id=\"test\", env_file_path=)\n", + "```\n", "\n", "Use \"keyword arguments\" to instantiate an OpenAI Chat Completion service and add it to the kernel:\n", "\n", "## Option 2: using Azure OpenAI\n", "\n", - "**Step 2**: Add your [Azure Open AI Service key](https://learn.microsoft.com/azure/cognitive-services/openai/quickstart?pivots=programming-language-studio) settings to a `.env` file in the same folder:\n", + "**Step 2**: Add your [Azure Open AI Service key](https://learn.microsoft.com/azure/cognitive-services/openai/quickstart?pivots=programming-language-studio) settings to either your system's environment variables or to the `.env` file in the same folder:\n", "\n", "```\n", "AZURE_OPENAI_API_KEY=\"...\"\n", "AZURE_OPENAI_ENDPOINT=\"https://...\"\n", - "AZURE_OPENAI_DEPLOYMENT_NAME=\"...\"\n", + "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=\"...\"\n", + "AZURE_OPENAI_TEXT_DEPLOYMENT_NAME=\"...\"\n", + "AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME=\"...\"\n", + "```\n", + "The environment variables names should match the names used in the `.env` file, as shown above.\n", + "\n", + "If using the `.env` file, please configure the `env_file_path` parameter with a valid path when creating the ChatCompletion class:\n", + "\n", + "```\n", + "chat_completion = AzureChatCompletion(service_id=\"test\", env_file_path=)\n", "```\n", "\n", "Use \"keyword arguments\" to instantiate an Azure OpenAI Chat Completion service and add it to the kernel:\n" @@ -84,24 +100,20 @@ "metadata": {}, "outputs": [], "source": [ - "from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env, openai_settings_from_dot_env\n", - "\n", "service_id = None\n", "if selectedService == Service.OpenAI:\n", " from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion\n", "\n", - " api_key, org_id = openai_settings_from_dot_env()\n", " service_id = \"default\"\n", " kernel.add_service(\n", - " OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\", api_key=api_key, org_id=org_id),\n", + " OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\"),\n", " )\n", "elif selectedService == Service.AzureOpenAI:\n", " from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion\n", "\n", - " deployment, api_key, endpoint = azure_openai_settings_from_dot_env()\n", " service_id = \"default\"\n", " kernel.add_service(\n", - " AzureChatCompletion(service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key),\n", + " AzureChatCompletion(service_id=service_id),\n", " )" ] }, @@ -155,7 +167,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/python/samples/getting_started/01-basic-loading-the-kernel.ipynb b/python/samples/getting_started/01-basic-loading-the-kernel.ipynb index 2f59281479..644822fa8c 100644 --- a/python/samples/getting_started/01-basic-loading-the-kernel.ipynb +++ b/python/samples/getting_started/01-basic-loading-the-kernel.ipynb @@ -39,6 +39,50 @@ "kernel = Kernel()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Configuring API Keys and Endpoints\n", + "\n", + "#### Option 1: using OpenAI\n", + "\n", + "Add your [OpenAI Key](https://openai.com/product/) key to either your environment variables or to the `.env` file in the same folder (org Id only if you have multiple orgs):\n", + "\n", + "```\n", + "OPENAI_API_KEY=\"sk-...\"\n", + "OPENAI_ORG_ID=\"\"\n", + "```\n", + "The environment variables names should match the names used in the `.env` file, as shown above.\n", + "\n", + "If using the `.env` file, please configure the `env_file_path` parameter with a valid path when creating the ChatCompletion class:\n", + "\n", + "```\n", + "chat_completion = OpenAIChatCompletion(service_id=\"test\", env_file_path=)\n", + "```\n", + "\n", + "Use \"keyword arguments\" to instantiate an OpenAI Chat Completion service and add it to the kernel:\n", + "\n", + "#### Option 2: using Azure OpenAI\n", + "\n", + "Add your [Azure Open AI Service key](https://learn.microsoft.com/azure/cognitive-services/openai/quickstart?pivots=programming-language-studio) settings to either your system's environment variables or to the `.env` file in the same folder:\n", + "\n", + "```\n", + "AZURE_OPENAI_API_KEY=\"...\"\n", + "AZURE_OPENAI_ENDPOINT=\"https://...\"\n", + "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=\"...\"\n", + "AZURE_OPENAI_TEXT_DEPLOYMENT_NAME=\"...\"\n", + "AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME=\"...\"\n", + "```\n", + "The environment variables names should match the names used in the `.env` file, as shown above.\n", + "\n", + "If using the `.env` file, please configure the `env_file_path` parameter with a valid path when creating the ChatCompletion class:\n", + "\n", + "```\n", + "chat_completion = AzureChatCompletion(service_id=\"test\", env_file_path=)\n", + "```\n" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -72,21 +116,17 @@ "service_id = None\n", "if selectedService == Service.OpenAI:\n", " from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion\n", - " from semantic_kernel.utils.settings import openai_settings_from_dot_env\n", "\n", - " api_key, org_id = openai_settings_from_dot_env()\n", " service_id = \"oai_chat_gpt\"\n", " kernel.add_service(\n", - " OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\", api_key=api_key, org_id=org_id),\n", + " OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\"),\n", " )\n", "elif selectedService == Service.AzureOpenAI:\n", " from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion\n", - " from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env\n", "\n", - " deployment, api_key, endpoint = azure_openai_settings_from_dot_env()\n", " service_id = \"aoai_chat_completion\"\n", " kernel.add_service(\n", - " AzureChatCompletion(service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key),\n", + " AzureChatCompletion(service_id=service_id),\n", " )" ] }, @@ -115,7 +155,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.11.9" }, "polyglot_notebook": { "kernelInfo": { diff --git a/python/samples/getting_started/02-running-prompts-from-file.ipynb b/python/samples/getting_started/02-running-prompts-from-file.ipynb index 769648e74d..abce1d3a83 100644 --- a/python/samples/getting_started/02-running-prompts-from-file.ipynb +++ b/python/samples/getting_started/02-running-prompts-from-file.ipynb @@ -135,21 +135,17 @@ "service_id = None\n", "if selectedService == Service.OpenAI:\n", " from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion\n", - " from semantic_kernel.utils.settings import openai_settings_from_dot_env\n", "\n", - " api_key, org_id = openai_settings_from_dot_env()\n", " service_id = \"default\"\n", " kernel.add_service(\n", - " OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\", api_key=api_key, org_id=org_id),\n", + " OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\"),\n", " )\n", "elif selectedService == Service.AzureOpenAI:\n", " from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion\n", - " from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env\n", "\n", - " deployment, api_key, endpoint = azure_openai_settings_from_dot_env()\n", " service_id = \"default\"\n", " kernel.add_service(\n", - " AzureChatCompletion(service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key),\n", + " AzureChatCompletion(service_id=service_id),\n", " )" ] }, diff --git a/python/samples/getting_started/03-prompt-function-inline.ipynb b/python/samples/getting_started/03-prompt-function-inline.ipynb index b13ecc1fbd..7b42a121d2 100644 --- a/python/samples/getting_started/03-prompt-function-inline.ipynb +++ b/python/samples/getting_started/03-prompt-function-inline.ipynb @@ -77,24 +77,18 @@ "\n", "service_id = None\n", "if selectedService == Service.OpenAI:\n", - " from semantic_kernel.connectors.ai.open_ai import OpenAITextCompletion\n", - " from semantic_kernel.utils.settings import openai_settings_from_dot_env\n", + " from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion\n", "\n", - " api_key, org_id = openai_settings_from_dot_env()\n", - " service_id = \"oai_text_completion\"\n", + " service_id = \"oai_chat_completion\"\n", " kernel.add_service(\n", - " OpenAITextCompletion(\n", - " service_id=service_id, ai_model_id=\"gpt-3.5-turbo-instruct\", api_key=api_key, org_id=org_id\n", - " ),\n", + " OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo-instruct\"),\n", " )\n", "elif selectedService == Service.AzureOpenAI:\n", - " from semantic_kernel.connectors.ai.open_ai import AzureTextCompletion\n", - " from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env\n", + " from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion\n", "\n", - " deployment, api_key, endpoint = azure_openai_settings_from_dot_env()\n", - " service_id = \"aoai_text_completion\"\n", + " service_id = \"aoai_chat_completion\"\n", " kernel.add_service(\n", - " AzureTextCompletion(service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key),\n", + " AzureChatCompletion(service_id=service_id),\n", " )" ] }, @@ -116,7 +110,7 @@ "metadata": {}, "outputs": [], "source": [ - "from semantic_kernel.connectors.ai.open_ai import OpenAITextPromptExecutionSettings\n", + "from semantic_kernel.connectors.ai.open_ai import OpenAIChatPromptExecutionSettings\n", "from semantic_kernel.prompt_template import PromptTemplateConfig, InputVariable\n", "\n", "\n", @@ -125,16 +119,16 @@ "\"\"\"\n", "\n", "if selectedService == Service.OpenAI:\n", - " execution_settings = OpenAITextPromptExecutionSettings(\n", + " execution_settings = OpenAIChatPromptExecutionSettings(\n", " service_id=service_id,\n", - " ai_model_id=\"gpt-3.5-turbo-instruct\",\n", + " ai_model_id=\"gpt-3.5-turbo\",\n", " max_tokens=2000,\n", " temperature=0.7,\n", " )\n", "elif selectedService == Service.AzureOpenAI:\n", - " execution_settings = OpenAITextPromptExecutionSettings(\n", + " execution_settings = OpenAIChatPromptExecutionSettings(\n", " service_id=service_id,\n", - " ai_model_id=deployment,\n", + " ai_model_id=\"gpt-35-turbo\"\n", " max_tokens=2000,\n", " temperature=0.7,\n", " )\n", @@ -248,18 +242,16 @@ "if selectedService == Service.OpenAI:\n", " from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion\n", "\n", - " api_key, org_id = openai_settings_from_dot_env()\n", " service_id = \"oai_chat_gpt\"\n", " kernel.add_service(\n", - " OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\", api_key=api_key, org_id=org_id),\n", + " OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\"),\n", " )\n", "elif selectedService == Service.AzureOpenAI:\n", " from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion\n", "\n", - " deployment, api_key, endpoint = azure_openai_settings_from_dot_env()\n", " service_id = \"aoai_chat_completion\"\n", " kernel.add_service(\n", - " AzureChatCompletion(service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key),\n", + " AzureChatCompletion(service_id=service_id),\n", " )" ] }, @@ -301,7 +293,7 @@ "elif selectedService == Service.AzureOpenAI:\n", " execution_settings = OpenAIChatPromptExecutionSettings(\n", " service_id=service_id,\n", - " ai_model_id=deployment,\n", + " ai_model_id=\"gpt-35-turbo\",\n", " max_tokens=2000,\n", " temperature=0.7,\n", " )\n", @@ -344,7 +336,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/python/samples/getting_started/04-kernel-arguments-chat.ipynb b/python/samples/getting_started/04-kernel-arguments-chat.ipynb index 0c0a86f814..07d7f19829 100644 --- a/python/samples/getting_started/04-kernel-arguments-chat.ipynb +++ b/python/samples/getting_started/04-kernel-arguments-chat.ipynb @@ -56,21 +56,17 @@ "service_id = None\n", "if selectedService == Service.OpenAI:\n", " from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion\n", - " from semantic_kernel.utils.settings import openai_settings_from_dot_env\n", "\n", - " api_key, org_id = openai_settings_from_dot_env()\n", " service_id = \"oai_chat_gpt\"\n", " kernel.add_service(\n", - " OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\", api_key=api_key, org_id=org_id),\n", + " OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\"),\n", " )\n", "elif selectedService == Service.AzureOpenAI:\n", " from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion\n", - " from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env\n", "\n", - " deployment, api_key, endpoint = azure_openai_settings_from_dot_env()\n", " service_id = \"aoai_chat_completion\"\n", " kernel.add_service(\n", - " AzureChatCompletion(service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key),\n", + " AzureChatCompletion(service_id=service_id),\n", " )" ] }, @@ -131,7 +127,7 @@ "elif selectedService == Service.AzureOpenAI:\n", " execution_settings = OpenAIChatPromptExecutionSettings(\n", " service_id=service_id,\n", - " ai_model_id=deployment,\n", + " ai_model_id=\"gpt-35-turbo\",\n", " max_tokens=2000,\n", " temperature=0.7,\n", " )\n", diff --git a/python/samples/getting_started/05-using-the-planner.ipynb b/python/samples/getting_started/05-using-the-planner.ipynb index 826be2db72..e451b9611c 100644 --- a/python/samples/getting_started/05-using-the-planner.ipynb +++ b/python/samples/getting_started/05-using-the-planner.ipynb @@ -95,26 +95,17 @@ "import semantic_kernel.connectors.ai.open_ai as sk_oai # noqa: F401\n", "from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt\n", "from semantic_kernel.core_plugins.text_plugin import TextPlugin\n", - "from semantic_kernel.utils.settings import openai_settings_from_dot_env, azure_openai_settings_from_dot_env\n", "\n", "kernel = sk.Kernel()\n", "service_id = \"default\"\n", "if selectedService == Service.OpenAI:\n", - " api_key, org_id = openai_settings_from_dot_env()\n", " kernel.add_service(\n", - " sk_oai.OpenAIChatCompletion(\n", - " service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\", api_key=api_key, org_id=org_id\n", - " ),\n", + " sk_oai.OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\"),\n", " )\n", "elif selectedService == Service.AzureOpenAI:\n", - " deployment, api_key, endpoint, api_version = azure_openai_settings_from_dot_env(include_api_version=True)\n", " kernel.add_service(\n", " sk_oai.AzureChatCompletion(\n", " service_id=service_id,\n", - " deployment_name=deployment,\n", - " endpoint=endpoint,\n", - " api_key=api_key,\n", - " api_version=api_version,\n", " ),\n", " )\n", "\n", @@ -281,26 +272,17 @@ "source": [ "import semantic_kernel as sk\n", "import semantic_kernel.connectors.ai.open_ai as sk_oai # noqa: F401\n", - "from semantic_kernel.utils.settings import openai_settings_from_dot_env, azure_openai_settings_from_dot_env\n", "\n", "kernel = sk.Kernel()\n", "service_id = \"default\"\n", "if selectedService == Service.OpenAI:\n", - " api_key, org_id = openai_settings_from_dot_env()\n", " kernel.add_service(\n", - " sk_oai.OpenAIChatCompletion(\n", - " service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\", api_key=api_key, org_id=org_id\n", - " ),\n", + " sk_oai.OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo-1106\"),\n", " )\n", "elif selectedService == Service.AzureOpenAI:\n", - " deployment, api_key, endpoint, api_version = azure_openai_settings_from_dot_env(include_api_version=True)\n", " kernel.add_service(\n", " sk_oai.AzureChatCompletion(\n", " service_id=service_id,\n", - " deployment_name=deployment,\n", - " endpoint=endpoint,\n", - " api_key=api_key,\n", - " api_version=api_version,\n", " ),\n", " )" ] diff --git a/python/samples/getting_started/06-memory-and-embeddings.ipynb b/python/samples/getting_started/06-memory-and-embeddings.ipynb index bfd29fd512..38890ce487 100644 --- a/python/samples/getting_started/06-memory-and-embeddings.ipynb +++ b/python/samples/getting_started/06-memory-and-embeddings.ipynb @@ -73,7 +73,6 @@ "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", - "from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env, openai_settings_from_dot_env\n", "\n", "kernel = Kernel()\n", "\n", @@ -81,20 +80,14 @@ "\n", "# Configure AI service used by the kernel\n", "if selectedService == Service.AzureOpenAI:\n", - " deployment, api_key, endpoint = azure_openai_settings_from_dot_env()\n", - " azure_chat_service = AzureChatCompletion(\n", - " service_id=chat_service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key\n", - " )\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\", endpoint=endpoint, api_key=api_key)\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", - " api_key, org_id = openai_settings_from_dot_env()\n", - " oai_chat_service = OpenAIChatCompletion(\n", - " service_id=chat_service_id, ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id\n", - " )\n", - " embedding_gen = OpenAITextEmbedding(ai_model_id=\"text-embedding-ada-002\", api_key=api_key, org_id=org_id)\n", + " 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", @@ -218,6 +211,7 @@ "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", @@ -431,9 +425,6 @@ "outputs": [], "source": [ "from semantic_kernel.connectors.memory.azure_cognitive_search import AzureCognitiveSearchMemoryStore\n", - "from semantic_kernel.utils.settings import azure_aisearch_settings_from_dot_env\n", - "\n", - "azure_ai_search_api_key, azure_ai_search_url = azure_aisearch_settings_from_dot_env()\n", "\n", "acs_memory_store = AzureCognitiveSearchMemoryStore(\n", " vector_size=1536,\n", diff --git a/python/samples/getting_started/08-native-function-inline.ipynb b/python/samples/getting_started/08-native-function-inline.ipynb index 7855ba627f..e48f003c6d 100644 --- a/python/samples/getting_started/08-native-function-inline.ipynb +++ b/python/samples/getting_started/08-native-function-inline.ipynb @@ -71,25 +71,20 @@ "source": [ "from semantic_kernel import Kernel\n", "from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, OpenAIChatCompletion\n", - "from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env, openai_settings_from_dot_env\n", "\n", "kernel = Kernel()\n", "\n", "if selectedService == Service.AzureOpenAI:\n", - " deployment, api_key, endpoint = azure_openai_settings_from_dot_env()\n", " service_id = \"aoai_chat\" # used later in the notebook\n", " azure_chat_service = AzureChatCompletion(\n", - " service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key\n", + " service_id=service_id\n", " ) # set the deployment name to the value of your chat model\n", " kernel.add_service(azure_chat_service)\n", "\n", "# Configure OpenAI service\n", "if selectedService == Service.OpenAI:\n", - " api_key, org_id = openai_settings_from_dot_env()\n", " service_id = \"oai_chat\" # used later in the notebook\n", - " oai_chat_service = OpenAIChatCompletion(\n", - " service_id=service_id, ai_model_id=\"gpt-4-turbo-1106\", api_key=api_key, org_id=org_id\n", - " )\n", + " oai_chat_service = OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-4-turbo-1106\")\n", " kernel.add_service(oai_chat_service)" ] }, @@ -187,7 +182,7 @@ "elif selectedService == Service.AzureOpenAI:\n", " execution_settings = OpenAIChatPromptExecutionSettings(\n", " service_id=service_id,\n", - " ai_model_id=deployment,\n", + " ai_model_id=\"gpt-35-turbo\",\n", " max_tokens=2000,\n", " temperature=0.7,\n", " )\n", @@ -281,20 +276,16 @@ "kernel = Kernel()\n", "\n", "if selectedService == Service.AzureOpenAI:\n", - " deployment, api_key, endpoint = azure_openai_settings_from_dot_env()\n", " service_id = \"aoai_chat\" # used later in the notebook\n", " azure_chat_service = AzureChatCompletion(\n", - " service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key\n", + " service_id=service_id\n", " ) # set the deployment name to the value of your chat model\n", " kernel.add_service(azure_chat_service)\n", "\n", "# Configure OpenAI service\n", "if selectedService == Service.OpenAI:\n", - " api_key, org_id = openai_settings_from_dot_env()\n", " service_id = \"oai_chat\" # used later in the notebook\n", - " oai_chat_service = OpenAIChatCompletion(\n", - " service_id=service_id, ai_model_id=\"gpt-4-turbo-1106\", api_key=api_key, org_id=org_id\n", - " )\n", + " oai_chat_service = OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-4-turbo-1106\")\n", " kernel.add_service(oai_chat_service)" ] }, @@ -402,7 +393,7 @@ "elif selectedService == Service.AzureOpenAI:\n", " execution_settings = OpenAIChatPromptExecutionSettings(\n", " service_id=service_id,\n", - " ai_model_id=deployment,\n", + " ai_model_id=\"gpt-35-turbo\",\n", " max_tokens=2000,\n", " temperature=0.7,\n", " )\n", @@ -574,7 +565,7 @@ "elif selectedService == Service.AzureOpenAI:\n", " execution_settings = OpenAIChatPromptExecutionSettings(\n", " service_id=service_id,\n", - " ai_model_id=deployment,\n", + " ai_model_id=\"gpt-35-turbo\",\n", " max_tokens=2000,\n", " temperature=0.7,\n", " )\n", diff --git a/python/samples/getting_started/09-groundedness-checking.ipynb b/python/samples/getting_started/09-groundedness-checking.ipynb index 91269b140a..20bb6c4591 100644 --- a/python/samples/getting_started/09-groundedness-checking.ipynb +++ b/python/samples/getting_started/09-groundedness-checking.ipynb @@ -94,7 +94,6 @@ "source": [ "from semantic_kernel import Kernel\n", "from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, OpenAIChatCompletion\n", - "from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env, openai_settings_from_dot_env\n", "\n", "kernel = Kernel()\n", "\n", @@ -102,18 +101,14 @@ "\n", "# Configure AI service used by the kernel\n", "if useAzureOpenAI:\n", - " deployment, api_key, endpoint = azure_openai_settings_from_dot_env()\n", " service_id = \"default\"\n", " azure_chat_service = AzureChatCompletion(\n", - " service_id=service_id, deployment_name=deployment, endpoint=endpoint, api_key=api_key\n", + " service_id=service_id\n", " ) # set the deployment name to the value of your chat model\n", " kernel.add_service(azure_chat_service)\n", "else:\n", - " api_key, org_id = openai_settings_from_dot_env()\n", " service_id = \"default\"\n", - " oai_chat_service = OpenAIChatCompletion(\n", - " service_id=service_id, ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id\n", - " )\n", + " oai_chat_service = OpenAIChatCompletion(service_id=service_id, ai_model_id=\"gpt-3.5-turbo\")\n", " kernel.add_service(oai_chat_service)" ] }, diff --git a/python/samples/getting_started/10-multiple-results-per-prompt.ipynb b/python/samples/getting_started/10-multiple-results-per-prompt.ipynb index f942d60571..80d89cc596 100644 --- a/python/samples/getting_started/10-multiple-results-per-prompt.ipynb +++ b/python/samples/getting_started/10-multiple-results-per-prompt.ipynb @@ -81,29 +81,22 @@ "outputs": [], "source": [ "from semantic_kernel import Kernel\n", - "from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env, openai_settings_from_dot_env\n", "\n", "kernel = Kernel()\n", "\n", "# Configure Azure LLM service\n", "if selectedService == Service.AzureOpenAI:\n", - " deployment, api_key, endpoint = azure_openai_settings_from_dot_env()\n", " azure_text_service = AzureTextCompletion(\n", - " service_id=\"aoai_text\", deployment_name=\"gpt-35-turbo-instruct\", endpoint=endpoint, api_key=api_key\n", + " service_id=\"aoai_text\"\n", " ) # set the deployment name to the value of your text model (e.g. gpt-35-turbo-instruct)\n", " azure_chat_service = AzureChatCompletion(\n", - " service_id=\"aoai_chat\", deployment_name=\"gpt-35-turbo\", endpoint=endpoint, api_key=api_key\n", + " service_id=\"aoai_chat\"\n", " ) # set the deployment name to the value of your chat model\n", "\n", "# Configure OpenAI service\n", "if selectedService == Service.OpenAI:\n", - " api_key, org_id = openai_settings_from_dot_env()\n", - " oai_text_service = OpenAITextCompletion(\n", - " service_id=\"oai_text\", ai_model_id=\"gpt-3.5-turbo-instruct\", api_key=api_key, org_id=org_id\n", - " )\n", - " oai_chat_service = OpenAIChatCompletion(\n", - " service_id=\"oai_chat\", ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id\n", - " )\n", + " oai_text_service = OpenAITextCompletion(service_id=\"oai_text\", ai_model_id=\"gpt-3.5-turbo-instruct\")\n", + " oai_chat_service = OpenAIChatCompletion(service_id=\"oai_chat\", ai_model_id=\"gpt-3.5-turbo\")\n", "\n", "# Configure Hugging Face service\n", "if selectedService == Service.HuggingFace:\n", @@ -183,7 +176,7 @@ "source": [ "if selectedService == Service.AzureOpenAI:\n", " prompt = \"provide me a list of possible meanings for the acronym 'ORLD'\"\n", - " \n", + "\n", " results = await azure_text_service.complete(prompt=prompt, settings=oai_text_prompt_execution_settings)\n", " i = 1\n", " for result in results:\n", @@ -226,7 +219,7 @@ "source": [ "if selectedService == Service.HuggingFace:\n", " prompt = \"The purpose of a rubber duck is\"\n", - " \n", + "\n", " results = await hf_text_service.complete(prompt=prompt, prompt_execution_settings=hf_prompt_execution_settings)\n", " print(\"\".join(results))" ] diff --git a/python/samples/getting_started/11-streaming-completions.ipynb b/python/samples/getting_started/11-streaming-completions.ipynb index 2855af3440..c74018b2f3 100644 --- a/python/samples/getting_started/11-streaming-completions.ipynb +++ b/python/samples/getting_started/11-streaming-completions.ipynb @@ -77,28 +77,27 @@ "outputs": [], "source": [ "from semantic_kernel import Kernel\n", - "from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env, openai_settings_from_dot_env\n", "\n", "kernel = Kernel()\n", "\n", "# Configure Azure LLM service\n", "if selectedService == Service.AzureOpenAI:\n", - " deployment, api_key, endpoint = azure_openai_settings_from_dot_env()\n", " azure_text_service = AzureTextCompletion(\n", - " service_id=\"aoai_text\", deployment_name=\"gpt-35-turbo-instruct\", endpoint=endpoint, api_key=api_key\n", - " ) # set the deployment name to the value of your text model (e.g. gpt-35-turbo-instruct)\n", + " service_id=\"aoai_text\",\n", + " ) # set the environment variable AZURE_OPENAI_TEXT_DEPLOYMENT_NAME to the value of your text model (e.g. gpt-35-turbo-instruct)\n", " azure_chat_service = AzureChatCompletion(\n", - " service_id=\"aoai_chat\", deployment_name=\"gpt-35-turbo\", endpoint=endpoint, api_key=api_key\n", - " ) # set the deployment name to the value of your chat model\n", + " service_id=\"aoai_chat\",\n", + " ) # set the environment variable AZURE_OPENAI_CHAT_DEPLOYMENT_NAME to the value of your chat model\n", "\n", "# Configure OpenAI service\n", "if selectedService == Service.OpenAI:\n", - " api_key, org_id = openai_settings_from_dot_env()\n", " oai_text_service = OpenAITextCompletion(\n", - " service_id=\"oai_text\", ai_model_id=\"gpt-3.5-turbo-instruct\", api_key=api_key, org_id=org_id\n", + " service_id=\"oai_text\",\n", + " ai_model_id=\"gpt-3.5-turbo-instruct\",\n", " )\n", " oai_chat_service = OpenAIChatCompletion(\n", - " service_id=\"oai_chat\", ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id\n", + " service_id=\"oai_chat\",\n", + " ai_model_id=\"gpt-3.5-turbo\",\n", " )\n", "\n", "# Configure Hugging Face service\n", diff --git a/python/samples/getting_started/third_party/weaviate-persistent-memory.ipynb b/python/samples/getting_started/third_party/weaviate-persistent-memory.ipynb index 6d2326aba7..0640236e0d 100644 --- a/python/samples/getting_started/third_party/weaviate-persistent-memory.ipynb +++ b/python/samples/getting_started/third_party/weaviate-persistent-memory.ipynb @@ -1,508 +1,504 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Introduction\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This notebook shows how to replace the `VolatileMemoryStore` memory storage used in a [previous notebook](./06-memory-and-embeddings.ipynb) with a `WeaviateMemoryStore`.\n", - "\n", - "`WeaviateMemoryStore` is an example of a persistent (i.e. long-term) memory store backed by the Weaviate vector database.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# About Weaviate\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Weaviate](https://weaviate.io/) is an open-source vector database designed to scale seamlessly into billions of data objects. This implementation supports hybrid search out-of-the-box (meaning it will perform better for keyword searches).\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can run Weaviate in 5 ways:\n", - "\n", - "- **SaaS** – with [Weaviate Cloud Services (WCS)](https://weaviate.io/pricing).\n", - "\n", - " WCS is a fully managed service that takes care of hosting, scaling, and updating your Weaviate instance. You can try it out for free with a sandbox that lasts for 14 days.\n", - "\n", - " To set up a SaaS Weaviate instance with WCS:\n", - "\n", - " 1. Navigate to [Weaviate Cloud Console](https://console.weaviate.cloud/).\n", - " 2. Register or sign in to your WCS account.\n", - " 3. Create a new cluster with the following settings:\n", - " - `Subscription Tier` – Free sandbox for a free trial, or contact [hello@weaviate.io](mailto:hello@weaviate.io) for other options.\n", - " - `Cluster name` – a unique name for your cluster. The name will become part of the URL used to access this instance.\n", - " - `Enable Authentication?` – Enabled by default. This will generate a static API key that you can use to authenticate.\n", - " 4. Wait for a few minutes until your cluster is ready. You will see a green tick ✔️ when it's done. Copy your cluster URL.\n", - "\n", - "- **Hybrid SaaS**\n", - "\n", - " > If you need to keep your data on-premise for security or compliance reasons, Weaviate also offers a Hybrid SaaS option: Weaviate runs within your cloud instances, but the cluster is managed remotely by Weaviate. This gives you the benefits of a managed service without sending data to an external party.\n", - "\n", - " The Weaviate Hybrid SaaS is a custom solution. If you are interested in this option, please reach out to [hello@weaviate.io](mailto:hello@weaviate.io).\n", - "\n", - "- **Self-hosted** – with a Docker container\n", - "\n", - " To set up a Weaviate instance with Docker:\n", - "\n", - " 1. [Install Docker](https://docs.docker.com/engine/install/) on your local machine if it is not already installed.\n", - " 2. [Install the Docker Compose Plugin](https://docs.docker.com/compose/install/)\n", - " 3. Download a `docker-compose.yml` file with this `curl` command:\n", - "\n", - " ```\n", - " curl -o docker-compose.yml \"https://configuration.weaviate.io/v2/docker-compose/docker-compose.yml?modules=standalone&runtime=docker-compose&weaviate_version=v1.19.6\"\n", - " ```\n", - "\n", - " Alternatively, you can use Weaviate's docker compose [configuration tool](https://weaviate.io/developers/weaviate/installation/docker-compose) to generate your own `docker-compose.yml` file.\n", - "\n", - " 4. Run `docker compose up -d` to spin up a Weaviate instance.\n", - "\n", - " > To shut it down, run `docker compose down`.\n", - "\n", - "- **Self-hosted** – with a Kubernetes cluster\n", - "\n", - " To configure a self-hosted instance with Kubernetes, follow Weaviate's [documentation](https://weaviate.io/developers/weaviate/installation/kubernetes).|\n", - "\n", - "- **Embedded** - start a weaviate instance right from your application code using the client library\n", - "\n", - " This code snippet shows how to instantiate an embedded weaviate instance and upload a document:\n", - "\n", - " ```python\n", - " import weaviate\n", - " from weaviate.embedded import EmbeddedOptions\n", - "\n", - " client = weaviate.Client(\n", - " embedded_options=EmbeddedOptions()\n", - " )\n", - "\n", - " data_obj = {\n", - " \"name\": \"Chardonnay\",\n", - " \"description\": \"Goes with fish\"\n", - " }\n", - "\n", - " client.data_object.create(data_obj, \"Wine\")\n", - " ```\n", - "\n", - " Refer to the [documentation](https://weaviate.io/developers/weaviate/installation/embedded) for more details about this deployment method.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Setup\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install semantic-kernel==0.9.8b1\n", - "!pip install weaviate-client\n", - "!pip install python-dotenv" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## OS-specific notes:\n", - "\n", - "- if you run into SSL errors when connecting to OpenAI on macOS, see this issue for a [potential solution](https://github.com/microsoft/semantic-kernel/issues/627#issuecomment-1580912248)\n", - "- on Windows, you may need to run Docker Desktop as administrator\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "First, we instantiate the Weaviate memory store. Uncomment ONE of the options below, depending on how you want to use Weaviate:\n", - "\n", - "- from a Docker instance\n", - "- from WCS\n", - "- directly from the client (embedded Weaviate), which works on Linux only at the moment\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from dotenv import load_dotenv\n", - "\n", - "from semantic_kernel.connectors.memory.weaviate import weaviate_memory_store\n", - "\n", - "load_dotenv(override=True)\n", - "\n", - "# Using Docker\n", - "config = weaviate_memory_store.WeaviateConfig(url=\"http://localhost:8080\")\n", - "\n", - "# Using WCS. Make sure the environment variables `WEAVIATE_URL` and `WEAVIATE_API_KEY`\n", - "# were set in the `.env` file.\n", - "#\n", - "# weaviate_api, weaviate_url = sk.weaviate_settings_from_dot_env()\n", - "#\n", - "# config = weaviate_memory_store.WeaviateConfig(\n", - "# url=weaviate_url,\n", - "# api_key=weaviate_api\n", - "# )\n", - "\n", - "# Using Embedded Weaviate\n", - "# config = weaviate_memory_store.WeaviateConfig(use_embed=True)\n", - "\n", - "store = weaviate_memory_store.WeaviateMemoryStore(config=config)\n", - "store.client.schema.delete_all()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Then, we register the memory store to the kernel:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from services import Service\n", - "\n", - "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", - "selectedService = Service.OpenAI" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kernel = sk.Kernel()\n", - "\n", - "chat_service_id = \"chat\"\n", - "if selectedService == Service.OpenAI:\n", - " api_key, org_id = sk.openai_settings_from_dot_env()\n", - " oai_chat_service = OpenAIChatCompletion(\n", - " service_id=chat_service_id, ai_model_id=\"gpt-3.5-turbo\", api_key=api_key, org_id=org_id\n", - " )\n", - " embedding_gen = OpenAITextEmbedding(ai_model_id=\"text-embedding-ada-002\", api_key=api_key, org_id=org_id)\n", - " kernel.add_service(oai_chat_service)\n", - " kernel.add_service(embedding_gen)\n", - "\n", - "memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embedding_gen)\n", - "kernel.add_plugin(TextMemoryPlugin(memory), \"TextMemoryPlugin\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Manually adding memories\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's create some initial memories \"About Me\". We can add memories to our weaviate memory store by using `save_information`\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "collection_id = \"generic\"\n", - "\n", - "\n", - "async def populate_memory(memory: SemanticTextMemory) -> None:\n", - " # Add some documents to the semantic memory\n", - " await memory.save_information(collection=collection_id, id=\"info1\", text=\"Your budget for 2024 is $100,000\")\n", - " await memory.save_information(collection=collection_id, id=\"info2\", text=\"Your savings from 2023 are $50,000\")\n", - " await memory.save_information(collection=collection_id, id=\"info3\", text=\"Your investments are $80,000\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await populate_memory(memory)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Searching is done through `search`:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "async def search_memory_examples(memory: SemanticTextMemory) -> None:\n", - " questions = [\"What is my budget for 2024?\", \"What are my savings from 2023?\", \"What are my investments?\"]\n", - "\n", - " for question in questions:\n", - " print(f\"Question: {question}\")\n", - " result = await memory.search(collection_id, question)\n", - " print(f\"Answer: {result[0].text}\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await search_memory_examples(memory)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here's how to use the weaviate memory store in a chat application:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "async def setup_chat_with_memory(\n", - " kernel: sk.Kernel,\n", - " service_id: str,\n", - ") -> sk.KernelFunction:\n", - " prompt = \"\"\"\n", - " ChatBot can have a conversation with you about any topic.\n", - " It can give explicit instructions or say 'I don't know' if\n", - " it does not have an answer.\n", - "\n", - " Information about me, from previous conversations:\n", - " - {{recall 'budget by year'}} What is my budget for 2024?\n", - " - {{recall 'savings from previous year'}} What are my savings from 2023?\n", - " - {{recall 'investments'}} What are my investments?\n", - "\n", - " {{$request}}\n", - " \"\"\".strip()\n", - "\n", - " prompt_template_config = PromptTemplateConfig(\n", - " template=prompt,\n", - " execution_settings={\n", - " service_id: kernel.get_service(service_id).get_prompt_execution_settings_class()(service_id=service_id)\n", - " },\n", - " )\n", - "\n", - " chat_func = kernel.add_function(\n", - " function_name=\"chat_with_memory\",\n", - " plugin_name=\"TextMemoryPlugin\",\n", - " prompt_template_config=prompt_template_config,\n", - " )\n", - "\n", - " return chat_func" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunction) -> bool:\n", - " try:\n", - " user_input = input(\"User:> \")\n", - " except KeyboardInterrupt:\n", - " print(\"\\n\\nExiting chat...\")\n", - " return False\n", - " except EOFError:\n", - " print(\"\\n\\nExiting chat...\")\n", - " return False\n", - "\n", - " if user_input == \"exit\":\n", - " print(\"\\n\\nExiting chat...\")\n", - " return False\n", - "\n", - " answer = await kernel.invoke(chat_func, request=user_input)\n", - "\n", - " print(f\"ChatBot:> {answer}\")\n", - " return True" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Populating memory...\")\n", - "await populate_memory(memory)\n", - "\n", - "print(\"Asking questions... (manually)\")\n", - "await search_memory_examples(memory)\n", - "\n", - "print(\"Setting up a chat (with memory!)\")\n", - "chat_func = await setup_chat_with_memory(kernel, chat_service_id)\n", - "\n", - "print(\"Begin chatting (type 'exit' to exit):\\n\")\n", - "print(\n", - " \"Welcome to the chat bot!\\\n", - " \\n Type 'exit' to exit.\\\n", - " \\n Try asking a question about your finances (i.e. \\\"talk to me about my finances\\\").\"\n", - ")\n", - "chatting = True\n", - "while chatting:\n", - " chatting = await chat(kernel, chat_func)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Adding documents to your memory\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create a dictionary to hold some files. The key is the hyperlink to the file and the value is the file's content:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "github_files = {}\n", - "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/README.md\"] = (\n", - " \"README: Installation, getting started, and how to contribute\"\n", - ")\n", - "github_files[\n", - " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/02-running-prompts-from-file.ipynb\"\n", - "] = \"Jupyter notebook describing how to pass prompts from a file to a semantic plugin or function\"\n", - "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/00-getting-started.ipynb\"] = (\n", - " \"Jupyter notebook describing how to get started with the Semantic Kernel\"\n", - ")\n", - "github_files[\"https://github.com/microsoft/semantic-kernel/tree/main/samples/plugins/ChatPlugin/ChatGPT\"] = (\n", - " \"Sample demonstrating how to create a chat plugin interfacing with ChatGPT\"\n", - ")\n", - "github_files[\n", - " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel/Memory/Volatile/VolatileMemoryStore.cs\"\n", - "] = \"C# class that defines a volatile embedding store\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Use `save_reference` to save the file:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "COLLECTION = \"SKGitHub\"\n", - "\n", - "print(\"Adding some GitHub file URLs and their descriptions to a volatile Semantic Memory.\")\n", - "i = 0\n", - "for entry, value in github_files.items():\n", - " await memory.save_reference(\n", - " collection=COLLECTION,\n", - " description=value,\n", - " text=value,\n", - " external_id=entry,\n", - " external_source_name=\"GitHub\",\n", - " )\n", - " i += 1\n", - " print(\" URL {} saved\".format(i))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Use `search` to ask a question:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ask = \"I love Jupyter notebooks, how should I get started?\"\n", - "print(\"===========================\\n\" + \"Query: \" + ask + \"\\n\")\n", - "\n", - "memories = await memory.search(COLLECTION, ask, limit=5, min_relevance_score=0.77)\n", - "\n", - "i = 0\n", - "for memory in memories:\n", - " i += 1\n", - " print(f\"Result {i}:\")\n", - " print(\" URL: : \" + memory.id)\n", - " print(\" Title : \" + memory.description)\n", - " print(\" Relevance: \" + str(memory.relevance))\n", - " print()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.13" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Introduction\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook shows how to replace the `VolatileMemoryStore` memory storage used in a [previous notebook](./06-memory-and-embeddings.ipynb) with a `WeaviateMemoryStore`.\n", + "\n", + "`WeaviateMemoryStore` is an example of a persistent (i.e. long-term) memory store backed by the Weaviate vector database.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# About Weaviate\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Weaviate](https://weaviate.io/) is an open-source vector database designed to scale seamlessly into billions of data objects. This implementation supports hybrid search out-of-the-box (meaning it will perform better for keyword searches).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can run Weaviate in 5 ways:\n", + "\n", + "- **SaaS** – with [Weaviate Cloud Services (WCS)](https://weaviate.io/pricing).\n", + "\n", + " WCS is a fully managed service that takes care of hosting, scaling, and updating your Weaviate instance. You can try it out for free with a sandbox that lasts for 14 days.\n", + "\n", + " To set up a SaaS Weaviate instance with WCS:\n", + "\n", + " 1. Navigate to [Weaviate Cloud Console](https://console.weaviate.cloud/).\n", + " 2. Register or sign in to your WCS account.\n", + " 3. Create a new cluster with the following settings:\n", + " - `Subscription Tier` – Free sandbox for a free trial, or contact [hello@weaviate.io](mailto:hello@weaviate.io) for other options.\n", + " - `Cluster name` – a unique name for your cluster. The name will become part of the URL used to access this instance.\n", + " - `Enable Authentication?` – Enabled by default. This will generate a static API key that you can use to authenticate.\n", + " 4. Wait for a few minutes until your cluster is ready. You will see a green tick ✔️ when it's done. Copy your cluster URL.\n", + "\n", + "- **Hybrid SaaS**\n", + "\n", + " > If you need to keep your data on-premise for security or compliance reasons, Weaviate also offers a Hybrid SaaS option: Weaviate runs within your cloud instances, but the cluster is managed remotely by Weaviate. This gives you the benefits of a managed service without sending data to an external party.\n", + "\n", + " The Weaviate Hybrid SaaS is a custom solution. If you are interested in this option, please reach out to [hello@weaviate.io](mailto:hello@weaviate.io).\n", + "\n", + "- **Self-hosted** – with a Docker container\n", + "\n", + " To set up a Weaviate instance with Docker:\n", + "\n", + " 1. [Install Docker](https://docs.docker.com/engine/install/) on your local machine if it is not already installed.\n", + " 2. [Install the Docker Compose Plugin](https://docs.docker.com/compose/install/)\n", + " 3. Download a `docker-compose.yml` file with this `curl` command:\n", + "\n", + " ```\n", + " curl -o docker-compose.yml \"https://configuration.weaviate.io/v2/docker-compose/docker-compose.yml?modules=standalone&runtime=docker-compose&weaviate_version=v1.19.6\"\n", + " ```\n", + "\n", + " Alternatively, you can use Weaviate's docker compose [configuration tool](https://weaviate.io/developers/weaviate/installation/docker-compose) to generate your own `docker-compose.yml` file.\n", + "\n", + " 4. Run `docker compose up -d` to spin up a Weaviate instance.\n", + "\n", + " > To shut it down, run `docker compose down`.\n", + "\n", + "- **Self-hosted** – with a Kubernetes cluster\n", + "\n", + " To configure a self-hosted instance with Kubernetes, follow Weaviate's [documentation](https://weaviate.io/developers/weaviate/installation/kubernetes).|\n", + "\n", + "- **Embedded** - start a weaviate instance right from your application code using the client library\n", + "\n", + " This code snippet shows how to instantiate an embedded weaviate instance and upload a document:\n", + "\n", + " ```python\n", + " import weaviate\n", + " from weaviate.embedded import EmbeddedOptions\n", + "\n", + " client = weaviate.Client(\n", + " embedded_options=EmbeddedOptions()\n", + " )\n", + "\n", + " data_obj = {\n", + " \"name\": \"Chardonnay\",\n", + " \"description\": \"Goes with fish\"\n", + " }\n", + "\n", + " client.data_object.create(data_obj, \"Wine\")\n", + " ```\n", + "\n", + " Refer to the [documentation](https://weaviate.io/developers/weaviate/installation/embedded) for more details about this deployment method.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Setup\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install semantic-kernel==0.9.8b1\n", + "!pip install weaviate-client\n", + "!pip install python-dotenv" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## OS-specific notes:\n", + "\n", + "- if you run into SSL errors when connecting to OpenAI on macOS, see this issue for a [potential solution](https://github.com/microsoft/semantic-kernel/issues/627#issuecomment-1580912248)\n", + "- on Windows, you may need to run Docker Desktop as administrator\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, we instantiate the Weaviate memory store. Uncomment ONE of the options below, depending on how you want to use Weaviate:\n", + "\n", + "- from a Docker instance\n", + "- from WCS\n", + "- directly from the client (embedded Weaviate), which works on Linux only at the moment\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.connectors.memory.weaviate import weaviate_memory_store\n", + "\n", + "# Note the Weaviate Config values need to be either configured as environment variables\n", + "# or in the .env file, as a back up. When creating the instance of the `weaviate_memory_store`\n", + "# pass in `env_file_path=` to read the config values from the `.env` file, otherwise\n", + "# the values will be read from environment variables.\n", + "# Env variables or .env file config should look like:\n", + "# WEAVIATE_URL=\"http://localhost:8080\"\n", + "# WEAVIATE_API_KEY=\"\"\n", + "# WEAVIATE_USE_EMBED=True|False\n", + "\n", + "store = weaviate_memory_store.WeaviateMemoryStore()\n", + "store.client.schema.delete_all()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, we register the memory store to the kernel:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from services import Service\n", + "\n", + "# Select a service to use for this notebook (available services: OpenAI, AzureOpenAI, HuggingFace)\n", + "selectedService = Service.OpenAI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.kernel import Kernel\n", + "from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAITextEmbedding\n", + "from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory\n", + "from semantic_kernel.memory.volatile_memory_store import VolatileMemoryStore\n", + "from semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin\n", + "\n", + "kernel = Kernel()\n", + "\n", + "chat_service_id = \"chat\"\n", + "if 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\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Manually adding memories\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's create some initial memories \"About Me\". We can add memories to our weaviate memory store by using `save_information`\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "collection_id = \"generic\"\n", + "\n", + "\n", + "async def populate_memory(memory: SemanticTextMemory) -> None:\n", + " # Add some documents to the semantic memory\n", + " await memory.save_information(collection=collection_id, id=\"info1\", text=\"Your budget for 2024 is $100,000\")\n", + " await memory.save_information(collection=collection_id, id=\"info2\", text=\"Your savings from 2023 are $50,000\")\n", + " await memory.save_information(collection=collection_id, id=\"info3\", text=\"Your investments are $80,000\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "await populate_memory(memory)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Searching is done through `search`:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def search_memory_examples(memory: SemanticTextMemory) -> None:\n", + " questions = [\"What is my budget for 2024?\", \"What are my savings from 2023?\", \"What are my investments?\"]\n", + "\n", + " for question in questions:\n", + " print(f\"Question: {question}\")\n", + " result = await memory.search(collection_id, question)\n", + " print(f\"Answer: {result[0].text}\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "await search_memory_examples(memory)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here's how to use the weaviate memory store in a chat application:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from semantic_kernel.functions.kernel_function import KernelFunction\n", + "from semantic_kernel.prompt_template.prompt_template_config 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=\"TextMemoryPlugin\",\n", + " prompt_template_config=prompt_template_config,\n", + " )\n", + "\n", + " return chat_func" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def chat(kernel: Kernel, chat_func: KernelFunction) -> bool:\n", + " try:\n", + " user_input = input(\"User:> \")\n", + " except KeyboardInterrupt:\n", + " print(\"\\n\\nExiting chat...\")\n", + " return False\n", + " except EOFError:\n", + " print(\"\\n\\nExiting chat...\")\n", + " return False\n", + "\n", + " if user_input == \"exit\":\n", + " print(\"\\n\\nExiting chat...\")\n", + " return False\n", + "\n", + " answer = await kernel.invoke(chat_func, request=user_input)\n", + "\n", + " print(f\"ChatBot:> {answer}\")\n", + " return True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Populating memory...\")\n", + "await populate_memory(memory)\n", + "\n", + "print(\"Asking questions... (manually)\")\n", + "await search_memory_examples(memory)\n", + "\n", + "print(\"Setting up a chat (with memory!)\")\n", + "chat_func = await setup_chat_with_memory(kernel, chat_service_id)\n", + "\n", + "print(\"Begin chatting (type 'exit' to exit):\\n\")\n", + "print(\n", + " \"Welcome to the chat bot!\\\n", + " \\n Type 'exit' to exit.\\\n", + " \\n Try asking a question about your finances (i.e. \\\"talk to me about my finances\\\").\"\n", + ")\n", + "chatting = True\n", + "while chatting:\n", + " chatting = await chat(kernel, chat_func)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Adding documents to your memory\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a dictionary to hold some files. The key is the hyperlink to the file and the value is the file's content:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "github_files = {}\n", + "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/README.md\"] = (\n", + " \"README: Installation, getting started, and how to contribute\"\n", + ")\n", + "github_files[\n", + " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/02-running-prompts-from-file.ipynb\"\n", + "] = \"Jupyter notebook describing how to pass prompts from a file to a semantic plugin or function\"\n", + "github_files[\"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/notebooks/00-getting-started.ipynb\"] = (\n", + " \"Jupyter notebook describing how to get started with the Semantic Kernel\"\n", + ")\n", + "github_files[\"https://github.com/microsoft/semantic-kernel/tree/main/samples/plugins/ChatPlugin/ChatGPT\"] = (\n", + " \"Sample demonstrating how to create a chat plugin interfacing with ChatGPT\"\n", + ")\n", + "github_files[\n", + " \"https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel/Memory/Volatile/VolatileMemoryStore.cs\"\n", + "] = \"C# class that defines a volatile embedding store\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Use `save_reference` to save the file:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "COLLECTION = \"SKGitHub\"\n", + "\n", + "print(\"Adding some GitHub file URLs and their descriptions to a volatile Semantic Memory.\")\n", + "i = 0\n", + "for entry, value in github_files.items():\n", + " await memory.save_reference(\n", + " collection=COLLECTION,\n", + " description=value,\n", + " text=value,\n", + " external_id=entry,\n", + " external_source_name=\"GitHub\",\n", + " )\n", + " i += 1\n", + " print(\" URL {} saved\".format(i))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Use `search` to ask a question:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ask = \"I love Jupyter notebooks, how should I get started?\"\n", + "print(\"===========================\\n\" + \"Query: \" + ask + \"\\n\")\n", + "\n", + "memories = await memory.search(COLLECTION, ask, limit=5, min_relevance_score=0.77)\n", + "\n", + "i = 0\n", + "for memory in memories:\n", + " i += 1\n", + " print(f\"Result {i}:\")\n", + " print(\" URL: : \" + memory.id)\n", + " print(\" Title : \" + memory.description)\n", + " print(\" Relevance: \" + str(memory.relevance))\n", + " print()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/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 97522e9a63..f6c381dbec 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 @@ -1,23 +1,18 @@ # Copyright (c) Microsoft. All rights reserved. import logging -import sys -from typing import Any, List, Optional, Tuple - -if sys.version_info >= (3, 9): - from typing import Annotated -else: - from typing_extensions import Annotated +from typing import Annotated, Any, List, Tuple import google.generativeai as palm from google.generativeai.types import ChatResponse, MessageDict -from pydantic import PrivateAttr, StringConstraints +from pydantic import PrivateAttr, StringConstraints, ValidationError from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase from semantic_kernel.connectors.ai.google_palm.gp_prompt_execution_settings import ( GooglePalmChatPromptExecutionSettings, GooglePalmPromptExecutionSettings, ) +from semantic_kernel.connectors.ai.google_palm.settings.google_palm_settings import GooglePalmSettings 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.author_role import AuthorRole @@ -33,14 +28,15 @@ int_to_role = {1: AuthorRole.USER, 2: AuthorRole.SYSTEM, 3: AuthorRole.ASSISTANT class GooglePalmChatCompletion(ChatCompletionClientBase, TextCompletionClientBase): api_key: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] - _message_history: Optional[ChatHistory] = PrivateAttr() - service_id: Optional[str] = None + _message_history: ChatHistory | None = PrivateAttr() + service_id: str | None = None def __init__( self, ai_model_id: str, - api_key: str, - message_history: Optional[ChatHistory] = None, + api_key: str | None = None, + message_history: ChatHistory | None = None, + env_file_path: str | None = None, ): """ Initializes a new instance of the GooglePalmChatCompletion class. @@ -48,10 +44,27 @@ class GooglePalmChatCompletion(ChatCompletionClientBase, TextCompletionClientBas Arguments: ai_model_id {str} -- GooglePalm model name, see https://developers.generativeai.google/models/language - api_key {str} -- GooglePalm API key, see - https://developers.generativeai.google/products/palm - message_history {Optional[ChatHistory]} -- The message history to use for context. (Optional) + api_key {str | None} -- The optional API key to use. If not provided, will be read from either + the env vars or the .env settings file + 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) """ + 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 + ) + super().__init__( ai_model_id=ai_model_id, api_key=api_key, 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 70e4b219ae..ff36bd8231 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 @@ -1,20 +1,15 @@ # Copyright (c) Microsoft. All rights reserved. import logging -import sys -from typing import List - -if sys.version_info >= (3, 9): - from typing import Annotated -else: - from typing_extensions import Annotated +from typing import Annotated, List import google.generativeai as palm from google.generativeai.types import Completion from google.generativeai.types.text_types import TextCompletion -from pydantic import StringConstraints +from pydantic import StringConstraints, ValidationError from semantic_kernel.connectors.ai.google_palm.gp_prompt_execution_settings import GooglePalmTextPromptExecutionSettings +from semantic_kernel.connectors.ai.google_palm.settings.google_palm_settings import GooglePalmSettings 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 @@ -26,16 +21,32 @@ logger: logging.Logger = logging.getLogger(__name__) class GooglePalmTextCompletion(TextCompletionClientBase): api_key: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] - def __init__(self, ai_model_id: str, api_key: str): + def __init__(self, ai_model_id: str, api_key: str | None = None, env_file_path: str | None = None): """ Initializes a new instance of the GooglePalmTextCompletion class. Arguments: ai_model_id {str} -- GooglePalm model name, see https://developers.generativeai.google/models/language - api_key {str} -- GooglePalm API key, see - https://developers.generativeai.google/products/palm + api_key {str | None} -- The optional API key to use. If not provided, will be + 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) """ + 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 + ) + super().__init__(ai_model_id=ai_model_id, api_key=api_key) async def complete(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 c50f58fd14..a4e08efc90 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 @@ -1,35 +1,49 @@ # Copyright (c) Microsoft. All rights reserved. - -import sys -from typing import Any, List - -if sys.version_info >= (3, 9): - from typing import Annotated -else: - from typing_extensions import Annotated +import logging +from typing import Annotated, Any, List import google.generativeai as palm from numpy import array, ndarray -from pydantic import StringConstraints +from pydantic import StringConstraints, ValidationError 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 +logger: logging.Logger = logging.getLogger(__name__) + 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: + def __init__(self, ai_model_id: str, api_key: str | None = None, env_file_path: str | None = None) -> None: """ Initializes a new instance of the GooglePalmTextEmbedding class. Arguments: ai_model_id {str} -- GooglePalm model name, see - https://developers.generativeai.google/models/language - api_key {str} -- GooglePalm API key, see - https://developers.generativeai.google/products/palm + https://developers.generativeai.google/models/language + api_key {str | None} -- The optional API key to use. If not provided, will be + 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) """ + 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}") + + 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=ai_model_id, api_key=api_key) async def generate_embeddings(self, texts: List[str], **kwargs: Any) -> ndarray: 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 new file mode 100644 index 0000000000..db0cdb2d64 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/google_palm/settings/google_palm_settings.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft. All rights reserved. + +from pydantic import SecretStr +from pydantic_settings import BaseSettings + + +class GooglePalmSettings(BaseSettings): + """Google Palm model settings + + The settings are first loaded from environment variables with the prefix 'GOOGLE_PALM_'. If the + environment variables are not found, the settings can be loaded from a .env file with the + encoding 'utf-8'. If the settings are not found in the .env file, the settings are ignored; + however, validation will fail alerting that the settings are missing. + + Optional settings for prefix 'GOOGLE_PALM_' are: + - api_key: SecretStr - GooglePalm API key, see https://developers.generativeai.google/products/palm + (Env var GOOGLE_PALM_API_KEY) + - env_file_path: {str | None} - Use the environment settings file as a fallback to environment variables. (Optional) + - chat_model_id: str | None - The GooglePalm chat model ID to use. + (Env var GOOGLE_PALM_CHAT_MODEL_ID) + - text_model_id: str | None - The GooglePalm text model ID to use. + (Env var GOOGLE_PALM_TEXT_MODEL_ID) + - embedding_model_id: str | None - The GooglePalm embedding model ID to use. + (Env var GOOGLE_PALM_EMBEDDING_MODEL_ID) + """ + + env_file_path: 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: + env_prefix = "GOOGLE_PALM_" + env_file = None + env_file_encoding = "utf-8" + extra = "ignore" + case_sensitive = False + + @classmethod + def create(cls, **kwargs): + 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/const.py b/python/semantic_kernel/connectors/ai/open_ai/const.py index 1d9ce6ad89..e8e89f0cc6 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/const.py +++ b/python/semantic_kernel/connectors/ai/open_ai/const.py @@ -2,6 +2,6 @@ from typing import Final -DEFAULT_AZURE_API_VERSION: Final[str] = "2023-05-15" +DEFAULT_AZURE_API_VERSION: Final[str] = "2024-02-01" USER_AGENT: Final[str] = "User-Agent" DEFAULT_CHAT_SYSTEM_PROMPT: Final[str] = "Assistant is a large language model." 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 c6db13ebcc..3ff528d57b 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 @@ -2,7 +2,7 @@ import json import logging from copy import deepcopy -from typing import Any, Dict, Mapping, Optional, Union, overload +from typing import Any, Dict, Mapping, Optional, Union from uuid import uuid4 from openai import AsyncAzureOpenAI @@ -10,6 +10,7 @@ from openai.lib.azure import AsyncAzureADTokenProvider from openai.types.chat.chat_completion import ChatCompletion, Choice from openai.types.chat.chat_completion_chunk import ChatCompletionChunk 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 ( @@ -19,6 +20,7 @@ from semantic_kernel.connectors.ai.open_ai.services.azure_config_base import Azu from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base import OpenAIChatCompletionBase 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.azure_open_ai_settings import AzureOpenAISettings from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.finish_reason import FinishReason @@ -26,6 +28,7 @@ from semantic_kernel.contents.function_call_content import FunctionCallContent from semantic_kernel.contents.function_result_content import FunctionResultContent from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent from semantic_kernel.contents.text_content import TextContent +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError from semantic_kernel.kernel_pydantic import HttpsUrl logger: logging.Logger = logging.getLogger(__name__) @@ -34,175 +37,70 @@ logger: logging.Logger = logging.getLogger(__name__) class AzureChatCompletion(AzureOpenAIConfigBase, OpenAIChatCompletionBase, OpenAITextCompletionBase): """Azure Chat completion class.""" - @overload def __init__( self, - deployment_name: str, - base_url: Union[HttpsUrl, str], - service_id: Optional[str] = None, - api_version: str = DEFAULT_AZURE_API_VERSION, - api_key: Optional[str] = None, - ad_token: Optional[str] = None, - ad_token_provider: Optional[AsyncAzureADTokenProvider] = None, - default_headers: Optional[Mapping[str, str]] = None, + service_id: str | None = None, + api_key: str | None = None, + deployment_name: str | None = None, + endpoint: str | None = None, + base_url: str | None = None, + api_version: str | None = None, + ad_token: str | None = None, + ad_token_provider: AsyncAzureADTokenProvider | None = None, + default_headers: Mapping[str, str] | None = None, + async_client: AsyncAzureOpenAI | None = None, + env_file_path: str | None = None, ) -> None: """ Initialize an AzureChatCompletion service. Arguments: - deployment_name: The name of the Azure deployment. This value - will correspond to the custom name you chose for your deployment - when you deployed a model. This value can be found under - Resource Management > Deployments in the Azure portal or, alternatively, - under Management > Deployments in Azure OpenAI Studio. - base_url: The url of the Azure deployment. This value - can be found in the Keys & Endpoint section when examining - your resource from the Azure portal, the base_url consists of the endpoint, - followed by /openai/deployments/{deployment_name}/, - use endpoint if you only want to supply the endpoint. - api_key: The API key for the Azure deployment. This value can be - found in the Keys & Endpoint section when examining your resource in - the Azure portal. You can use either KEY1 or KEY2. - api_version: The API version to use. (Optional) - The default value is "2023-05-15". - ad_auth: Whether to use Azure Active Directory authentication. (Optional) - The default value is False. - default_headers: The default headers mapping of string keys to + service_id {str | None}: The service ID for the Azure deployment. (Optional) + api_key {str | None}: The optional api key. If provided, will override the value in the + env vars or .env file. + deployment_name {str | None}: The optional deployment. If provided, will override the value + (chat_deployment_name) in the env vars or .env file. + endpoint {str | None}: The optional deployment endpoint. If provided will override the value + in the env vars or .env file. + base_url {str | None}: The optional deployment base_url. If provided will override the value + in the env vars or .env file. + api_version {str | None}: The optional deployment api version. If provided will override the value + in the env vars or .env file. + ad_token {str | None}: The Azure Active Directory token. (Optional) + ad_token_provider {AsyncAzureADTokenProvider}: The Azure Active Directory token provider. (Optional) + default_headers {Mapping[str, str]}: The default headers mapping of string keys to 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. """ + 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}") - @overload - def __init__( - self, - deployment_name: str, - endpoint: Union[HttpsUrl, str], - api_version: str = DEFAULT_AZURE_API_VERSION, - service_id: Optional[str] = None, - api_key: Optional[str] = None, - ad_token: Optional[str] = None, - ad_token_provider: Optional[AsyncAzureADTokenProvider] = None, - default_headers: Optional[Mapping[str, str]] = None, - ) -> None: - """ - Initialize an AzureChatCompletion service. + 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 + ) - Arguments: - deployment_name: The name of the Azure deployment. This value - will correspond to the custom name you chose for your deployment - when you deployed a model. This value can be found under - Resource Management > Deployments in the Azure portal or, alternatively, - under Management > Deployments in Azure OpenAI Studio. - endpoint: The endpoint of the Azure deployment. This value - can be found in the Keys & Endpoint section when examining - your resource from the Azure portal, the endpoint should end in openai.azure.com. - api_key: The API key for the Azure deployment. This value can be - found in the Keys & Endpoint section when examining your resource in - the Azure portal. You can use either KEY1 or KEY2. - api_version: The API version to use. (Optional) - The default value is "2023-05-15". - ad_auth: Whether to use Azure Active Directory authentication. (Optional) - The default value is False. - default_headers: The default headers mapping of string keys to - string values for HTTP requests. (Optional) - """ + if api_version is None: + api_version = DEFAULT_AZURE_API_VERSION - @overload - def __init__( - self, - deployment_name: str, - async_client: AsyncAzureOpenAI, - service_id: Optional[str] = None, - ) -> None: - """ - Initialize an AzureChatCompletion service. + if not base_url and not endpoint: + raise ServiceInitializationError("At least one of base_url or endpoint must be provided.") - Arguments: - deployment_name: The name of the Azure deployment. This value - will correspond to the custom name you chose for your deployment - when you deployed a model. This value can be found under - Resource Management > Deployments in the Azure portal or, alternatively, - under Management > Deployments in Azure OpenAI Studio. - async_client {AsyncAzureOpenAI} -- An existing client to use. - """ - - @overload - def __init__( - self, - deployment_name: str, - endpoint: Union[HttpsUrl, str], - api_version: str = DEFAULT_AZURE_API_VERSION, - service_id: Optional[str] = None, - api_key: Optional[str] = None, - ad_token: Optional[str] = None, - ad_token_provider: Optional[AsyncAzureADTokenProvider] = None, - default_headers: Optional[Mapping[str, str]] = None, - ) -> None: - """ - Initialize an AzureChatCompletion service. - - Arguments: - deployment_name: The name of the Azure deployment. This value - will correspond to the custom name you chose for your deployment - when you deployed a model. This value can be found under - Resource Management > Deployments in the Azure portal or, alternatively, - under Management > Deployments in Azure OpenAI Studio. - endpoint: The endpoint of the Azure deployment. This value - can be found in the Keys & Endpoint section when examining - your resource from the Azure portal, the endpoint should end in openai.azure.com. - api_key: The API key for the Azure deployment. This value can be - found in the Keys & Endpoint section when examining your resource in - the Azure portal. You can use either KEY1 or KEY2. - api_version: The API version to use. (Optional) - The default value is "2023-05-15". - ad_auth: Whether to use Azure Active Directory authentication. (Optional) - The default value is False. - default_headers: The default headers mapping of string keys to - string values for HTTP requests. (Optional) - log: The logger instance to use. (Optional) - """ - - def __init__( - self, - deployment_name: str, - endpoint: Optional[Union[HttpsUrl, str]] = None, - base_url: Optional[Union[HttpsUrl, str]] = None, - api_version: str = DEFAULT_AZURE_API_VERSION, - service_id: Optional[str] = None, - api_key: Optional[str] = None, - ad_token: Optional[str] = None, - ad_token_provider: Optional[AsyncAzureADTokenProvider] = None, - default_headers: Optional[Mapping[str, str]] = None, - async_client: Optional[AsyncAzureOpenAI] = None, - ) -> None: - """ - Initialize an AzureChatCompletion service. - - Arguments: - deployment_name: The name of the Azure deployment. This value - will correspond to the custom name you chose for your deployment - when you deployed a model. This value can be found under - Resource Management > Deployments in the Azure portal or, alternatively, - under Management > Deployments in Azure OpenAI Studio. - base_url: The url of the Azure deployment. This value - can be found in the Keys & Endpoint section when examining - your resource from the Azure portal, the base_url consists of the endpoint, - followed by /openai/deployments/{deployment_name}/, - use endpoint if you only want to supply the endpoint. - endpoint: The endpoint of the Azure deployment. This value - can be found in the Keys & Endpoint section when examining - your resource from the Azure portal, the endpoint should end in openai.azure.com. - If both base_url and endpoint are supplied, base_url will be used. - api_key: The API key for the Azure deployment. This value can be - found in the Keys & Endpoint section when examining your resource in - the Azure portal. You can use either KEY1 or KEY2. - api_version: The API version to use. (Optional) - The default value is "2023-05-15". - ad_auth: Whether to use Azure Active Directory authentication. (Optional) - The default value is False. - default_headers: The default headers mapping of string keys to - string values for HTTP requests. (Optional) - async_client {Optional[AsyncAzureOpenAI]} -- An existing client to use. (Optional) - """ if base_url and isinstance(base_url, str): base_url = HttpsUrl(base_url) if endpoint and deployment_name: @@ -228,19 +126,21 @@ class AzureChatCompletion(AzureOpenAIConfigBase, OpenAIChatCompletionBase, OpenA Arguments: settings: A dictionary of settings for the service. - should contains keys: deployment_name, endpoint, api_key - and optionally: api_version, ad_auth, default_headers + should contains keys: service_id, and optionally: + ad_auth, ad_token_provider, default_headers """ + return AzureChatCompletion( - deployment_name=settings.get("deployment_name"), - endpoint=settings.get("endpoint"), - base_url=settings.get("base_url"), - api_version=settings.get("api_version", DEFAULT_AZURE_API_VERSION), service_id=settings.get("service_id"), - api_key=settings.get("api_key"), + api_key=settings.get("api_key", None), + deployment_name=settings.get("deployment_name", None), + endpoint=settings.get("endpoint", None), + base_url=settings.get("base_url", None), + api_version=settings.get("api_version", None), ad_token=settings.get("ad_token"), ad_token_provider=settings.get("ad_token_provider"), default_headers=settings.get("default_headers"), + env_file_path=settings.get("env_file_path", None), ) def get_prompt_execution_settings_class(self) -> "PromptExecutionSettings": 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 bb74445b90..bdceb5f710 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 @@ -1,10 +1,11 @@ # Copyright (c) Microsoft. All rights reserved. import logging -from typing import Any, Dict, Mapping, Optional, overload +from typing import Mapping from openai import AsyncAzureOpenAI 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 ( @@ -16,6 +17,9 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import ( from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion_base import ( OpenAITextCompletionBase, ) +from semantic_kernel.connectors.ai.open_ai.settings.azure_open_ai_settings import AzureOpenAISettings +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError +from semantic_kernel.kernel_pydantic import HttpsUrl logger: logging.Logger = logging.getLogger(__name__) @@ -23,134 +27,79 @@ logger: logging.Logger = logging.getLogger(__name__) class AzureTextCompletion(AzureOpenAIConfigBase, OpenAITextCompletionBase): """Azure Text Completion class.""" - @overload def __init__( self, - base_url: str, - api_version: str = DEFAULT_AZURE_API_VERSION, - service_id: Optional[str] = None, - api_key: Optional[str] = None, - ad_token: Optional[str] = None, - ad_token_provider: Optional[AsyncAzureADTokenProvider] = None, - default_headers: Optional[Mapping[str, str]] = None, + service_id: str | None = None, + api_key: str | None = None, + deployment_name: str | None = None, + endpoint: str | None = None, + base_url: str | None = None, + api_version: str | None = None, + ad_token: str | None = None, + ad_token_provider: AsyncAzureADTokenProvider | None = None, + default_headers: Mapping[str, str] | None = None, + async_client: AsyncAzureOpenAI | None = None, + env_file_path: str | None = None, ) -> None: """ Initialize an AzureTextCompletion service. Arguments: - deployment_name: The name of the Azure deployment. This value - will correspond to the custom name you chose for your deployment - when you deployed a model. This value can be found under - Resource Management > Deployments in the Azure portal or, alternatively, - under Management > Deployments in Azure OpenAI Studio. - endpoint: The endpoint of the Azure deployment. This value - can be found in the Keys & Endpoint section when examining - your resource from the Azure portal. - api_key: The API key for the Azure deployment. This value can be - found in the Keys & Endpoint section when examining your resource in - the Azure portal. You can use either KEY1 or KEY2. - api_version: The API version to use. (Optional) - The default value is "2023-05-15". - ad_auth: Whether to use Azure Active Directory authentication. (Optional) - The default value is False. + service_id: The service ID for the Azure deployment. (Optional) + api_key {str | None}: The optional api key. If provided, will override the value in the + env vars or .env file. + deployment_name {str | None}: The optional deployment. If provided, will override the value + (text_deployment_name) in the env vars or .env file. + endpoint {str | None}: The optional deployment endpoint. If provided will override the value + in the env vars or .env file. + base_url {str | None}: The optional deployment base_url. If provided will override the value + in the env vars or .env file. + api_version {str | None}: The optional deployment api version. If provided will override the value + in the env vars or .env file. + ad_token: The Azure Active Directory token. (Optional) + ad_token_provider: The Azure Active Directory token provider. (Optional) default_headers: The default headers mapping of string keys to string values for HTTP requests. (Optional) + async_client {Optional[AsyncAzureOpenAI]} -- An existing client to use. (Optional) + 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}") - @overload - def __init__( - self, - deployment_name: str, - endpoint: str, - api_version: str = DEFAULT_AZURE_API_VERSION, - service_id: Optional[str] = None, - api_key: Optional[str] = None, - ad_token: Optional[str] = None, - ad_token_provider: Optional[AsyncAzureADTokenProvider] = None, - default_headers: Optional[Mapping[str, str]] = None, - log: Optional[Any] = None, - ) -> None: - """ - Initialize an AzureTextCompletion service. + 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 + ) - Arguments: - deployment_name: The name of the Azure deployment. This value - will correspond to the custom name you chose for your deployment - when you deployed a model. This value can be found under - Resource Management > Deployments in the Azure portal or, alternatively, - under Management > Deployments in Azure OpenAI Studio. - endpoint: The endpoint of the Azure deployment. This value - can be found in the Keys & Endpoint section when examining - your resource from the Azure portal. - api_key: The API key for the Azure deployment. This value can be - found in the Keys & Endpoint section when examining your resource in - the Azure portal. You can use either KEY1 or KEY2. - api_version: The API version to use. (Optional) - The default value is "2023-05-15". - ad_auth: Whether to use Azure Active Directory authentication. (Optional) - The default value is False. - default_headers: The default headers mapping of string keys to - string values for HTTP requests. (Optional) - """ + if api_version is None: + api_version = DEFAULT_AZURE_API_VERSION - @overload - def __init__( - self, - deployment_name: str, - async_client: AsyncAzureOpenAI, - service_id: Optional[str] = None, - ) -> None: - """ - Initialize an AzureChatCompletion service. + if not base_url and not endpoint: + raise ServiceInitializationError("At least one of base_url or endpoint must be provided.") - Arguments: - deployment_name: The name of the Azure deployment. This value - will correspond to the custom name you chose for your deployment - when you deployed a model. This value can be found under - Resource Management > Deployments in the Azure portal or, alternatively, - under Management > Deployments in Azure OpenAI Studio. - async_client {AsyncAzureOpenAI} -- An existing client to use. - """ + 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}") - def __init__( - self, - deployment_name: Optional[str] = None, - endpoint: Optional[str] = None, - base_url: Optional[str] = None, - api_version: str = DEFAULT_AZURE_API_VERSION, - service_id: Optional[str] = None, - api_key: Optional[str] = None, - ad_token: Optional[str] = None, - ad_token_provider: Optional[AsyncAzureADTokenProvider] = None, - default_headers: Optional[Mapping[str, str]] = None, - async_client: Optional[AsyncAzureOpenAI] = None, - ) -> None: - """ - Initialize an AzureTextCompletion service. - - Arguments: - deployment_name: The name of the Azure deployment. This value - will correspond to the custom name you chose for your deployment - when you deployed a model. This value can be found under - Resource Management > Deployments in the Azure portal or, alternatively, - under Management > Deployments in Azure OpenAI Studio. - endpoint: The endpoint of the Azure deployment. This value - can be found in the Keys & Endpoint section when examining - your resource from the Azure portal. - api_key: The API key for the Azure deployment. This value can be - found in the Keys & Endpoint section when examining your resource in - the Azure portal. You can use either KEY1 or KEY2. - api_version: The API version to use. (Optional) - The default value is "2023-03-15-preview". - ad_auth: Whether to use Azure Active Directory authentication. (Optional) - The default value is False. - default_headers: The default headers mapping of string keys to - string values for HTTP requests. (Optional) - async_client {Optional[AsyncAzureOpenAI]} -- An existing client to use. - """ super().__init__( deployment_name=deployment_name, - endpoint=endpoint, + endpoint=endpoint if not isinstance(endpoint, str) else HttpsUrl(endpoint), base_url=base_url, api_version=api_version, service_id=service_id, @@ -163,7 +112,7 @@ class AzureTextCompletion(AzureOpenAIConfigBase, OpenAITextCompletionBase): ) @classmethod - def from_dict(cls, settings: Dict[str, str]) -> "AzureTextCompletion": + def from_dict(cls, settings: dict[str, str]) -> "AzureTextCompletion": """ Initialize an Azure OpenAI service from a dictionary of settings. @@ -174,13 +123,14 @@ class AzureTextCompletion(AzureOpenAIConfigBase, OpenAITextCompletionBase): """ return AzureTextCompletion( - deployment_name=settings.get("deployment_name"), - endpoint=settings.get("endpoint"), - base_url=settings.get("base_url"), - api_version=settings.get("api_version", DEFAULT_AZURE_API_VERSION), service_id=settings.get("service_id"), - api_key=settings["api_key"], + api_key=settings.get("api_key", None), + deployment_name=settings.get("deployment_name", None), + endpoint=settings.get("endpoint", None), + base_url=settings.get("base_url", None), + api_version=settings.get("api_version", None), ad_token=settings.get("ad_token"), ad_token_provider=settings.get("ad_token_provider"), default_headers=settings.get("default_headers"), + env_file_path=settings.get("env_file_path", None), ) 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 4e2b2e39cb..1faf8ba28e 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 @@ -2,10 +2,11 @@ import logging -from typing import Dict, Mapping, Optional, overload +from typing import Mapping from openai import AsyncAzureOpenAI 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 ( @@ -17,6 +18,9 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import ( from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_embedding_base import ( OpenAITextEmbeddingBase, ) +from semantic_kernel.connectors.ai.open_ai.settings.azure_open_ai_settings import AzureOpenAISettings +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError +from semantic_kernel.kernel_pydantic import HttpsUrl logger: logging.Logger = logging.getLogger(__name__) @@ -24,67 +28,80 @@ logger: logging.Logger = logging.getLogger(__name__) class AzureTextEmbedding(AzureOpenAIConfigBase, OpenAITextEmbeddingBase): """Azure Text Embedding class.""" - @overload def __init__( self, - deployment_name: str, - async_client: AsyncAzureOpenAI, - service_id: Optional[str] = None, - ) -> None: - """ - Initialize an AzureChatCompletion service. - - Arguments: - deployment_name: The name of the Azure deployment. This value - will correspond to the custom name you chose for your deployment - when you deployed a model. This value can be found under - Resource Management > Deployments in the Azure portal or, alternatively, - under Management > Deployments in Azure OpenAI Studio. - async_client {AsyncAzureOpenAI} -- An existing client to use. - """ - - def __init__( - self, - deployment_name: str, - endpoint: Optional[str] = None, - api_version: str = DEFAULT_AZURE_API_VERSION, - service_id: Optional[str] = None, - api_key: Optional[str] = None, - ad_token: Optional[str] = None, - ad_token_provider: Optional[AsyncAzureADTokenProvider] = None, - default_headers: Optional[Mapping[str, str]] = None, - async_client: Optional[AsyncAzureOpenAI] = None, + service_id: str | None = None, + api_key: str | None = None, + deployment_name: str | None = None, + endpoint: str | None = None, + base_url: str | None = None, + api_version: str | None = None, + ad_token: str | None = None, + ad_token_provider: AsyncAzureADTokenProvider | None = None, + default_headers: Mapping[str, str] | None = None, + async_client: AsyncAzureOpenAI | None = None, + env_file_path: str | None = None, ) -> None: """ Initialize an AzureTextEmbedding service. - You must provide: - - A deployment_name, endpoint, and api_key (plus, optionally: ad_auth) - - :param deployment_name: The name of the Azure deployment. This value - will correspond to the custom name you chose for your deployment - when you deployed a model. This value can be found under - Resource Management > Deployments in the Azure portal or, alternatively, - under Management > Deployments in Azure OpenAI Studio. - :param endpoint: The endpoint of the Azure deployment. This value - can be found in the Keys & Endpoint section when examining - your resource from the Azure portal. - :param api_version: The API version to use. (Optional) - The default value is "2023-05-15". - :param api_key: The API key for the Azure deployment. This value can be - found in the Keys & Endpoint section when examining your resource in - the Azure portal. You can use either KEY1 or KEY2. - :param ad_token : The Azure AD token for authentication. (Optional) - :param ad_auth: Whether to use Azure Active Directory authentication. + service_id: The service ID. (Optional) + api_key {str | None}: The optional api key. If provided, will override the value in the + env vars or .env file. + deployment_name {str | None}: The optional deployment. If provided, will override the value + (text_deployment_name) in the env vars or .env file. + endpoint {str | None}: The optional deployment endpoint. If provided will override the value + in the env vars or .env file. + base_url {str | None}: The optional deployment base_url. If provided will override the value + in the env vars or .env file. + api_version {str | None}: The optional deployment api version. If provided will override the value + in the env vars or .env file. + ad_token {str | None}: The Azure AD token for authentication. (Optional) + ad_auth {AsyncAzureADTokenProvider | None}: Whether to use Azure Active Directory authentication. (Optional) The default value is False. - :param default_headers: The default headers mapping of string keys to - string values for HTTP requests. (Optional) - :param async_client: An existing client to use. (Optional) - + default_headers: The default headers mapping of string keys to + string values for HTTP requests. (Optional) + async_client {Optional[AsyncAzureOpenAI]} -- An existing client to use. (Optional) + 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: + 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}") + super().__init__( deployment_name=deployment_name, - endpoint=endpoint, + endpoint=endpoint if not isinstance(endpoint, str) else HttpsUrl(endpoint), + base_url=base_url, api_version=api_version, service_id=service_id, api_key=api_key, @@ -96,7 +113,7 @@ class AzureTextEmbedding(AzureOpenAIConfigBase, OpenAITextEmbeddingBase): ) @classmethod - def from_dict(cls, settings: Dict[str, str]) -> "AzureTextEmbedding": + def from_dict(cls, settings: dict[str, str]) -> "AzureTextEmbedding": """ Initialize an Azure OpenAI service from a dictionary of settings. @@ -106,12 +123,14 @@ class AzureTextEmbedding(AzureOpenAIConfigBase, OpenAITextEmbeddingBase): and optionally: api_version, ad_auth """ return AzureTextEmbedding( - deployment_name=settings["deployment_name"], - endpoint=settings["endpoint"], - api_key=settings["api_key"], - api_version=settings.get("api_version", DEFAULT_AZURE_API_VERSION), service_id=settings.get("service_id"), + api_key=settings.get("api_key", None), + deployment_name=settings.get("deployment_name", None), + endpoint=settings.get("endpoint", None), + base_url=settings.get("base_url", None), + api_version=settings.get("api_version", None), ad_token=settings.get("ad_token"), ad_token_provider=settings.get("ad_token_provider"), default_headers=settings.get("default_headers"), + env_file_path=settings.get("env_file_path", None), ) 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 dcc674c6d2..cdf88fbe36 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 @@ -4,11 +4,10 @@ import logging from typing import ( Dict, Mapping, - Optional, - overload, ) from openai import AsyncOpenAI +from pydantic import ValidationError from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base import OpenAIChatCompletionBase from semantic_kernel.connectors.ai.open_ai.services.open_ai_config_base import OpenAIConfigBase @@ -18,6 +17,7 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import ( 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 logger: logging.Logger = logging.getLogger(__name__) @@ -25,12 +25,15 @@ logger: logging.Logger = logging.getLogger(__name__) class OpenAIChatCompletion(OpenAIConfigBase, OpenAIChatCompletionBase, OpenAITextCompletionBase): """OpenAI Chat completion class.""" - @overload def __init__( self, - ai_model_id: str, - async_client: AsyncOpenAI, - service_id: Optional[str] = None, + ai_model_id: str | None = None, + service_id: str | None = None, + api_key: str | None = None, + org_id: str | None = None, + default_headers: Mapping[str, str] | None = None, + async_client: AsyncOpenAI | None = None, + env_file_path: str | None = None, ) -> None: """ Initialize an OpenAIChatCompletion service. @@ -38,77 +41,31 @@ class OpenAIChatCompletion(OpenAIConfigBase, OpenAIChatCompletionBase, OpenAITex Arguments: ai_model_id {str} -- OpenAI model name, see https://platform.openai.com/docs/models - async_client {AsyncOpenAI} -- An existing client to use. - """ - - @overload - def __init__( - self, - ai_model_id: str, - api_key: Optional[str] = None, - org_id: Optional[str] = None, - service_id: Optional[str] = None, - default_headers: Optional[Mapping[str, str]] = None, - ) -> None: - """ - Initialize an OpenAIChatCompletion service. - - Arguments: - ai_model_id {str} -- OpenAI model name, see - https://platform.openai.com/docs/models - api_key {Optional[str]} -- OpenAI API key, see - https://platform.openai.com/account/api-keys - org_id {Optional[str]} -- OpenAI organization ID. - This is usually optional unless your - account belongs to multiple organizations. - default_headers: The default headers mapping of string keys to - string values for HTTP requests. (Optional) - """ - - @overload - def __init__( - self, - ai_model_id: str, - api_key: Optional[str] = None, - service_id: Optional[str] = None, - default_headers: Optional[Mapping[str, str]] = None, - ) -> None: - """ - Initialize an OpenAIChatCompletion service. - - Arguments: - ai_model_id {str} -- OpenAI model name, see - https://platform.openai.com/docs/models - api_key {Optional[str]} -- OpenAI API key, see - https://platform.openai.com/account/api-keys - default_headers: The default headers mapping of string keys to - string values for HTTP requests. (Optional) - """ - - def __init__( - self, - ai_model_id: str, - api_key: Optional[str] = None, - org_id: Optional[str] = None, - service_id: Optional[str] = None, - default_headers: Optional[Mapping[str, str]] = None, - async_client: Optional[AsyncOpenAI] = None, - ) -> None: - """ - Initialize an OpenAIChatCompletion service. - - Arguments: - ai_model_id {str} -- OpenAI model name, see - https://platform.openai.com/docs/models - api_key {Optional[str]} -- OpenAI API key, see - https://platform.openai.com/account/api-keys - org_id {Optional[str]} -- OpenAI organization ID. - This is usually optional unless your - account belongs to multiple organizations. + service_id {str | None} -- Service ID tied to the execution settings. + api_key {str | None} -- The optional API key to use. If provided will override, + the env vars or .env file value. + org_id {str | None} -- The optional org ID to use. If provided will override, + the env vars or .env file value. default_headers: The default headers mapping of string keys to string values for HTTP requests. (Optional) 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) """ + 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 + ) + super().__init__( ai_model_id=ai_model_id, api_key=api_key, @@ -130,8 +87,6 @@ class OpenAIChatCompletion(OpenAIConfigBase, OpenAIChatCompletionBase, OpenAITex return OpenAIChatCompletion( ai_model_id=settings["ai_model_id"], - api_key=settings["api_key"], - org_id=settings.get("org_id"), service_id=settings.get("service_id"), default_headers=settings.get("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 0fd9e85cda..824b83e684 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 @@ -2,9 +2,10 @@ import json import logging -from typing import Dict, Mapping, Optional, overload +from typing import Dict, Mapping, Optional from openai import AsyncOpenAI +from pydantic import ValidationError from semantic_kernel.connectors.ai.open_ai.services.open_ai_config_base import ( OpenAIConfigBase, @@ -15,6 +16,7 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import ( 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 logger: logging.Logger = logging.getLogger(__name__) @@ -22,90 +24,46 @@ logger: logging.Logger = logging.getLogger(__name__) class OpenAITextCompletion(OpenAITextCompletionBase, OpenAIConfigBase): """OpenAI Text Completion class.""" - @overload def __init__( self, - ai_model_id: str, - async_client: AsyncOpenAI, - service_id: Optional[str] = None, - ) -> None: - """ - Initialize an OpenAITextCompletion service. - - Arguments: - ai_model_id {str} -- OpenAI model name, see - https://platform.openai.com/docs/models - async_client {AsyncOpenAI} -- An existing client to use. - """ - - @overload - def __init__( - self, - ai_model_id: str, - api_key: Optional[str] = None, - org_id: Optional[str] = None, - service_id: Optional[str] = None, - default_headers: Optional[Mapping[str, str]] = None, - ) -> None: - """ - Initialize an OpenAITextCompletion service. - - Arguments: - ai_model_id {str} -- OpenAI model name, see - https://platform.openai.com/docs/models - api_key {Optional[str]} -- OpenAI API key, see - https://platform.openai.com/account/api-keys (Optional) - org_id {Optional[str]} -- OpenAI organization ID. - This is usually optional unless your - account belongs to multiple organizations. - default_headers: The default headers mapping of string keys to - string values for HTTP requests. (Optional) - """ - - @overload - def __init__( - self, - ai_model_id: str, - api_key: Optional[str] = None, - service_id: Optional[str] = None, - default_headers: Optional[Mapping[str, str]] = None, - ) -> None: - """ - Initialize an OpenAITextCompletion service. - - Arguments: - ai_model_id {str} -- OpenAI model name, see - https://platform.openai.com/docs/models - api_key {Optional[str]} -- OpenAI API key, see - https://platform.openai.com/account/api-keys (Optional) - default_headers: The default headers mapping of string keys to - string values for HTTP requests. (Optional) - """ - - def __init__( - self, - ai_model_id: str, - api_key: Optional[str] = None, - org_id: Optional[str] = None, + ai_model_id: str | None = None, + api_key: str | None = None, + org_id: str | None = None, service_id: Optional[str] = None, default_headers: Optional[Mapping[str, str]] = None, async_client: Optional[AsyncOpenAI] = None, + env_file_path: str | None = None, ) -> None: """ Initialize an OpenAITextCompletion service. Arguments: - ai_model_id {str} -- OpenAI model name, see + ai_model_id {str | None} -- OpenAI model name, see https://platform.openai.com/docs/models - api_key {Optional[str]} -- OpenAI API key, see - https://platform.openai.com/account/api-keys (Optional) - org_id {Optional[str]} -- OpenAI organization ID. - This is usually optional unless your - account belongs to multiple organizations. + service_id {str | None} -- Service ID tied to the execution settings. + api_key {str | None} -- The optional API key to use. If provided will override, + the env vars or .env file value. + org_id {str | None} -- The optional org ID to use. If provided will override, + the env vars or .env file value. default_headers: The default headers mapping of string keys to string values for HTTP requests. (Optional) 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) """ + 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 + ) + super().__init__( ai_model_id=ai_model_id, api_key=api_key, @@ -127,9 +85,10 @@ class OpenAITextCompletion(OpenAITextCompletionBase, OpenAIConfigBase): if "default_headers" in settings and isinstance(settings["default_headers"], str): settings["default_headers"] = json.loads(settings["default_headers"]) return OpenAITextCompletion( - ai_model_id=settings["ai_model_id"], - api_key=settings["api_key"], - org_id=settings.get("org_id"), + ai_model_id=settings.get("ai_model_id", None), + api_key=settings.get("api_key", None), + org_id=settings.get("org_id", None), service_id=settings.get("service_id"), default_headers=settings.get("default_headers"), + env_file_path=settings.get("env_file_path", None), ) 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 7b1c2476fa..e8ad1025b5 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 @@ -1,9 +1,10 @@ # Copyright (c) Microsoft. All rights reserved. import logging -from typing import Dict, Mapping, Optional, overload +from typing import Dict, Mapping, Optional from openai import AsyncOpenAI +from pydantic import ValidationError from semantic_kernel.connectors.ai.open_ai.services.open_ai_config_base import ( OpenAIConfigBase, @@ -14,6 +15,7 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import ( 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 logger: logging.Logger = logging.getLogger(__name__) @@ -21,30 +23,15 @@ logger: logging.Logger = logging.getLogger(__name__) class OpenAITextEmbedding(OpenAIConfigBase, OpenAITextEmbeddingBase): """OpenAI Text Embedding class.""" - @overload def __init__( self, ai_model_id: str, - async_client: AsyncOpenAI, - service_id: Optional[str] = None, - ) -> None: - """ - Initialize an OpenAITextEmbedding service. - - Arguments: - ai_model_id {str} -- OpenAI model name, see - https://platform.openai.com/docs/models - async_client {AsyncOpenAI} -- An existing client to use. - """ - - def __init__( - self, - ai_model_id: str, - api_key: Optional[str] = None, - org_id: Optional[str] = None, + api_key: str | None = None, + org_id: str | None = None, service_id: Optional[str] = None, default_headers: Optional[Mapping[str, str]] = None, async_client: Optional[AsyncOpenAI] = None, + env_file_path: str | None = None, ) -> None: """ Initializes a new instance of the OpenAITextCompletion class. @@ -52,15 +39,30 @@ class OpenAITextEmbedding(OpenAIConfigBase, OpenAITextEmbeddingBase): Arguments: ai_model_id {str} -- OpenAI model name, see https://platform.openai.com/docs/models - api_key {str} -- OpenAI API key, see - https://platform.openai.com/account/api-keys - org_id {Optional[str]} -- OpenAI organization ID. - This is usually optional unless your - account belongs to multiple organizations. + service_id {str | None} -- Service ID tied to the execution settings. + api_key {str | None} -- The optional API key to use. If provided will override, + the env vars or .env file value. + org_id {str | None} -- The optional org ID to use. If provided will override, + the env vars or .env file value. default_headers {Optional[Mapping[str,str]]}: The default headers mapping of string keys to string values for HTTP requests. (Optional) 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) """ + 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 + ) + super().__init__( ai_model_id=ai_model_id, api_key=api_key, @@ -82,8 +84,9 @@ class OpenAITextEmbedding(OpenAIConfigBase, OpenAITextEmbeddingBase): return OpenAITextEmbedding( ai_model_id=settings["ai_model_id"], - api_key=settings["api_key"], - org_id=settings.get("org_id"), + api_key=settings.get("api_key", None), + org_id=settings.get("org_id", None), service_id=settings.get("service_id"), default_headers=settings.get("default_headers"), + env_file_path=settings.get("env_file_path", None), ) 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 new file mode 100644 index 0000000000..27ecc718d1 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/open_ai/settings/azure_open_ai_settings.py @@ -0,0 +1,79 @@ +# Copyright (c) Microsoft. All rights reserved. + + +from pydantic import SecretStr +from pydantic_settings import BaseSettings + +from semantic_kernel.kernel_pydantic import HttpsUrl + + +class AzureOpenAISettings(BaseSettings): + """AzureOpenAI model settings + + The settings are first loaded from environment variables with the prefix 'AZURE_OPENAI_'. + If the environment variables are not found, the settings can be loaded from a .env file + with the encoding 'utf-8'. If the settings are not found in the .env file, the settings + are ignored; however, validation will fail alerting that the settings are missing. + + Optional settings for prefix 'AZURE_OPENAI_' are: + - chat_deployment_name: str - The name of the Azure Chat deployment. This value + will correspond to the custom name you chose for your deployment + when you deployed a model. This value can be found under + Resource Management > Deployments in the Azure portal or, alternatively, + under Management > Deployments in Azure OpenAI Studio. + (Env var AZURE_OPENAI_CHAT_DEPLOYMENT_NAME) + - text_deployment_name: str - The name of the Azure Text deployment. This value + will correspond to the custom name you chose for your deployment + when you deployed a model. This value can be found under + Resource Management > Deployments in the Azure portal or, alternatively, + under Management > Deployments in Azure OpenAI Studio. + (Env var AZURE_OPENAI_TEXT_DEPLOYMENT_NAME) + - embedding_deployment_name: str - The name of the Azure Embedding deployment. This value + will correspond to the custom name you chose for your deployment + when you deployed a model. This value can be found under + Resource Management > Deployments in the Azure portal or, alternatively, + under Management > Deployments in Azure OpenAI Studio. + (Env var AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME) + - api_key: SecretStr - The API key for the Azure deployment. This value can be + found in the Keys & Endpoint section when examining your resource in + the Azure portal. You can use either KEY1 or KEY2. + (Env var AZURE_OPENAI_API_KEY) + - base_url: HttpsUrl | None - base_url: The url of the Azure deployment. This value + can be found in the Keys & Endpoint section when examining + your resource from the Azure portal, the base_url consists of the endpoint, + followed by /openai/deployments/{deployment_name}/, + use endpoint if you only want to supply the endpoint. + (Env var AZURE_OPENAI_BASE_URL) + - endpoint: HttpsUrl - The endpoint of the Azure deployment. This value + can be found in the Keys & Endpoint section when examining + your resource from the Azure portal, the endpoint should end in openai.azure.com. + If both base_url and endpoint are supplied, base_url will be used. + (Env var AZURE_OPENAI_ENDPOINT) + - api_version: str | None - The API version to use. The default value is "2024-02-01". + (Env var AZURE_OPENAI_API_VERSION) + - env_file_path: str | None - if provided, the .env settings are read from this file path location + """ + + env_file_path: str | None = None + 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: + env_prefix = "AZURE_OPENAI_" + env_file = None + env_file_encoding = "utf-8" + extra = "ignore" + case_sensitive = False + + @classmethod + def create(cls, **kwargs): + 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/settings/open_ai_settings.py b/python/semantic_kernel/connectors/ai/open_ai/settings/open_ai_settings.py new file mode 100644 index 0000000000..7898296553 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/open_ai/settings/open_ai_settings.py @@ -0,0 +1,49 @@ +# Copyright (c) Microsoft. All rights reserved. + +from pydantic import SecretStr +from pydantic_settings import BaseSettings + + +class OpenAISettings(BaseSettings): + """OpenAI model settings + + The settings are first loaded from environment variables with the prefix 'OPENAI_'. If the + environment variables are not found, the settings can be loaded from a .env file with the + encoding 'utf-8'. If the settings are not found in the .env file, the settings are ignored; + however, validation will fail alerting that the settings are missing. + + Optional settings for prefix 'OPENAI_' are: + - api_key: SecretStr - OpenAI API key, see https://platform.openai.com/account/api-keys + (Env var OPENAI_API_KEY) + - org_id: str | None - This is usually optional unless your account belongs to multiple organizations. + (Env var OPENAI_ORG_ID) + - chat_model_id: str | None - The OpenAI chat model ID to use, for example, gpt-3.5-turbo or gpt-4. + (Env var OPENAI_CHAT_MODEL_ID) + - text_model_id: str | None - The OpenAI text model ID to use, for example, gpt-3.5-turbo-instruct. + (Env var OPENAI_TEXT_MODEL_ID) + - embedding_model_id: str | None - The OpenAI embedding model ID to use, for example, text-embedding-ada-002. + (Env var OPENAI_EMBEDDING_MODEL_ID) + - env_file_path: str | None - if provided, the .env settings are read from this file path location + """ + + env_file_path: str | None = None + 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: + env_prefix = "OPENAI_" + env_file = None + env_file_encoding = "utf-8" + extra = "ignore" + case_sensitive = False + + @classmethod + def create(cls, **kwargs): + 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/__init__.py b/python/semantic_kernel/connectors/memory/astradb/__init__.py index b8907d8388..fd1e8448b1 100644 --- a/python/semantic_kernel/connectors/memory/astradb/__init__.py +++ b/python/semantic_kernel/connectors/memory/astradb/__init__.py @@ -3,5 +3,6 @@ from semantic_kernel.connectors.memory.astradb.astradb_memory_store import ( AstraDBMemoryStore, ) +from semantic_kernel.connectors.memory.astradb.astradb_settings import AstraDBSettings -__all__ = ["AstraDBMemoryStore"] +__all__ = ["AstraDBMemoryStore", "AstraDBSettings"] 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 ed1254a16d..ce38e562da 100644 --- a/python/semantic_kernel/connectors/memory/astradb/astradb_memory_store.py +++ b/python/semantic_kernel/connectors/memory/astradb/astradb_memory_store.py @@ -6,13 +6,15 @@ from typing import List, Optional, Tuple import aiohttp from numpy import ndarray +from pydantic import ValidationError 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, ) -from semantic_kernel.exceptions import ServiceInitializationError +from semantic_kernel.exceptions import MemoryConnectorInitializationError from semantic_kernel.memory.memory_record import MemoryRecord from semantic_kernel.memory.memory_store_base import MemoryStoreBase @@ -37,7 +39,8 @@ class AstraDBMemoryStore(MemoryStoreBase): keyspace_name: str, embedding_dim: int, similarity: str, - session: Optional[aiohttp.ClientSession] = None, + session: aiohttp.ClientSession | None = None, + env_file_path: str | None = None, ) -> None: """Initializes a new instance of the AstraDBMemoryStore class. @@ -49,13 +52,37 @@ class AstraDBMemoryStore(MemoryStoreBase): embedding_dim {int} -- The dimensionality to use for new collections. similarity {str} -- TODO session -- Optional session parameter + env_file_path {str | None} -- Use the environment settings file as a + fallback to environment variables. (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 + ) + assert astra_application_token is not None, "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) + assert astra_id is not None, "The astra_id cannot be None." + astra_region = astra_region or ( + astradb_settings.region if astradb_settings and astradb_settings.region else None + ) + assert astra_region is not None, "The astra_region cannot be None." + keyspace_name = keyspace_name or ( + astradb_settings.keyspace if astradb_settings and astradb_settings.keyspace else None + ) + assert keyspace_name is not None, "The keyspace_name cannot be None." + self._embedding_dim = embedding_dim self._similarity = similarity self._session = session if self._embedding_dim > MAX_DIMENSIONALITY: - raise ServiceInitializationError( + raise MemoryConnectorInitializationError( f"Dimensionality of {self._embedding_dim} exceeds " + f"the maximum allowed value of {MAX_DIMENSIONALITY}." ) diff --git a/python/semantic_kernel/connectors/memory/astradb/astradb_settings.py b/python/semantic_kernel/connectors/memory/astradb/astradb_settings.py new file mode 100644 index 0000000000..d010e4e128 --- /dev/null +++ b/python/semantic_kernel/connectors/memory/astradb/astradb_settings.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft. All rights reserved. + +from pydantic import SecretStr + +from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings + + +class AstraDBSettings(BaseModelSettings): + """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) + """ + + app_token: SecretStr + db_id: str + region: str + keyspace: str + + class Config(BaseModelSettings.Config): + env_prefix = "ASTRADB_" diff --git a/python/semantic_kernel/connectors/memory/azure_cognitive_search/__init__.py b/python/semantic_kernel/connectors/memory/azure_cognitive_search/__init__.py index 8592bc7b7c..3c04124667 100644 --- a/python/semantic_kernel/connectors/memory/azure_cognitive_search/__init__.py +++ b/python/semantic_kernel/connectors/memory/azure_cognitive_search/__init__.py @@ -1,7 +1,8 @@ # Copyright (c) Microsoft. All rights reserved. +from semantic_kernel.connectors.memory.azure_cognitive_search.azure_ai_search_settings import AzureAISearchSettings from semantic_kernel.connectors.memory.azure_cognitive_search.azure_cognitive_search_memory_store import ( AzureCognitiveSearchMemoryStore, ) -__all__ = ["AzureCognitiveSearchMemoryStore"] +__all__ = ["AzureCognitiveSearchMemoryStore", "AzureAISearchSettings"] 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 new file mode 100644 index 0000000000..42e416dd49 --- /dev/null +++ b/python/semantic_kernel/connectors/memory/azure_cognitive_search/azure_ai_search_settings.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft. All rights reserved. + +from pydantic import SecretStr + +from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings +from semantic_kernel.kernel_pydantic import HttpsUrl + + +class AzureAISearchSettings(BaseModelSettings): + """Azure AI Search model settings currently used by the AzureCognitiveSearchMemoryStore connector + + Optional: + - api_key: SecretStr - Azure AI Search API key (Env var AZURE_AI_SEARCH_API_KEY) + - endpoint: HttpsUrl - Azure AI Search endpoint (Env var AZURE_AI_SEARCH_ENDPOINT) + - 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): + 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, + } 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 22e5593356..415d20415d 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 @@ -18,6 +18,7 @@ from azure.search.documents.indexes.models import ( ) from azure.search.documents.models import VectorizedQuery from numpy import ndarray +from pydantic import ValidationError from semantic_kernel.connectors.memory.azure_cognitive_search.utils import ( SEARCH_FIELD_EMBEDDING, @@ -29,7 +30,7 @@ from semantic_kernel.connectors.memory.azure_cognitive_search.utils import ( get_search_index_async_client, memory_record_to_search_record, ) -from semantic_kernel.exceptions import ServiceInitializationError, ServiceResourceNotFoundError +from semantic_kernel.exceptions import MemoryConnectorInitializationError, MemoryConnectorResourceNotFound from semantic_kernel.memory.memory_record import MemoryRecord from semantic_kernel.memory.memory_store_base import MemoryStoreBase @@ -43,30 +44,51 @@ class AzureCognitiveSearchMemoryStore(MemoryStoreBase): def __init__( self, vector_size: int, - search_endpoint: Optional[str] = None, - admin_key: Optional[str] = None, - azure_credentials: Optional[AzureKeyCredential] = None, - token_credentials: Optional[TokenCredential] = None, - **kwargs, + search_endpoint: str | None = None, + admin_key: str | None = None, + azure_credentials: AzureKeyCredential | None = None, + token_credentials: TokenCredential | None = None, + env_file_path: str | None = None, ) -> None: """Initializes a new instance of the AzureCognitiveSearchMemoryStore class. Arguments: vector_size {int} -- Embedding vector size. - search_endpoint {Optional[str]} -- The endpoint of the Azure Cognitive Search service + search_endpoint {str | None} -- The endpoint of the Azure Cognitive Search service (default: {None}). - admin_key {Optional[str]} -- Azure Cognitive Search API key (default: {None}). - azure_credentials {Optional[AzureKeyCredential]} -- Azure Cognitive Search credentials (default: {None}). - token_credentials {Optional[TokenCredential]} -- Azure Cognitive Search token credentials + 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}). + env_file_path {str | None} -- Use the environment settings file as a fallback + to environment variables Instantiate using Async Context Manager: async with AzureCognitiveSearchMemoryStore(<...>) as memory: await memory.<...> """ + from semantic_kernel.connectors.memory.azure_cognitive_search 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 + ) + assert admin_key, "The ACS admin_key is required to connect to Azure Cognitive Search." + search_endpoint = search_endpoint or ( + acs_memory_settings.endpoint if acs_memory_settings and acs_memory_settings.endpoint else None + ) + assert search_endpoint, "The ACS endpoint is required to connect to Azure Cognitive Search." + self._vector_size = vector_size self._search_index_client = get_search_index_async_client( - search_endpoint, admin_key, azure_credentials, token_credentials + str(search_endpoint), admin_key, azure_credentials, token_credentials ) async def close(self): @@ -122,7 +144,7 @@ class AzureCognitiveSearchMemoryStore(MemoryStoreBase): ) if not self._search_index_client: - raise ServiceInitializationError("Error: self._search_index_client not set 1.") + raise MemoryConnectorInitializationError("Error: self._search_index_client not set 1.") # Check to see if collection exists collection_index = None @@ -264,7 +286,7 @@ class AzureCognitiveSearchMemoryStore(MemoryStoreBase): ) except ResourceNotFoundError as exc: await search_client.close() - raise ServiceResourceNotFoundError("Memory record not found") from exc + raise MemoryConnectorResourceNotFound("Memory record not found") from exc await search_client.close() diff --git a/python/semantic_kernel/connectors/memory/azure_cosmosdb/__init__.py b/python/semantic_kernel/connectors/memory/azure_cosmosdb/__init__.py index ca310d9b09..2c29757473 100644 --- a/python/semantic_kernel/connectors/memory/azure_cosmosdb/__init__.py +++ b/python/semantic_kernel/connectors/memory/azure_cosmosdb/__init__.py @@ -3,5 +3,6 @@ from semantic_kernel.connectors.memory.azure_cosmosdb.azure_cosmos_db_memory_store import ( AzureCosmosDBMemoryStore, ) +from semantic_kernel.connectors.memory.azure_cosmosdb.azure_cosmosdb_settings import AzureCosmosDBSettings -__all__ = ["AzureCosmosDBMemoryStore"] +__all__ = ["AzureCosmosDBMemoryStore", "AzureCosmosDBSettings"] 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 fc008b8d22..dd0f6c4b41 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,19 +1,25 @@ # Copyright (c) Microsoft. All rights reserved. + +import logging from typing import List, Tuple from numpy import ndarray +from pydantic import ValidationError 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 ( CosmosDBSimilarityType, CosmosDBVectorSearchType, get_mongodb_search_client, ) from semantic_kernel.connectors.memory.azure_cosmosdb.mongo_vcore_store_api import MongoStoreApi -from semantic_kernel.exceptions import ServiceInitializationError +from semantic_kernel.exceptions import MemoryConnectorInitializationError from semantic_kernel.memory.memory_record import MemoryRecord from semantic_kernel.memory.memory_store_base import MemoryStoreBase +logger: logging.Logger = logging.getLogger(__name__) + class AzureCosmosDBMemoryStore(MemoryStoreBase): """A memory store that uses AzureCosmosDB for MongoDB vCore, to perform vector similarity search on a fully @@ -48,13 +54,13 @@ class AzureCosmosDBMemoryStore(MemoryStoreBase): ef_search: int = 40, ): if vector_dimensions <= 0: - raise ServiceInitializationError("Vector dimensions must be a positive number.") + raise MemoryConnectorInitializationError("Vector dimensions must be a positive number.") # if connection_string is None: # raise ValueError("Connection String cannot be empty.") if database_name is None: - raise ServiceInitializationError("Database Name cannot be empty.") + raise MemoryConnectorInitializationError("Database Name cannot be empty.") if index_name is None: - raise ServiceInitializationError("Index Name cannot be empty.") + raise MemoryConnectorInitializationError("Index Name cannot be empty.") self.cosmosStore = cosmosStore self.index_name = index_name @@ -80,11 +86,25 @@ class AzureCosmosDBMemoryStore(MemoryStoreBase): m, ef_construction, ef_search, + 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 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 + ) + mongodb_client = get_mongodb_search_client(cosmos_connstr, application_name) database = mongodb_client[database_name] apiStore = MongoStoreApi( @@ -100,7 +120,7 @@ class AzureCosmosDBMemoryStore(MemoryStoreBase): ef_search=ef_search, ) else: - raise NotImplementedError(f"API type {cosmos_api} is not supported.") + raise MemoryConnectorInitializationError(f"API type {cosmos_api} is not supported.") store = AzureCosmosDBMemoryStore( apiStore, 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 new file mode 100644 index 0000000000..6dadde931e --- /dev/null +++ b/python/semantic_kernel/connectors/memory/azure_cosmosdb/azure_cosmosdb_settings.py @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft. All rights reserved. + +from pydantic import SecretStr + +from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings + + +class AzureCosmosDBSettings(BaseModelSettings): + """Azure CosmosDB model settings + + Optional: + - 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): + env_prefix = "COSMOSDB_" diff --git a/python/semantic_kernel/connectors/memory/memory_settings_base.py b/python/semantic_kernel/connectors/memory/memory_settings_base.py new file mode 100644 index 0000000000..ec65ddd611 --- /dev/null +++ b/python/semantic_kernel/connectors/memory/memory_settings_base.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft. All rights reserved. + +from pydantic_settings import BaseSettings + + +class BaseModelSettings(BaseSettings): + env_file_path: str | None = None + + class Config: + env_file = None + env_file_encoding = "utf-8" + extra = "ignore" + case_sensitive = False + + @classmethod + def create(cls, **kwargs): + 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/__init__.py b/python/semantic_kernel/connectors/memory/mongodb_atlas/__init__.py index 4ee1e46966..3e3c3775c9 100644 --- a/python/semantic_kernel/connectors/memory/mongodb_atlas/__init__.py +++ b/python/semantic_kernel/connectors/memory/mongodb_atlas/__init__.py @@ -1,5 +1,8 @@ +# Copyright (c) Microsoft. All rights reserved. + 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 -__all__ = ["MongoDBAtlasMemoryStore"] +__all__ = ["MongoDBAtlasMemoryStore", "MongoDBAtlasSettings"] 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 16a7204a09..31e75e6f63 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 @@ -3,10 +3,11 @@ from __future__ import annotations import logging from importlib import metadata -from typing import Any, List, Mapping, Optional, Tuple +from typing import Any, List, Mapping, Tuple from motor import core, motor_asyncio from numpy import ndarray +from pydantic import ValidationError from pymongo import DeleteOne, ReadPreference, UpdateOne, results from pymongo.driver_info import DriverInfo @@ -22,7 +23,6 @@ from semantic_kernel.connectors.memory.mongodb_atlas.utils import ( from semantic_kernel.exceptions import ServiceResourceNotFoundError from semantic_kernel.memory.memory_record import MemoryRecord from semantic_kernel.memory.memory_store_base import MemoryStoreBase -from semantic_kernel.utils.settings import mongodb_atlas_settings_from_dot_env logger: logging.Logger = logging.getLogger(__name__) @@ -38,16 +38,28 @@ class MongoDBAtlasMemoryStore(MemoryStoreBase): def __init__( self, - index_name: Optional[str] = None, - connection_string: Optional[str] = None, - database_name: Optional[str] = None, - read_preference: Optional[ReadPreference] = ReadPreference.PRIMARY, - **kwargs, + index_name: str | None = None, + connection_string: str | None = None, + database_name: str | None = None, + read_preference: ReadPreference | None = ReadPreference.PRIMARY, + env_file_path: str | None = None, ): - if kwargs.get("logger"): - logger.warning("The `logger` parameter is deprecated. Please use the `logging` module instead.") + from semantic_kernel.connectors.memory.mongodb_atlas 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 + ) + self._mongo_client = motor_asyncio.AsyncIOMotorClient( - connection_string or mongodb_atlas_settings_from_dot_env(), + connection_string, read_preference=read_preference, driver=DriverInfo("Microsoft Semantic Kernel", metadata.version("semantic-kernel")), ) 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 new file mode 100644 index 0000000000..a9223fd9c4 --- /dev/null +++ b/python/semantic_kernel/connectors/memory/mongodb_atlas/mongodb_atlas_settings.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft. All rights reserved. + +from pydantic import SecretStr + +from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings + + +class MongoDBAtlasSettings(BaseModelSettings): + """MongoDB Atlas model settings + + Optional: + - connection_string: str - MongoDB Atlas connection string + (Env var MONGODB_ATLAS_CONNECTION_STRING) + """ + + connection_string: SecretStr | None = None + + class Config(BaseModelSettings.Config): + env_prefix = "MONGODB_ATLAS_" diff --git a/python/semantic_kernel/connectors/memory/pinecone/__init__.py b/python/semantic_kernel/connectors/memory/pinecone/__init__.py index 92a5f112ed..61f63d4333 100644 --- a/python/semantic_kernel/connectors/memory/pinecone/__init__.py +++ b/python/semantic_kernel/connectors/memory/pinecone/__init__.py @@ -3,5 +3,8 @@ from semantic_kernel.connectors.memory.pinecone.pinecone_memory_store import ( PineconeMemoryStore, ) +from semantic_kernel.connectors.memory.pinecone.pinecone_settings import ( + PineconeSettings, +) -__all__ = ["PineconeMemoryStore"] +__all__ = ["PineconeMemoryStore", "PineconeSettings"] 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 89b86e0bc5..c0f9a78db8 100644 --- a/python/semantic_kernel/connectors/memory/pinecone/pinecone_memory_store.py +++ b/python/semantic_kernel/connectors/memory/pinecone/pinecone_memory_store.py @@ -5,7 +5,9 @@ from typing import List, NamedTuple, Optional, Tuple from numpy import ndarray from pinecone import FetchResponse, IndexDescription, IndexList, Pinecone, ServerlessSpec +from pydantic import ValidationError +from semantic_kernel.connectors.memory.pinecone.pinecone_settings import PineconeSettings from semantic_kernel.connectors.memory.pinecone.utils import ( build_payload, parse_payload, @@ -45,21 +47,33 @@ class PineconeMemoryStore(MemoryStoreBase): self, api_key: str, default_dimensionality: int, - **kwargs, + env_file_path: str | None = None, ) -> None: """Initializes a new instance of the PineconeMemoryStore class. Arguments: pinecone_api_key {str} -- The Pinecone API key. 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) """ - if kwargs.get("logger"): - logger.warning("The `logger` parameter is deprecated. Please use the `logging` module instead.") if default_dimensionality > MAX_DIMENSIONALITY: raise ServiceInitializationError( 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 + ) + assert api_key, "The Pinecone api_key cannot be None." + self._pinecone_api_key = api_key self._default_dimensionality = default_dimensionality diff --git a/python/semantic_kernel/connectors/memory/pinecone/pinecone_settings.py b/python/semantic_kernel/connectors/memory/pinecone/pinecone_settings.py new file mode 100644 index 0000000000..190521a0e7 --- /dev/null +++ b/python/semantic_kernel/connectors/memory/pinecone/pinecone_settings.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft. All rights reserved. + +from pydantic import SecretStr + +from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings + + +class PineconeSettings(BaseModelSettings): + """Pinecone model settings + + Required: + - api_key: SecretStr - Pinecone API key + (Env var PINECONE_API_KEY) + """ + + api_key: SecretStr | None = None + + class Config(BaseModelSettings.Config): + env_prefix = "PINECONE_" diff --git a/python/semantic_kernel/connectors/memory/postgres/__init__.py b/python/semantic_kernel/connectors/memory/postgres/__init__.py index 029e7fed4c..7a0e7301d8 100644 --- a/python/semantic_kernel/connectors/memory/postgres/__init__.py +++ b/python/semantic_kernel/connectors/memory/postgres/__init__.py @@ -3,5 +3,6 @@ from semantic_kernel.connectors.memory.postgres.postgres_memory_store import ( PostgresMemoryStore, ) +from semantic_kernel.connectors.memory.postgres.postgres_settings import PostgresSettings -__all__ = ["PostgresMemoryStore"] +__all__ = ["PostgresMemoryStore", "PostgresSettings"] 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 7c8dcb352b..22306606bd 100644 --- a/python/semantic_kernel/connectors/memory/postgres/postgres_memory_store.py +++ b/python/semantic_kernel/connectors/memory/postgres/postgres_memory_store.py @@ -10,7 +10,9 @@ from numpy import ndarray from psycopg import Cursor from psycopg.sql import SQL, Identifier from psycopg_pool import ConnectionPool +from pydantic import ValidationError +from semantic_kernel.connectors.memory.postgres.postgres_settings import PostgresSettings from semantic_kernel.exceptions import ( ServiceInitializationError, ServiceResourceNotFoundError, @@ -41,7 +43,7 @@ class PostgresMemoryStore(MemoryStoreBase): min_pool: int, max_pool: int, schema: str = DEFAULT_SCHEMA, - **kwargs, + env_file_path: str | None = None, ) -> None: """Initializes a new instance of the PostgresMemoryStore class. @@ -52,10 +54,22 @@ class PostgresMemoryStore(MemoryStoreBase): max_pool {int} -- The maximum number of connections in the connection pool.\n schema {str} -- The schema to use. (default: {"public"})\n timezone_offset {Optional[str]} -- The timezone offset to use. (default: {None}) - Expected format '-7:00'. Uses the local timezone offset when not provided.\n + Expected format '-7:00'. Uses the local timezone offset when not provided.\n + env_file_path {str | None} -- Use the environment settings file as a fallback + to environment variables. (Optional) """ - if kwargs.get("logger"): - logger.warning("The `logger` parameter is deprecated. Please use the `logging` module instead.") + 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 + ) + self._check_dimensionality(default_dimensionality) self._connection_string = connection_string diff --git a/python/semantic_kernel/connectors/memory/postgres/postgres_settings.py b/python/semantic_kernel/connectors/memory/postgres/postgres_settings.py new file mode 100644 index 0000000000..e4df824f08 --- /dev/null +++ b/python/semantic_kernel/connectors/memory/postgres/postgres_settings.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft. All rights reserved. + +from pydantic import SecretStr + +from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings + + +class PostgresSettings(BaseModelSettings): + """Postgres model settings + + Required: + - connection_string: str - Postgres connection string + (Env var POSTGRES_CONNECTION_STRING) + """ + + connection_string: SecretStr | None = None + + class Config(BaseModelSettings.Config): + env_prefix = "POSTGRES_" diff --git a/python/semantic_kernel/connectors/memory/redis/__init__.py b/python/semantic_kernel/connectors/memory/redis/__init__.py index 85a1b31919..16e086af74 100644 --- a/python/semantic_kernel/connectors/memory/redis/__init__.py +++ b/python/semantic_kernel/connectors/memory/redis/__init__.py @@ -3,5 +3,6 @@ from semantic_kernel.connectors.memory.redis.redis_memory_store import ( RedisMemoryStore, ) +from semantic_kernel.connectors.memory.redis.redis_settings import RedisSettings -__all__ = ["RedisMemoryStore"] +__all__ = ["RedisMemoryStore", "RedisSettings"] 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 95e0511ee6..841e99757b 100644 --- a/python/semantic_kernel/connectors/memory/redis/redis_memory_store.py +++ b/python/semantic_kernel/connectors/memory/redis/redis_memory_store.py @@ -6,11 +6,13 @@ from typing import List, Tuple import numpy as np import redis from numpy import ndarray +from pydantic import ValidationError from redis.commands.search.field import TextField, VectorField from redis.commands.search.indexDefinition import IndexDefinition, IndexType from redis.commands.search.query import Query from redis.exceptions import ResponseError +from semantic_kernel.connectors.memory.redis.redis_settings import RedisSettings from semantic_kernel.connectors.memory.redis.utils import ( deserialize_document_to_record, deserialize_redis_to_record, @@ -50,7 +52,7 @@ class RedisMemoryStore(MemoryStoreBase): vector_type: str = "FLOAT32", vector_index_algorithm: str = "HNSW", query_dialect: int = 2, - **kwargs, + env_file_path: str | None = None, ) -> None: """ RedisMemoryStore is an abstracted interface to interact with a Redis node connection. @@ -64,10 +66,21 @@ class RedisMemoryStore(MemoryStoreBase): vector_type {str} -- Vector type, defaults to FLOAT32 vector_index_algorithm {str} -- Indexing algorithm for vectors, defaults to HNSW 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 """ - if kwargs.get("logger"): - logger.warning("The `logger` parameter is deprecated. Please use the `logging` module instead.") + 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 + ) + if vector_size <= 0: raise ServiceInitializationError("Vector dimension must be a positive integer") diff --git a/python/semantic_kernel/connectors/memory/redis/redis_settings.py b/python/semantic_kernel/connectors/memory/redis/redis_settings.py new file mode 100644 index 0000000000..93fd02831c --- /dev/null +++ b/python/semantic_kernel/connectors/memory/redis/redis_settings.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft. All rights reserved. + +from pydantic import SecretStr + +from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings + + +class RedisSettings(BaseModelSettings): + """Redis model settings + + Optional: + - connection_string: str | None - Redis connection string + (Env var REDIS_CONNECTION_STRING) + """ + + connection_string: SecretStr | None = None + + class Config(BaseModelSettings.Config): + env_prefix = "REDIS_" diff --git a/python/semantic_kernel/connectors/memory/weaviate/__init__.py b/python/semantic_kernel/connectors/memory/weaviate/__init__.py index dacbcb42bb..3f53c056d1 100644 --- a/python/semantic_kernel/connectors/memory/weaviate/__init__.py +++ b/python/semantic_kernel/connectors/memory/weaviate/__init__.py @@ -2,5 +2,6 @@ from semantic_kernel.connectors.memory.weaviate.weaviate_memory_store import ( WeaviateMemoryStore, ) +from semantic_kernel.connectors.memory.weaviate.weaviate_settings import WeaviateSettings -__all__ = ["WeaviateMemoryStore"] +__all__ = ["WeaviateMemoryStore", "WeaviateSettings"] 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 4cca2a814a..116998ad93 100644 --- a/python/semantic_kernel/connectors/memory/weaviate/weaviate_memory_store.py +++ b/python/semantic_kernel/connectors/memory/weaviate/weaviate_memory_store.py @@ -7,9 +7,9 @@ from typing import List, Tuple import numpy as np import weaviate -from weaviate.embedded import EmbeddedOptions +from pydantic import ValidationError -from semantic_kernel.exceptions import ServiceInitializationError +from semantic_kernel.connectors.memory.weaviate.weaviate_settings import WeaviateSettings from semantic_kernel.memory.memory_record import MemoryRecord from semantic_kernel.memory.memory_store_base import MemoryStoreBase @@ -115,25 +115,59 @@ class WeaviateMemoryStore(MemoryStoreBase): """ return {key.lstrip("_"): value for key, value in sk_dict.items()} - def __init__(self, config: WeaviateConfig, **kwargs): - if kwargs.get("logger"): - logger.warning("The `logger` parameter is deprecated. Please use the `logging` module instead.") - self.config = config + def __init__(self, config: WeaviateConfig | None = None, env_file_path: 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 _initialize_client(self): - if self.config.use_embed: - return weaviate.Client(embedded_options=EmbeddedOptions()) - elif self.config.url: - if self.config.api_key: - return weaviate.Client( - url=self.config.url, - auth_client_secret=weaviate.auth.AuthApiKey(api_key=self.config.api_key), - ) - else: - return weaviate.Client(url=self.config.url) - else: - raise ServiceInitializationError("Weaviate config must have either url or use_embed set") + 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. + """ + 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) + ), + ) + + 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) + ) + + return weaviate.Client(url=self.settings.url) async def create_collection(self, collection_name: str) -> None: schema = SCHEMA.copy() diff --git a/python/semantic_kernel/connectors/memory/weaviate/weaviate_settings.py b/python/semantic_kernel/connectors/memory/weaviate/weaviate_settings.py new file mode 100644 index 0000000000..866f82e996 --- /dev/null +++ b/python/semantic_kernel/connectors/memory/weaviate/weaviate_settings.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft. All rights reserved. + +from pydantic import SecretStr + +from semantic_kernel.connectors.memory.memory_settings_base import BaseModelSettings +from semantic_kernel.kernel_pydantic import HttpsUrl + + +class WeaviateSettings(BaseModelSettings): + """Weaviate model settings + + Optional: + - url: HttpsUrl | None - Weaviate URL (Env var WEAVIATE_URL) + - api_key: SecretStr | None - Weaviate token (Env var WEAVIATE_API_KEY) + - use_embed: bool - Whether to use the client embedding options + (Env var WEAVIATE_USE_EMBED) + """ + + url: HttpsUrl | None = None + api_key: SecretStr | None = None + use_embed: bool = False + + class Config(BaseModelSettings.Config): + env_prefix = "WEAVIATE_" + + def validate_settings(self): + if not self.use_embed and not self.url: + raise ValueError("Weaviate config must have either url or use_embed set") diff --git a/python/semantic_kernel/connectors/search_engine/bing_connector.py b/python/semantic_kernel/connectors/search_engine/bing_connector.py index 6c019fb88c..0d0cb27152 100644 --- a/python/semantic_kernel/connectors/search_engine/bing_connector.py +++ b/python/semantic_kernel/connectors/search_engine/bing_connector.py @@ -5,9 +5,11 @@ import urllib from typing import List 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 -from semantic_kernel.exceptions import ServiceInitializationError, ServiceInvalidRequestError +from semantic_kernel.exceptions import ServiceInvalidRequestError logger: logging.Logger = logging.getLogger(__name__) @@ -19,13 +21,25 @@ class BingConnector(ConnectorBase): _api_key: str - def __init__(self, api_key: str) -> None: - self._api_key = api_key + def __init__(self, api_key: str | None = None, env_file_path: str | None = None) -> None: + """Initializes a new instance of the BingConnector class. - if not self._api_key: - raise ServiceInitializationError( - "Bing API key cannot be null. Please set environment variable BING_API_KEY." - ) + Arguments: + api_key {str | None}: The Bing Search API key. If provided, will override + 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. + """ + 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 + ) + assert self._api_key, "API key cannot be 'None' or empty." async def search(self, query: str, num_results: int = 1, offset: int = 0) -> List[str]: """ diff --git a/python/semantic_kernel/connectors/search_engine/bing_connector_settings.py b/python/semantic_kernel/connectors/search_engine/bing_connector_settings.py new file mode 100644 index 0000000000..38a4966d50 --- /dev/null +++ b/python/semantic_kernel/connectors/search_engine/bing_connector_settings.py @@ -0,0 +1,36 @@ +# Copyright (c) Microsoft. All rights reserved. + +from pydantic import SecretStr +from pydantic_settings import BaseSettings + + +class BingSettings(BaseSettings): + """Bing Connector settings + + The settings are first loaded from environment variables with the prefix 'BING_'. If the + environment variables are not found, the settings can be loaded from a .env file with the + encoding 'utf-8'. If the settings are not found in the .env file, the settings are ignored; + however, validation will fail alerting that the settings are missing. + + Optional settings for prefix 'BING_' are: + - api_key: SecretStr - The Bing API key (Env var BING_API_KEY) + + """ + + env_file_path: str | None = None + api_key: SecretStr | None = None + + class Config: + env_prefix = "BING_" + env_file = None + env_file_encoding = "utf-8" + extra = "ignore" + case_sensitive = False + + @classmethod + def create(cls, **kwargs): + 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/core_plugins/sessions_python_tool/README.md b/python/semantic_kernel/core_plugins/sessions_python_tool/README.md index 9ac97aafa8..eb700ae07f 100644 --- a/python/semantic_kernel/core_plugins/sessions_python_tool/README.md +++ b/python/semantic_kernel/core_plugins/sessions_python_tool/README.md @@ -88,7 +88,7 @@ chat_service = AzureChatCompletion( kernel.add_service(chat_service) python_code_interpreter = SessionsPythonTool( - **azure_container_apps_settings_from_dot_env_as_dict(), auth_callback=auth_callback + auth_callback=auth_callback ) sessions_tool = kernel.add_plugin(python_code_interpreter, "PythonCodeInterpreter") 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 38c62178ac..96a3a87c35 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 @@ -9,11 +9,12 @@ from io import BufferedReader, BytesIO from typing import Annotated, Any, Awaitable, Callable import httpx -from pydantic import field_validator +from pydantic import ValidationError, field_validator from semantic_kernel.connectors.ai.open_ai.const import USER_AGENT from semantic_kernel.connectors.telemetry import HTTP_USER_AGENT, version_info from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_settings import ( + ACASessionsSettings, SessionsPythonSettings, ) from semantic_kernel.core_plugins.sessions_python_tool.sessions_remote_file_metadata import SessionsRemoteFileMetadata @@ -37,10 +38,11 @@ class SessionsPythonTool(KernelBaseModel): def __init__( self, - pool_management_endpoint: str, auth_callback: Callable[..., Awaitable[Any]], + pool_management_endpoint: str | None = None, settings: SessionsPythonSettings | None = None, http_client: httpx.AsyncClient | None = None, + env_file_path: str | None = None, **kwargs, ): """Initializes a new instance of the SessionsPythonTool class.""" @@ -50,8 +52,16 @@ class SessionsPythonTool(KernelBaseModel): if not http_client: http_client = httpx.AsyncClient() + try: + aca_settings = ACASessionsSettings.create(env_file_path=env_file_path) + 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 + + endpoint = pool_management_endpoint or aca_settings.pool_management_endpoint + super().__init__( - pool_management_endpoint=pool_management_endpoint, + pool_management_endpoint=endpoint, auth_callback=auth_callback, settings=settings, http_client=http_client, @@ -61,6 +71,8 @@ class SessionsPythonTool(KernelBaseModel): @field_validator("pool_management_endpoint", mode="before") @classmethod def _validate_endpoint(cls, endpoint: str): + endpoint = str(endpoint) + """Validates the pool management endpoint.""" if "/python/execute" in endpoint: # Remove '/python/execute/' and ensure the endpoint ends with a '/' 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 4ea3457ed5..7b008b59df 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 @@ -6,8 +6,9 @@ import uuid from enum import Enum from pydantic import Field +from pydantic_settings import BaseSettings -from semantic_kernel.kernel_pydantic import KernelBaseModel +from semantic_kernel.kernel_pydantic import HttpsUrl, KernelBaseModel class CodeInputType(str, Enum): @@ -32,3 +33,30 @@ class SessionsPythonSettings(KernelBaseModel): python_code: str | None = Field(alias="pythonCode", default=None) timeout_in_sec: int | None = Field(default=100, alias="timeoutInSeconds") sanitize_input: bool | None = Field(default=True, alias="sanitizeInput") + + +class ACASessionsSettings(BaseSettings): + """Azure Container Apps sessions settings. + + Required: + - pool_management_endpoint: HttpsUrl - The URL of the Azure Container Apps pool management endpoint. + (Env var ACA_POOL_MANAGEMENT_ENDPOINT) + """ + + env_file_path: str | None = None + pool_management_endpoint: HttpsUrl + + class Config: + env_prefix = "ACA_" + env_file = None + env_file_encoding = "utf-8" + extra = "ignore" + case_sensitive = False + + @classmethod + def create(cls, **kwargs): + 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/exceptions/__init__.py b/python/semantic_kernel/exceptions/__init__.py index c4d62eb82e..5b9b21b91d 100644 --- a/python/semantic_kernel/exceptions/__init__.py +++ b/python/semantic_kernel/exceptions/__init__.py @@ -3,6 +3,7 @@ from semantic_kernel.exceptions.content_exceptions import * # noqa: F401, F403 from semantic_kernel.exceptions.function_exceptions import * # noqa: F401, F403 from semantic_kernel.exceptions.kernel_exceptions import * # noqa: F401, F403 +from semantic_kernel.exceptions.memory_connector_exceptions import * # noqa: F401, F403 from semantic_kernel.exceptions.planner_exceptions import * # noqa: F401, F403 from semantic_kernel.exceptions.service_exceptions import * # noqa: F401, F403 from semantic_kernel.exceptions.template_engine_exceptions import * # noqa: F401, F403 diff --git a/python/semantic_kernel/exceptions/memory_connector_exceptions.py b/python/semantic_kernel/exceptions/memory_connector_exceptions.py new file mode 100644 index 0000000000..b72a266762 --- /dev/null +++ b/python/semantic_kernel/exceptions/memory_connector_exceptions.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft. All rights reserved. + + +from semantic_kernel.exceptions.kernel_exceptions import KernelException + + +class MemoryConnectorException(KernelException): + pass + + +class MemoryConnectorInitializationError(MemoryConnectorException): + pass + + +class MemoryConnectorResourceNotFound(MemoryConnectorException): + pass + + +__all__ = [ + "MemoryConnectorException", + "MemoryConnectorInitializationError", + "MemoryConnectorResourceNotFound", +] diff --git a/python/semantic_kernel/utils/settings.py b/python/semantic_kernel/utils/settings.py deleted file mode 100644 index 63f3c0d933..0000000000 --- a/python/semantic_kernel/utils/settings.py +++ /dev/null @@ -1,377 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from __future__ import annotations - -from typing import Optional, Tuple, Union - -from dotenv import dotenv_values - - -def openai_settings_from_dot_env() -> Tuple[str, Optional[str]]: - """ - Reads the OpenAI API key and organization ID from the .env file. - - Returns: - Tuple[str, str]: The OpenAI API key, the OpenAI organization ID - """ - - config = dotenv_values(".env") - api_key = config.get("OPENAI_API_KEY", None) - org_id = config.get("OPENAI_ORG_ID", None) - - assert api_key, "OpenAI API key not found in .env file" - - # It's okay if the org ID is not found (not required) - return api_key, org_id - - -def azure_openai_settings_from_dot_env( - include_deployment: bool = True, include_api_version: bool = False -) -> Union[Tuple[str, str, str], Tuple[str, str, str, str]]: - """ - Reads the Azure OpenAI API key and endpoint from the .env file. - - Arguments: - include_deployment {bool} -- Whether to include the deployment name in the return value - include_api_version {bool} -- Whether to include the API version in the return value, - when set to True, this will also make the output a Tuple[str, str, str, str]. - - Returns: - Union[Tuple[str, str, str], Tuple[str, str, str, str]]: The deployment name (or empty), Azure OpenAI API key, - the endpoint and optionally the api version - """ - - deployment, api_key, endpoint, api_version = None, None, None, None - config = dotenv_values(".env") - deployment = config.get("AZURE_OPENAI_DEPLOYMENT_NAME", None) - api_key = config.get("AZURE_OPENAI_API_KEY", None) - endpoint = config.get("AZURE_OPENAI_ENDPOINT", None) - api_version = config.get("AZURE_OPENAI_API_VERSION", None) - - # Azure requires the deployment name, the API key and the endpoint URL. - if include_deployment: - assert deployment is not None, "Azure OpenAI deployment name not found in .env file" - if include_api_version: - assert api_version is not None, "Azure OpenAI API version not found in .env file" - - assert api_key, "Azure OpenAI API key not found in .env file" - assert endpoint, "Azure OpenAI endpoint not found in .env file" - - if include_api_version: - return deployment or "", api_key, endpoint, api_version or "" - return deployment or "", api_key, endpoint - - -def azure_openai_settings_from_dot_env_as_dict( - include_deployment: bool = True, include_api_version: bool = False -) -> dict[str, str]: - """ - Reads the Azure OpenAI API key and endpoint from the .env file. - - Returns: - dict[str, str]: The deployment name (or empty), Azure OpenAI API key, - endpoint and api version (or empty) - """ - ( - deployment_name, - api_key, - endpoint, - api_version, - ) = azure_openai_settings_from_dot_env(include_deployment, include_api_version) - ret = { - "api_key": api_key, - "endpoint": endpoint, - } - if include_deployment: - ret["deployment_name"] = deployment_name - if include_api_version: - ret["api_version"] = api_version - return ret - - -def postgres_settings_from_dot_env() -> str: - """Reads the Postgres connection string from the .env file. - - Returns: - str: The Postgres connection string - """ - connection_string = None - config = dotenv_values(".env") - connection_string = config.get("POSTGRES_CONNECTION_STRING", None) - - assert connection_string, "Postgres connection string not found in .env file" - - return connection_string - - -def pinecone_settings_from_dot_env() -> str: - """ - Reads the Pinecone API key from the .env file. - Returns: - str: The Pinecone API key - """ - - config = dotenv_values(".env") - api_key = config.get("PINECONE_API_KEY", None) - - assert api_key, "Pinecone API key not found in .env file" - - return api_key - - -def astradb_settings_from_dot_env() -> Tuple[str, Optional[str]]: - """ - Reads the Astradb API key and Environment from the .env file. - Returns: - Tuple[str, str]: The Astradb API key, the Astradb Environment - """ - - app_token, db_id, region, keyspace = None, None, None, None - with open(".env", "r") as f: - lines = f.readlines() - - for line in lines: - if line.startswith("ASTRADB_APP_TOKEN"): - parts = line.split("=")[1:] - app_token = "=".join(parts).strip().strip('"') - continue - - if line.startswith("ASTRADB_ID"): - parts = line.split("=")[1:] - db_id = "=".join(parts).strip().strip('"') - continue - - if line.startswith("ASTRADB_REGION"): - parts = line.split("=")[1:] - region = "=".join(parts).strip().strip('"') - continue - - if line.startswith("ASTRADB_KEYSPACE"): - parts = line.split("=")[1:] - keyspace = "=".join(parts).strip().strip('"') - continue - - assert app_token, "Astradb Application token not found in .env file" - assert db_id, "Astradb ID not found in .env file" - assert region, "Astradb Region not found in .env file" - assert keyspace, "Astradb Keyspace name not found in .env file" - - return app_token, db_id, region, keyspace - - -def weaviate_settings_from_dot_env() -> Tuple[Optional[str], str]: - """ - Reads the Weaviate API key and URL from the .env file. - - Returns: - Tuple[str, str]: The Weaviate API key, the Weaviate URL - """ - - config = dotenv_values(".env") - api_key = config.get("WEAVIATE_API_KEY", None) - url = config.get("WEAVIATE_URL", None) - - # API key not needed for local Weaviate deployment, URL still needed - assert url is not None, "Weaviate instance URL not found in .env file" - - return api_key, url - - -def bing_search_settings_from_dot_env() -> str: - """Reads the Bing Search API key from the .env file. - - Returns: - str: The Bing Search API key - """ - - api_key = None - config = dotenv_values(".env") - api_key = config.get("BING_API_KEY", None) - - assert api_key is not None, "Bing Search API key not found in .env file" - - return api_key - - -def mongodb_atlas_settings_from_dot_env() -> str: - """Returns the Atlas MongoDB Connection String from the .env file. - - Returns: - str: MongoDB Connection String URI - """ - - config = dotenv_values(".env") - uri = config.get("MONGODB_ATLAS_CONNECTION_STRING") - assert uri is not None, "MongoDB Connection String not found in .env file" - - return uri - - -def google_palm_settings_from_dot_env() -> str: - """ - Reads the Google PaLM API key from the .env file. - - Returns: - str: The Google PaLM API key - """ - - config = dotenv_values(".env") - api_key = config.get("GOOGLE_PALM_API_KEY", None) - - assert api_key is not None, "Google PaLM API key not found in .env file" - - return api_key - - -def azure_cosmos_db_settings_from_dot_env() -> Tuple[str, str]: - """ - Reads the Azure CosmosDB environment variables for the .env file. - Returns: - dict: The Azure CosmosDB environment variables - """ - config = dotenv_values(".env") - cosmos_api = config.get("AZCOSMOS_API") - cosmos_connstr = config.get("AZCOSMOS_CONNSTR") - - assert cosmos_connstr is not None, "Azure Cosmos Connection String not found in .env file" - - return cosmos_api, cosmos_connstr - - -def redis_settings_from_dot_env() -> str: - """Reads the Redis connection string from the .env file. - - Returns: - str: The Redis connection string - """ - config = dotenv_values(".env") - connection_string = config.get("REDIS_CONNECTION_STRING", None) - - assert connection_string is not None, "Redis connection string not found in .env file" - - return connection_string - - -def azure_aisearch_settings_from_dot_env( - include_index_name=False, -) -> Union[Tuple[str, str], Tuple[str, str, str]]: - """ - Reads the Azure AI Search environment variables for the .env file. - - Returns: - Tuple[str, str]: Azure AI Search API key, the Azure AI Search URL - """ - config = dotenv_values(".env") - api_key = config.get("AZURE_AISEARCH_API_KEY", None) - url = config.get("AZURE_AISEARCH_URL", None) - - assert url is not None, "Azure AI Search URL not found in .env file" - assert api_key is not None, "Azure AI Search API key not found in .env file" - - if not include_index_name: - return api_key, url - else: - index_name = config.get("AZURE_AISEARCH_INDEX_NAME", None) - assert index_name is not None, "Azure AI Search index name not found in .env file" - return api_key, url, index_name - - -def azure_aisearch_settings_from_dot_env_as_dict() -> dict[str, str]: - """ - Reads the Azure AI Search environment variables including index name from the .env file. - - Returns: - dict[str, str]: the Azure AI search environment variables - """ - api_key, url, index_name = azure_aisearch_settings_from_dot_env(include_index_name=True) - return {"authentication": {"type": "api_key", "key": api_key}, "endpoint": url, "index_name": index_name} - - -def azure_key_vault_settings_from_dot_env( - include_client_id: bool = True, include_client_secret: bool = True -) -> Tuple[str, Optional[str], Optional[str]]: - """ - Reads the Azure Key Vault environment variables for the .env file. - - Returns: - Tuple[str, str, str]: Azure Key Vault endpoint, the Azure Key Vault client ID, the Azure Key Vault client secret - """ - config = dotenv_values(".env") - endpoint = config.get("AZURE_KEY_VAULT_ENDPOINT", None) - client_id = config.get("AZURE_KEY_VAULT_CLIENT_ID", None) - client_secret = config.get("AZURE_KEY_VAULT_CLIENT_SECRET", None) - - assert endpoint is not None, "Azure Key Vault endpoint not found in .env file" - if include_client_id: - assert client_id is not None, "Azure Key Vault client ID not found in .env file" - if include_client_secret: - assert client_secret is not None, "Azure Key Vault client secret not found in .env file" - - if include_client_id and include_client_secret: - return endpoint, client_id, client_secret - return endpoint, client_id - - -def azure_key_vault_settings_from_dot_env_as_dict() -> dict[str, str]: - """ - Reads the Azure Key Vault environment variables for the .env file. - - Returns: - dict[str, str]: Azure Key Vault environment variables - """ - endpoint, client_id, client_secret = azure_key_vault_settings_from_dot_env() - return {"endpoint": endpoint, "client_id": client_id, "client_secret": client_secret} - - -def booking_sample_settings_from_dot_env() -> Tuple[str, str, str]: - """ - Reads the Booking Sample environment variables for the .env file. - - Returns: - Tuple[str, str]: Booking Sample environment variables - """ - config = dotenv_values(".env") - client_id = config.get("BOOKING_SAMPLE_CLIENT_ID", None) - tenant_id = config.get("BOOKING_SAMPLE_TENANT_ID", None) - client_secret = config.get("BOOKING_SAMPLE_CLIENT_SECRET", None) - - assert client_id, "Booking Sample Client ID not found in .env file" - assert tenant_id, "Booking Sample Tenant ID not found in .env file" - assert client_secret, "Booking Sample Client Secret not found in .env file" - - return client_id, tenant_id, client_secret - - -def booking_sample_settings_from_dot_env_as_dict() -> dict[str, str]: - """ - Reads the Booking Sample environment variables for the .env file. - - Returns: - dict[str, str]: Booking Sample environment variables - """ - client_id, tenant_id, client_secret = booking_sample_settings_from_dot_env() - return {"client_id": client_id, "tenant_id": tenant_id, "client_secret": client_secret} - - -def azure_container_apps_settings_from_dot_env() -> str: - """ - Reads the Azure Container Apps environment variables from the .env file. - Returns: - str: Azure Container Apps pool management connection string - """ - config = dotenv_values(".env") - connection_string = config.get("ACA_POOL_MANAGEMENT_ENDPOINT", None) - - assert connection_string is not None, "Azure Container Apps connection string not found in .env file" - - return connection_string - - -def azure_container_apps_settings_from_dot_env_as_dict() -> dict[str, str]: - """ - Reads the Azure Container Apps environment variables from the .env file. - Returns: - Dict[str, str]: Azure Container Apps environment variables - """ - pool_management_endpoint = azure_container_apps_settings_from_dot_env() - return {"pool_management_endpoint": pool_management_endpoint} diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 34d1b4557c..10a3e66dab 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -2,7 +2,6 @@ from __future__ import annotations -import os import warnings from typing import TYPE_CHECKING, Callable, List from unittest.mock import Mock @@ -174,44 +173,148 @@ def enable_debug_mode(): builtins.pr = snoop.pp -@pytest.fixture(scope="session") -def get_aoai_config(): - from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env - - if "Python_Integration_Tests" in os.environ: - deployment_name = os.environ["AzureOpenAIEmbeddings__DeploymentName"] - api_key = os.environ["AzureOpenAI_EastUS__ApiKey"] - endpoint = os.environ["AzureOpenAI_EastUS__Endpoint"] - else: - # Load credentials from .env file - deployment_name, api_key, endpoint = azure_openai_settings_from_dot_env() - deployment_name = "text-embedding-ada-002" - - return deployment_name, api_key, endpoint +@pytest.fixture +def exclude_list(request): + """Fixture that returns a list of environment variables to exclude.""" + return request.param if hasattr(request, "param") else [] -@pytest.fixture(scope="session") -def get_oai_config(): - from semantic_kernel.utils.settings import openai_settings_from_dot_env - - if "Python_Integration_Tests" in os.environ: - api_key = os.environ["OpenAI__ApiKey"] - org_id = None - else: - # Load credentials from .env file - api_key, org_id = openai_settings_from_dot_env() - - return api_key, org_id +@pytest.fixture +def override_env_param_dict(request): + """Fixture that returns a dict of environment variables to override.""" + return request.param if hasattr(request, "param") else {} -@pytest.fixture(scope="session") -def get_gp_config(): - from semantic_kernel.utils.settings import google_palm_settings_from_dot_env +@pytest.fixture() +def azure_openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): + """Fixture to set environment variables for AzureOpenAISettings.""" + if exclude_list is None: + exclude_list = [] - if "Python_Integration_Tests" in os.environ: - api_key = os.environ["GOOGLE_PALM_API_KEY"] - else: - # Load credentials from .env file - api_key = google_palm_settings_from_dot_env() + if override_env_param_dict is None: + override_env_param_dict = {} - return api_key + env_vars = { + "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "test_chat_deployment", + "AZURE_OPENAI_TEXT_DEPLOYMENT_NAME": "test_text_deployment", + "AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME": "test_embedding_deployment", + "AZURE_OPENAI_API_KEY": "test_api_key", + "AZURE_OPENAI_ENDPOINT": "https://test-endpoint.com", + "AZURE_OPENAI_API_VERSION": "2023-03-15-preview", + "AZURE_OPENAI_BASE_URL": "https://test_text_deployment.test-base-url.com", + } + + env_vars.update(override_env_param_dict) + + for key, value in env_vars.items(): + if key not in exclude_list: + monkeypatch.setenv(key, value) + else: + monkeypatch.delenv(key, raising=False) + + return env_vars + + +@pytest.fixture() +def openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): + """Fixture to set environment variables for OpenAISettings.""" + if exclude_list is None: + exclude_list = [] + + if override_env_param_dict is None: + override_env_param_dict = {} + + env_vars = { + "OPENAI_API_KEY": "test_api_key", + "OPENAI_ORG_ID": "test_org_id", + "OPENAI_CHAT_MODEL_ID": "test_chat_model_id", + "OPENAI_TEXT_MODEL_ID": "test_text_model_id", + "OPENAI_EMBEDDING_MODEL_ID": "test_embedding_model_id", + } + + env_vars.update(override_env_param_dict) + + for key, value in env_vars.items(): + if key not in exclude_list: + monkeypatch.setenv(key, value) + else: + monkeypatch.delenv(key, raising=False) + + return env_vars + + +@pytest.fixture() +def google_palm_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): + """Fixture to set environment variables for Google Palm.""" + if exclude_list is None: + exclude_list = [] + + if override_env_param_dict is None: + override_env_param_dict = {} + + env_vars = { + "GOOGLE_PALM_API_KEY": "test_api_key", + "OPENAI_CHAT_MODEL_ID": "test_chat_model_id", + "OPENAI_TEXT_MODEL_ID": "test_text_model_id", + "OPENAI_EMBEDDING_MODEL_ID": "test_embedding_model_id", + } + + env_vars.update(override_env_param_dict) + + for key, value in env_vars.items(): + if key not in exclude_list: + monkeypatch.setenv(key, value) + else: + monkeypatch.delenv(key, raising=False) + + return env_vars + + +@pytest.fixture() +def aca_python_sessions_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): + """Fixture to set environment variables for ACA Python Unit Tests.""" + if exclude_list is None: + exclude_list = [] + + if override_env_param_dict is None: + override_env_param_dict = {} + + env_vars = { + "ACA_POOL_MANAGEMENT_ENDPOINT": "https://test.endpoint/python/excute/", + } + + env_vars.update(override_env_param_dict) + + for key, value in env_vars.items(): + if key not in exclude_list: + monkeypatch.setenv(key, value) + else: + monkeypatch.delenv(key, raising=False) + + return env_vars + + +@pytest.fixture() +def azure_ai_search_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): + """Fixture to set environment variables for ACA Python Unit Tests.""" + if exclude_list is None: + exclude_list = [] + + if override_env_param_dict is None: + override_env_param_dict = {} + + env_vars = { + "AZURE_AI_SEARCH_API_KEY": "test-api-key", + "AZURE_AI_SEARCH_ENDPOINT": "https://test-endpoint.com", + "AZURE_AI_SEARCH_INDEX_NAME": "test-index-name", + } + + env_vars.update(override_env_param_dict) + + for key, value in env_vars.items(): + if key not in exclude_list: + monkeypatch.setenv(key, value) + else: + monkeypatch.delenv(key, raising=False) + + return env_vars diff --git a/python/tests/integration/completions/conftest.py b/python/tests/integration/completions/conftest.py index 129aeffbcd..9d775ac11a 100644 --- a/python/tests/integration/completions/conftest.py +++ b/python/tests/integration/completions/conftest.py @@ -84,10 +84,9 @@ def setup_summarize_conversation_using_plugin(kernel: Kernel): @pytest.fixture(scope="function") -def setup_gp_text_completion_function(kernel: Kernel, get_gp_config): - api_key = get_gp_config +def setup_gp_text_completion_function(kernel: Kernel): # Configure LLM service - palm_text_completion = sk_gp.GooglePalmTextCompletion(ai_model_id="models/text-bison-001", api_key=api_key) + palm_text_completion = sk_gp.GooglePalmTextCompletion(ai_model_id="models/text-bison-001") kernel.add_service(palm_text_completion) # Define semantic function using SK prompt template language diff --git a/python/tests/integration/completions/test_azure_oai_chat_service.py b/python/tests/integration/completions/test_azure_oai_chat_service.py index afe660b1d4..e98af4853d 100644 --- a/python/tests/integration/completions/test_azure_oai_chat_service.py +++ b/python/tests/integration/completions/test_azure_oai_chat_service.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. -import os import pytest from openai import AsyncAzureOpenAI @@ -11,6 +10,7 @@ from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBeh from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import ( AzureChatPromptExecutionSettings, ) +from semantic_kernel.connectors.ai.open_ai.settings.azure_open_ai_settings import AzureOpenAISettings from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent from semantic_kernel.core_plugins.math_plugin import MathPlugin @@ -20,24 +20,13 @@ from semantic_kernel.prompt_template.prompt_template_config import PromptTemplat @pytest.mark.asyncio -async def test_azure_e2e_chat_completion_with_plugin(setup_tldr_function_for_oai_models, get_aoai_config): +async def test_azure_e2e_chat_completion_with_plugin(setup_tldr_function_for_oai_models): kernel, prompt, text_to_summarize = setup_tldr_function_for_oai_models - _, api_key, endpoint = get_aoai_config - - if "Python_Integration_Tests" in os.environ: - deployment_name = os.environ["AzureOpenAIChat__DeploymentName"] - else: - deployment_name = "gpt-35-turbo" - - print("* Service: Azure OpenAI Chat Completion") - print(f"* Endpoint: {endpoint}") - print(f"* Deployment: {deployment_name}") - # Configure LLM service kernel.add_service( sk_oai.AzureChatCompletion( - service_id="chat", deployment_name=deployment_name, endpoint=endpoint, api_key=api_key + service_id="chat", ), ) @@ -62,27 +51,20 @@ async def test_azure_e2e_chat_completion_with_plugin(setup_tldr_function_for_oai @pytest.mark.asyncio -async def test_azure_e2e_chat_completion_with_plugin_and_provided_client( - setup_tldr_function_for_oai_models, get_aoai_config -): +async def test_azure_e2e_chat_completion_with_plugin_and_provided_client(setup_tldr_function_for_oai_models): kernel, prompt, text_to_summarize = setup_tldr_function_for_oai_models - _, api_key, endpoint = get_aoai_config - - if "Python_Integration_Tests" in os.environ: - deployment_name = os.environ["AzureOpenAIChat__DeploymentName"] - else: - deployment_name = "gpt-35-turbo" - - print("* Service: Azure OpenAI Chat Completion") - print(f"* Endpoint: {endpoint}") - print(f"* Deployment: {deployment_name}") + azure_openai_settings = AzureOpenAISettings.create() + endpoint = azure_openai_settings.endpoint + deployment_name = azure_openai_settings.chat_deployment_name + api_key = azure_openai_settings.api_key.get_secret_value() + api_version = azure_openai_settings.api_version client = AsyncAzureOpenAI( azure_endpoint=endpoint, azure_deployment=deployment_name, api_key=api_key, - api_version="2023-05-15", + api_version=api_version, default_headers={"Test-User-X-ID": "test"}, ) @@ -90,7 +72,6 @@ async def test_azure_e2e_chat_completion_with_plugin_and_provided_client( kernel.add_service( sk_oai.AzureChatCompletion( service_id="chat_completion", - deployment_name=deployment_name, async_client=client, ), ) @@ -116,23 +97,18 @@ async def test_azure_e2e_chat_completion_with_plugin_and_provided_client( @pytest.mark.asyncio -async def test_azure_oai_chat_service_with_tool_call(kernel: Kernel, get_aoai_config): - _, api_key, endpoint = get_aoai_config - - if "Python_Integration_Tests" in os.environ: - deployment_name = os.environ["AzureOpenAIChat__DeploymentName"] - else: - deployment_name = "gpt-35-turbo-0613" - - print("* Service: Azure OpenAI Chat Completion") - print(f"* Endpoint: {endpoint}") - print(f"* Deployment: {deployment_name}") +async def test_azure_oai_chat_service_with_tool_call(kernel: Kernel): + azure_openai_settings = AzureOpenAISettings.create() + endpoint = azure_openai_settings.endpoint + deployment_name = azure_openai_settings.chat_deployment_name + api_key = azure_openai_settings.api_key.get_secret_value() + api_version = azure_openai_settings.api_version client = AsyncAzureOpenAI( azure_endpoint=endpoint, azure_deployment=deployment_name, api_key=api_key, - api_version="2023-05-15", + api_version=api_version, default_headers={"Test-User-X-ID": "test"}, ) @@ -140,7 +116,6 @@ async def test_azure_oai_chat_service_with_tool_call(kernel: Kernel, get_aoai_co kernel.add_service( sk_oai.AzureChatCompletion( service_id="chat_completion", - deployment_name=deployment_name, async_client=client, ), ) @@ -176,23 +151,18 @@ async def test_azure_oai_chat_service_with_tool_call(kernel: Kernel, get_aoai_co @pytest.mark.asyncio -async def test_azure_oai_chat_service_with_tool_call_streaming(kernel: Kernel, get_aoai_config): - _, api_key, endpoint = get_aoai_config - - if "Python_Integration_Tests" in os.environ: - deployment_name = os.environ["AzureOpenAIChat__DeploymentName"] - else: - deployment_name = "gpt-35-turbo-0613" - - print("* Service: Azure OpenAI Chat Completion") - print(f"* Endpoint: {endpoint}") - print(f"* Deployment: {deployment_name}") +async def test_azure_oai_chat_service_with_tool_call_streaming(kernel: Kernel): + azure_openai_settings = AzureOpenAISettings.create() + endpoint = azure_openai_settings.endpoint + deployment_name = azure_openai_settings.chat_deployment_name + api_key = azure_openai_settings.api_key.get_secret_value() + api_version = azure_openai_settings.api_version client = AsyncAzureOpenAI( azure_endpoint=endpoint, azure_deployment=deployment_name, api_key=api_key, - api_version="2024-02-01", + api_version=api_version, default_headers={"Test-User-X-ID": "test"}, ) @@ -200,7 +170,6 @@ async def test_azure_oai_chat_service_with_tool_call_streaming(kernel: Kernel, g kernel.add_service( sk_oai.AzureChatCompletion( service_id="chat_completion", - deployment_name=deployment_name, async_client=client, ), ) @@ -208,7 +177,7 @@ async def test_azure_oai_chat_service_with_tool_call_streaming(kernel: Kernel, g kernel.add_plugin(MathPlugin(), plugin_name="Math") # Create the prompt function - kernel.add_function(prompt="{{$input}}", function_name="chat", plugin_name="chat") + kernel.add_function(prompt="Keep the answer short. {{$input}}", function_name="chat", plugin_name="chat") execution_settings = sk_oai.AzureChatPromptExecutionSettings( service_id="chat_completion", max_tokens=2000, @@ -227,4 +196,4 @@ async def test_azure_oai_chat_service_with_tool_call_streaming(kernel: Kernel, g print(f"Math output: '{output}'") assert "2" in output - assert 0 < len(output) < 100 + assert 0 < len(output) < 500 diff --git a/python/tests/integration/completions/test_azure_oai_chat_service_extensions.py b/python/tests/integration/completions/test_azure_oai_chat_service_extensions.py index c240985a95..e6087f585c 100644 --- a/python/tests/integration/completions/test_azure_oai_chat_service_extensions.py +++ b/python/tests/integration/completions/test_azure_oai_chat_service_extensions.py @@ -77,20 +77,9 @@ of climate change.", @pytest.fixture(scope="function") @pytest.mark.asyncio -async def create_with_data_chat_function(get_aoai_config, kernel: Kernel, create_memory_store): +async def create_with_data_chat_function(kernel: Kernel, create_memory_store): collection, memory_store = await create_memory_store try: - deployment_name, api_key, endpoint = get_aoai_config - - if "Python_Integration_Tests" in os.environ: - deployment_name = os.environ["AzureOpenAIChat__DeploymentName"] - else: - deployment_name = "gpt-35-turbo" - - print("* Service: Azure OpenAI Chat Completion") - print(f"* Endpoint: {endpoint}") - print(f"* Deployment: {deployment_name}") - # Load Azure OpenAI with data settings search_endpoint = os.getenv("AZURE_COGNITIVE_SEARCH_ENDPOINT") search_api_key = os.getenv("AZURE_COGNITIVE_SEARCH_ADMIN_KEY") @@ -112,13 +101,8 @@ async def create_with_data_chat_function(get_aoai_config, kernel: Kernel, create ) ] ) - print(f"deployment: {deployment_name}, endpoint: {endpoint}") chat_service = sk_oai.AzureChatCompletion( service_id="chat-gpt-extensions", - deployment_name=deployment_name, - api_key=api_key, - endpoint=endpoint, - api_version="2024-02-01", ) kernel.add_service(chat_service) diff --git a/python/tests/integration/completions/test_azure_oai_text_service.py b/python/tests/integration/completions/test_azure_oai_text_service.py index 30c8b501aa..dbc9e40dea 100644 --- a/python/tests/integration/completions/test_azure_oai_text_service.py +++ b/python/tests/integration/completions/test_azure_oai_text_service.py @@ -1,44 +1,32 @@ # Copyright (c) Microsoft. All rights reserved. -import os import pytest from openai import AsyncAzureOpenAI from test_utils import retry import semantic_kernel.connectors.ai.open_ai as sk_oai +from semantic_kernel.connectors.ai.open_ai.settings.azure_open_ai_settings import AzureOpenAISettings from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig @pytest.mark.asyncio -async def test_azure_e2e_text_completion_with_plugin(setup_tldr_function_for_oai_models, get_aoai_config): +async def test_azure_e2e_text_completion_with_plugin(setup_tldr_function_for_oai_models): kernel, prompt, text_to_summarize = setup_tldr_function_for_oai_models - _, api_key, endpoint = get_aoai_config - - if "Python_Integration_Tests" in os.environ: - deployment_name = os.environ["AzureOpenAI__Text__DeploymentName"] - else: - deployment_name = "gpt-35-turbo-instruct" - - print("* Service: Azure OpenAI Text Completion") - print(f"* Endpoint: {endpoint}") - print(f"* Deployment: {deployment_name}") + service_id = "text_completion" # Configure LLM service kernel.add_service( sk_oai.AzureTextCompletion( - service_id="text_completion", - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, + service_id=service_id, ), ) exec_settings = PromptExecutionSettings( - service_id="text_completion", extension_data={"max_tokens": 200, "temperature": 0, "top_p": 0.5} + service_id=service_id, extension_data={"max_tokens": 200, "temperature": 0, "top_p": 0.5} ) prompt_template_config = PromptTemplateConfig( @@ -59,42 +47,36 @@ async def test_azure_e2e_text_completion_with_plugin(setup_tldr_function_for_oai @pytest.mark.asyncio -async def test_azure_e2e_text_completion_with_plugin_with_provided_client( - setup_tldr_function_for_oai_models, get_aoai_config -): +async def test_azure_e2e_text_completion_with_plugin_with_provided_client(setup_tldr_function_for_oai_models): kernel, prompt, text_to_summarize = setup_tldr_function_for_oai_models - _, api_key, endpoint = get_aoai_config - - if "Python_Integration_Tests" in os.environ: - deployment_name = os.environ["AzureOpenAI__Text__DeploymentName"] - else: - deployment_name = "gpt-35-turbo-instruct" - - print("* Service: Azure OpenAI Text Completion") - print(f"* Endpoint: {endpoint}") - print(f"* Deployment: {deployment_name}") + azure_openai_settings = AzureOpenAISettings.create() + endpoint = azure_openai_settings.endpoint + deployment_name = azure_openai_settings.chat_deployment_name + api_key = azure_openai_settings.api_key.get_secret_value() + api_version = azure_openai_settings.api_version client = AsyncAzureOpenAI( azure_endpoint=endpoint, azure_deployment=deployment_name, api_key=api_key, - api_version="2023-05-15", + api_version=api_version, default_headers={"Test-User-X-ID": "test"}, ) + service_id = "text_completion" + # Configure LLM service kernel.add_service( sk_oai.AzureTextCompletion( - service_id="text_completion", - deployment_name=deployment_name, + service_id=service_id, async_client=client, ), overwrite=True, # Overwrite the service for the test if it already exists ) exec_settings = PromptExecutionSettings( - service_id="text_completion", extension_data={"max_tokens": 200, "temperature": 0, "top_p": 0.5} + service_id=service_id, extension_data={"max_tokens": 200, "temperature": 0, "top_p": 0.5} ) prompt_template_config = PromptTemplateConfig( @@ -111,4 +93,4 @@ async def test_azure_e2e_text_completion_with_plugin_with_provided_client( summary = await retry(lambda: kernel.invoke(tldr_function, arguments)) output = str(summary).strip() print(f"TLDR using input string: '{output}'") - assert len(output) < 100 + assert len(output) > 0 diff --git a/python/tests/integration/completions/test_conversation_summary_plugin.py b/python/tests/integration/completions/test_conversation_summary_plugin.py index f4b58cc409..c6fbd0448f 100644 --- a/python/tests/integration/completions/test_conversation_summary_plugin.py +++ b/python/tests/integration/completions/test_conversation_summary_plugin.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. -import os import pytest from test_utils import retry @@ -12,22 +11,12 @@ from semantic_kernel.core_plugins.conversation_summary_plugin import ( ) from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig -from semantic_kernel.utils.settings import openai_settings_from_dot_env @pytest.mark.asyncio -async def test_azure_summarize_conversation_using_plugin(setup_summarize_conversation_using_plugin, get_aoai_config): +async def test_azure_summarize_conversation_using_plugin(setup_summarize_conversation_using_plugin): kernel, chatTranscript = setup_summarize_conversation_using_plugin - if "Python_Integration_Tests" in os.environ: - deployment_name = os.environ["AzureOpenAI__DeploymentName"] - api_key = os.environ["AzureOpenAI__ApiKey"] - endpoint = os.environ["AzureOpenAI__Endpoint"] - else: - # Load credentials from .env file - deployment_name, api_key, endpoint = get_aoai_config - deployment_name = "gpt-35-turbo-instruct" - service_id = "text_completion" execution_settings = PromptExecutionSettings( @@ -41,7 +30,7 @@ async def test_azure_summarize_conversation_using_plugin(setup_summarize_convers kernel.add_service( sk_oai.AzureTextCompletion( - service_id=service_id, deployment_name=deployment_name, endpoint=endpoint, api_key=api_key + service_id=service_id, ), ) @@ -65,13 +54,6 @@ async def test_oai_summarize_conversation_using_plugin( ): kernel, chatTranscript = setup_summarize_conversation_using_plugin - if "Python_Integration_Tests" in os.environ: - api_key = os.environ["OpenAI__ApiKey"] - org_id = None - else: - # Load credentials from .env file - api_key, org_id = openai_settings_from_dot_env() - execution_settings = PromptExecutionSettings( service_id="conversation_summary", max_tokens=ConversationSummaryPlugin._max_tokens, temperature=0.1, top_p=0.5 ) @@ -83,7 +65,8 @@ async def test_oai_summarize_conversation_using_plugin( kernel.add_service( sk_oai.OpenAITextCompletion( - service_id="conversation_summary", ai_model_id="gpt-3.5-turbo-instruct", api_key=api_key, org_id=org_id + service_id="conversation_summary", + ai_model_id="gpt-3.5-turbo-instruct", ), ) diff --git a/python/tests/integration/completions/test_gp_chat_service.py b/python/tests/integration/completions/test_gp_chat_service.py index 061897f274..a337d675b6 100644 --- a/python/tests/integration/completions/test_gp_chat_service.py +++ b/python/tests/integration/completions/test_gp_chat_service.py @@ -23,14 +23,13 @@ pytestmark = [ @pytest.mark.asyncio -async def test_gp_chat_service_with_plugins(setup_tldr_function_for_oai_models, get_gp_config): +async def test_gp_chat_service_with_plugins(setup_tldr_function_for_oai_models): kernel, prompt, text_to_summarize = setup_tldr_function_for_oai_models - api_key = get_gp_config print("* Service: Google PaLM Chat Completion") print("* Model: chat-bison-001") model_id = "models/chat-bison-001" - palm_chat_completion = sk_gp.GooglePalmChatCompletion(ai_model_id=model_id, api_key=api_key) + palm_chat_completion = sk_gp.GooglePalmChatCompletion(ai_model_id=model_id) kernel.add_service(palm_chat_completion) exec_settings = PromptExecutionSettings( @@ -49,5 +48,4 @@ async def test_gp_chat_service_with_plugins(setup_tldr_function_for_oai_models, summary = await retry(lambda: kernel.invoke(tldr_function, arguments)) output = str(summary).strip() print(f"TLDR using input string: '{output}'") - # assert "First Law" not in output and ("human" in output or "Human" in output or "preserve" in output) assert len(output) > 0 diff --git a/python/tests/integration/completions/test_oai_chat_service.py b/python/tests/integration/completions/test_oai_chat_service.py index e7e758acff..edd2d7ba32 100644 --- a/python/tests/integration/completions/test_oai_chat_service.py +++ b/python/tests/integration/completions/test_oai_chat_service.py @@ -7,6 +7,7 @@ from test_utils import retry import semantic_kernel.connectors.ai.open_ai as sk_oai from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBehavior +from semantic_kernel.connectors.ai.open_ai.settings.open_ai_settings import OpenAISettings from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.core_plugins.math_plugin import MathPlugin @@ -14,17 +15,11 @@ from semantic_kernel.prompt_template.prompt_template_config import PromptTemplat @pytest.mark.asyncio -async def test_oai_chat_service_with_plugins(setup_tldr_function_for_oai_models, get_oai_config): +async def test_oai_chat_service_with_plugins(setup_tldr_function_for_oai_models): kernel, prompt, text_to_summarize = setup_tldr_function_for_oai_models - api_key, org_id = get_oai_config - - print("* Service: OpenAI Chat Completion") - print("* Endpoint: OpenAI") - print("* Model: gpt-3.5-turbo") - kernel.add_service( - sk_oai.OpenAIChatCompletion(service_id="chat-gpt", ai_model_id="gpt-3.5-turbo", api_key=api_key, org_id=org_id), + sk_oai.OpenAIChatCompletion(service_id="chat-gpt", ai_model_id="gpt-3.5-turbo"), ) exec_settings = PromptExecutionSettings( @@ -48,18 +43,13 @@ async def test_oai_chat_service_with_plugins(setup_tldr_function_for_oai_models, @pytest.mark.asyncio -async def test_oai_chat_service_with_tool_call(setup_tldr_function_for_oai_models, get_oai_config): +async def test_oai_chat_service_with_tool_call(setup_tldr_function_for_oai_models): kernel, _, _ = setup_tldr_function_for_oai_models - api_key, org_id = get_oai_config - - print("* Service: OpenAI Chat Completion") - print("* Endpoint: OpenAI") - print("* Model: gpt-3.5-turbo-1106") - kernel.add_service( sk_oai.OpenAIChatCompletion( - service_id="chat-gpt", ai_model_id="gpt-3.5-turbo-1106", api_key=api_key, org_id=org_id + service_id="chat-gpt", + ai_model_id="gpt-3.5-turbo-1106", ), ) @@ -92,18 +82,13 @@ async def test_oai_chat_service_with_tool_call(setup_tldr_function_for_oai_model @pytest.mark.asyncio -async def test_oai_chat_service_with_tool_call_streaming(setup_tldr_function_for_oai_models, get_oai_config): +async def test_oai_chat_service_with_tool_call_streaming(setup_tldr_function_for_oai_models): kernel, _, _ = setup_tldr_function_for_oai_models - api_key, org_id = get_oai_config - - print("* Service: OpenAI Chat Completion") - print("* Endpoint: OpenAI") - print("* Model: gpt-3.5-turbo-1106") - kernel.add_service( sk_oai.OpenAIChatCompletion( - service_id="chat-gpt", ai_model_id="gpt-3.5-turbo-1106", api_key=api_key, org_id=org_id + service_id="chat-gpt", + ai_model_id="gpt-3.5-turbo-1106", ), ) @@ -139,14 +124,12 @@ async def test_oai_chat_service_with_tool_call_streaming(setup_tldr_function_for @pytest.mark.asyncio -async def test_oai_chat_service_with_plugins_with_provided_client(setup_tldr_function_for_oai_models, get_oai_config): +async def test_oai_chat_service_with_plugins_with_provided_client(setup_tldr_function_for_oai_models): kernel, prompt, text_to_summarize = setup_tldr_function_for_oai_models - api_key, org_id = get_oai_config - - print("* Service: OpenAI Chat Completion") - print("* Endpoint: OpenAI") - print("* Model: gpt-3.5-turbo") + openai_settings = OpenAISettings.create() + api_key = openai_settings.api_key.get_secret_value() + org_id = openai_settings.org_id client = AsyncOpenAI( api_key=api_key, @@ -185,24 +168,13 @@ async def test_oai_chat_service_with_plugins_with_provided_client(setup_tldr_fun @pytest.mark.asyncio -async def test_oai_chat_stream_service_with_plugins(setup_tldr_function_for_oai_models, get_aoai_config): +async def test_azure_oai_chat_stream_service_with_plugins(setup_tldr_function_for_oai_models): kernel, prompt, text_to_summarize = setup_tldr_function_for_oai_models - _, api_key, endpoint = get_aoai_config - - if "Python_Integration_Tests" in os.environ: - deployment_name = os.environ["AzureOpenAIChat__DeploymentName"] - else: - deployment_name = "gpt-35-turbo" - - print("* Service: Azure OpenAI Chat Completion") - print(f"* Endpoint: {endpoint}") - print(f"* Deployment: {deployment_name}") - # Configure LLM service kernel.add_service( sk_oai.AzureChatCompletion( - service_id="chat_completion", deployment_name=deployment_name, endpoint=endpoint, api_key=api_key + service_id="chat_completion", ), overwrite=True, ) @@ -233,14 +205,12 @@ async def test_oai_chat_stream_service_with_plugins(setup_tldr_function_for_oai_ @pytest.mark.asyncio -async def test_oai_chat_service_with_yaml_jinja2(setup_tldr_function_for_oai_models, get_oai_config): +async def test_oai_chat_service_with_yaml_jinja2(setup_tldr_function_for_oai_models): kernel, _, _ = setup_tldr_function_for_oai_models - api_key, org_id = get_oai_config - - print("* Service: OpenAI Chat Completion") - print("* Endpoint: OpenAI") - print("* Model: gpt-3.5-turbo") + openai_settings = OpenAISettings.create() + api_key = openai_settings.api_key.get_secret_value() + org_id = openai_settings.org_id client = AsyncOpenAI( api_key=api_key, @@ -272,14 +242,12 @@ async def test_oai_chat_service_with_yaml_jinja2(setup_tldr_function_for_oai_mod @pytest.mark.asyncio -async def test_oai_chat_service_with_yaml_handlebars(setup_tldr_function_for_oai_models, get_oai_config): +async def test_oai_chat_service_with_yaml_handlebars(setup_tldr_function_for_oai_models): kernel, _, _ = setup_tldr_function_for_oai_models - api_key, org_id = get_oai_config - - print("* Service: OpenAI Chat Completion") - print("* Endpoint: OpenAI") - print("* Model: gpt-3.5-turbo") + openai_settings = OpenAISettings.create() + api_key = openai_settings.api_key.get_secret_value() + org_id = openai_settings.org_id client = AsyncOpenAI( api_key=api_key, diff --git a/python/tests/integration/completions/test_oai_text_service.py b/python/tests/integration/completions/test_oai_text_service.py index 8de1fad490..0c2df6baad 100644 --- a/python/tests/integration/completions/test_oai_text_service.py +++ b/python/tests/integration/completions/test_oai_text_service.py @@ -1,30 +1,22 @@ # Copyright (c) Microsoft. All rights reserved. -import os import pytest from openai import AsyncOpenAI from test_utils import retry import semantic_kernel.connectors.ai.open_ai as sk_oai +from semantic_kernel.connectors.ai.open_ai.settings.open_ai_settings import OpenAISettings from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig @pytest.mark.asyncio -async def test_oai_text_completion_with_plugins(setup_tldr_function_for_oai_models, get_oai_config): +async def test_oai_text_completion_with_plugins(setup_tldr_function_for_oai_models): kernel, prompt, text_to_summarize = setup_tldr_function_for_oai_models - api_key, org_id = get_oai_config - - print("* Service: OpenAI Text Completion") - print("* Endpoint: OpenAI") - print("* Model: gpt-3.5-turbo-instruct") - kernel.add_service( - sk_oai.OpenAITextCompletion( - service_id="text-completion", ai_model_id="gpt-3.5-turbo-instruct", api_key=api_key, org_id=org_id - ), + sk_oai.OpenAITextCompletion(service_id="text-completion", ai_model_id="gpt-3.5-turbo-instruct"), ) exec_settings = PromptExecutionSettings( @@ -50,16 +42,12 @@ async def test_oai_text_completion_with_plugins(setup_tldr_function_for_oai_mode @pytest.mark.asyncio -async def test_oai_text_completion_with_plugins_with_provided_client( - setup_tldr_function_for_oai_models, get_oai_config -): +async def test_oai_text_completion_with_plugins_with_provided_client(setup_tldr_function_for_oai_models): kernel, prompt, text_to_summarize = setup_tldr_function_for_oai_models - api_key, org_id = get_oai_config - - print("* Service: OpenAI Text Completion") - print("* Endpoint: OpenAI") - print("* Model: gpt-3.5-turbo-instruct") + openai_settings = OpenAISettings.create() + api_key = openai_settings.api_key.get_secret_value() + org_id = openai_settings.org_id client = AsyncOpenAI( api_key=api_key, @@ -100,27 +88,13 @@ async def test_oai_text_completion_with_plugins_with_provided_client( @pytest.mark.asyncio -async def test_oai_text_stream_completion_with_plugins(setup_tldr_function_for_oai_models, get_aoai_config): +async def test_azure_oai_text_stream_completion_with_plugins(setup_tldr_function_for_oai_models): kernel, prompt, text_to_summarize = setup_tldr_function_for_oai_models - _, api_key, endpoint = get_aoai_config - - if "Python_Integration_Tests" in os.environ: - deployment_name = os.environ["AzureOpenAI__DeploymentName"] - else: - deployment_name = "gpt-35-turbo-instruct" - - print("* Service: Azure OpenAI Text Completion") - print(f"* Endpoint: {endpoint}") - print(f"* Deployment: {deployment_name}") - # Configure LLM service kernel.add_service( sk_oai.AzureTextCompletion( service_id="text_completion", - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, ), ) diff --git a/python/tests/integration/connectors/memory/test_astradb.py b/python/tests/integration/connectors/memory/test_astradb.py index b01b90bc26..01b742fa82 100644 --- a/python/tests/integration/connectors/memory/test_astradb.py +++ b/python/tests/integration/connectors/memory/test_astradb.py @@ -4,9 +4,10 @@ import os import time import pytest +from pydantic import ValidationError from semantic_kernel.connectors.memory.astradb import AstraDBMemoryStore -from semantic_kernel.utils.settings import astradb_settings_from_dot_env +from semantic_kernel.connectors.memory.astradb.astradb_settings import AstraDBSettings astradb_installed: bool try: @@ -36,16 +37,15 @@ def slow_down_tests(): @pytest.fixture(scope="session") def get_astradb_config(): - if "Python_Integration_Tests" in os.environ: - app_token = os.environ["ASTRADB_APP_TOKEN"] - db_id = os.environ["ASTRADB_ID"] - region = os.environ["ASTRADB_REGION"] - keyspace = os.environ["ASTRADB_KEYSPACE"] - else: - # Load credentials from .env file - app_token, db_id, region, keyspace = astradb_settings_from_dot_env() - - return app_token, db_id, region, keyspace + try: + astradb_settings = AstraDBSettings() + app_token = astradb_settings.app_token.get_secret_value() + db_id = astradb_settings.db_id + region = astradb_settings.region + keyspace = astradb_settings.keyspace + return app_token, db_id, region, keyspace + except ValidationError: + pytest.skip("AsbtraDBSettings not found in env vars.") @pytest.mark.asyncio 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 703159019a..ac3da61389 100644 --- a/python/tests/integration/connectors/memory/test_azure_cognitive_search.py +++ b/python/tests/integration/connectors/memory/test_azure_cognitive_search.py @@ -10,7 +10,7 @@ import pytest from semantic_kernel.connectors.memory.azure_cognitive_search.azure_cognitive_search_memory_store import ( AzureCognitiveSearchMemoryStore, ) -from semantic_kernel.exceptions import ServiceResourceNotFoundError +from semantic_kernel.exceptions import MemoryConnectorResourceNotFound from semantic_kernel.memory.memory_record import MemoryRecord try: @@ -117,7 +117,7 @@ async def test_record_not_found(): # Clean up and fail await memory_store.delete_collection(collection) assert False - except ServiceResourceNotFoundError: + except MemoryConnectorResourceNotFound: pass await memory_store.delete_collection(collection) diff --git a/python/tests/integration/connectors/memory/test_mongodb_atlas.py b/python/tests/integration/connectors/memory/test_mongodb_atlas.py index e4def4f719..8d45666de3 100644 --- a/python/tests/integration/connectors/memory/test_mongodb_atlas.py +++ b/python/tests/integration/connectors/memory/test_mongodb_atlas.py @@ -1,16 +1,19 @@ # Copyright (c) Microsoft. All rights reserved. -import os 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.memory.memory_record import MemoryRecord mongodb_atlas_installed: bool @@ -64,11 +67,18 @@ def test_collection(): return f"AVSTest-{random.randint(0,9999)}" +@pytest.fixture(scope="session") +def connection_string(): + try: + mongodb_atlas_settings = MongoDBAtlasSettings.create() + return mongodb_atlas_settings.api_key.get_secret_value() + except ValidationError: + pytest.skip("MongoDB Atlas connection string not found in env vars.") + + @pytest_asyncio.fixture async def vector_search_store(): - if "Python_Integration_Tests" in os.environ: - connection_string = os.environ["MONGODB_ATLAS_CONNECTION_STRING"] - async with MongoDBAtlasMemoryStore(connection_string=connection_string, database_name="pyMSKTest") as memory: + async with MongoDBAtlasMemoryStore(connection_string, database_name="pyMSKTest") as memory: # Delete all collections before and after for cname in await memory.get_collections(): await memory.delete_collection(cname) @@ -105,9 +115,7 @@ async def vector_search_store(): @pytest_asyncio.fixture async def nearest_match_store(): """Fixture for read only vector store; the URI for test needs atlas configured""" - if "Python_Integration_Tests" in os.environ: - connection_string = os.environ["MONGODB_ATLAS_CONNECTION_STRING"] - async with MongoDBAtlasMemoryStore(connection_string=connection_string, database_name="pyMSKTest") as memory: + async with MongoDBAtlasMemoryStore(connection_string, database_name="pyMSKTest") as memory: if not await memory.does_collection_exist("nearestSearch"): pytest.skip( reason="db: readOnly collection: nearestSearch not found, " diff --git a/python/tests/integration/connectors/memory/test_pinecone.py b/python/tests/integration/connectors/memory/test_pinecone.py index aaca0d9b70..d9b3603213 100644 --- a/python/tests/integration/connectors/memory/test_pinecone.py +++ b/python/tests/integration/connectors/memory/test_pinecone.py @@ -1,16 +1,16 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio -import os import time import numpy as np import pytest +from pydantic import ValidationError from semantic_kernel.connectors.memory.pinecone import PineconeMemoryStore +from semantic_kernel.connectors.memory.pinecone.pinecone_settings import PineconeSettings from semantic_kernel.exceptions.service_exceptions import ServiceResourceNotFoundError from semantic_kernel.memory.memory_record import MemoryRecord -from semantic_kernel.utils.settings import pinecone_settings_from_dot_env try: import pinecone # noqa: F401 @@ -43,13 +43,11 @@ def slow_down_tests(): @pytest.fixture(scope="session") def api_key(): - if "Python_Integration_Tests" in os.environ: - api_key = os.environ["Pinecone__ApiKey"] - else: - # Load credentials from .env file - api_key = pinecone_settings_from_dot_env() - - return api_key + try: + pinecone_settings = PineconeSettings.create() + return pinecone_settings.api_key.get_secret_value() + except ValidationError: + pytest.skip("Pinecone API key not found in env vars.") @pytest.fixture diff --git a/python/tests/integration/connectors/memory/test_postgres.py b/python/tests/integration/connectors/memory/test_postgres.py index 201ddb91cb..738d2a87c5 100644 --- a/python/tests/integration/connectors/memory/test_postgres.py +++ b/python/tests/integration/connectors/memory/test_postgres.py @@ -1,12 +1,12 @@ # Copyright (c) Microsoft. All rights reserved. -import os import time import pytest +from pydantic import ValidationError -import semantic_kernel as sk from semantic_kernel.connectors.memory.postgres import PostgresMemoryStore +from semantic_kernel.connectors.memory.postgres.postgres_settings import PostgresSettings from semantic_kernel.exceptions import ServiceResourceNotFoundError try: @@ -37,13 +37,11 @@ def wait_between_tests(): @pytest.fixture(scope="session") def connection_string(): - if "Python_Integration_Tests" in os.environ: - connection_string = os.environ["Postgres__Connectionstr"] - else: - # Load credentials from .env file - connection_string = sk.postgres_settings_from_dot_env() - - return connection_string + try: + postgres_settings = PostgresSettings.create() + return postgres_settings.connection_string.get_secret_value() + except ValidationError: + pytest.skip("Postgres Connection string not found in env vars.") def test_constructor(connection_string): diff --git a/python/tests/integration/connectors/memory/test_redis.py b/python/tests/integration/connectors/memory/test_redis.py index e17b4b6b21..83f6684d5e 100644 --- a/python/tests/integration/connectors/memory/test_redis.py +++ b/python/tests/integration/connectors/memory/test_redis.py @@ -1,13 +1,12 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio -import os import platform import pytest -import semantic_kernel as sk from semantic_kernel.connectors.memory.redis import RedisMemoryStore +from semantic_kernel.connectors.memory.redis.redis_settings import RedisSettings try: import redis # noqa: F401 @@ -21,7 +20,7 @@ except ImportError: pytestmark = pytest.mark.skipif(not redis_installed, reason="Redis is not installed") pytestmark = pytest.mark.skipif( - platform.system() != "Linux" and "Python_Integration_Tests" in os.environ, + platform.system() != "Linux", reason="local redis docker container is not available on all non-Linux platforms", ) @@ -29,11 +28,13 @@ pytestmark = pytest.mark.skipif( @pytest.fixture(scope="session") def connection_string(): try: - connection_string = sk.redis_settings_from_dot_env() + redis_settings = RedisSettings.create() + if redis_settings.connection_string: + return redis_settings.connection_string.get_secret_value() + else: + return "redis://localhost:6379" except Exception: - connection_string = "redis://localhost:6379" - - return connection_string + pytest.skip("Redis connection string not found in env vars.") @pytest.fixture 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 84b884dc0e..e51b70ab66 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 as np import numpy.testing as npt import pytest -from semantic_kernel.connectors.memory.weaviate import weaviate_memory_store +from semantic_kernel.connectors.memory.weaviate.weaviate_memory_store import WeaviateConfig, WeaviateMemoryStore from semantic_kernel.memory.memory_record import MemoryRecord if not sys.platform.startswith("linux"): @@ -74,19 +74,19 @@ def documents(): @pytest.fixture def memory_store(): max_attempts = 5 # the number of retry attempts - delay = 30 # delay in seconds between each attempt + delay = 3 # delay in seconds between each attempt - config = weaviate_memory_store.WeaviateConfig(use_embed=True) + config = WeaviateConfig(use_embed=True) for attempt in range(max_attempts): try: - store = weaviate_memory_store.WeaviateMemoryStore(config) + store = WeaviateMemoryStore(config=config) store.client.schema.delete_all() except Exception: if attempt < max_attempts - 1: # it's not the final attempt time.sleep(delay) # wait before retrying continue # go to the next attempt else: # it's the final attempt - raise # re-raise the last exception + pytest.skip("Unable to start Weaviate memory store.") else: break # successful attempt, get out of the loop @@ -116,8 +116,8 @@ def memory_store_with_collection(memory_store, event_loop, documents): def test_embedded_weaviate(): - config = weaviate_memory_store.WeaviateConfig(use_embed=True) - memory_store = weaviate_memory_store.WeaviateMemoryStore(config=config) + config = WeaviateConfig(use_embed=True) + memory_store = WeaviateMemoryStore(config=config) assert memory_store.client._connection.embedded_db diff --git a/python/tests/integration/embeddings/test_azure_oai_embedding_service.py b/python/tests/integration/embeddings/test_azure_oai_embedding_service.py index 49de10ae55..957fd455c3 100644 --- a/python/tests/integration/embeddings/test_azure_oai_embedding_service.py +++ b/python/tests/integration/embeddings/test_azure_oai_embedding_service.py @@ -1,36 +1,28 @@ # Copyright (c) Microsoft. All rights reserved. -import os import pytest from openai import AsyncAzureOpenAI import semantic_kernel as sk import semantic_kernel.connectors.ai.open_ai as sk_oai +from semantic_kernel.connectors.ai.open_ai.settings.azure_open_ai_settings import AzureOpenAISettings +from semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin from semantic_kernel.kernel import Kernel from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory +from semantic_kernel.memory.volatile_memory_store import VolatileMemoryStore @pytest.mark.asyncio -async def test_azure_text_embedding_service(kernel: Kernel, get_aoai_config): - _, api_key, endpoint = get_aoai_config - - if "Python_Integration_Tests" in os.environ: - deployment_name = os.environ["AzureOpenAIEmbeddings_EastUS__DeploymentName"] - else: - deployment_name = "text-embedding-ada-002" - +async def test_azure_text_embedding_service(kernel: Kernel): embeddings_gen = sk_oai.AzureTextEmbedding( service_id="aoai-ada", - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, ) kernel.add_service(embeddings_gen) - memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embeddings_gen) - kernel.add_plugin(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") + memory = SemanticTextMemory(storage=VolatileMemoryStore(), embeddings_generator=embeddings_gen) + kernel.add_plugin(TextMemoryPlugin(memory), "TextMemoryPlugin") await memory.save_information(collection="generic", id="info1", text="My budget for 2024 is $100,000") await memory.save_reference( @@ -42,31 +34,30 @@ async def test_azure_text_embedding_service(kernel: Kernel, get_aoai_config): @pytest.mark.asyncio -async def test_azure_text_embedding_service_with_provided_client(kernel: Kernel, get_aoai_config): - _, api_key, endpoint = get_aoai_config +async def test_azure_text_embedding_service_with_provided_client(kernel: Kernel): - if "Python_Integration_Tests" in os.environ: - deployment_name = os.environ["AzureOpenAIEmbeddings_EastUS__DeploymentName"] - else: - deployment_name = "text-embedding-ada-002" + azure_openai_settings = AzureOpenAISettings.create() + endpoint = azure_openai_settings.endpoint + deployment_name = azure_openai_settings.embedding_deployment_name + api_key = azure_openai_settings.api_key.get_secret_value() + api_version = azure_openai_settings.api_version client = AsyncAzureOpenAI( azure_endpoint=endpoint, azure_deployment=deployment_name, api_key=api_key, - api_version="2023-05-15", + api_version=api_version, default_headers={"Test-User-X-ID": "test"}, ) embeddings_gen = sk_oai.AzureTextEmbedding( service_id="aoai-ada-2", - deployment_name=deployment_name, async_client=client, ) kernel.add_service(embeddings_gen) memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embeddings_gen) - kernel.add_plugin(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") + kernel.add_plugin(TextMemoryPlugin(memory), "TextMemoryPlugin") await memory.save_information(collection="generic", id="info1", text="My budget for 2024 is $100,000") await memory.save_reference( @@ -78,21 +69,9 @@ async def test_azure_text_embedding_service_with_provided_client(kernel: Kernel, @pytest.mark.asyncio -async def test_batch_azure_embeddings(get_aoai_config): +async def test_batch_azure_embeddings(): # Configure LLM service - _, api_key, endpoint = get_aoai_config - - if "Python_Integration_Tests" in os.environ: - deployment_name = os.environ["AzureOpenAIEmbeddings_EastUS__DeploymentName"] - - else: - deployment_name = "text-embedding-ada-002" - - embeddings_service = sk_oai.AzureTextEmbedding( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - ) + embeddings_service = sk_oai.AzureTextEmbedding(service_id="aoai-ada") texts = ["hello world"] results = await embeddings_service.generate_embeddings(texts) batch_results = await embeddings_service.generate_embeddings(texts, batch_size=1) diff --git a/python/tests/integration/embeddings/test_gp_embedding_service.py b/python/tests/integration/embeddings/test_gp_embedding_service.py index fcc944b239..59b7bd0ae1 100644 --- a/python/tests/integration/embeddings/test_gp_embedding_service.py +++ b/python/tests/integration/embeddings/test_gp_embedding_service.py @@ -6,6 +6,7 @@ import sys import pytest import semantic_kernel as sk +from semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin from semantic_kernel.kernel import Kernel from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory @@ -22,14 +23,12 @@ pytestmark = [ @pytest.mark.asyncio -async def test_gp_embedding_service(kernel: Kernel, get_gp_config): - api_key = get_gp_config - - palm_text_embed = sk_gp.GooglePalmTextEmbedding("models/embedding-gecko-001", api_key) +async def test_gp_embedding_service(kernel: Kernel): + palm_text_embed = sk_gp.GooglePalmTextEmbedding("models/embedding-gecko-001") kernel.add_service(palm_text_embed) memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=palm_text_embed) - kernel.add_plugin(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") + kernel.add_plugin(TextMemoryPlugin(memory), "TextMemoryPlugin") await memory.save_information(collection="generic", id="info1", text="My budget for 2024 is $100,000") await memory.save_reference( diff --git a/python/tests/integration/embeddings/test_oai_embedding_service.py b/python/tests/integration/embeddings/test_oai_embedding_service.py index 58542e3333..9ca74c28e6 100644 --- a/python/tests/integration/embeddings/test_oai_embedding_service.py +++ b/python/tests/integration/embeddings/test_oai_embedding_service.py @@ -5,22 +5,23 @@ from openai import AsyncOpenAI import semantic_kernel as sk import semantic_kernel.connectors.ai.open_ai as sk_oai +from semantic_kernel.connectors.ai.open_ai.settings.open_ai_settings import OpenAISettings +from semantic_kernel.core_plugins.text_memory_plugin import TextMemoryPlugin from semantic_kernel.kernel import Kernel from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory @pytest.mark.asyncio -async def test_oai_embedding_service(kernel: Kernel, get_oai_config): - api_key, org_id = get_oai_config - +async def test_oai_embedding_service(kernel: Kernel): embedding_gen = sk_oai.OpenAITextEmbedding( - service_id="oai-ada", ai_model_id="text-embedding-ada-002", api_key=api_key, org_id=org_id + service_id="oai-ada", + ai_model_id="text-embedding-ada-002", ) kernel.add_service(embedding_gen) memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embedding_gen) - kernel.add_plugin(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") + kernel.add_plugin(TextMemoryPlugin(memory), "TextMemoryPlugin") await memory.save_reference( "test", @@ -31,8 +32,10 @@ async def test_oai_embedding_service(kernel: Kernel, get_oai_config): @pytest.mark.asyncio -async def test_oai_embedding_service_with_provided_client(kernel: Kernel, get_oai_config): - api_key, org_id = get_oai_config +async def test_oai_embedding_service_with_provided_client(kernel: Kernel): + openai_settings = OpenAISettings.create() + api_key = openai_settings.api_key.get_secret_value() + org_id = openai_settings.org_id client = AsyncOpenAI( api_key=api_key, @@ -45,7 +48,7 @@ async def test_oai_embedding_service_with_provided_client(kernel: Kernel, get_oa kernel.add_service(embedding_gen) memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=embedding_gen) - kernel.add_plugin(sk.core_plugins.TextMemoryPlugin(memory), "TextMemoryPlugin") + kernel.add_plugin(TextMemoryPlugin(memory), "TextMemoryPlugin") await memory.save_reference( "test", diff --git a/python/tests/integration/planning/function_calling_stepwise_planner/test_int_function_calling_stepwise_planner.py b/python/tests/integration/planning/function_calling_stepwise_planner/test_int_function_calling_stepwise_planner.py index 8cee73be73..37d616a558 100644 --- a/python/tests/integration/planning/function_calling_stepwise_planner/test_int_function_calling_stepwise_planner.py +++ b/python/tests/integration/planning/function_calling_stepwise_planner/test_int_function_calling_stepwise_planner.py @@ -19,15 +19,13 @@ from semantic_kernel.planners.function_calling_stepwise_planner.function_calling @pytest.mark.asyncio -async def test_can_execute_function_calling_stepwise_plan(kernel: Kernel, get_oai_config): - api_key, _ = get_oai_config +async def test_can_execute_function_calling_stepwise_plan(kernel: Kernel): service_id = "planner" kernel.add_service( OpenAIChatCompletion( service_id=service_id, ai_model_id="gpt-3.5-turbo-1106", - api_key=api_key, ), ) diff --git a/python/tests/integration/planning/sequential_planner/test_sequential_plan_parser.py b/python/tests/integration/planning/sequential_planner/test_sequential_plan_parser.py index 960630971f..fc4f2f6629 100644 --- a/python/tests/integration/planning/sequential_planner/test_sequential_plan_parser.py +++ b/python/tests/integration/planning/sequential_planner/test_sequential_plan_parser.py @@ -11,17 +11,13 @@ from tests.integration.fakes.writer_plugin_fake import WriterPluginFake @pytest.mark.asyncio -async def test_can_call_to_plan_from_xml(get_aoai_config): - deployment_name, api_key, endpoint = get_aoai_config +async def test_can_call_to_plan_from_xml(): kernel = Kernel() # Configure LLM service kernel.add_service( sk_oai.AzureChatCompletion( service_id="text_completion", - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, ), ) kernel.add_plugin(EmailPluginFake(), "email") diff --git a/python/tests/integration/planning/sequential_planner/test_sequential_planner.py b/python/tests/integration/planning/sequential_planner/test_sequential_planner.py index b2f422365a..c94f6b0473 100644 --- a/python/tests/integration/planning/sequential_planner/test_sequential_planner.py +++ b/python/tests/integration/planning/sequential_planner/test_sequential_planner.py @@ -27,26 +27,19 @@ async def retry(func, retries=3): time.sleep(max(min(i, max_delay), min_delay)) -def initialize_kernel(get_aoai_config, use_embeddings=False, use_chat_model=False): - _, api_key, endpoint = get_aoai_config +def initialize_kernel(use_embeddings=False, use_chat_model=False): kernel = Kernel() if use_chat_model: kernel.add_service( sk_oai.AzureChatCompletion( service_id="chat_completion", - deployment_name="gpt-35-turbo-0613", - endpoint=endpoint, - api_key=api_key, ), ) else: kernel.add_service( sk_oai.AzureTextCompletion( service_id="text_completion", - deployment_name="gpt-35-turbo-instruct", - endpoint=endpoint, - api_key=api_key, ), ) @@ -54,9 +47,6 @@ def initialize_kernel(get_aoai_config, use_embeddings=False, use_chat_model=Fals kernel.add_service( sk_oai.AzureTextEmbedding( service_id="text_embedding", - deployment_name="text-embedding-ada-002", - endpoint=endpoint, - api_key=api_key, ), ) return kernel @@ -84,11 +74,11 @@ def initialize_kernel(get_aoai_config, use_embeddings=False, use_chat_model=Fals raises=PlannerException, reason="Test is known to occasionally produce unexpected results.", ) -async def test_create_plan_function_flow(get_aoai_config, use_chat_model, prompt, expected_function, expected_plugin): +async def test_create_plan_function_flow(use_chat_model, prompt, expected_function, expected_plugin): # Arrange service_id = "chat_completion" if use_chat_model else "text_completion" - kernel = initialize_kernel(get_aoai_config, False, use_chat_model) + kernel = initialize_kernel(False, use_chat_model) kernel.add_plugin(EmailPluginFake(), "email_plugin_fake") kernel.add_plugin(FunPluginFake(), "fun_plugin_fake") @@ -117,9 +107,9 @@ async def test_create_plan_function_flow(get_aoai_config, use_chat_model, prompt raises=PlannerException, reason="Test is known to occasionally produce unexpected results.", ) -async def test_create_plan_with_defaults(get_aoai_config, prompt, expected_function, expected_plugin, expected_default): +async def test_create_plan_with_defaults(prompt, expected_function, expected_plugin, expected_default): # Arrange - kernel = initialize_kernel(get_aoai_config) + kernel = initialize_kernel() kernel.add_plugin(EmailPluginFake(), "email_plugin_fake") kernel.add_plugin(WriterPluginFake(), "WriterPlugin") @@ -152,9 +142,9 @@ async def test_create_plan_with_defaults(get_aoai_config, prompt, expected_funct raises=PlannerException, reason="Test is known to occasionally produce unexpected results.", ) -async def test_create_plan_goal_relevant(get_aoai_config, prompt, expected_function, expected_plugin): +async def test_create_plan_goal_relevant(prompt, expected_function, expected_plugin): # Arrange - kernel = initialize_kernel(get_aoai_config, use_embeddings=True) + kernel = initialize_kernel(use_embeddings=True) kernel.add_plugin(EmailPluginFake(), "email_plugin_fake") kernel.add_plugin(FunPluginFake(), "fun_plugin_fake") kernel.add_plugin(WriterPluginFake(), "writer_plugin_fake") 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 074c89b8af..8606b4db66 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 @@ -1,50 +1,41 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio -import sys from unittest.mock import MagicMock, patch import pytest +from google.generativeai.types import ChatResponse, MessageDict from pydantic import ValidationError -if sys.version_info >= (3, 9): - from google.generativeai.types import ChatResponse, MessageDict - - 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.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 -pytestmark = pytest.mark.skipif(sys.version_info < (3, 9), reason="Google Palm requires Python 3.9 or greater") - - -def test_google_palm_chat_completion_init() -> None: +def test_google_palm_chat_completion_init(google_palm_unit_test_env) -> None: ai_model_id = "test_model_id" - api_key = "test_api_key" gp_chat_completion = GooglePalmChatCompletion( ai_model_id=ai_model_id, - api_key=api_key, ) assert gp_chat_completion.ai_model_id == ai_model_id - assert gp_chat_completion.api_key == api_key + assert gp_chat_completion.api_key == google_palm_unit_test_env["GOOGLE_PALM_API_KEY"] assert isinstance(gp_chat_completion, GooglePalmChatCompletion) -def test_google_palm_chat_completion_init_with_empty_api_key() -> None: +@pytest.mark.parametrize("exclude_list", [["GOOGLE_PALM_API_KEY"]], indirect=True) +def test_google_palm_chat_completion_init_with_empty_api_key(google_palm_unit_test_env) -> None: ai_model_id = "test_model_id" - # api_key = "test_api_key" - with pytest.raises(ValidationError, match="api_key"): + with pytest.raises(ValidationError): GooglePalmChatCompletion( ai_model_id=ai_model_id, - api_key="", ) @pytest.mark.asyncio -async def test_google_palm_text_completion_complete_chat_call_with_parameters() -> None: +async def test_google_palm_text_completion_complete_chat_call_with_parameters(google_palm_unit_test_env) -> None: class MockChatResponse(ChatResponse): def last(self): return "" @@ -65,12 +56,10 @@ async def test_google_palm_text_completion_complete_chat_call_with_parameters() new=mock_gp, ): ai_model_id = "test_model_id" - api_key = "test_api_key" chats = ChatHistory() chats.add_user_message("Hello word") gp_chat_completion = GooglePalmChatCompletion( ai_model_id=ai_model_id, - api_key=api_key, ) settings = GooglePalmChatPromptExecutionSettings() response = await gp_chat_completion.complete_chat(chats, settings) 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 431da12947..3d6098411a 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 @@ -1,53 +1,44 @@ # Copyright (c) Microsoft. All rights reserved. -import sys from unittest.mock import MagicMock, patch import pytest +from google.generativeai.types import Completion +from google.generativeai.types.text_types import TextCompletion from pydantic import ValidationError -if sys.version_info >= (3, 9): - from google.generativeai.types import Completion - from google.generativeai.types.text_types import TextCompletion - - 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.connectors.ai.google_palm import ( + GooglePalmTextPromptExecutionSettings, +) +from semantic_kernel.connectors.ai.google_palm.services.gp_text_completion import ( + GooglePalmTextCompletion, +) -pytestmark = pytest.mark.skipif(sys.version_info < (3, 9), reason="Google Palm requires Python 3.9 or greater") - - -def test_google_palm_text_completion_init() -> None: +def test_google_palm_text_completion_init(google_palm_unit_test_env) -> None: ai_model_id = "test_model_id" - api_key = "test_api_key" # Test successful initialization gp_text_completion = GooglePalmTextCompletion( ai_model_id=ai_model_id, - api_key=api_key, ) assert gp_text_completion.ai_model_id == ai_model_id - assert gp_text_completion.api_key == api_key + assert gp_text_completion.api_key == google_palm_unit_test_env["GOOGLE_PALM_API_KEY"] assert isinstance(gp_text_completion, GooglePalmTextCompletion) -def test_google_palm_text_completion_init_with_empty_api_key() -> None: +@pytest.mark.parametrize("exclude_list", [["GOOGLE_PALM_API_KEY"]], indirect=True) +def test_google_palm_text_completion_init_with_empty_api_key(google_palm_unit_test_env) -> None: ai_model_id = "test_model_id" - # api_key = "test_api_key" - with pytest.raises(ValidationError, match="api_key"): + with pytest.raises(ValidationError): GooglePalmTextCompletion( ai_model_id=ai_model_id, - api_key="", ) @pytest.mark.asyncio -async def test_google_palm_text_completion_complete_call_with_parameters() -> None: +async def test_google_palm_text_completion_complete_call_with_parameters(google_palm_unit_test_env) -> None: gp_completion = Completion() gp_completion.candidates = [TextCompletion(output="Example response")] gp_completion.filters = None @@ -59,11 +50,9 @@ async def test_google_palm_text_completion_complete_call_with_parameters() -> No new=mock_gp, ): ai_model_id = "test_model_id" - api_key = "test_api_key" prompt = "hello world" gp_text_completion = GooglePalmTextCompletion( ai_model_id=ai_model_id, - api_key=api_key, ) settings = GooglePalmTextPromptExecutionSettings() response = await gp_text_completion.complete(prompt, settings) 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 6e9f99df47..42a022d229 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 @@ -1,48 +1,40 @@ # Copyright (c) Microsoft. All rights reserved. -import sys from unittest.mock import MagicMock, patch import pytest from pydantic import ValidationError -if sys.version_info >= (3, 9): - from semantic_kernel.connectors.ai.google_palm.services.gp_text_embedding import ( - GooglePalmTextEmbedding, - ) +from semantic_kernel.connectors.ai.google_palm.services.gp_text_embedding import ( + GooglePalmTextEmbedding, +) -pytestmark = pytest.mark.skipif(sys.version_info < (3, 9), reason="Google Palm requires Python 3.9 or greater") - - -def test_google_palm_text_embedding_init() -> None: +def test_google_palm_text_embedding_init(google_palm_unit_test_env) -> None: ai_model_id = "test_model_id" - api_key = "test_api_key" # Test successful initialization gp_text_embed = GooglePalmTextEmbedding( ai_model_id=ai_model_id, - api_key=api_key, ) assert gp_text_embed.ai_model_id == ai_model_id - assert gp_text_embed.api_key == api_key + assert gp_text_embed.api_key == google_palm_unit_test_env["GOOGLE_PALM_API_KEY"] assert isinstance(gp_text_embed, GooglePalmTextEmbedding) -def test_google_palm_text_embedding_init_with_empty_api_key() -> None: +@pytest.mark.parametrize("exclude_list", [["GOOGLE_PALM_API_KEY"]], indirect=True) +def test_google_palm_text_embedding_init_with_empty_api_key(google_palm_unit_test_env) -> None: ai_model_id = "test_model_id" - # api_key = "test_api_key" - with pytest.raises(ValidationError, match="api_key"): + with pytest.raises(ValidationError): GooglePalmTextEmbedding( ai_model_id=ai_model_id, - api_key="", ) @pytest.mark.asyncio -async def test_google_palm_text_embedding_calls_with_parameters() -> None: +async def test_google_palm_text_embedding_calls_with_parameters(google_palm_unit_test_env) -> None: mock_gp = MagicMock() mock_gp.generate_embeddings.return_value = {"embedding": [0.1, 0.2, 0.3]} with patch( @@ -50,13 +42,11 @@ async def test_google_palm_text_embedding_calls_with_parameters() -> None: new=mock_gp, ): ai_model_id = "test_model_id" - api_key = "test_api_key" texts = ["hello world"] text = "hello world" gp_text_embedding = GooglePalmTextEmbedding( ai_model_id=ai_model_id, - api_key=api_key, ) await gp_text_embedding.generate_embeddings(texts) 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 7dab06baff..1ee41b24c8 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 @@ -1,5 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. +import os from unittest.mock import AsyncMock, patch import openai @@ -28,150 +29,71 @@ from semantic_kernel.exceptions.service_exceptions import ServiceResponseExcepti from semantic_kernel.kernel import Kernel -def test_azure_chat_completion_init() -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - +def test_azure_chat_completion_init(azure_openai_unit_test_env) -> None: # Test successful initialization - azure_chat_completion = AzureChatCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) + azure_chat_completion = AzureChatCompletion() assert azure_chat_completion.client is not None assert isinstance(azure_chat_completion.client, AsyncAzureOpenAI) - assert azure_chat_completion.ai_model_id == deployment_name + assert azure_chat_completion.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] assert isinstance(azure_chat_completion, ChatCompletionClientBase) -def test_azure_chat_completion_init_base_url() -> None: - deployment_name = "test_deployment" - base_url = "https://test-endpoint.com/openai/deployment/test_deployment" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - +def test_azure_chat_completion_init_base_url(azure_openai_unit_test_env) -> None: # Custom header for testing default_headers = {"X-Unit-Test": "test-guid"} azure_chat_completion = AzureChatCompletion( - deployment_name=deployment_name, - base_url=base_url, - api_key=api_key, - api_version=api_version, default_headers=default_headers, ) assert azure_chat_completion.client is not None assert isinstance(azure_chat_completion.client, AsyncAzureOpenAI) - assert azure_chat_completion.ai_model_id == deployment_name + assert azure_chat_completion.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] assert isinstance(azure_chat_completion, ChatCompletionClientBase) for key, value in default_headers.items(): assert key in azure_chat_completion.client.default_headers assert azure_chat_completion.client.default_headers[key] == value -def test_azure_chat_completion_init_with_empty_deployment_name() -> None: - # deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - - with pytest.raises(ValidationError, match="ai_model_id"): - AzureChatCompletion( - deployment_name="", - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) +@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): + AzureChatCompletion() -def test_azure_chat_completion_init_with_empty_api_key() -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - # api_key = "test_api_key" - api_version = "2023-03-15-preview" - - with pytest.raises(ServiceInitializationError, match="api_key"): - AzureChatCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key="", - api_version=api_version, - ) +@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_API_KEY"]], indirect=True) +def test_azure_chat_completion_init_with_empty_api_key(azure_openai_unit_test_env) -> None: + with pytest.raises(ServiceInitializationError): + AzureChatCompletion() -def test_azure_chat_completion_init_with_empty_endpoint() -> None: - deployment_name = "test_deployment" - # endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - - with pytest.raises(ValidationError, match="url"): - AzureChatCompletion( - deployment_name=deployment_name, - endpoint="", - api_key=api_key, - api_version=api_version, - ) +@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_BASE_URL"]], indirect=True) +def test_azure_chat_completion_init_with_empty_endpoint_and_base_url(azure_openai_unit_test_env) -> None: + with pytest.raises(ServiceInitializationError): + AzureChatCompletion() -def test_azure_chat_completion_init_with_invalid_endpoint() -> None: - deployment_name = "test_deployment" - endpoint = "http://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - - with pytest.raises(ValidationError, match="url"): - AzureChatCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) - - -def test_azure_chat_completion_init_with_base_url() -> None: - deployment_name = "test_deployment" - base_url = "http://test-endpoint.com/openai/deployment/test_deployment" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - - with pytest.raises(ValidationError, match="url"): - AzureChatCompletion( - deployment_name=deployment_name, - base_url=base_url, - api_key=api_key, - api_version=api_version, - ) +@pytest.mark.parametrize("override_env_param_dict", [{"AZURE_OPENAI_ENDPOINT": "http://test.com"}], indirect=True) +def test_azure_chat_completion_init_with_invalid_endpoint(azure_openai_unit_test_env) -> None: + with pytest.raises(ServiceInitializationError): + AzureChatCompletion() @pytest.mark.asyncio @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_azure_chat_completion_call_with_parameters( - mock_create, kernel: Kernel, chat_history: ChatHistory + mock_create, kernel: Kernel, azure_openai_unit_test_env, chat_history: ChatHistory ) -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" chat_history.add_user_message("hello world") complete_prompt_execution_settings = AzureChatPromptExecutionSettings(service_id="test_service_id") - azure_chat_completion = AzureChatCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_version=api_version, - api_key=api_key, - ) + azure_chat_completion = AzureChatCompletion() await azure_chat_completion.complete_chat( chat_history=chat_history, settings=complete_prompt_execution_settings, kernel=kernel ) mock_create.assert_awaited_once_with( - model=deployment_name, + model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], frequency_penalty=complete_prompt_execution_settings.frequency_penalty, logit_bias={}, max_tokens=complete_prompt_execution_settings.max_tokens, @@ -187,13 +109,8 @@ async def test_azure_chat_completion_call_with_parameters( @pytest.mark.asyncio @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_azure_chat_completion_call_with_parameters_and_Logit_Bias_Defined( - mock_create, kernel: Kernel, chat_history: ChatHistory + mock_create, kernel: Kernel, azure_openai_unit_test_env, chat_history: ChatHistory ) -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - prompt = "hello world" chat_history.add_user_message(prompt) complete_prompt_execution_settings = AzureChatPromptExecutionSettings() @@ -201,19 +118,14 @@ async def test_azure_chat_completion_call_with_parameters_and_Logit_Bias_Defined token_bias = {"1": -100} complete_prompt_execution_settings.logit_bias = token_bias - azure_chat_completion = AzureChatCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) + azure_chat_completion = AzureChatCompletion() await azure_chat_completion.complete_chat( chat_history=chat_history, settings=complete_prompt_execution_settings, kernel=kernel ) mock_create.assert_awaited_once_with( - model=deployment_name, + model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], messages=azure_chat_completion._prepare_chat_history_for_request(chat_history), temperature=complete_prompt_execution_settings.temperature, top_p=complete_prompt_execution_settings.top_p, @@ -230,12 +142,8 @@ async def test_azure_chat_completion_call_with_parameters_and_Logit_Bias_Defined @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_azure_chat_completion_call_with_parameters_and_Stop_Defined( mock_create, + azure_openai_unit_test_env, ) -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - prompt = "hello world" messages = [{"role": "user", "content": prompt}] complete_prompt_execution_settings = AzureChatPromptExecutionSettings() @@ -243,17 +151,12 @@ async def test_azure_chat_completion_call_with_parameters_and_Stop_Defined( stop = ["!"] complete_prompt_execution_settings.stop = stop - azure_chat_completion = AzureChatCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) + azure_chat_completion = AzureChatCompletion() await azure_chat_completion.complete(prompt=prompt, settings=complete_prompt_execution_settings) mock_create.assert_awaited_once_with( - model=deployment_name, + model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], messages=messages, temperature=complete_prompt_execution_settings.temperature, top_p=complete_prompt_execution_settings.top_p, @@ -267,18 +170,14 @@ async def test_azure_chat_completion_call_with_parameters_and_Stop_Defined( ) -def test_azure_chat_completion_serialize() -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" +def test_azure_chat_completion_serialize(azure_openai_unit_test_env) -> None: default_headers = {"X-Test": "test"} settings = { - "deployment_name": deployment_name, - "endpoint": endpoint, - "api_key": api_key, - "api_version": api_version, + "deployment_name": azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], + "endpoint": azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"], + "api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"], + "api_version": azure_openai_unit_test_env["AZURE_OPENAI_API_VERSION"], "default_headers": default_headers, } @@ -302,12 +201,8 @@ def test_azure_chat_completion_serialize() -> None: @pytest.mark.asyncio @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_azure_chat_completion_with_data_call_with_parameters( - mock_create, kernel: Kernel, chat_history: ChatHistory + mock_create, kernel: Kernel, azure_openai_unit_test_env, chat_history: ChatHistory ) -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" prompt = "hello world" messages_in = chat_history messages_in.add_user_message(prompt) @@ -329,19 +224,14 @@ async def test_azure_chat_completion_with_data_call_with_parameters( complete_prompt_execution_settings = AzureChatPromptExecutionSettings(extra_body=expected_data_settings) - azure_chat_completion = AzureChatCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_version=api_version, - api_key=api_key, - ) + azure_chat_completion = AzureChatCompletion() await azure_chat_completion.complete_chat( chat_history=messages_in, settings=complete_prompt_execution_settings, kernel=kernel ) mock_create.assert_awaited_once_with( - model=deployment_name, + model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], messages=azure_chat_completion._prepare_chat_history_for_request(messages_out), temperature=complete_prompt_execution_settings.temperature, frequency_penalty=complete_prompt_execution_settings.frequency_penalty, @@ -358,12 +248,8 @@ async def test_azure_chat_completion_with_data_call_with_parameters( @pytest.mark.asyncio @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_azure_chat_completion_call_with_data_parameters_and_function_calling( - mock_create, kernel: Kernel, chat_history: ChatHistory + mock_create, kernel: Kernel, azure_openai_unit_test_env, chat_history: ChatHistory ) -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" prompt = "hello world" chat_history.add_user_message(prompt) @@ -376,12 +262,7 @@ async def test_azure_chat_completion_call_with_data_parameters_and_function_call ) extra = ExtraBody(data_sources=[ai_source]) - azure_chat_completion = AzureChatCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) + azure_chat_completion = AzureChatCompletion() functions = [{"name": "test-function", "description": "test-description"}] complete_prompt_execution_settings = AzureChatPromptExecutionSettings( @@ -399,7 +280,7 @@ async def test_azure_chat_completion_call_with_data_parameters_and_function_call expected_data_settings = extra.model_dump(exclude_none=True, by_alias=True) mock_create.assert_awaited_once_with( - model=deployment_name, + model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], messages=azure_chat_completion._prepare_chat_history_for_request(chat_history), temperature=complete_prompt_execution_settings.temperature, top_p=complete_prompt_execution_settings.top_p, @@ -418,12 +299,8 @@ async def test_azure_chat_completion_call_with_data_parameters_and_function_call @pytest.mark.asyncio @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_azure_chat_completion_call_with_data_with_parameters_and_Stop_Defined( - mock_create, kernel: Kernel, chat_history: ChatHistory + mock_create, kernel: Kernel, azure_openai_unit_test_env, chat_history: ChatHistory ) -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" chat_history.add_user_message("hello world") complete_prompt_execution_settings = AzureChatPromptExecutionSettings() @@ -441,19 +318,14 @@ async def test_azure_chat_completion_call_with_data_with_parameters_and_Stop_Def complete_prompt_execution_settings.extra_body = extra - azure_chat_completion = AzureChatCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) + azure_chat_completion = AzureChatCompletion() await azure_chat_completion.complete_chat(chat_history, complete_prompt_execution_settings, kernel=kernel) expected_data_settings = extra.model_dump(exclude_none=True, by_alias=True) mock_create.assert_awaited_once_with( - model=deployment_name, + model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], messages=azure_chat_completion._prepare_chat_history_for_request(chat_history), temperature=complete_prompt_execution_settings.temperature, top_p=complete_prompt_execution_settings.top_p, @@ -484,19 +356,16 @@ CONTENT_FILTERED_ERROR_FULL_MESSAGE = ( @pytest.mark.asyncio @patch.object(AsyncChatCompletions, "create") async def test_azure_chat_completion_content_filtering_raises_correct_exception( - mock_create, kernel: Kernel, chat_history: ChatHistory + mock_create, kernel: Kernel, azure_openai_unit_test_env, chat_history: ChatHistory ) -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" prompt = "some prompt that would trigger the content filtering" chat_history.add_user_message(prompt) complete_prompt_execution_settings = AzureChatPromptExecutionSettings() + test_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") mock_create.side_effect = openai.BadRequestError( CONTENT_FILTERED_ERROR_FULL_MESSAGE, - response=Response(400, request=Request("POST", endpoint)), + response=Response(400, request=Request("POST", test_endpoint)), body={ "message": CONTENT_FILTERED_ERROR_MESSAGE, "type": None, @@ -515,12 +384,7 @@ async def test_azure_chat_completion_content_filtering_raises_correct_exception( }, ) - azure_chat_completion = AzureChatCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) + azure_chat_completion = AzureChatCompletion() with pytest.raises(ContentFilterAIException, match="service encountered a content error") as exc_info: await azure_chat_completion.complete_chat(chat_history, complete_prompt_execution_settings, kernel=kernel) @@ -534,19 +398,16 @@ async def test_azure_chat_completion_content_filtering_raises_correct_exception( @pytest.mark.asyncio @patch.object(AsyncChatCompletions, "create") async def test_azure_chat_completion_content_filtering_without_response_code_raises_with_default_code( - mock_create, kernel: Kernel, chat_history: ChatHistory + mock_create, kernel: Kernel, azure_openai_unit_test_env, chat_history: ChatHistory ) -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" prompt = "some prompt that would trigger the content filtering" chat_history.add_user_message(prompt) complete_prompt_execution_settings = AzureChatPromptExecutionSettings() + test_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") mock_create.side_effect = openai.BadRequestError( CONTENT_FILTERED_ERROR_FULL_MESSAGE, - response=Response(400, request=Request("POST", endpoint)), + response=Response(400, request=Request("POST", test_endpoint)), body={ "message": CONTENT_FILTERED_ERROR_MESSAGE, "type": None, @@ -564,12 +425,7 @@ async def test_azure_chat_completion_content_filtering_without_response_code_rai }, ) - azure_chat_completion = AzureChatCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) + azure_chat_completion = AzureChatCompletion() with pytest.raises(ContentFilterAIException, match="service encountered a content error"): await azure_chat_completion.complete_chat(chat_history, complete_prompt_execution_settings, kernel=kernel) @@ -578,26 +434,18 @@ async def test_azure_chat_completion_content_filtering_without_response_code_rai @pytest.mark.asyncio @patch.object(AsyncChatCompletions, "create") async def test_azure_chat_completion_bad_request_non_content_filter( - mock_create, kernel: Kernel, chat_history: ChatHistory + mock_create, kernel: Kernel, azure_openai_unit_test_env, chat_history: ChatHistory ) -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" prompt = "some prompt that would trigger the content filtering" chat_history.add_user_message(prompt) complete_prompt_execution_settings = AzureChatPromptExecutionSettings() + test_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") mock_create.side_effect = openai.BadRequestError( - "The request was bad.", response=Response(400, request=Request("POST", endpoint)), body={} + "The request was bad.", response=Response(400, request=Request("POST", test_endpoint)), body={} ) - azure_chat_completion = AzureChatCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) + azure_chat_completion = AzureChatCompletion() with pytest.raises(ServiceResponseException, match="service failed to complete the prompt"): await azure_chat_completion.complete_chat(chat_history, complete_prompt_execution_settings, kernel=kernel) @@ -605,27 +453,21 @@ async def test_azure_chat_completion_bad_request_non_content_filter( @pytest.mark.asyncio @patch.object(AsyncChatCompletions, "create") -async def test_azure_chat_completion_no_kernel_provided_throws_error(mock_create, chat_history: ChatHistory) -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" +async def test_azure_chat_completion_no_kernel_provided_throws_error( + mock_create, azure_openai_unit_test_env, chat_history: ChatHistory +) -> None: prompt = "some prompt that would trigger the content filtering" chat_history.add_user_message(prompt) complete_prompt_execution_settings = AzureChatPromptExecutionSettings( function_call_behavior=FunctionCallBehavior.AutoInvokeKernelFunctions() ) + test_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") mock_create.side_effect = openai.BadRequestError( - "The request was bad.", response=Response(400, request=Request("POST", endpoint)), body={} + "The request was bad.", response=Response(400, request=Request("POST", test_endpoint)), body={} ) - azure_chat_completion = AzureChatCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) + azure_chat_completion = AzureChatCompletion() with pytest.raises( ServiceInvalidExecutionSettingsError, 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 9ae02c6bf2..92b86fb2cc 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 @@ -15,134 +15,69 @@ from semantic_kernel.connectors.ai.text_completion_client_base import TextComple from semantic_kernel.exceptions import ServiceInitializationError -def test_azure_text_completion_init() -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - +def test_azure_text_completion_init(azure_openai_unit_test_env) -> None: # Test successful initialization - azure_text_completion = AzureTextCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) + azure_text_completion = AzureTextCompletion() assert azure_text_completion.client is not None assert isinstance(azure_text_completion.client, AsyncAzureOpenAI) - assert azure_text_completion.ai_model_id == deployment_name + assert azure_text_completion.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_TEXT_DEPLOYMENT_NAME"] assert isinstance(azure_text_completion, TextCompletionClientBase) -def test_azure_text_completion_init_with_custom_header() -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - +def test_azure_text_completion_init_with_custom_header(azure_openai_unit_test_env) -> None: # Custom header for testing default_headers = {"X-Unit-Test": "test-guid"} # Test successful initialization azure_text_completion = AzureTextCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, default_headers=default_headers, ) assert azure_text_completion.client is not None assert isinstance(azure_text_completion.client, AsyncAzureOpenAI) - assert azure_text_completion.ai_model_id == deployment_name + assert azure_text_completion.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_TEXT_DEPLOYMENT_NAME"] assert isinstance(azure_text_completion, TextCompletionClientBase) for key, value in default_headers.items(): assert key in azure_text_completion.client.default_headers assert azure_text_completion.client.default_headers[key] == value -def test_azure_text_completion_init_with_empty_deployment_name() -> None: - # deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - - with pytest.raises(ValidationError, match="ai_model_id"): - AzureTextCompletion( - deployment_name="", - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) +@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): + AzureTextCompletion() -def test_azure_text_completion_init_with_empty_api_key() -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - # api_key = "test_api_key" - api_version = "2023-03-15-preview" - - with pytest.raises(ServiceInitializationError, match="api_key"): - AzureTextCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key="", - api_version=api_version, - ) +@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_API_KEY"]], indirect=True) +def test_azure_text_completion_init_with_empty_api_key(azure_openai_unit_test_env) -> None: + with pytest.raises(ServiceInitializationError): + AzureTextCompletion() -def test_azure_text_completion_init_with_empty_endpoint() -> None: - deployment_name = "test_deployment" - # endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - - with pytest.raises(ValidationError, match="endpoint"): - AzureTextCompletion( - deployment_name=deployment_name, - endpoint="", - api_key=api_key, - api_version=api_version, - ) +@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_BASE_URL"]], indirect=True) +def test_azure_text_completion_init_with_empty_endpoint_and_base_url(azure_openai_unit_test_env) -> None: + with pytest.raises(ServiceInitializationError): + AzureTextCompletion() -def test_azure_text_completion_init_with_invalid_endpoint() -> None: - deployment_name = "test_deployment" - endpoint = "http://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - - with pytest.raises(ValidationError, match="https"): - AzureTextCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) +@pytest.mark.parametrize("override_env_param_dict", [{"AZURE_OPENAI_ENDPOINT": "http://test.com"}], indirect=True) +def test_azure_text_completion_init_with_invalid_endpoint(azure_openai_unit_test_env) -> None: + with pytest.raises(ServiceInitializationError): + AzureTextCompletion() @pytest.mark.asyncio @patch.object(AsyncCompletions, "create", new_callable=AsyncMock) -async def test_azure_text_completion_call_with_parameters(mock_create) -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - +async def test_azure_text_completion_call_with_parameters(mock_create, azure_openai_unit_test_env) -> None: prompt = "hello world" complete_prompt_execution_settings = OpenAITextPromptExecutionSettings() - azure_text_completion = AzureTextCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) + azure_text_completion = AzureTextCompletion() await azure_text_completion.complete(prompt, complete_prompt_execution_settings) mock_create.assert_awaited_once_with( - model=deployment_name, + model=azure_openai_unit_test_env["AZURE_OPENAI_TEXT_DEPLOYMENT_NAME"], frequency_penalty=complete_prompt_execution_settings.frequency_penalty, logit_bias={}, max_tokens=complete_prompt_execution_settings.max_tokens, @@ -160,29 +95,20 @@ async def test_azure_text_completion_call_with_parameters(mock_create) -> None: @patch.object(AsyncCompletions, "create", new_callable=AsyncMock) async def test_azure_text_completion_call_with_parameters_logit_bias_not_none( mock_create, + azure_openai_unit_test_env, ) -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - prompt = "hello world" complete_prompt_execution_settings = OpenAITextPromptExecutionSettings() token_bias = {"200": 100} complete_prompt_execution_settings.logit_bias = token_bias - azure_text_completion = AzureTextCompletion( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) + azure_text_completion = AzureTextCompletion() await azure_text_completion.complete(prompt, complete_prompt_execution_settings) mock_create.assert_awaited_once_with( - model=deployment_name, + model=azure_openai_unit_test_env["AZURE_OPENAI_TEXT_DEPLOYMENT_NAME"], frequency_penalty=complete_prompt_execution_settings.frequency_penalty, logit_bias=complete_prompt_execution_settings.logit_bias, max_tokens=complete_prompt_execution_settings.max_tokens, @@ -196,18 +122,15 @@ async def test_azure_text_completion_call_with_parameters_logit_bias_not_none( ) -def test_azure_text_completion_serialize() -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" +def test_azure_text_completion_serialize(azure_openai_unit_test_env) -> None: default_headers = {"X-Test": "test"} settings = { - "deployment_name": deployment_name, - "endpoint": endpoint, - "api_key": api_key, - "api_version": api_version, + "deployment_name": azure_openai_unit_test_env["AZURE_OPENAI_TEXT_DEPLOYMENT_NAME"], + "endpoint": azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"], + "base_url": azure_openai_unit_test_env["AZURE_OPENAI_BASE_URL"], + "api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"], + "api_version": azure_openai_unit_test_env["AZURE_OPENAI_API_VERSION"], "default_headers": default_headers, } 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 393a9d5ec0..0c1853324d 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 @@ -12,98 +12,53 @@ from semantic_kernel.connectors.ai.open_ai.services.azure_text_embedding import from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError -def test_azure_text_embedding_init() -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - +def test_azure_text_embedding_init(azure_openai_unit_test_env) -> None: # Test successful initialization - azure_text_embedding = AzureTextEmbedding( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) + azure_text_embedding = AzureTextEmbedding() assert azure_text_embedding.client is not None assert isinstance(azure_text_embedding.client, AsyncAzureOpenAI) - assert azure_text_embedding.ai_model_id == deployment_name + assert azure_text_embedding.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"] assert isinstance(azure_text_embedding, EmbeddingGeneratorBase) -def test_azure_text_embedding_init_with_empty_deployment_name() -> None: - # deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - - with pytest.raises(ValidationError, match="ai_model_id"): - AzureTextEmbedding( - deployment_name="", - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) +@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): + AzureTextEmbedding() -def test_azure_text_embedding_init_with_empty_api_key() -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - # api_key = "test_api_key" - api_version = "2023-03-15-preview" - - with pytest.raises(ServiceInitializationError, match="api_key"): - AzureTextEmbedding( - deployment_name=deployment_name, - endpoint=endpoint, - api_key="", - api_version=api_version, - ) +@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_API_KEY"]], indirect=True) +def test_azure_text_embedding_init_with_empty_api_key(azure_openai_unit_test_env) -> None: + with pytest.raises(ServiceInitializationError): + AzureTextEmbedding() -def test_azure_text_embedding_init_with_empty_endpoint() -> None: - deployment_name = "test_deployment" - # endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - - with pytest.raises(ValidationError, match="endpoint"): - AzureTextEmbedding( - deployment_name=deployment_name, - endpoint="", - api_key=api_key, - api_version=api_version, - ) +@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_BASE_URL"]], indirect=True) +def test_azure_text_embedding_init_with_empty_endpoint_and_base_url(azure_openai_unit_test_env) -> None: + with pytest.raises(ServiceInitializationError): + AzureTextEmbedding() -def test_azure_text_embedding_init_with_invalid_endpoint() -> None: - deployment_name = "test_deployment" - endpoint = "http://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" - - with pytest.raises(ValidationError, match="https"): - AzureTextEmbedding( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) +@pytest.mark.parametrize("override_env_param_dict", [{"AZURE_OPENAI_ENDPOINT": "http://test.com"}], indirect=True) +def test_azure_text_embedding_init_with_invalid_endpoint(azure_openai_unit_test_env) -> None: + with pytest.raises(ServiceInitializationError): + AzureTextEmbedding() -def test_azure_text_embedding_init_with_from_dict() -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" +@pytest.mark.parametrize( + "override_env_param_dict", + [{"AZURE_OPENAI_BASE_URL": "https://test_embedding_deployment.test-base-url.com"}], + indirect=True, +) +def test_azure_text_embedding_init_with_from_dict(azure_openai_unit_test_env) -> None: default_headers = {"test_header": "test_value"} settings = { - "deployment_name": deployment_name, - "endpoint": endpoint, - "api_key": api_key, - "api_version": api_version, + "deployment_name": azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"], + "endpoint": azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"], + "api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"], + "api_version": azure_openai_unit_test_env["AZURE_OPENAI_API_VERSION"], "default_headers": default_headers, } @@ -111,10 +66,10 @@ def test_azure_text_embedding_init_with_from_dict() -> None: assert azure_text_embedding.client is not None assert isinstance(azure_text_embedding.client, AsyncAzureOpenAI) - assert azure_text_embedding.ai_model_id == deployment_name + assert azure_text_embedding.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"] assert isinstance(azure_text_embedding, EmbeddingGeneratorBase) - assert endpoint in str(azure_text_embedding.client.base_url) - assert azure_text_embedding.client.api_key == api_key + assert settings["deployment_name"] in str(azure_text_embedding.client.base_url) + assert azure_text_embedding.client.api_key == azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"] # Assert that the default header we added is present in the client's default headers for key, value in default_headers.items(): @@ -124,56 +79,38 @@ def test_azure_text_embedding_init_with_from_dict() -> None: @pytest.mark.asyncio @patch.object(AsyncEmbeddings, "create", new_callable=AsyncMock) -async def test_azure_text_embedding_calls_with_parameters(mock_create) -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" +async def test_azure_text_embedding_calls_with_parameters(mock_create, azure_openai_unit_test_env) -> None: texts = ["hello world", "goodbye world"] embedding_dimensions = 1536 - azure_text_embedding = AzureTextEmbedding( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) + azure_text_embedding = AzureTextEmbedding() await azure_text_embedding.generate_embeddings(texts, dimensions=embedding_dimensions) mock_create.assert_awaited_once_with( input=texts, - model=deployment_name, + model=azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"], dimensions=embedding_dimensions, ) @pytest.mark.asyncio @patch.object(AsyncEmbeddings, "create", new_callable=AsyncMock) -async def test_azure_text_embedding_calls_with_batches(mock_create) -> None: - deployment_name = "test_deployment" - endpoint = "https://test-endpoint.com" - api_key = "test_api_key" - api_version = "2023-03-15-preview" +async def test_azure_text_embedding_calls_with_batches(mock_create, azure_openai_unit_test_env) -> None: texts = [i for i in range(0, 5)] - azure_text_embedding = AzureTextEmbedding( - deployment_name=deployment_name, - endpoint=endpoint, - api_key=api_key, - api_version=api_version, - ) + azure_text_embedding = AzureTextEmbedding() await azure_text_embedding.generate_embeddings(texts, batch_size=3) mock_create.assert_has_awaits( [ call( - model=deployment_name, + model=azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"], input=texts[0:3], ), call( - model=deployment_name, + model=azure_openai_unit_test_env["AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"], input=texts[3:5], ), ], diff --git a/python/tests/unit/connectors/open_ai/services/test_openai_chat_completion.py b/python/tests/unit/connectors/open_ai/services/test_openai_chat_completion.py index 1292ffac4a..b535bb8493 100644 --- a/python/tests/unit/connectors/open_ai/services/test_openai_chat_completion.py +++ b/python/tests/unit/connectors/open_ai/services/test_openai_chat_completion.py @@ -2,40 +2,39 @@ import pytest -from pydantic import ValidationError from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase from semantic_kernel.connectors.ai.open_ai.const import USER_AGENT from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError -def test_open_ai_chat_completion_init() -> None: - ai_model_id = "test_model_id" - api_key = "test_api_key" - +def test_open_ai_chat_completion_init(openai_unit_test_env) -> None: # Test successful initialization - open_ai_chat_completion = OpenAIChatCompletion( - ai_model_id=ai_model_id, - api_key=api_key, - ) + open_ai_chat_completion = OpenAIChatCompletion() + + assert open_ai_chat_completion.ai_model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + assert isinstance(open_ai_chat_completion, ChatCompletionClientBase) + + +def test_open_ai_chat_completion_init_ai_model_id_constructor(openai_unit_test_env) -> None: + # Test successful initialization + ai_model_id = "test_model_id" + open_ai_chat_completion = OpenAIChatCompletion(ai_model_id=ai_model_id) assert open_ai_chat_completion.ai_model_id == ai_model_id assert isinstance(open_ai_chat_completion, ChatCompletionClientBase) -def test_open_ai_chat_completion_init_with_default_header() -> None: - ai_model_id = "test_model_id" - api_key = "test_api_key" +def test_open_ai_chat_completion_init_with_default_header(openai_unit_test_env) -> None: default_headers = {"X-Unit-Test": "test-guid"} # Test successful initialization open_ai_chat_completion = OpenAIChatCompletion( - ai_model_id=ai_model_id, - api_key=api_key, default_headers=default_headers, ) - assert open_ai_chat_completion.ai_model_id == ai_model_id + assert open_ai_chat_completion.ai_model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] assert isinstance(open_ai_chat_completion, ChatCompletionClientBase) # Assert that the default header we added is present in the client's default headers @@ -44,43 +43,35 @@ def test_open_ai_chat_completion_init_with_default_header() -> None: assert open_ai_chat_completion.client.default_headers[key] == value -def test_open_ai_chat_completion_init_with_empty_model_id() -> None: - # ai_model_id = "test_model_id" - api_key = "test_api_key" - - with pytest.raises(ValidationError, match="ai_model_id"): - OpenAIChatCompletion( - ai_model_id="", - api_key=api_key, - ) +@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True) +def test_open_ai_chat_completion_init_with_empty_model_id(openai_unit_test_env) -> None: + with pytest.raises(ServiceInitializationError): + OpenAIChatCompletion() -def test_open_ai_chat_completion_init_with_empty_api_key() -> None: +@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True) +def test_open_ai_chat_completion_init_with_empty_api_key(openai_unit_test_env) -> None: ai_model_id = "test_model_id" - # api_key = "test_api_key" - with pytest.raises(ValidationError, match="api_key"): + with pytest.raises(ServiceInitializationError): OpenAIChatCompletion( ai_model_id=ai_model_id, - api_key="", ) -def test_open_ai_chat_completion_serialize() -> None: - ai_model_id = "test_model_id" - api_key = "test_api_key" +def test_open_ai_chat_completion_serialize(openai_unit_test_env) -> None: default_headers = {"X-Unit-Test": "test-guid"} settings = { - "ai_model_id": ai_model_id, - "api_key": api_key, + "ai_model_id": openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], + "api_key": openai_unit_test_env["OPENAI_API_KEY"], "default_headers": default_headers, } open_ai_chat_completion = OpenAIChatCompletion.from_dict(settings) dumped_settings = open_ai_chat_completion.to_dict() - assert dumped_settings["ai_model_id"] == ai_model_id - assert dumped_settings["api_key"] == api_key + assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"] # Assert that the default header we added is present in the dumped_settings default headers for key, value in default_headers.items(): assert key in dumped_settings["default_headers"] @@ -89,21 +80,17 @@ def test_open_ai_chat_completion_serialize() -> None: assert USER_AGENT not in dumped_settings["default_headers"] -def test_open_ai_chat_completion_serialize_with_org_id() -> None: - ai_model_id = "test_model_id" - api_key = "test_api_key" - org_id = "test_org_id" - +def test_open_ai_chat_completion_serialize_with_org_id(openai_unit_test_env) -> None: settings = { - "ai_model_id": ai_model_id, - "api_key": api_key, - "org_id": org_id, + "ai_model_id": openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], + "api_key": openai_unit_test_env["OPENAI_API_KEY"], + "org_id": openai_unit_test_env["OPENAI_ORG_ID"], } open_ai_chat_completion = OpenAIChatCompletion.from_dict(settings) dumped_settings = open_ai_chat_completion.to_dict() - assert dumped_settings["ai_model_id"] == ai_model_id - assert dumped_settings["api_key"] == api_key - assert dumped_settings["org_id"] == org_id + assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"] + assert dumped_settings["org_id"] == openai_unit_test_env["OPENAI_ORG_ID"] # Assert that the 'User-agent' header is not present in the dumped_settings default headers assert USER_AGENT not in dumped_settings["default_headers"] diff --git a/python/tests/unit/connectors/open_ai/services/test_openai_text_completion.py b/python/tests/unit/connectors/open_ai/services/test_openai_text_completion.py index f1e06161e2..4be7199cf7 100644 --- a/python/tests/unit/connectors/open_ai/services/test_openai_text_completion.py +++ b/python/tests/unit/connectors/open_ai/services/test_openai_text_completion.py @@ -2,101 +2,78 @@ import pytest -from pydantic import ValidationError from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion import OpenAITextCompletion from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError -def test_open_ai_text_completion_init() -> None: - ai_model_id = "test_model_id" - api_key = "test_api_key" - +def test_open_ai_text_completion_init(openai_unit_test_env) -> None: # Test successful initialization - open_ai_text_completion = OpenAITextCompletion( - ai_model_id=ai_model_id, - api_key=api_key, - ) + open_ai_text_completion = OpenAITextCompletion() + + assert open_ai_text_completion.ai_model_id == openai_unit_test_env["OPENAI_TEXT_MODEL_ID"] + assert isinstance(open_ai_text_completion, TextCompletionClientBase) + + +def test_open_ai_text_completion_init_with_ai_model_id(openai_unit_test_env) -> None: + # Test successful initialization + ai_model_id = "test_model_id" + open_ai_text_completion = OpenAITextCompletion(ai_model_id=ai_model_id) assert open_ai_text_completion.ai_model_id == ai_model_id assert isinstance(open_ai_text_completion, TextCompletionClientBase) -def test_open_ai_text_completion_init_with_default_header() -> None: - ai_model_id = "test_model_id" - api_key = "test_api_key" +def test_open_ai_text_completion_init_with_default_header(openai_unit_test_env) -> None: default_headers = {"X-Unit-Test": "test-guid"} # Test successful initialization open_ai_text_completion = OpenAITextCompletion( - ai_model_id=ai_model_id, - api_key=api_key, default_headers=default_headers, ) - assert open_ai_text_completion.ai_model_id == ai_model_id + assert open_ai_text_completion.ai_model_id == openai_unit_test_env["OPENAI_TEXT_MODEL_ID"] assert isinstance(open_ai_text_completion, TextCompletionClientBase) for key, value in default_headers.items(): assert key in open_ai_text_completion.client.default_headers assert open_ai_text_completion.client.default_headers[key] == value -def test_open_ai_text_completion_init_with_empty_model_id() -> None: - # ai_model_id = "test_model_id" - api_key = "test_api_key" - - with pytest.raises(ValidationError, match="ai_model_id"): - OpenAITextCompletion( - ai_model_id="", - api_key=api_key, - ) +@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True) +def test_open_ai_text_completion_init_with_empty_api_key(openai_unit_test_env) -> None: + with pytest.raises(ServiceInitializationError): + OpenAITextCompletion() -def test_open_ai_text_completion_init_with_empty_api_key() -> None: - ai_model_id = "test_model_id" - # api_key = "test_api_key" - - with pytest.raises(ValidationError, match="api_key"): - OpenAITextCompletion( - ai_model_id=ai_model_id, - api_key="", - ) - - -def test_open_ai_text_completion_serialize() -> None: - ai_model_id = "test_model_id" - api_key = "test_api_key" +def test_open_ai_text_completion_serialize(openai_unit_test_env) -> None: default_headers = {"X-Unit-Test": "test-guid"} settings = { - "ai_model_id": ai_model_id, - "api_key": api_key, + "ai_model_id": openai_unit_test_env["OPENAI_TEXT_MODEL_ID"], + "api_key": openai_unit_test_env["OPENAI_API_KEY"], "default_headers": default_headers, } open_ai_text_completion = OpenAITextCompletion.from_dict(settings) dumped_settings = open_ai_text_completion.to_dict() - assert dumped_settings["ai_model_id"] == ai_model_id - assert dumped_settings["api_key"] == api_key + assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_TEXT_MODEL_ID"] + assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"] # Assert that the default header we added is present in the dumped_settings default headers for key, value in default_headers.items(): assert key in dumped_settings["default_headers"] assert dumped_settings["default_headers"][key] == value -def test_open_ai_text_completion_serialize_with_org_id() -> None: - ai_model_id = "test_model_id" - api_key = "test_api_key" - org_id = "test_org_id" - +def test_open_ai_text_completion_serialize_with_org_id(openai_unit_test_env) -> None: settings = { - "ai_model_id": ai_model_id, - "api_key": api_key, - "org_id": org_id, + "ai_model_id": openai_unit_test_env["OPENAI_TEXT_MODEL_ID"], + "api_key": openai_unit_test_env["OPENAI_API_KEY"], + "org_id": openai_unit_test_env["OPENAI_ORG_ID"], } open_ai_text_completion = OpenAITextCompletion.from_dict(settings) dumped_settings = open_ai_text_completion.to_dict() - assert dumped_settings["ai_model_id"] == ai_model_id - assert dumped_settings["api_key"] == api_key - assert dumped_settings["org_id"] == org_id + assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_TEXT_MODEL_ID"] + assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"] + assert dumped_settings["org_id"] == openai_unit_test_env["OPENAI_ORG_ID"] diff --git a/python/tests/unit/connectors/open_ai/services/test_openai_text_embedding.py b/python/tests/unit/connectors/open_ai/services/test_openai_text_embedding.py index 4dac491305..533493c162 100644 --- a/python/tests/unit/connectors/open_ai/services/test_openai_text_embedding.py +++ b/python/tests/unit/connectors/open_ai/services/test_openai_text_embedding.py @@ -10,15 +10,13 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_embedding impor @pytest.mark.asyncio @patch.object(AsyncEmbeddings, "create", new_callable=AsyncMock) -async def test_openai_text_embedding_calls_with_parameters(mock_create) -> None: +async def test_openai_text_embedding_calls_with_parameters(mock_create, openai_unit_test_env) -> None: ai_model_id = "test_model_id" - api_key = "test_api_key" texts = ["hello world", "goodbye world"] embedding_dimensions = 1536 openai_text_embedding = OpenAITextEmbedding( ai_model_id=ai_model_id, - api_key=api_key, ) await openai_text_embedding.generate_embeddings(texts, dimensions=embedding_dimensions) diff --git a/python/tests/unit/core_plugins/test_sessions_python_plugin.py b/python/tests/unit/core_plugins/test_sessions_python_plugin.py index 2c2daf0c9e..86a867fa8d 100644 --- a/python/tests/unit/core_plugins/test_sessions_python_plugin.py +++ b/python/tests/unit/core_plugins/test_sessions_python_plugin.py @@ -16,21 +16,19 @@ def test_auth_callback(): return "sample_token" -def test_it_can_be_instantiated(): - plugin = SessionsPythonTool(pool_management_endpoint="https://example.com", auth_callback=test_auth_callback) +def test_it_can_be_instantiated(aca_python_sessions_unit_test_env): + plugin = SessionsPythonTool(auth_callback=test_auth_callback) assert plugin is not None -def test_validate_endpoint(): - plugin = SessionsPythonTool( - pool_management_endpoint="https://example.com/python/execute/", auth_callback=test_auth_callback - ) +def test_validate_endpoint(aca_python_sessions_unit_test_env): + plugin = SessionsPythonTool(auth_callback=test_auth_callback) assert plugin is not None - assert plugin.pool_management_endpoint == "https://example.com/" + assert plugin.pool_management_endpoint == aca_python_sessions_unit_test_env["ACA_POOL_MANAGEMENT_ENDPOINT"] -def test_it_can_be_imported(kernel: Kernel): - plugin = SessionsPythonTool(pool_management_endpoint="https://example.com", auth_callback=test_auth_callback) +def test_it_can_be_imported(kernel: Kernel, aca_python_sessions_unit_test_env): + plugin = SessionsPythonTool(auth_callback=test_auth_callback) assert kernel.add_plugin(plugin=plugin, plugin_name="PythonCodeInterpreter") assert kernel.plugins["PythonCodeInterpreter"] is not None assert kernel.plugins["PythonCodeInterpreter"].name == "PythonCodeInterpreter" @@ -38,7 +36,7 @@ def test_it_can_be_imported(kernel: Kernel): @pytest.mark.asyncio @patch("httpx.AsyncClient.post") -async def test_call_to_container_succeeds(mock_post): +async def test_call_to_container_succeeds(mock_post, aca_python_sessions_unit_test_env): async def async_return(result): return result @@ -54,9 +52,7 @@ async def test_call_to_container_succeeds(mock_post): mock_post.return_value = await async_return(mock_response) - plugin = SessionsPythonTool( - pool_management_endpoint="https://example.com/python/execute/", auth_callback=test_auth_callback - ) + plugin = SessionsPythonTool(auth_callback=test_auth_callback) result = await plugin.execute_code("print('hello world')") assert result is not None @@ -65,7 +61,7 @@ async def test_call_to_container_succeeds(mock_post): @pytest.mark.asyncio @patch("httpx.AsyncClient.post") -async def test_call_to_container_fails_raises_exception(mock_post): +async def test_call_to_container_fails_raises_exception(mock_post, aca_python_sessions_unit_test_env): async def async_return(result): return result @@ -79,9 +75,7 @@ async def test_call_to_container_fails_raises_exception(mock_post): mock_post.return_value = await async_return(mock_response) - plugin = SessionsPythonTool( - pool_management_endpoint="https://example.com/python/execute/", auth_callback=test_auth_callback - ) + plugin = SessionsPythonTool(auth_callback=test_auth_callback) with pytest.raises(Exception): _ = await plugin.execute_code("print('hello world')") @@ -89,7 +83,7 @@ async def test_call_to_container_fails_raises_exception(mock_post): @pytest.mark.asyncio @patch("httpx.AsyncClient.post") -async def test_upload_file_with_local_path(mock_post): +async def test_upload_file_with_local_path(mock_post, aca_python_sessions_unit_test_env): """Test upload_file when providing a local file path.""" async def async_return(result): @@ -106,9 +100,7 @@ async def test_upload_file_with_local_path(mock_post): ) mock_post.return_value = await async_return(mock_response) - plugin = SessionsPythonTool( - pool_management_endpoint="https://example.com/python/", auth_callback=lambda: "sample_token" - ) + plugin = SessionsPythonTool(auth_callback=lambda: "sample_token") result = await plugin.upload_file(local_file_path="test.txt", remote_file_path="uploaded_test.txt") assert result.filename == "test.txt" @@ -118,7 +110,7 @@ async def test_upload_file_with_local_path(mock_post): @pytest.mark.asyncio @patch("httpx.AsyncClient.post") -async def test_upload_file_with_buffer(mock_post): +async def test_upload_file_with_buffer(mock_post, aca_python_sessions_unit_test_env): """Test upload_file when providing file data as a BufferedReader.""" async def async_return(result): @@ -135,9 +127,7 @@ async def test_upload_file_with_buffer(mock_post): ) mock_post.return_value = await async_return(mock_response) - plugin = SessionsPythonTool( - pool_management_endpoint="https://example.com/python/", auth_callback=lambda: "sample_token" - ) + plugin = SessionsPythonTool(auth_callback=lambda: "sample_token") data_buffer = BufferedReader(BytesIO(b"file data")) @@ -149,7 +139,7 @@ async def test_upload_file_with_buffer(mock_post): @pytest.mark.asyncio @patch("httpx.AsyncClient.get") -async def test_list_files(mock_get): +async def test_list_files(mock_get, aca_python_sessions_unit_test_env): """Test list_files function.""" async def async_return(result): @@ -174,9 +164,7 @@ async def test_list_files(mock_get): ) mock_get.return_value = await async_return(mock_response) - plugin = SessionsPythonTool( - pool_management_endpoint="https://example.com/python/", auth_callback=lambda: "sample_token" - ) + plugin = SessionsPythonTool(auth_callback=lambda: "sample_token") files = await plugin.list_files() assert len(files) == 2 @@ -189,7 +177,7 @@ async def test_list_files(mock_get): @pytest.mark.asyncio @patch("httpx.AsyncClient.get") -async def test_download_file_to_local(mock_get): +async def test_download_file_to_local(mock_get, aca_python_sessions_unit_test_env): """Test download_file when saving to a local file path.""" async def async_return(result): @@ -209,9 +197,7 @@ async def test_download_file_to_local(mock_get): mock_response = httpx.Response(status_code=200, content=b"file data", request=mock_request) mock_get.return_value = await async_return(mock_response) - plugin = SessionsPythonTool( - pool_management_endpoint="https://example.com/python/", auth_callback=mock_auth_callback - ) + plugin = SessionsPythonTool(auth_callback=mock_auth_callback) await plugin.download_file(remote_file_path="remote_test.txt", local_file_path="local_test.txt") mock_get.assert_awaited_once() @@ -221,7 +207,7 @@ async def test_download_file_to_local(mock_get): @pytest.mark.asyncio @patch("httpx.AsyncClient.get") -async def test_download_file_to_buffer(mock_get): +async def test_download_file_to_buffer(mock_get, aca_python_sessions_unit_test_env): """Test download_file when returning as a BufferedReader.""" async def async_return(result): @@ -241,9 +227,7 @@ async def test_download_file_to_buffer(mock_get): mock_response = httpx.Response(status_code=200, content=b"file data", request=mock_request) mock_get.return_value = await async_return(mock_response) - plugin = SessionsPythonTool( - pool_management_endpoint="https://example.com/python/", auth_callback=mock_auth_callback - ) + plugin = SessionsPythonTool(auth_callback=mock_auth_callback) buffer = await plugin.download_file(remote_file_path="remote_test.txt") assert buffer is not None @@ -274,10 +258,8 @@ async def test_download_file_to_buffer(mock_get): (" ", ""), ], ) -def test_sanitize_input(input_code, expected_output): +def test_sanitize_input(input_code, expected_output, aca_python_sessions_unit_test_env): """Test the `_sanitize_input` function with various inputs.""" - plugin = SessionsPythonTool( - pool_management_endpoint="https://example.com/python/", auth_callback=lambda: "sample_token" - ) + plugin = SessionsPythonTool(auth_callback=lambda: "sample_token") sanitized_code = plugin._sanitize_input(input_code) assert sanitized_code == expected_output diff --git a/python/tests/unit/functions/test_kernel_function_from_method.py b/python/tests/unit/functions/test_kernel_function_from_method.py index b521202cbe..0fe816c850 100644 --- a/python/tests/unit/functions/test_kernel_function_from_method.py +++ b/python/tests/unit/functions/test_kernel_function_from_method.py @@ -191,9 +191,9 @@ async def test_invoke_gen_async(): @pytest.mark.asyncio -async def test_service_execution(): +async def test_service_execution(openai_unit_test_env): kernel = Kernel() - service = OpenAIChatCompletion(service_id="test", ai_model_id="test", api_key="test") + service = OpenAIChatCompletion(service_id="test", ai_model_id="test") req_settings = service.get_prompt_execution_settings_class()(service_id="test") req_settings.temperature = 0.5 kernel.add_service(service) diff --git a/python/tests/unit/functions/test_kernel_function_from_prompt.py b/python/tests/unit/functions/test_kernel_function_from_prompt.py index 506f8393d5..3f285da917 100644 --- a/python/tests/unit/functions/test_kernel_function_from_prompt.py +++ b/python/tests/unit/functions/test_kernel_function_from_prompt.py @@ -140,9 +140,9 @@ def test_init_prompt_execution_settings_dict(): @pytest.mark.asyncio -async def test_invoke_chat_stream(): +async def test_invoke_chat_stream(openai_unit_test_env): kernel = Kernel() - kernel.add_service(OpenAIChatCompletion(service_id="test", ai_model_id="test", api_key="test")) + kernel.add_service(OpenAIChatCompletion(service_id="test", ai_model_id="test")) function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", @@ -169,9 +169,9 @@ async def test_invoke_chat_stream(): @pytest.mark.asyncio -async def test_invoke_exception(): +async def test_invoke_exception(openai_unit_test_env): kernel = Kernel() - kernel.add_service(OpenAIChatCompletion(service_id="test", ai_model_id="test", api_key="test")) + kernel.add_service(OpenAIChatCompletion(service_id="test", ai_model_id="test")) function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", @@ -198,9 +198,9 @@ async def test_invoke_exception(): @pytest.mark.asyncio -async def test_invoke_text(): +async def test_invoke_text(openai_unit_test_env): kernel = Kernel() - kernel.add_service(OpenAITextCompletion(service_id="test", ai_model_id="test", api_key="test")) + kernel.add_service(OpenAITextCompletion(service_id="test", ai_model_id="test")) function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", @@ -223,9 +223,9 @@ async def test_invoke_text(): @pytest.mark.asyncio -async def test_invoke_exception_text(): +async def test_invoke_exception_text(openai_unit_test_env): kernel = Kernel() - kernel.add_service(OpenAITextCompletion(service_id="test", ai_model_id="test", api_key="test")) + kernel.add_service(OpenAITextCompletion(service_id="test", ai_model_id="test")) function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", @@ -250,9 +250,9 @@ async def test_invoke_exception_text(): @pytest.mark.asyncio -async def test_invoke_defaults(): +async def test_invoke_defaults(openai_unit_test_env): kernel = Kernel() - kernel.add_service(OpenAIChatCompletion(service_id="test", ai_model_id="test", api_key="test")) + kernel.add_service(OpenAIChatCompletion(service_id="test", ai_model_id="test")) function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", @@ -291,9 +291,9 @@ def test_create_with_multiple_settings(): @pytest.mark.asyncio -async def test_create_with_multiple_settings_one_service_registered(): +async def test_create_with_multiple_settings_one_service_registered(openai_unit_test_env): kernel = Kernel() - kernel.add_service(OpenAIChatCompletion(service_id="test2", ai_model_id="test", api_key="test")) + kernel.add_service(OpenAIChatCompletion(service_id="test2", ai_model_id="test")) function = KernelFunctionFromPrompt( function_name="test", plugin_name="test", diff --git a/python/tests/unit/memory/test_azure_cognitive_search_memory_store.py b/python/tests/unit/memory/test_azure_cognitive_search_memory_store.py index 36561e4697..f667068158 100644 --- a/python/tests/unit/memory/test_azure_cognitive_search_memory_store.py +++ b/python/tests/unit/memory/test_azure_cognitive_search_memory_store.py @@ -11,7 +11,7 @@ from semantic_kernel.connectors.memory.azure_cognitive_search import AzureCognit @pytest.fixture -def azure_cognitive_search_memory_store(): +def azure_cognitive_search_memory_store(azure_ai_search_unit_test_env): """Fixture to instantiate AzureCognitiveSearchMemoryStore with basic configuration.""" store = AzureCognitiveSearchMemoryStore( 1536, "https://test.search.windows.net", azure_credentials=AzureKeyCredential("test_key")