Skip to content

Commit 46f5ea1

Browse files
authored
Python: Introduce Pydantic settings (microsoft#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. <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> ### Description The PR introduces the use of Pydantic settings and removes the use of the python-dotenv library. - Closes microsoft#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 <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [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 😄
1 parent b95f05c commit 46f5ea1

File tree

136 files changed

+2609
-2986
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

136 files changed

+2609
-2986
lines changed

.github/workflows/python-integration-tests.yml

+30-38
Original file line numberDiff line numberDiff line change
@@ -76,25 +76,21 @@ jobs:
7676
env: # Set Azure credentials secret as an input
7777
HNSWLIB_NO_NATIVE: 1
7878
Python_Integration_Tests: Python_Integration_Tests
79-
AzureOpenAI__Label: azure-text-davinci-003
80-
AzureOpenAIEmbedding__Label: azure-text-embedding-ada-002
81-
AzureOpenAI__DeploymentName: ${{ vars.AZUREOPENAI__DEPLOYMENTNAME }}
82-
AzureOpenAI__Text__DeploymentName: ${{ vars.AZUREOPENAI__TEXT__DEPLOYMENTNAME }}
83-
AzureOpenAIChat__DeploymentName: ${{ vars.AZUREOPENAI__CHAT__DEPLOYMENTNAME }}
84-
AzureOpenAIEmbeddings__DeploymentName: ${{ vars.AZUREOPENAIEMBEDDINGS__DEPLOYMENTNAME2 }}
85-
AzureOpenAIEmbeddings_EastUS__DeploymentName: ${{ vars.AZUREOPENAIEMBEDDINGS_EASTUS__DEPLOYMENTNAME}}
86-
AzureOpenAI__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }}
87-
AzureOpenAI_EastUS__Endpoint: ${{ secrets.AZUREOPENAI_EASTUS__ENDPOINT }}
88-
AzureOpenAI_EastUS__ApiKey: ${{ secrets.AZUREOPENAI_EASTUS__APIKEY }}
89-
AzureOpenAIEmbeddings__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }}
90-
AzureOpenAI__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }}
91-
AzureOpenAIEmbeddings__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }}
92-
Bing__ApiKey: ${{ secrets.BING__APIKEY }}
93-
OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
94-
Pinecone__ApiKey: ${{ secrets.PINECONE__APIKEY }}
95-
Postgres__Connectionstr: ${{secrets.POSTGRES__CONNECTIONSTR}}
96-
AZURE_COGNITIVE_SEARCH_ADMIN_KEY: ${{secrets.AZURE_COGNITIVE_SEARCH_ADMIN_KEY}}
97-
AZURE_COGNITIVE_SEARCH_ENDPOINT: ${{secrets.AZURE_COGNITIVE_SEARCH_ENDPOINT}}
79+
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }} # azure-text-embedding-ada-002
80+
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_CHAT_DEPLOYMENT_NAME }}
81+
AZURE_OPENAI_TEXT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_TEXT_DEPLOYMENT_NAME }}
82+
AZURE_OPENAI_API_VERSION: ${{ vars.AZURE_OPENAI_API_VERSION }}
83+
AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
84+
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
85+
BING_API_KEY: ${{ secrets.BING_API_KEY }}
86+
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI_CHAT_MODEL_ID }}
87+
OPENAI_TEXT_MODEL_ID: ${{ vars.OPENAI_TEXT_MODEL_ID }}
88+
OPENAI_EMBEDDING_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
89+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
90+
PINECONE_API_KEY: ${{ secrets.PINECONE__APIKEY }}
91+
POSTGRES_CONNECTION_STRING: ${{secrets.POSTGRES__CONNECTIONSTR}}
92+
AZURE_AI_SEARCH_API_KEY: ${{secrets.AZURE_AI_SEARCH_API_KEY}}
93+
AZURE_AI_SEARCH_ENDPOINT: ${{secrets.AZURE_AI_SEARCH_ENDPOINT}}
9894
MONGODB_ATLAS_CONNECTION_STRING: ${{secrets.MONGODB_ATLAS_CONNECTION_STRING}}
9995
run: |
10096
if ${{ matrix.os == 'ubuntu-latest' }}; then
@@ -142,25 +138,21 @@ jobs:
142138
env: # Set Azure credentials secret as an input
143139
HNSWLIB_NO_NATIVE: 1
144140
Python_Integration_Tests: Python_Integration_Tests
145-
AzureOpenAI__Label: azure-text-davinci-003
146-
AzureOpenAIEmbedding__Label: azure-text-embedding-ada-002
147-
AzureOpenAI__DeploymentName: ${{ vars.AZUREOPENAI__DEPLOYMENTNAME }}
148-
AzureOpenAI__Text__DeploymentName: ${{ vars.AZUREOPENAI__TEXT__DEPLOYMENTNAME }}
149-
AzureOpenAIChat__DeploymentName: ${{ vars.AZUREOPENAI__CHAT__DEPLOYMENTNAME }}
150-
AzureOpenAIEmbeddings__DeploymentName: ${{ vars.AZUREOPENAIEMBEDDINGS__DEPLOYMENTNAME2 }}
151-
AzureOpenAIEmbeddings_EastUS__DeploymentName: ${{ vars.AZUREOPENAIEMBEDDINGS_EASTUS__DEPLOYMENTNAME}}
152-
AzureOpenAI__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }}
153-
AzureOpenAIEmbeddings__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }}
154-
AzureOpenAI__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }}
155-
AzureOpenAI_EastUS__Endpoint: ${{ secrets.AZUREOPENAI_EASTUS__ENDPOINT }}
156-
AzureOpenAI_EastUS__ApiKey: ${{ secrets.AZUREOPENAI_EASTUS__APIKEY }}
157-
AzureOpenAIEmbeddings__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }}
158-
Bing__ApiKey: ${{ secrets.BING__APIKEY }}
159-
OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
160-
Pinecone__ApiKey: ${{ secrets.PINECONE__APIKEY }}
161-
Postgres__Connectionstr: ${{secrets.POSTGRES__CONNECTIONSTR}}
162-
AZURE_COGNITIVE_SEARCH_ADMIN_KEY: ${{secrets.AZURE_COGNITIVE_SEARCH_ADMIN_KEY}}
163-
AZURE_COGNITIVE_SEARCH_ENDPOINT: ${{secrets.AZURE_COGNITIVE_SEARCH_ENDPOINT}}
141+
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }} # azure-text-embedding-ada-002
142+
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_CHAT_DEPLOYMENT_NAME }}
143+
AZURE_OPENAI_TEXT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_TEXT_DEPLOYMENT_NAME }}
144+
AZURE_OPENAI_API_VERSION: ${{ vars.AZURE_OPENAI_API_VERSION }}
145+
AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
146+
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
147+
BING_API_KEY: ${{ secrets.BING_API_KEY }}
148+
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI_CHAT_MODEL_ID }}
149+
OPENAI_TEXT_MODEL_ID: ${{ vars.OPENAI_TEXT_MODEL_ID }}
150+
OPENAI_EMBEDDING_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
151+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
152+
PINECONE_API_KEY: ${{ secrets.PINECONE__APIKEY }}
153+
POSTGRES_CONNECTION_STRING: ${{secrets.POSTGRES__CONNECTIONSTR}}
154+
AZURE_AI_SEARCH_API_KEY: ${{secrets.AZURE_AI_SEARCH_API_KEY}}
155+
AZURE_AI_SEARCH_ENDPOINT: ${{secrets.AZURE_AI_SEARCH_ENDPOINT}}
164156
MONGODB_ATLAS_CONNECTION_STRING: ${{secrets.MONGODB_ATLAS_CONNECTION_STRING}}
165157
run: |
166158
if ${{ matrix.os == 'ubuntu-latest' }}; then

python/.env.example

+6-1
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,9 @@ ASTRADB_APP_TOKEN=""
4646
ASTRADB_ID=""
4747
ASTRADB_REGION=""
4848
ASTRADB_KEYSPACE=""
49-
ACA_POOL_MANAGEMENT_ENDPOINT=""
49+
ACA_POOL_MANAGEMENT_ENDPOINT=""
50+
BOOKING_SAMPLE_CLIENT_ID=""
51+
BOOKING_SAMPLE_TENANT_ID=""
52+
BOOKING_SAMPLE_CLIENT_SECRET=""
53+
BOOKING_SAMPLE_BUSINESS_ID=""
54+
BOOKING_SAMPLE_SERVICE_ID=""

python/DEV_SETUP.md

+18-4
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,31 @@ Make sure you have an
1010
[OpenAI API Key](https://platform.openai.com) or
1111
[Azure OpenAI service key](https://learn.microsoft.com/azure/cognitive-services/openai/quickstart?pivots=rest-api)
1212

13-
Copy those keys into a `.env` file (see the `.env.example` file):
13+
There are two methods to manage keys, secrets, and endpoints:
1414

15-
```bash
15+
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.
16+
17+
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):
18+
19+
```
1620
OPENAI_API_KEY=""
1721
OPENAI_ORG_ID=""
18-
AZURE_OPENAI_DEPLOYMENT_NAME=""
22+
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=""
23+
AZURE_OPENAI_TEXT_DEPLOYMENT_NAME=""
24+
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME=""
1925
AZURE_OPENAI_ENDPOINT=""
2026
AZURE_OPENAI_API_KEY=""
2127
```
2228

23-
We suggest adding a copy of the `.env` file under these folders:
29+
You will then configure the Text/ChatCompletion class with the keyword argument `env_file_path`:
30+
31+
```python
32+
chat_completion = OpenAIChatCompletion(service_id="test", env_file_path=<path_to_file>)
33+
```
34+
35+
This optional `env_file_path` parameter will allow pydantic settings to use the `.env` file as a fallback to read the settings.
36+
37+
If using the second method, we suggest adding a copy of the `.env` file under these folders:
2438

2539
- [python/tests](tests)
2640
- [./samples/getting_started](./samples/getting_started).

python/README.md

+20-15
Original file line numberDiff line numberDiff line change
@@ -20,47 +20,52 @@ Make sure you have an
2020
[OpenAI API Key](https://platform.openai.com) or
2121
[Azure OpenAI service key](https://learn.microsoft.com/azure/cognitive-services/openai/quickstart?pivots=rest-api)
2222

23-
Copy those keys into a `.env` file (see the `.env.example` file):
23+
There are two methods to manage keys, secrets, and endpoints:
24+
25+
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.
26+
27+
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):
2428

2529
```
2630
OPENAI_API_KEY=""
2731
OPENAI_ORG_ID=""
28-
AZURE_OPENAI_DEPLOYMENT_NAME=""
32+
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=""
33+
AZURE_OPENAI_TEXT_DEPLOYMENT_NAME=""
34+
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME=""
2935
AZURE_OPENAI_ENDPOINT=""
3036
AZURE_OPENAI_API_KEY=""
3137
```
3238

39+
You will then configure the Text/ChatCompletion class with the keyword argument `env_file_path`:
40+
41+
```python
42+
chat_completion = OpenAIChatCompletion(service_id="test", env_file_path=<path_to_file>)
43+
```
44+
45+
This optional `env_file_path` parameter will allow pydantic settings to use the `.env` file as a fallback to read the settings.
46+
3347
# Running a prompt
3448

3549
```python
3650
import asyncio
3751
from semantic_kernel import Kernel
3852
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, AzureChatCompletion
3953
from semantic_kernel.prompt_template import PromptTemplateConfig
40-
from semantic_kernel.utils.settings import openai_settings_from_dot_env, azure_openai_settings_from_dot_env
4154

4255
kernel = Kernel()
4356

4457
# Prepare OpenAI service using credentials stored in the `.env` file
45-
api_key, org_id = openai_settings_from_dot_env()
4658
service_id="chat-gpt"
4759
kernel.add_service(
4860
OpenAIChatCompletion(
4961
service_id=service_id,
50-
ai_model_id="gpt-3.5-turbo",
51-
api_key=api_key,
52-
org_id=org_id
5362
)
5463
)
5564

5665
# Alternative using Azure:
57-
# deployment, api_key, endpoint = azure_openai_settings_from_dot_env()
5866
# kernel.add_service(
5967
# AzureChatCompletion(
6068
# service_id=service_id,
61-
# deployment_name=deployment,
62-
# endpoint=endpoint,
63-
# api_key=api_key
6469
# )
6570
# )
6671

@@ -112,10 +117,10 @@ if __name__ == "__main__":
112117
```python
113118
# Create a reusable function summarize function
114119
summarize = kernel.add_function(
115-
function_name="tldr_function",
116-
plugin_name="tldr_plugin",
117-
prompt="{{$input}}\n\nOne line TLDR with the fewest words.",
118-
prompt_template_settings=req_settings,
120+
function_name="tldr_function",
121+
plugin_name="tldr_plugin",
122+
prompt="{{$input}}\n\nOne line TLDR with the fewest words.",
123+
prompt_template_settings=req_settings,
119124
)
120125

121126
# Summarize the laws of thermodynamics

python/poetry.lock

+21-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

python/pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ grpcio = [
2424
{ version = ">=1.60.0", python = ">=3.12" }
2525
]
2626
openai = ">=1.0"
27-
python-dotenv = "^1.0.1"
2827
regex = "^2023.6.3"
2928
openapi_core = ">=0.18,<0.20"
3029
prance = "^23.6.21.0"
3130
pydantic = "^2"
31+
pydantic-settings = "^2.2.1"
3232
motor = "^3.3.2"
3333
defusedxml = "^0.7.1"
3434
pybars4 = "^0.9.13"

python/samples/concepts/auto_function_calling/azure_python_code_interpreter_function_calling.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@
2020
from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException
2121
from semantic_kernel.functions.kernel_arguments import KernelArguments
2222
from semantic_kernel.kernel import Kernel
23-
from semantic_kernel.utils.settings import (
24-
azure_container_apps_settings_from_dot_env_as_dict,
25-
azure_openai_settings_from_dot_env_as_dict,
26-
)
2723

2824
auth_token: AccessToken | None = None
2925

@@ -56,12 +52,11 @@ async def auth_callback() -> str:
5652

5753
service_id = "sessions-tool"
5854
chat_service = AzureChatCompletion(
59-
service_id=service_id, **azure_openai_settings_from_dot_env_as_dict(include_api_version=True)
55+
service_id=service_id,
6056
)
6157
kernel.add_service(chat_service)
6258

6359
sessions_tool = SessionsPythonTool(
64-
**azure_container_apps_settings_from_dot_env_as_dict(),
6560
auth_callback=auth_callback,
6661
)
6762

python/samples/concepts/auto_function_calling/chat_gpt_api_function_calling.py

-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
1818
from semantic_kernel.core_plugins import MathPlugin, TimePlugin
1919
from semantic_kernel.functions import KernelArguments
20-
from semantic_kernel.utils.settings import openai_settings_from_dot_env
2120

2221
if TYPE_CHECKING:
2322
from semantic_kernel.functions import KernelFunction
@@ -38,12 +37,10 @@
3837
kernel = Kernel()
3938

4039
# Note: the underlying gpt-35/gpt-4 model version needs to be at least version 0613 to support tools.
41-
api_key, org_id = openai_settings_from_dot_env()
4240
kernel.add_service(
4341
OpenAIChatCompletion(
4442
service_id="chat",
4543
ai_model_id="gpt-3.5-turbo-1106",
46-
api_key=api_key,
4744
),
4845
)
4946

python/samples/concepts/chat_completion/azure_chat_gpt_api.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBehavior
88
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
99
from semantic_kernel.contents import ChatHistory
10-
from semantic_kernel.utils.settings import azure_openai_settings_from_dot_env_as_dict
1110

1211
logging.basicConfig(level=logging.WARNING)
1312

@@ -24,7 +23,7 @@
2423

2524
service_id = "chat-gpt"
2625
chat_service = AzureChatCompletion(
27-
service_id=service_id, **azure_openai_settings_from_dot_env_as_dict(include_api_version=True)
26+
service_id=service_id,
2827
)
2928
kernel.add_service(chat_service)
3029

python/samples/concepts/chat_completion/chat.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
77
from semantic_kernel.contents import ChatHistory
88
from semantic_kernel.prompt_template import InputVariable, PromptTemplateConfig
9-
from semantic_kernel.utils.settings import openai_settings_from_dot_env
109

1110
prompt = """
1211
ChatBot can have a conversation with you about any topic.
@@ -21,11 +20,8 @@
2120

2221
kernel = Kernel()
2322

24-
api_key, org_id = openai_settings_from_dot_env()
2523
service_id = "chat"
26-
kernel.add_service(
27-
OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo-1106", api_key=api_key, org_id=org_id)
28-
)
24+
kernel.add_service(OpenAIChatCompletion(service_id=service_id))
2925

3026
settings = kernel.get_prompt_execution_settings_from_service_id(service_id)
3127
settings.max_tokens = 2000

python/samples/concepts/chat_completion/chat_gpt_api.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
77
from semantic_kernel.contents import ChatHistory
88
from semantic_kernel.functions import KernelArguments
9-
from semantic_kernel.utils.settings import openai_settings_from_dot_env
109

1110
system_message = """
1211
You are a chat bot. Your name is Mosscap and
@@ -19,11 +18,8 @@
1918

2019
kernel = Kernel()
2120

22-
api_key, org_id = openai_settings_from_dot_env()
2321
service_id = "chat-gpt"
24-
kernel.add_service(
25-
OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo", api_key=api_key, org_id=org_id)
26-
)
22+
kernel.add_service(OpenAIChatCompletion(service_id=service_id))
2723

2824
settings = kernel.get_prompt_execution_settings_from_service_id(service_id)
2925
settings.max_tokens = 2000

0 commit comments

Comments
 (0)