From 68fc50772ffeab48ec64986d88c438c2749b974d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 May 2025 15:52:28 +0000 Subject: [PATCH 01/36] Initial plan for issue From 0c6960e963bb7861d0594fce7aa494fe7656de3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 May 2025 15:56:59 +0000 Subject: [PATCH 02/36] Create Python ChatApp example for AI configuration Co-authored-by: mrm9084 <1054559+mrm9084@users.noreply.github.com> --- examples/Python/ChatApp/README.md | 66 ++++++ examples/Python/ChatApp/app.py | 193 ++++++++++++++++++ examples/Python/ChatApp/models.py | 46 +++++ examples/Python/ChatApp/requirements.txt | 4 + examples/Python/ChatApp/templates/base.html | 55 +++++ .../Python/ChatApp/templates/console.html | 23 +++ examples/Python/ChatApp/templates/index.html | 57 ++++++ 7 files changed, 444 insertions(+) create mode 100644 examples/Python/ChatApp/README.md create mode 100644 examples/Python/ChatApp/app.py create mode 100644 examples/Python/ChatApp/models.py create mode 100644 examples/Python/ChatApp/requirements.txt create mode 100644 examples/Python/ChatApp/templates/base.html create mode 100644 examples/Python/ChatApp/templates/console.html create mode 100644 examples/Python/ChatApp/templates/index.html diff --git a/examples/Python/ChatApp/README.md b/examples/Python/ChatApp/README.md new file mode 100644 index 00000000..334d6822 --- /dev/null +++ b/examples/Python/ChatApp/README.md @@ -0,0 +1,66 @@ +# Azure App Configuration - Python ChatApp Sample + +This sample demonstrates using Azure App Configuration to configure Azure OpenAI settings for a chat application built with Python and Flask. + +## Features + +- Built with Python and Flask +- Uses azure-appconfiguration-provider for configuration management +- Integrates with Azure OpenAI for chat completions +- Dynamically refreshes configuration from Azure App Configuration +- Supports both web interface and console-like views + +## Prerequisites + +- Python 3.8 or later +- An Azure subscription with access to: + - Azure App Configuration service + - Azure OpenAI service +- Required environment variables: + - `AZURE_APPCONFIG_ENDPOINT`: URL of your Azure App Configuration instance + - `AZURE_OPENAI_API_KEY`: API key for Azure OpenAI (optional if using DefaultAzureCredential) + +## Setup + +1. Clone the repository +2. Install the required packages: + ```bash + pip install -r requirements.txt + ``` +3. Configure your Azure App Configuration store with these settings: + ``` + ChatApp:AzureOpenAI:Endpoint - Your Azure OpenAI endpoint URL + ChatApp:AzureOpenAI:DeploymentName - Your Azure OpenAI deployment name + ChatApp:Model:model - Model name (e.g., "gpt-35-turbo") + ChatApp:Model:max_tokens - Maximum tokens for completion (e.g., 1000) + ChatApp:Model:temperature - Temperature parameter (e.g., 0.7) + ChatApp:Model:top_p - Top p parameter (e.g., 0.95) + ChatApp:Model:messages:0:role - Role for message 0 (e.g., "system") + ChatApp:Model:messages:0:content - Content for message 0 + ChatApp:Model:messages:1:role - Role for message 1 (e.g., "user") + ChatApp:Model:messages:1:content - Content for message 1 + ``` + +4. Set the required environment variables: + ```bash + export AZURE_APPCONFIG_ENDPOINT="https://your-appconfig.azconfig.io" + export AZURE_OPENAI_API_KEY="your-openai-api-key" # Optional if using DefaultAzureCredential + ``` + +## Running the Application + +Start the Flask application: +```bash +python app.py +``` + +The application will be available at http://localhost:5000/ + +## Views + +- **Chat Interface** (/) - A web interface for chatting with the AI +- **Console Mode** (/console) - A view that mimics the .NET Core console application experience + +## Configuration Refresh + +The application refreshes the configuration on each request, so any changes made in Azure App Configuration will be reflected immediately in the app. \ No newline at end of file diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py new file mode 100644 index 00000000..96d1b4b1 --- /dev/null +++ b/examples/Python/ChatApp/app.py @@ -0,0 +1,193 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# +import os +import time +from flask import Flask, render_template, request +from azure.identity import DefaultAzureCredential +from azure.appconfiguration.provider import load, SettingSelector +from openai import AzureOpenAI +from models import ModelConfiguration + +# Initialize Flask app +app = Flask(__name__) + +# Get Azure App Configuration endpoint from environment variable +ENDPOINT = os.environ.get("AZURE_APPCONFIG_ENDPOINT") +if not ENDPOINT: + raise ValueError("The environment variable 'AZURE_APPCONFIG_ENDPOINT' is not set or is empty.") + +# Initialize Azure credentials +credential = DefaultAzureCredential() + +# Create selector for ChatApp configuration +chat_app_selector = SettingSelector(key_filter="ChatApp:*") + +# Load configuration from Azure App Configuration +def callback(): + """Callback function for configuration refresh""" + app.config.update(azure_app_config) + print("Configuration refreshed successfully") + +global azure_app_config +azure_app_config = load( + endpoint=ENDPOINT, + selects=[chat_app_selector], + credential=credential, + keyvault_credential=credential, + on_refresh_success=callback, +) + +# Update Flask app config with Azure App Configuration +app.config.update(azure_app_config) + +# Get OpenAI configuration +def get_openai_client(): + """Create and return an Azure OpenAI client""" + endpoint = app.config.get("ChatApp:AzureOpenAI:Endpoint") + api_key = os.environ.get("AZURE_OPENAI_API_KEY") # Using environment variable for API key + + # For DefaultAzureCredential auth + if not api_key: + return AzureOpenAI( + azure_endpoint=endpoint, + api_version="2023-05-15", + azure_ad_token_provider=credential + ) + + # For API key auth + return AzureOpenAI( + azure_endpoint=endpoint, + api_key=api_key, + api_version="2023-05-15" + ) + +def get_model_configuration(): + """Get model configuration from app config""" + model_config = {} + + # Extract model configuration from app.config + prefix = "ChatApp:Model:" + for key in app.config: + if key.startswith(prefix): + # Get the part of the key after the prefix + config_key = key[len(prefix):].lower() + model_config[config_key] = app.config[key] + + # Handle messages specially (they're nested) + messages = [] + messages_prefix = "ChatApp:Model:messages:" + + # Group message configs by index + message_configs = {} + for key in app.config: + if key.startswith(messages_prefix): + # Extract the index and property (e.g., "0:role" -> ("0", "role")) + parts = key[len(messages_prefix):].split(':') + if len(parts) == 2: + index, prop = parts + if index not in message_configs: + message_configs[index] = {} + message_configs[index][prop.lower()] = app.config[key] + + # Create message list in the right order + for index in sorted(message_configs.keys()): + messages.append(message_configs[index]) + + model_config['messages'] = messages + + return ModelConfiguration.from_dict(model_config) + +def get_chat_messages(model_config): + """Convert from model configuration messages to OpenAI messages format""" + return [{"role": msg.role, "content": msg.content} for msg in model_config.messages] + +@app.route('/') +def index(): + """Main route to display and handle chat""" + # Refresh configuration from Azure App Configuration + azure_app_config.refresh() + + # Get model configuration + model_config = get_model_configuration() + + # Get OpenAI client + client = get_openai_client() + + # Get deployment name + deployment_name = app.config.get("ChatApp:AzureOpenAI:DeploymentName") + + # Get chat history from model config for display + chat_history = [{"role": msg.role, "content": msg.content} for msg in model_config.messages] + + # Check if a new message was submitted + if request.args.get('message'): + user_message = request.args.get('message') + + # Add user message to chat messages + messages = get_chat_messages(model_config) + messages.append({"role": "user", "content": user_message}) + + # Add user message to chat history + chat_history.append({"role": "user", "content": user_message}) + + # Get response from OpenAI + response = client.chat.completions.create( + model=deployment_name, + messages=messages, + max_tokens=model_config.max_tokens, + temperature=model_config.temperature, + top_p=model_config.top_p + ) + + # Extract assistant message + assistant_message = response.choices[0].message.content + + # Add assistant message to chat history + chat_history.append({"role": "assistant", "content": assistant_message}) + + return render_template('index.html', + chat_history=chat_history, + model_config=model_config) + +@app.route('/console') +def console(): + """Console mode similar to the .NET Core example""" + # Refresh configuration from Azure App Configuration + azure_app_config.refresh() + + # Get model configuration + model_config = get_model_configuration() + + # Get OpenAI client + client = get_openai_client() + + # Get deployment name + deployment_name = app.config.get("ChatApp:AzureOpenAI:DeploymentName") + + # Display messages from configuration + messages_display = "" + for msg in model_config.messages: + messages_display += f"{msg.role}: {msg.content}\n" + + # Get chat messages for the API + messages = get_chat_messages(model_config) + + # Get response from OpenAI + response = client.chat.completions.create( + model=deployment_name, + messages=messages, + max_tokens=model_config.max_tokens, + temperature=model_config.temperature, + top_p=model_config.top_p + ) + + # Extract assistant message + assistant_message = response.choices[0].message.content + + return render_template('console.html', + messages=messages_display, + response=assistant_message) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/examples/Python/ChatApp/models.py b/examples/Python/ChatApp/models.py new file mode 100644 index 00000000..1b5e76b4 --- /dev/null +++ b/examples/Python/ChatApp/models.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# + +class Message: + """ + Represents a chat message with a role and content. + Maps to configuration values with keys 'role' and 'content'. + """ + def __init__(self, role=None, content=None): + self.role = role + self.content = content + + @classmethod + def from_dict(cls, data): + """Create a Message instance from a dictionary.""" + return cls( + role=data.get('role'), + content=data.get('content') + ) + + +class ModelConfiguration: + """ + Represents the configuration for an AI model including messages and parameters. + Maps to configuration values with keys 'model', 'messages', 'max_tokens', 'temperature', and 'top_p'. + """ + def __init__(self, model=None, messages=None, max_tokens=1024, temperature=0.7, top_p=0.95): + self.model = model + self.messages = messages or [] + self.max_tokens = max_tokens + self.temperature = temperature + self.top_p = top_p + + @classmethod + def from_dict(cls, data): + """Create a ModelConfiguration instance from a dictionary.""" + messages = [Message.from_dict(msg) for msg in data.get('messages', [])] + + return cls( + model=data.get('model'), + messages=messages, + max_tokens=data.get('max_tokens', 1024), + temperature=data.get('temperature', 0.7), + top_p=data.get('top_p', 0.95) + ) \ No newline at end of file diff --git a/examples/Python/ChatApp/requirements.txt b/examples/Python/ChatApp/requirements.txt new file mode 100644 index 00000000..e39e046e --- /dev/null +++ b/examples/Python/ChatApp/requirements.txt @@ -0,0 +1,4 @@ +Flask==2.3.3 +azure-identity==1.16.1 +azure-appconfiguration-provider==1.3.0 +openai==1.6.1 \ No newline at end of file diff --git a/examples/Python/ChatApp/templates/base.html b/examples/Python/ChatApp/templates/base.html new file mode 100644 index 00000000..9708f416 --- /dev/null +++ b/examples/Python/ChatApp/templates/base.html @@ -0,0 +1,55 @@ + + + + + + AI Chat App with Azure App Configuration + + + + +
+
+
+

Azure App Configuration Chat Sample

+ +
+
+ + {% block content %}{% endblock %} +
+ + + + \ No newline at end of file diff --git a/examples/Python/ChatApp/templates/console.html b/examples/Python/ChatApp/templates/console.html new file mode 100644 index 00000000..ee42b8c2 --- /dev/null +++ b/examples/Python/ChatApp/templates/console.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

Console Mode

+ This view mimics the .NET Core console application experience +
+
+

Messages from Configuration:

+
{{ messages }}
+ +

AI Response:

+
{{ response }}
+ + +
+
+
+{% endblock %} \ No newline at end of file diff --git a/examples/Python/ChatApp/templates/index.html b/examples/Python/ChatApp/templates/index.html new file mode 100644 index 00000000..b849aba2 --- /dev/null +++ b/examples/Python/ChatApp/templates/index.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

Chat

+
+
+
+ {% for message in chat_history %} +
+ {{ message.role|title }}: {{ message.content }} +
+ {% endfor %} +
+ +
+
+ + +
+
+
+
+ +
+
+

Current Configuration

+ This configuration is loaded from Azure App Configuration +
+
+ + + + + + + + + + + + + + + + + +
Model{{ model_config.model }}
Max Tokens{{ model_config.max_tokens }}
Temperature{{ model_config.temperature }}
Top P{{ model_config.top_p }}
+
+ Configuration is refreshed on each request. Changes made in Azure App Configuration will be reflected here. +
+
+
+
+{% endblock %} \ No newline at end of file From caf9cd07cf0c429be90eb86a7dd902ef97296016 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 May 2025 16:01:52 +0000 Subject: [PATCH 03/36] Convert Flask web app to console app Co-authored-by: mrm9084 <1054559+mrm9084@users.noreply.github.com> --- examples/Python/ChatApp/README.md | 16 +-- examples/Python/ChatApp/app.py | 114 +++++------------- examples/Python/ChatApp/requirements.txt | 1 - examples/Python/ChatApp/templates/base.html | 55 --------- .../Python/ChatApp/templates/console.html | 23 ---- examples/Python/ChatApp/templates/index.html | 57 --------- 6 files changed, 35 insertions(+), 231 deletions(-) delete mode 100644 examples/Python/ChatApp/templates/base.html delete mode 100644 examples/Python/ChatApp/templates/console.html delete mode 100644 examples/Python/ChatApp/templates/index.html diff --git a/examples/Python/ChatApp/README.md b/examples/Python/ChatApp/README.md index 334d6822..9502cd85 100644 --- a/examples/Python/ChatApp/README.md +++ b/examples/Python/ChatApp/README.md @@ -1,14 +1,13 @@ # Azure App Configuration - Python ChatApp Sample -This sample demonstrates using Azure App Configuration to configure Azure OpenAI settings for a chat application built with Python and Flask. +This sample demonstrates using Azure App Configuration to configure Azure OpenAI settings for a chat application built with Python. ## Features -- Built with Python and Flask +- Built with Python - Uses azure-appconfiguration-provider for configuration management - Integrates with Azure OpenAI for chat completions - Dynamically refreshes configuration from Azure App Configuration -- Supports both web interface and console-like views ## Prerequisites @@ -49,18 +48,13 @@ This sample demonstrates using Azure App Configuration to configure Azure OpenAI ## Running the Application -Start the Flask application: +Start the console application: ```bash python app.py ``` -The application will be available at http://localhost:5000/ - -## Views - -- **Chat Interface** (/) - A web interface for chatting with the AI -- **Console Mode** (/console) - A view that mimics the .NET Core console application experience +The application will display the configured messages and the AI's response. Press Enter to continue and refresh the configuration from Azure App Configuration. ## Configuration Refresh -The application refreshes the configuration on each request, so any changes made in Azure App Configuration will be reflected immediately in the app. \ No newline at end of file +The application refreshes the configuration on each loop iteration, so any changes made in Azure App Configuration will be reflected in the next response after you press Enter. \ No newline at end of file diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index 96d1b4b1..c6a9e28b 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -3,15 +3,11 @@ # import os import time -from flask import Flask, render_template, request from azure.identity import DefaultAzureCredential from azure.appconfiguration.provider import load, SettingSelector from openai import AzureOpenAI from models import ModelConfiguration -# Initialize Flask app -app = Flask(__name__) - # Get Azure App Configuration endpoint from environment variable ENDPOINT = os.environ.get("AZURE_APPCONFIG_ENDPOINT") if not ENDPOINT: @@ -26,11 +22,10 @@ # Load configuration from Azure App Configuration def callback(): """Callback function for configuration refresh""" - app.config.update(azure_app_config) print("Configuration refreshed successfully") -global azure_app_config -azure_app_config = load( +global config +config = load( endpoint=ENDPOINT, selects=[chat_app_selector], credential=credential, @@ -38,13 +33,10 @@ def callback(): on_refresh_success=callback, ) -# Update Flask app config with Azure App Configuration -app.config.update(azure_app_config) - # Get OpenAI configuration def get_openai_client(): """Create and return an Azure OpenAI client""" - endpoint = app.config.get("ChatApp:AzureOpenAI:Endpoint") + endpoint = config.get("ChatApp:AzureOpenAI:Endpoint") api_key = os.environ.get("AZURE_OPENAI_API_KEY") # Using environment variable for API key # For DefaultAzureCredential auth @@ -63,16 +55,16 @@ def get_openai_client(): ) def get_model_configuration(): - """Get model configuration from app config""" + """Get model configuration from config""" model_config = {} - # Extract model configuration from app.config + # Extract model configuration from config prefix = "ChatApp:Model:" - for key in app.config: + for key in config: if key.startswith(prefix): # Get the part of the key after the prefix config_key = key[len(prefix):].lower() - model_config[config_key] = app.config[key] + model_config[config_key] = config[key] # Handle messages specially (they're nested) messages = [] @@ -80,7 +72,7 @@ def get_model_configuration(): # Group message configs by index message_configs = {} - for key in app.config: + for key in config: if key.startswith(messages_prefix): # Extract the index and property (e.g., "0:role" -> ("0", "role")) parts = key[len(messages_prefix):].split(':') @@ -88,7 +80,7 @@ def get_model_configuration(): index, prop = parts if index not in message_configs: message_configs[index] = {} - message_configs[index][prop.lower()] = app.config[key] + message_configs[index][prop.lower()] = config[key] # Create message list in the right order for index in sorted(message_configs.keys()): @@ -102,34 +94,27 @@ def get_chat_messages(model_config): """Convert from model configuration messages to OpenAI messages format""" return [{"role": msg.role, "content": msg.content} for msg in model_config.messages] -@app.route('/') -def index(): - """Main route to display and handle chat""" - # Refresh configuration from Azure App Configuration - azure_app_config.refresh() - - # Get model configuration - model_config = get_model_configuration() - +def main(): + """Main entry point for the console app""" # Get OpenAI client client = get_openai_client() # Get deployment name - deployment_name = app.config.get("ChatApp:AzureOpenAI:DeploymentName") - - # Get chat history from model config for display - chat_history = [{"role": msg.role, "content": msg.content} for msg in model_config.messages] + deployment_name = config.get("ChatApp:AzureOpenAI:DeploymentName") - # Check if a new message was submitted - if request.args.get('message'): - user_message = request.args.get('message') + while True: + # Refresh configuration from Azure App Configuration + config.refresh() - # Add user message to chat messages - messages = get_chat_messages(model_config) - messages.append({"role": "user", "content": user_message}) + # Get model configuration + model_config = get_model_configuration() - # Add user message to chat history - chat_history.append({"role": "user", "content": user_message}) + # Display messages from configuration + for msg in model_config.messages: + print(f"{msg.role}: {msg.content}") + + # Get chat messages for the API + messages = get_chat_messages(model_config) # Get response from OpenAI response = client.chat.completions.create( @@ -143,51 +128,12 @@ def index(): # Extract assistant message assistant_message = response.choices[0].message.content - # Add assistant message to chat history - chat_history.append({"role": "assistant", "content": assistant_message}) - - return render_template('index.html', - chat_history=chat_history, - model_config=model_config) - -@app.route('/console') -def console(): - """Console mode similar to the .NET Core example""" - # Refresh configuration from Azure App Configuration - azure_app_config.refresh() - - # Get model configuration - model_config = get_model_configuration() - - # Get OpenAI client - client = get_openai_client() - - # Get deployment name - deployment_name = app.config.get("ChatApp:AzureOpenAI:DeploymentName") - - # Display messages from configuration - messages_display = "" - for msg in model_config.messages: - messages_display += f"{msg.role}: {msg.content}\n" - - # Get chat messages for the API - messages = get_chat_messages(model_config) - - # Get response from OpenAI - response = client.chat.completions.create( - model=deployment_name, - messages=messages, - max_tokens=model_config.max_tokens, - temperature=model_config.temperature, - top_p=model_config.top_p - ) - - # Extract assistant message - assistant_message = response.choices[0].message.content - - return render_template('console.html', - messages=messages_display, - response=assistant_message) + # Display the response + print(f"AI response: {assistant_message}") + + # Wait for user to continue + print("Press Enter to continue...") + input() if __name__ == '__main__': - app.run(debug=True) \ No newline at end of file + main() \ No newline at end of file diff --git a/examples/Python/ChatApp/requirements.txt b/examples/Python/ChatApp/requirements.txt index e39e046e..738d6b56 100644 --- a/examples/Python/ChatApp/requirements.txt +++ b/examples/Python/ChatApp/requirements.txt @@ -1,4 +1,3 @@ -Flask==2.3.3 azure-identity==1.16.1 azure-appconfiguration-provider==1.3.0 openai==1.6.1 \ No newline at end of file diff --git a/examples/Python/ChatApp/templates/base.html b/examples/Python/ChatApp/templates/base.html deleted file mode 100644 index 9708f416..00000000 --- a/examples/Python/ChatApp/templates/base.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - AI Chat App with Azure App Configuration - - - - -
-
-
-

Azure App Configuration Chat Sample

- -
-
- - {% block content %}{% endblock %} -
- - - - \ No newline at end of file diff --git a/examples/Python/ChatApp/templates/console.html b/examples/Python/ChatApp/templates/console.html deleted file mode 100644 index ee42b8c2..00000000 --- a/examples/Python/ChatApp/templates/console.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -
-
-
-

Console Mode

- This view mimics the .NET Core console application experience -
-
-

Messages from Configuration:

-
{{ messages }}
- -

AI Response:

-
{{ response }}
- - -
-
-
-{% endblock %} \ No newline at end of file diff --git a/examples/Python/ChatApp/templates/index.html b/examples/Python/ChatApp/templates/index.html deleted file mode 100644 index b849aba2..00000000 --- a/examples/Python/ChatApp/templates/index.html +++ /dev/null @@ -1,57 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -
-
-
-

Chat

-
-
-
- {% for message in chat_history %} -
- {{ message.role|title }}: {{ message.content }} -
- {% endfor %} -
- -
-
- - -
-
-
-
- -
-
-

Current Configuration

- This configuration is loaded from Azure App Configuration -
-
- - - - - - - - - - - - - - - - - -
Model{{ model_config.model }}
Max Tokens{{ model_config.max_tokens }}
Temperature{{ model_config.temperature }}
Top P{{ model_config.top_p }}
-
- Configuration is refreshed on each request. Changes made in Azure App Configuration will be reflected here. -
-
-
-
-{% endblock %} \ No newline at end of file From e19bc99bfc4377d8570efc4c6ac957d0b002f2db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 May 2025 16:09:06 +0000 Subject: [PATCH 04/36] Update azure-appconfiguration-provider to version 2.1.0 Co-authored-by: mrm9084 <1054559+mrm9084@users.noreply.github.com> --- examples/Python/ChatApp/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Python/ChatApp/requirements.txt b/examples/Python/ChatApp/requirements.txt index 738d6b56..2e711a4e 100644 --- a/examples/Python/ChatApp/requirements.txt +++ b/examples/Python/ChatApp/requirements.txt @@ -1,3 +1,3 @@ azure-identity==1.16.1 -azure-appconfiguration-provider==1.3.0 +azure-appconfiguration-provider==2.1.0 openai==1.6.1 \ No newline at end of file From 5d8b373d199b34d28e6cdf3b98c2111727c5dd02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 May 2025 16:16:32 +0000 Subject: [PATCH 05/36] Address review feedback for Python ChatApp example Co-authored-by: mrm9084 <1054559+mrm9084@users.noreply.github.com> --- examples/Python/ChatApp/app.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index c6a9e28b..57e9f4af 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -20,23 +20,18 @@ chat_app_selector = SettingSelector(key_filter="ChatApp:*") # Load configuration from Azure App Configuration -def callback(): - """Callback function for configuration refresh""" - print("Configuration refreshed successfully") - global config config = load( endpoint=ENDPOINT, selects=[chat_app_selector], credential=credential, - keyvault_credential=credential, - on_refresh_success=callback, + trim_prefixes=["ChatApp:"], ) # Get OpenAI configuration def get_openai_client(): """Create and return an Azure OpenAI client""" - endpoint = config.get("ChatApp:AzureOpenAI:Endpoint") + endpoint = config.get("AzureOpenAI:Endpoint") api_key = os.environ.get("AZURE_OPENAI_API_KEY") # Using environment variable for API key # For DefaultAzureCredential auth @@ -59,16 +54,16 @@ def get_model_configuration(): model_config = {} # Extract model configuration from config - prefix = "ChatApp:Model:" + model_prefix = "Model:" for key in config: - if key.startswith(prefix): + if key.startswith(model_prefix): # Get the part of the key after the prefix - config_key = key[len(prefix):].lower() + config_key = key[len(model_prefix):].lower() model_config[config_key] = config[key] # Handle messages specially (they're nested) messages = [] - messages_prefix = "ChatApp:Model:messages:" + messages_prefix = "Model:messages:" # Group message configs by index message_configs = {} @@ -100,7 +95,7 @@ def main(): client = get_openai_client() # Get deployment name - deployment_name = config.get("ChatApp:AzureOpenAI:DeploymentName") + deployment_name = config.get("AzureOpenAI:DeploymentName") while True: # Refresh configuration from Azure App Configuration @@ -131,9 +126,12 @@ def main(): # Display the response print(f"AI response: {assistant_message}") - # Wait for user to continue - print("Press Enter to continue...") - input() + # Wait for user to continue or exit + print("Press Enter to continue or 'exit' to quit...") + user_input = input().strip().lower() + if user_input == 'exit': + print("Exiting application...") + break if __name__ == '__main__': main() \ No newline at end of file From 6466ad88366c2bf24c4d7669856363550536795e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 May 2025 17:38:00 +0000 Subject: [PATCH 06/36] Implement data binding for Python ChatApp configuration Co-authored-by: zhenlan <10566826+zhenlan@users.noreply.github.com> --- examples/Python/ChatApp/app.py | 95 ++++++++++++++---------- examples/Python/ChatApp/models.py | 44 +++-------- examples/Python/ChatApp/requirements.txt | 3 +- 3 files changed, 70 insertions(+), 72 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index 57e9f4af..34a712b5 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -6,7 +6,9 @@ from azure.identity import DefaultAzureCredential from azure.appconfiguration.provider import load, SettingSelector from openai import AzureOpenAI -from models import ModelConfiguration +from models import ModelConfiguration, Message +from pydantic import create_model +from typing import Dict, List, Any, Optional, Type, TypeVar, cast, get_type_hints # Get Azure App Configuration endpoint from environment variable ENDPOINT = os.environ.get("AZURE_APPCONFIG_ENDPOINT") @@ -28,6 +30,57 @@ trim_prefixes=["ChatApp:"], ) +T = TypeVar('T') + +def bind_config_section(section_prefix: str, model_class: Type[T]) -> T: + """ + Bind configuration values to a Pydantic model. + + Args: + section_prefix: The prefix for the configuration section (without trailing colon) + model_class: The Pydantic model class to bind to + + Returns: + An instance of the model class with values from configuration + """ + config_data: Dict[str, Any] = {} + type_hints = get_type_hints(model_class) + + # Extract all keys for this section + for key in config: + if key.startswith(f"{section_prefix}:") or key == section_prefix: + config_key = key[len(section_prefix):].strip(':').lower() + if config_key: + config_data[config_key] = config[key] + elif key == section_prefix: # Handle the case where the key is exactly the prefix + config_data['value'] = config[key] + + # Handle nested structures like messages + if 'messages' in type_hints: + messages = [] + messages_prefix = f"{section_prefix}:messages:" + + # Group message configs by index + message_configs: Dict[str, Dict[str, Any]] = {} + for key in config: + if key.startswith(messages_prefix): + # Extract the index and property (e.g., "0:role" -> ("0", "role")) + parts = key[len(messages_prefix):].split(':') + if len(parts) == 2: + index, prop = parts + if index not in message_configs: + message_configs[index] = {} + message_configs[index][prop.lower()] = config[key] + + # Create message list in the right order + for index in sorted(message_configs.keys()): + messages.append(Message(**message_configs[index])) + + config_data['messages'] = messages + + # Create and return the model instance + return model_class(**config_data) + # Get OpenAI configuration def get_openai_client(): """Create and return an Azure OpenAI client""" @@ -49,42 +102,6 @@ def get_openai_client(): api_version="2023-05-15" ) -def get_model_configuration(): - """Get model configuration from config""" - model_config = {} - - # Extract model configuration from config - model_prefix = "Model:" - for key in config: - if key.startswith(model_prefix): - # Get the part of the key after the prefix - config_key = key[len(model_prefix):].lower() - model_config[config_key] = config[key] - - # Handle messages specially (they're nested) - messages = [] - messages_prefix = "Model:messages:" - - # Group message configs by index - message_configs = {} - for key in config: - if key.startswith(messages_prefix): - # Extract the index and property (e.g., "0:role" -> ("0", "role")) - parts = key[len(messages_prefix):].split(':') - if len(parts) == 2: - index, prop = parts - if index not in message_configs: - message_configs[index] = {} - message_configs[index][prop.lower()] = config[key] - - # Create message list in the right order - for index in sorted(message_configs.keys()): - messages.append(message_configs[index]) - - model_config['messages'] = messages - - return ModelConfiguration.from_dict(model_config) - def get_chat_messages(model_config): """Convert from model configuration messages to OpenAI messages format""" return [{"role": msg.role, "content": msg.content} for msg in model_config.messages] @@ -101,8 +118,8 @@ def main(): # Refresh configuration from Azure App Configuration config.refresh() - # Get model configuration - model_config = get_model_configuration() + # Get model configuration using data binding + model_config = bind_config_section("Model", ModelConfiguration) # Display messages from configuration for msg in model_config.messages: diff --git a/examples/Python/ChatApp/models.py b/examples/Python/ChatApp/models.py index 1b5e76b4..d818c4e8 100644 --- a/examples/Python/ChatApp/models.py +++ b/examples/Python/ChatApp/models.py @@ -1,46 +1,26 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. # +from pydantic import BaseModel, Field +from typing import List, Optional -class Message: + +class Message(BaseModel): """ Represents a chat message with a role and content. Maps to configuration values with keys 'role' and 'content'. """ - def __init__(self, role=None, content=None): - self.role = role - self.content = content - - @classmethod - def from_dict(cls, data): - """Create a Message instance from a dictionary.""" - return cls( - role=data.get('role'), - content=data.get('content') - ) + role: Optional[str] = None + content: Optional[str] = None -class ModelConfiguration: +class ModelConfiguration(BaseModel): """ Represents the configuration for an AI model including messages and parameters. Maps to configuration values with keys 'model', 'messages', 'max_tokens', 'temperature', and 'top_p'. """ - def __init__(self, model=None, messages=None, max_tokens=1024, temperature=0.7, top_p=0.95): - self.model = model - self.messages = messages or [] - self.max_tokens = max_tokens - self.temperature = temperature - self.top_p = top_p - - @classmethod - def from_dict(cls, data): - """Create a ModelConfiguration instance from a dictionary.""" - messages = [Message.from_dict(msg) for msg in data.get('messages', [])] - - return cls( - model=data.get('model'), - messages=messages, - max_tokens=data.get('max_tokens', 1024), - temperature=data.get('temperature', 0.7), - top_p=data.get('top_p', 0.95) - ) \ No newline at end of file + model: Optional[str] = None + messages: List[Message] = Field(default_factory=list) + max_tokens: int = 1024 + temperature: float = 0.7 + top_p: float = 0.95 \ No newline at end of file diff --git a/examples/Python/ChatApp/requirements.txt b/examples/Python/ChatApp/requirements.txt index 2e711a4e..c660709b 100644 --- a/examples/Python/ChatApp/requirements.txt +++ b/examples/Python/ChatApp/requirements.txt @@ -1,3 +1,4 @@ azure-identity==1.16.1 azure-appconfiguration-provider==2.1.0 -openai==1.6.1 \ No newline at end of file +openai==1.6.1 +pydantic==2.11.5 \ No newline at end of file From d377f84cb1e01065d65ba98de5658f17ab55b9a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 May 2025 17:44:39 +0000 Subject: [PATCH 07/36] Implement data binding without Pydantic dependency for Python ChatApp Co-authored-by: mrm9084 <1054559+mrm9084@users.noreply.github.com> --- examples/Python/ChatApp/app.py | 53 ++++++++++++------------ examples/Python/ChatApp/models.py | 49 +++++++++++++++++----- examples/Python/ChatApp/requirements.txt | 3 +- 3 files changed, 65 insertions(+), 40 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index 34a712b5..bbd5f5c5 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -7,8 +7,7 @@ from azure.appconfiguration.provider import load, SettingSelector from openai import AzureOpenAI from models import ModelConfiguration, Message -from pydantic import create_model -from typing import Dict, List, Any, Optional, Type, TypeVar, cast, get_type_hints +from typing import Dict, List, Any, Optional, Type, TypeVar # Get Azure App Configuration endpoint from environment variable ENDPOINT = os.environ.get("AZURE_APPCONFIG_ENDPOINT") @@ -34,17 +33,16 @@ def bind_config_section(section_prefix: str, model_class: Type[T]) -> T: """ - Bind configuration values to a Pydantic model. + Bind configuration values to a model class. Args: section_prefix: The prefix for the configuration section (without trailing colon) - model_class: The Pydantic model class to bind to + model_class: The model class to bind to Returns: An instance of the model class with values from configuration """ config_data: Dict[str, Any] = {} - type_hints = get_type_hints(model_class) # Extract all keys for this section for key in config: @@ -56,30 +54,31 @@ def bind_config_section(section_prefix: str, model_class: Type[T]) -> T: config_data['value'] = config[key] # Handle nested structures like messages - if 'messages' in type_hints: - messages = [] - messages_prefix = f"{section_prefix}:messages:" - - # Group message configs by index - message_configs: Dict[str, Dict[str, Any]] = {} - for key in config: - if key.startswith(messages_prefix): - # Extract the index and property (e.g., "0:role" -> ("0", "role")) - parts = key[len(messages_prefix):].split(':') - if len(parts) == 2: - index, prop = parts - if index not in message_configs: - message_configs[index] = {} - message_configs[index][prop.lower()] = config[key] - - # Create message list in the right order - for index in sorted(message_configs.keys()): - messages.append(Message(**message_configs[index])) - + messages = [] + messages_prefix = f"{section_prefix}:messages:" + + # Group message configs by index + message_configs: Dict[str, Dict[str, Any]] = {} + for key in config: + if key.startswith(messages_prefix): + # Extract the index and property (e.g., "0:role" -> ("0", "role")) + parts = key[len(messages_prefix):].split(':') + if len(parts) == 2: + index, prop = parts + if index not in message_configs: + message_configs[index] = {} + message_configs[index][prop.lower()] = config[key] + + # Create message list in the right order + for index in sorted(message_configs.keys()): + messages.append(Message.from_dict(message_configs[index])) + + # Add messages to config data if we found any + if messages: config_data['messages'] = messages - # Create and return the model instance - return model_class(**config_data) + # Create and return the model instance using from_dict + return model_class.from_dict(config_data) # Get OpenAI configuration def get_openai_client(): diff --git a/examples/Python/ChatApp/models.py b/examples/Python/ChatApp/models.py index d818c4e8..cf3fddc7 100644 --- a/examples/Python/ChatApp/models.py +++ b/examples/Python/ChatApp/models.py @@ -1,26 +1,53 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. # -from pydantic import BaseModel, Field -from typing import List, Optional +from typing import List, Optional, Dict, Any -class Message(BaseModel): +class Message: """ Represents a chat message with a role and content. Maps to configuration values with keys 'role' and 'content'. """ - role: Optional[str] = None - content: Optional[str] = None + def __init__(self, role: Optional[str] = None, content: Optional[str] = None): + self.role = role + self.content = content + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'Message': + """Create a Message instance from a dictionary.""" + return cls( + role=data.get('role'), + content=data.get('content') + ) -class ModelConfiguration(BaseModel): +class ModelConfiguration: """ Represents the configuration for an AI model including messages and parameters. Maps to configuration values with keys 'model', 'messages', 'max_tokens', 'temperature', and 'top_p'. """ - model: Optional[str] = None - messages: List[Message] = Field(default_factory=list) - max_tokens: int = 1024 - temperature: float = 0.7 - top_p: float = 0.95 \ No newline at end of file + def __init__(self, + model: Optional[str] = None, + messages: Optional[List[Message]] = None, + max_tokens: int = 1024, + temperature: float = 0.7, + top_p: float = 0.95): + self.model = model + self.messages = messages or [] + self.max_tokens = int(max_tokens) if max_tokens is not None else 1024 + self.temperature = float(temperature) if temperature is not None else 0.7 + self.top_p = float(top_p) if top_p is not None else 0.95 + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'ModelConfiguration': + """Create a ModelConfiguration instance from a dictionary.""" + messages = [Message.from_dict(msg) for msg in data.get('messages', [])] + + return cls( + model=data.get('model'), + messages=messages, + max_tokens=data.get('max_tokens', 1024), + temperature=data.get('temperature', 0.7), + top_p=data.get('top_p', 0.95) + ) \ No newline at end of file diff --git a/examples/Python/ChatApp/requirements.txt b/examples/Python/ChatApp/requirements.txt index c660709b..2e711a4e 100644 --- a/examples/Python/ChatApp/requirements.txt +++ b/examples/Python/ChatApp/requirements.txt @@ -1,4 +1,3 @@ azure-identity==1.16.1 azure-appconfiguration-provider==2.1.0 -openai==1.6.1 -pydantic==2.11.5 \ No newline at end of file +openai==1.6.1 \ No newline at end of file From 4cfe6d2ba39d9f891fff1b9d96a2f75e62bc835a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 May 2025 17:48:27 +0000 Subject: [PATCH 08/36] Update Python ChatApp README.md based on review feedback Co-authored-by: zhenlan <10566826+zhenlan@users.noreply.github.com> --- examples/Python/ChatApp/README.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/examples/Python/ChatApp/README.md b/examples/Python/ChatApp/README.md index 9502cd85..f835b1a7 100644 --- a/examples/Python/ChatApp/README.md +++ b/examples/Python/ChatApp/README.md @@ -4,8 +4,6 @@ This sample demonstrates using Azure App Configuration to configure Azure OpenAI ## Features -- Built with Python -- Uses azure-appconfiguration-provider for configuration management - Integrates with Azure OpenAI for chat completions - Dynamically refreshes configuration from Azure App Configuration @@ -30,14 +28,12 @@ This sample demonstrates using Azure App Configuration to configure Azure OpenAI ``` ChatApp:AzureOpenAI:Endpoint - Your Azure OpenAI endpoint URL ChatApp:AzureOpenAI:DeploymentName - Your Azure OpenAI deployment name - ChatApp:Model:model - Model name (e.g., "gpt-35-turbo") - ChatApp:Model:max_tokens - Maximum tokens for completion (e.g., 1000) - ChatApp:Model:temperature - Temperature parameter (e.g., 0.7) - ChatApp:Model:top_p - Top p parameter (e.g., 0.95) - ChatApp:Model:messages:0:role - Role for message 0 (e.g., "system") - ChatApp:Model:messages:0:content - Content for message 0 - ChatApp:Model:messages:1:role - Role for message 1 (e.g., "user") - ChatApp:Model:messages:1:content - Content for message 1 + ChatApp:Model - An AI configuration entry containing the following settings: + - model - Model name (e.g., "gpt-35-turbo") + - max_tokens - Maximum tokens for completion (e.g., 1000) + - temperature - Temperature parameter (e.g., 0.7) + - top_p - Top p parameter (e.g., 0.95) + - messages - An array of messages with role and content for each message ``` 4. Set the required environment variables: From f36d608e84ce948c7a6efee5ca9535bd49a4ff6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 May 2025 17:55:58 +0000 Subject: [PATCH 09/36] Read API version from App Configuration Co-authored-by: zhenlan <10566826+zhenlan@users.noreply.github.com> --- examples/Python/ChatApp/README.md | 1 + examples/Python/ChatApp/app.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/Python/ChatApp/README.md b/examples/Python/ChatApp/README.md index f835b1a7..27c77588 100644 --- a/examples/Python/ChatApp/README.md +++ b/examples/Python/ChatApp/README.md @@ -28,6 +28,7 @@ This sample demonstrates using Azure App Configuration to configure Azure OpenAI ``` ChatApp:AzureOpenAI:Endpoint - Your Azure OpenAI endpoint URL ChatApp:AzureOpenAI:DeploymentName - Your Azure OpenAI deployment name + ChatApp:AzureOpenAI:ApiVersion - API version for Azure OpenAI (e.g., "2023-05-15") ChatApp:Model - An AI configuration entry containing the following settings: - model - Model name (e.g., "gpt-35-turbo") - max_tokens - Maximum tokens for completion (e.g., 1000) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index bbd5f5c5..02c7f83c 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -85,12 +85,13 @@ def get_openai_client(): """Create and return an Azure OpenAI client""" endpoint = config.get("AzureOpenAI:Endpoint") api_key = os.environ.get("AZURE_OPENAI_API_KEY") # Using environment variable for API key + api_version = config.get("AzureOpenAI:ApiVersion", "2023-05-15") # Read API version from config or use default # For DefaultAzureCredential auth if not api_key: return AzureOpenAI( azure_endpoint=endpoint, - api_version="2023-05-15", + api_version=api_version, azure_ad_token_provider=credential ) @@ -98,7 +99,7 @@ def get_openai_client(): return AzureOpenAI( azure_endpoint=endpoint, api_key=api_key, - api_version="2023-05-15" + api_version=api_version ) def get_chat_messages(model_config): From da033f9106ce8ce0bde9f109fde111e7d8e21543 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 May 2025 17:58:22 +0000 Subject: [PATCH 10/36] Update ChatApp:Model description to specify JSON object with AI chat completion MIME profile Co-authored-by: mrm9084 <1054559+mrm9084@users.noreply.github.com> --- examples/Python/ChatApp/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Python/ChatApp/README.md b/examples/Python/ChatApp/README.md index 27c77588..70530558 100644 --- a/examples/Python/ChatApp/README.md +++ b/examples/Python/ChatApp/README.md @@ -29,7 +29,7 @@ This sample demonstrates using Azure App Configuration to configure Azure OpenAI ChatApp:AzureOpenAI:Endpoint - Your Azure OpenAI endpoint URL ChatApp:AzureOpenAI:DeploymentName - Your Azure OpenAI deployment name ChatApp:AzureOpenAI:ApiVersion - API version for Azure OpenAI (e.g., "2023-05-15") - ChatApp:Model - An AI configuration entry containing the following settings: + ChatApp:Model - A JSON object with the content type of application/json; profile="https://azconfig.io/mime-profiles/ai/chat-completion" containing the following settings: - model - Model name (e.g., "gpt-35-turbo") - max_tokens - Maximum tokens for completion (e.g., 1000) - temperature - Temperature parameter (e.g., 0.7) From bf6b106063ae73e725877d99448e982dd9814f1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 May 2025 18:02:31 +0000 Subject: [PATCH 11/36] Read API key from App Configuration with Key Vault reference support Co-authored-by: zhenlan <10566826+zhenlan@users.noreply.github.com> --- examples/Python/ChatApp/README.md | 5 +++-- examples/Python/ChatApp/app.py | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/examples/Python/ChatApp/README.md b/examples/Python/ChatApp/README.md index 70530558..8f0ea408 100644 --- a/examples/Python/ChatApp/README.md +++ b/examples/Python/ChatApp/README.md @@ -15,7 +15,7 @@ This sample demonstrates using Azure App Configuration to configure Azure OpenAI - Azure OpenAI service - Required environment variables: - `AZURE_APPCONFIG_ENDPOINT`: URL of your Azure App Configuration instance - - `AZURE_OPENAI_API_KEY`: API key for Azure OpenAI (optional if using DefaultAzureCredential) + - `AZURE_OPENAI_API_KEY`: API key for Azure OpenAI (optional if using Azure App Configuration or DefaultAzureCredential) ## Setup @@ -29,6 +29,7 @@ This sample demonstrates using Azure App Configuration to configure Azure OpenAI ChatApp:AzureOpenAI:Endpoint - Your Azure OpenAI endpoint URL ChatApp:AzureOpenAI:DeploymentName - Your Azure OpenAI deployment name ChatApp:AzureOpenAI:ApiVersion - API version for Azure OpenAI (e.g., "2023-05-15") + ChatApp:AzureOpenAI:ApiKey - Your Azure OpenAI API key (preferably as a Key Vault reference) ChatApp:Model - A JSON object with the content type of application/json; profile="https://azconfig.io/mime-profiles/ai/chat-completion" containing the following settings: - model - Model name (e.g., "gpt-35-turbo") - max_tokens - Maximum tokens for completion (e.g., 1000) @@ -40,7 +41,7 @@ This sample demonstrates using Azure App Configuration to configure Azure OpenAI 4. Set the required environment variables: ```bash export AZURE_APPCONFIG_ENDPOINT="https://your-appconfig.azconfig.io" - export AZURE_OPENAI_API_KEY="your-openai-api-key" # Optional if using DefaultAzureCredential + export AZURE_OPENAI_API_KEY="your-openai-api-key" # Optional if using App Configuration or DefaultAzureCredential ``` ## Running the Application diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index 02c7f83c..e481b355 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -84,10 +84,14 @@ def bind_config_section(section_prefix: str, model_class: Type[T]) -> T: def get_openai_client(): """Create and return an Azure OpenAI client""" endpoint = config.get("AzureOpenAI:Endpoint") - api_key = os.environ.get("AZURE_OPENAI_API_KEY") # Using environment variable for API key + # Try to get API key from App Configuration first (could be a Key Vault reference) + api_key = config.get("AzureOpenAI:ApiKey") + # Fall back to environment variable if not in App Configuration + if not api_key: + api_key = os.environ.get("AZURE_OPENAI_API_KEY") api_version = config.get("AzureOpenAI:ApiVersion", "2023-05-15") # Read API version from config or use default - # For DefaultAzureCredential auth + # For DefaultAzureCredential auth if no API key is available if not api_key: return AzureOpenAI( azure_endpoint=endpoint, From 97c5be0411b7ff6d4846ccba9980a797b150fcc4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 May 2025 18:08:04 +0000 Subject: [PATCH 12/36] Revert ChatApp:Model description to focus on AI configuration Co-authored-by: zhenlan <10566826+zhenlan@users.noreply.github.com> --- examples/Python/ChatApp/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Python/ChatApp/README.md b/examples/Python/ChatApp/README.md index 8f0ea408..f9d01db5 100644 --- a/examples/Python/ChatApp/README.md +++ b/examples/Python/ChatApp/README.md @@ -30,7 +30,7 @@ This sample demonstrates using Azure App Configuration to configure Azure OpenAI ChatApp:AzureOpenAI:DeploymentName - Your Azure OpenAI deployment name ChatApp:AzureOpenAI:ApiVersion - API version for Azure OpenAI (e.g., "2023-05-15") ChatApp:AzureOpenAI:ApiKey - Your Azure OpenAI API key (preferably as a Key Vault reference) - ChatApp:Model - A JSON object with the content type of application/json; profile="https://azconfig.io/mime-profiles/ai/chat-completion" containing the following settings: + ChatApp:Model - An AI configuration entry containing the following settings: - model - Model name (e.g., "gpt-35-turbo") - max_tokens - Maximum tokens for completion (e.g., 1000) - temperature - Temperature parameter (e.g., 0.7) From e2fd91fd86fd3c352bc8f3af209ecd9976df75e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 May 2025 18:12:25 +0000 Subject: [PATCH 13/36] Address reviewers' feedback on Python ChatApp example Co-authored-by: mrm9084 <1054559+mrm9084@users.noreply.github.com> --- examples/Python/ChatApp/README.md | 24 ++++++++++++++++++++++-- examples/Python/ChatApp/app.py | 7 ++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/examples/Python/ChatApp/README.md b/examples/Python/ChatApp/README.md index f9d01db5..70789761 100644 --- a/examples/Python/ChatApp/README.md +++ b/examples/Python/ChatApp/README.md @@ -15,7 +15,7 @@ This sample demonstrates using Azure App Configuration to configure Azure OpenAI - Azure OpenAI service - Required environment variables: - `AZURE_APPCONFIG_ENDPOINT`: URL of your Azure App Configuration instance - - `AZURE_OPENAI_API_KEY`: API key for Azure OpenAI (optional if using Azure App Configuration or DefaultAzureCredential) + - `AZURE_OPENAI_API_KEY`: API key for Azure OpenAI (optional if stored in Azure App Configuration) ## Setup @@ -36,12 +36,32 @@ This sample demonstrates using Azure App Configuration to configure Azure OpenAI - temperature - Temperature parameter (e.g., 0.7) - top_p - Top p parameter (e.g., 0.95) - messages - An array of messages with role and content for each message + + Example JSON object for ChatApp:Model: + ```json + { + "model": "gpt-35-turbo", + "max_tokens": 1000, + "temperature": 0.7, + "top_p": 0.95, + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": "Tell me about Azure App Configuration." + } + ] + } + ``` ``` 4. Set the required environment variables: ```bash export AZURE_APPCONFIG_ENDPOINT="https://your-appconfig.azconfig.io" - export AZURE_OPENAI_API_KEY="your-openai-api-key" # Optional if using App Configuration or DefaultAzureCredential + export AZURE_OPENAI_API_KEY="your-openai-api-key" # Optional if stored in Azure App Configuration ``` ## Running the Application diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index e481b355..7c396b07 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -84,11 +84,8 @@ def bind_config_section(section_prefix: str, model_class: Type[T]) -> T: def get_openai_client(): """Create and return an Azure OpenAI client""" endpoint = config.get("AzureOpenAI:Endpoint") - # Try to get API key from App Configuration first (could be a Key Vault reference) - api_key = config.get("AzureOpenAI:ApiKey") - # Fall back to environment variable if not in App Configuration - if not api_key: - api_key = os.environ.get("AZURE_OPENAI_API_KEY") + # Get API key from App Configuration or fall back to environment variable + api_key = config.get("AzureOpenAI:ApiKey", os.environ.get("AZURE_OPENAI_API_KEY")) api_version = config.get("AzureOpenAI:ApiVersion", "2023-05-15") # Read API version from config or use default # For DefaultAzureCredential auth if no API key is available From 7325b6fcd6396e33f5de23abf7ba41d9a021fda7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 May 2025 18:16:06 +0000 Subject: [PATCH 14/36] Add Key Vault credential for loading API key from Key Vault references Co-authored-by: mrm9084 <1054559+mrm9084@users.noreply.github.com> --- examples/Python/ChatApp/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index 7c396b07..4941e2c0 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -26,6 +26,7 @@ endpoint=ENDPOINT, selects=[chat_app_selector], credential=credential, + key_vault_credential=credential, # Use the same credential for Key Vault references trim_prefixes=["ChatApp:"], ) From e3daa4dc47d5100cea63dfc84ff14d7d5c05110d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 May 2025 18:21:53 +0000 Subject: [PATCH 15/36] Fix credential parameter name from key_vault_credential to keyvault_credential Co-authored-by: mrm9084 <1054559+mrm9084@users.noreply.github.com> --- examples/Python/ChatApp/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index 4941e2c0..e8c89223 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -26,7 +26,7 @@ endpoint=ENDPOINT, selects=[chat_app_selector], credential=credential, - key_vault_credential=credential, # Use the same credential for Key Vault references + keyvault_credential=credential, # Use the same credential for Key Vault references trim_prefixes=["ChatApp:"], ) From c2c1e17a7f5e124ba8007ac18df639d96959b659 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 23 May 2025 15:18:36 -0700 Subject: [PATCH 16/36] Fixing --- examples/Python/ChatApp/app.py | 51 +----------------------- examples/Python/ChatApp/requirements.txt | 6 +-- 2 files changed, 4 insertions(+), 53 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index e8c89223..82fe9fc6 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -32,55 +32,6 @@ T = TypeVar('T') -def bind_config_section(section_prefix: str, model_class: Type[T]) -> T: - """ - Bind configuration values to a model class. - - Args: - section_prefix: The prefix for the configuration section (without trailing colon) - model_class: The model class to bind to - - Returns: - An instance of the model class with values from configuration - """ - config_data: Dict[str, Any] = {} - - # Extract all keys for this section - for key in config: - if key.startswith(f"{section_prefix}:") or key == section_prefix: - config_key = key[len(section_prefix):].strip(':').lower() - if config_key: - config_data[config_key] = config[key] - elif key == section_prefix: # Handle the case where the key is exactly the prefix - config_data['value'] = config[key] - - # Handle nested structures like messages - messages = [] - messages_prefix = f"{section_prefix}:messages:" - - # Group message configs by index - message_configs: Dict[str, Dict[str, Any]] = {} - for key in config: - if key.startswith(messages_prefix): - # Extract the index and property (e.g., "0:role" -> ("0", "role")) - parts = key[len(messages_prefix):].split(':') - if len(parts) == 2: - index, prop = parts - if index not in message_configs: - message_configs[index] = {} - message_configs[index][prop.lower()] = config[key] - - # Create message list in the right order - for index in sorted(message_configs.keys()): - messages.append(Message.from_dict(message_configs[index])) - - # Add messages to config data if we found any - if messages: - config_data['messages'] = messages - - # Create and return the model instance using from_dict - return model_class.from_dict(config_data) - # Get OpenAI configuration def get_openai_client(): """Create and return an Azure OpenAI client""" @@ -121,7 +72,7 @@ def main(): config.refresh() # Get model configuration using data binding - model_config = bind_config_section("Model", ModelConfiguration) + model_config = ModelConfiguration.from_dict( config.get("Model")) # Display messages from configuration for msg in model_config.messages: diff --git a/examples/Python/ChatApp/requirements.txt b/examples/Python/ChatApp/requirements.txt index 2e711a4e..2c073155 100644 --- a/examples/Python/ChatApp/requirements.txt +++ b/examples/Python/ChatApp/requirements.txt @@ -1,3 +1,3 @@ -azure-identity==1.16.1 -azure-appconfiguration-provider==2.1.0 -openai==1.6.1 \ No newline at end of file +azure-identity +azure-appconfiguration-provider +openai \ No newline at end of file From 907227e9a4d107db2e4b26205e7b0567469b673c Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 23 May 2025 15:26:01 -0700 Subject: [PATCH 17/36] Fixing auth --- examples/Python/ChatApp/app.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index 82fe9fc6..ecf5a4e5 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -2,12 +2,11 @@ # Licensed under the MIT license. # import os -import time -from azure.identity import DefaultAzureCredential +from azure.identity import DefaultAzureCredential, get_bearer_token_provider from azure.appconfiguration.provider import load, SettingSelector from openai import AzureOpenAI -from models import ModelConfiguration, Message -from typing import Dict, List, Any, Optional, Type, TypeVar +from models import ModelConfiguration +from typing import TypeVar # Get Azure App Configuration endpoint from environment variable ENDPOINT = os.environ.get("AZURE_APPCONFIG_ENDPOINT") @@ -42,10 +41,13 @@ def get_openai_client(): # For DefaultAzureCredential auth if no API key is available if not api_key: + token_provider = get_bearer_token_provider( + DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default" + ) return AzureOpenAI( azure_endpoint=endpoint, api_version=api_version, - azure_ad_token_provider=credential + azure_ad_token_provider=token_provider ) # For API key auth From 00136bda5ef0c6d33a1c84b43e08cd6fae55ce7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 May 2025 22:32:50 +0000 Subject: [PATCH 18/36] Add user input chat functionality to Python ChatApp Co-authored-by: mrm9084 <1054559+mrm9084@users.noreply.github.com> --- examples/Python/ChatApp/README.md | 8 ++++-- examples/Python/ChatApp/app.py | 48 +++++++++++++++++++++---------- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/examples/Python/ChatApp/README.md b/examples/Python/ChatApp/README.md index 70789761..74b0fe36 100644 --- a/examples/Python/ChatApp/README.md +++ b/examples/Python/ChatApp/README.md @@ -71,8 +71,12 @@ Start the console application: python app.py ``` -The application will display the configured messages and the AI's response. Press Enter to continue and refresh the configuration from Azure App Configuration. +The application will: +1. Display the initial configured messages from Azure App Configuration +2. Generate a response from the AI +3. Prompt you to enter your message (type 'exit' to quit) +4. Maintain conversation history during the session ## Configuration Refresh -The application refreshes the configuration on each loop iteration, so any changes made in Azure App Configuration will be reflected in the next response after you press Enter. \ No newline at end of file +The application refreshes the configuration at the beginning of each conversation cycle, so any changes made to the base configuration in Azure App Configuration will be incorporated into the model parameters (temperature, max_tokens, etc.) while maintaining your ongoing conversation history. \ No newline at end of file diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index ecf5a4e5..ff9d0024 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -5,8 +5,8 @@ from azure.identity import DefaultAzureCredential, get_bearer_token_provider from azure.appconfiguration.provider import load, SettingSelector from openai import AzureOpenAI -from models import ModelConfiguration -from typing import TypeVar +from models import ModelConfiguration, Message +from typing import TypeVar, List # Get Azure App Configuration endpoint from environment variable ENDPOINT = os.environ.get("AZURE_APPCONFIG_ENDPOINT") @@ -57,9 +57,9 @@ def get_openai_client(): api_version=api_version ) -def get_chat_messages(model_config): - """Convert from model configuration messages to OpenAI messages format""" - return [{"role": msg.role, "content": msg.content} for msg in model_config.messages] +def get_chat_messages(messages: List[Message]): + """Convert from model Message objects to OpenAI messages format""" + return [{"role": msg.role, "content": msg.content} for msg in messages] def main(): """Main entry point for the console app""" @@ -69,19 +69,29 @@ def main(): # Get deployment name deployment_name = config.get("AzureOpenAI:DeploymentName") + # Initialize conversation history with the configuration messages + conversation_history = [] + first_run = True + + print("Chat Application - type 'exit' to quit\n") + while True: # Refresh configuration from Azure App Configuration config.refresh() # Get model configuration using data binding - model_config = ModelConfiguration.from_dict( config.get("Model")) + model_config = ModelConfiguration.from_dict(config.get("Model")) - # Display messages from configuration - for msg in model_config.messages: - print(f"{msg.role}: {msg.content}") + # On first run, initialize conversation history with configuration messages + # and display the initial messages + if first_run: + conversation_history = model_config.messages.copy() + for msg in conversation_history: + print(f"{msg.role}: {msg.content}") + first_run = False # Get chat messages for the API - messages = get_chat_messages(model_config) + messages = get_chat_messages(conversation_history) # Get response from OpenAI response = client.chat.completions.create( @@ -96,14 +106,22 @@ def main(): assistant_message = response.choices[0].message.content # Display the response - print(f"AI response: {assistant_message}") + print(f"assistant: {assistant_message}") + + # Add assistant response to conversation history + conversation_history.append(Message(role="assistant", content=assistant_message)) - # Wait for user to continue or exit - print("Press Enter to continue or 'exit' to quit...") - user_input = input().strip().lower() - if user_input == 'exit': + # Get user input for the next message + print("\nuser: ", end="") + user_input = input().strip() + + # Check if user wants to exit + if user_input.lower() == 'exit': print("Exiting application...") break + + # Add user input to conversation history + conversation_history.append(Message(role="user", content=user_input)) if __name__ == '__main__': main() \ No newline at end of file From 433030d8ca04616b9d89dde823aa924092fdbdf6 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 23 May 2025 15:48:42 -0700 Subject: [PATCH 19/36] Updated to use black formating --- examples/Python/ChatApp/app.py | 66 +++++++++++++++++-------------- examples/Python/ChatApp/models.py | 43 ++++++++++---------- 2 files changed, 59 insertions(+), 50 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index ff9d0024..8a904207 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -11,7 +11,9 @@ # Get Azure App Configuration endpoint from environment variable ENDPOINT = os.environ.get("AZURE_APPCONFIG_ENDPOINT") if not ENDPOINT: - raise ValueError("The environment variable 'AZURE_APPCONFIG_ENDPOINT' is not set or is empty.") + raise ValueError( + "The environment variable 'AZURE_APPCONFIG_ENDPOINT' is not set or is empty." + ) # Initialize Azure credentials credential = DefaultAzureCredential() @@ -29,7 +31,8 @@ trim_prefixes=["ChatApp:"], ) -T = TypeVar('T') +T = TypeVar("T") + # Get OpenAI configuration def get_openai_client(): @@ -37,51 +40,53 @@ def get_openai_client(): endpoint = config.get("AzureOpenAI:Endpoint") # Get API key from App Configuration or fall back to environment variable api_key = config.get("AzureOpenAI:ApiKey", os.environ.get("AZURE_OPENAI_API_KEY")) - api_version = config.get("AzureOpenAI:ApiVersion", "2023-05-15") # Read API version from config or use default - + api_version = config.get( + "AzureOpenAI:ApiVersion", "2023-05-15" + ) # Read API version from config or use default + # For DefaultAzureCredential auth if no API key is available if not api_key: token_provider = get_bearer_token_provider( DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default" ) return AzureOpenAI( - azure_endpoint=endpoint, + azure_endpoint=endpoint, api_version=api_version, - azure_ad_token_provider=token_provider + azure_ad_token_provider=token_provider, ) - + # For API key auth return AzureOpenAI( - azure_endpoint=endpoint, - api_key=api_key, - api_version=api_version + azure_endpoint=endpoint, api_key=api_key, api_version=api_version ) + def get_chat_messages(messages: List[Message]): """Convert from model Message objects to OpenAI messages format""" return [{"role": msg.role, "content": msg.content} for msg in messages] + def main(): """Main entry point for the console app""" # Get OpenAI client client = get_openai_client() - + # Get deployment name deployment_name = config.get("AzureOpenAI:DeploymentName") - + # Initialize conversation history with the configuration messages conversation_history = [] first_run = True - + print("Chat Application - type 'exit' to quit\n") - + while True: # Refresh configuration from Azure App Configuration config.refresh() - + # Get model configuration using data binding model_config = ModelConfiguration.from_dict(config.get("Model")) - + # On first run, initialize conversation history with configuration messages # and display the initial messages if first_run: @@ -89,39 +94,42 @@ def main(): for msg in conversation_history: print(f"{msg.role}: {msg.content}") first_run = False - + # Get chat messages for the API messages = get_chat_messages(conversation_history) - + # Get response from OpenAI response = client.chat.completions.create( model=deployment_name, messages=messages, max_tokens=model_config.max_tokens, temperature=model_config.temperature, - top_p=model_config.top_p + top_p=model_config.top_p, ) - + # Extract assistant message assistant_message = response.choices[0].message.content - + # Display the response print(f"assistant: {assistant_message}") - + # Add assistant response to conversation history - conversation_history.append(Message(role="assistant", content=assistant_message)) - + conversation_history.append( + Message(role="assistant", content=assistant_message) + ) + # Get user input for the next message print("\nuser: ", end="") user_input = input().strip() - + # Check if user wants to exit - if user_input.lower() == 'exit': + if user_input.lower() == "exit": print("Exiting application...") break - + # Add user input to conversation history conversation_history.append(Message(role="user", content=user_input)) -if __name__ == '__main__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() diff --git a/examples/Python/ChatApp/models.py b/examples/Python/ChatApp/models.py index cf3fddc7..e6002064 100644 --- a/examples/Python/ChatApp/models.py +++ b/examples/Python/ChatApp/models.py @@ -9,17 +9,15 @@ class Message: Represents a chat message with a role and content. Maps to configuration values with keys 'role' and 'content'. """ + def __init__(self, role: Optional[str] = None, content: Optional[str] = None): self.role = role self.content = content - + @classmethod - def from_dict(cls, data: Dict[str, Any]) -> 'Message': + def from_dict(cls, data: Dict[str, Any]) -> "Message": """Create a Message instance from a dictionary.""" - return cls( - role=data.get('role'), - content=data.get('content') - ) + return cls(role=data.get("role"), content=data.get("content")) class ModelConfiguration: @@ -27,27 +25,30 @@ class ModelConfiguration: Represents the configuration for an AI model including messages and parameters. Maps to configuration values with keys 'model', 'messages', 'max_tokens', 'temperature', and 'top_p'. """ - def __init__(self, - model: Optional[str] = None, - messages: Optional[List[Message]] = None, - max_tokens: int = 1024, - temperature: float = 0.7, - top_p: float = 0.95): + + def __init__( + self, + model: Optional[str] = None, + messages: Optional[List[Message]] = None, + max_tokens: int = 1024, + temperature: float = 0.7, + top_p: float = 0.95, + ): self.model = model self.messages = messages or [] self.max_tokens = int(max_tokens) if max_tokens is not None else 1024 self.temperature = float(temperature) if temperature is not None else 0.7 self.top_p = float(top_p) if top_p is not None else 0.95 - + @classmethod - def from_dict(cls, data: Dict[str, Any]) -> 'ModelConfiguration': + def from_dict(cls, data: Dict[str, Any]) -> "ModelConfiguration": """Create a ModelConfiguration instance from a dictionary.""" - messages = [Message.from_dict(msg) for msg in data.get('messages', [])] - + messages = [Message.from_dict(msg) for msg in data.get("messages", [])] + return cls( - model=data.get('model'), + model=data.get("model"), messages=messages, - max_tokens=data.get('max_tokens', 1024), - temperature=data.get('temperature', 0.7), - top_p=data.get('top_p', 0.95) - ) \ No newline at end of file + max_tokens=data.get("max_tokens", 1024), + temperature=data.get("temperature", 0.7), + top_p=data.get("top_p", 0.95), + ) From 3a0f7fecfa18e1ea9c69a032d5cae035ad46ae12 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 23 May 2025 15:57:57 -0700 Subject: [PATCH 20/36] Fixing a few pylint issues --- examples/Python/ChatApp/app.py | 16 +++++++++++----- examples/Python/ChatApp/models.py | 15 ++++++++++++--- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index 8a904207..b0959465 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -1,12 +1,19 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -# +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +""" +Azure App Configuration Chat Application using Azure OpenAI. +This module provides a simple chat application that uses configurations from Azure App Configuration +and connects to Azure OpenAI services for chat completions. +""" import os +from typing import TypeVar, List from azure.identity import DefaultAzureCredential, get_bearer_token_provider from azure.appconfiguration.provider import load, SettingSelector from openai import AzureOpenAI from models import ModelConfiguration, Message -from typing import TypeVar, List # Get Azure App Configuration endpoint from environment variable ENDPOINT = os.environ.get("AZURE_APPCONFIG_ENDPOINT") @@ -22,7 +29,6 @@ chat_app_selector = SettingSelector(key_filter="ChatApp:*") # Load configuration from Azure App Configuration -global config config = load( endpoint=ENDPOINT, selects=[chat_app_selector], diff --git a/examples/Python/ChatApp/models.py b/examples/Python/ChatApp/models.py index e6002064..f688fd53 100644 --- a/examples/Python/ChatApp/models.py +++ b/examples/Python/ChatApp/models.py @@ -1,6 +1,13 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -# +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +""" +Model classes for Azure OpenAI Chat Application. +This module provides data classes for representing chat messages and model configurations +used in the Azure OpenAI-powered chat application. +""" from typing import List, Optional, Dict, Any @@ -11,6 +18,7 @@ class Message: """ def __init__(self, role: Optional[str] = None, content: Optional[str] = None): + """Initialize a Message instance with role and content.""" self.role = role self.content = content @@ -34,6 +42,7 @@ def __init__( temperature: float = 0.7, top_p: float = 0.95, ): + """Initialize model configuration with parameters for OpenAI API calls.""" self.model = model self.messages = messages or [] self.max_tokens = int(max_tokens) if max_tokens is not None else 1024 From 334fb2c4f19cdad87e8a2e85a5cda3738217f351 Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Fri, 23 May 2025 16:07:37 -0700 Subject: [PATCH 21/36] Update examples/Python/ChatApp/app.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- examples/Python/ChatApp/app.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index b0959465..304e6102 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -37,9 +37,6 @@ trim_prefixes=["ChatApp:"], ) -T = TypeVar("T") - - # Get OpenAI configuration def get_openai_client(): """Create and return an Azure OpenAI client""" From 5064f99260db18329c7213b1cc04c18d61f7c56a Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 3 Jul 2025 14:17:13 -0700 Subject: [PATCH 22/36] Updated to match dotnet --- examples/Python/ChatApp/app.py | 270 ++++++++++++++++++------------ examples/Python/ChatApp/models.py | 20 +++ 2 files changed, 182 insertions(+), 108 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index 304e6102..a0bdf16f 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -4,134 +4,188 @@ # license information. # -------------------------------------------------------------------------- """ -Azure App Configuration Chat Application using Azure OpenAI. -This module provides a simple chat application that uses configurations from Azure App Configuration -and connects to Azure OpenAI services for chat completions. +Azure OpenAI Chat Application using Azure App Configuration. +This script demonstrates how to create a chat application that uses Azure App Configuration +to manage settings and Azure OpenAI to power chat interactions. """ + import os -from typing import TypeVar, List +from typing import List, Dict, Any +from azure.core.credentials import TokenCredential from azure.identity import DefaultAzureCredential, get_bearer_token_provider -from azure.appconfiguration.provider import load, SettingSelector +from azure.appconfiguration.provider import load, SettingSelector, WatchKey from openai import AzureOpenAI -from models import ModelConfiguration, Message +from models import AzureOpenAIConfiguration, Message, ModelConfiguration -# Get Azure App Configuration endpoint from environment variable -ENDPOINT = os.environ.get("AZURE_APPCONFIG_ENDPOINT") -if not ENDPOINT: - raise ValueError( - "The environment variable 'AZURE_APPCONFIG_ENDPOINT' is not set or is empty." - ) -# Initialize Azure credentials -credential = DefaultAzureCredential() - -# Create selector for ChatApp configuration -chat_app_selector = SettingSelector(key_filter="ChatApp:*") - -# Load configuration from Azure App Configuration -config = load( - endpoint=ENDPOINT, - selects=[chat_app_selector], - credential=credential, - keyvault_credential=credential, # Use the same credential for Key Vault references - trim_prefixes=["ChatApp:"], -) - -# Get OpenAI configuration -def get_openai_client(): - """Create and return an Azure OpenAI client""" - endpoint = config.get("AzureOpenAI:Endpoint") - # Get API key from App Configuration or fall back to environment variable - api_key = config.get("AzureOpenAI:ApiKey", os.environ.get("AZURE_OPENAI_API_KEY")) - api_version = config.get( - "AzureOpenAI:ApiVersion", "2023-05-15" - ) # Read API version from config or use default - - # For DefaultAzureCredential auth if no API key is available - if not api_key: - token_provider = get_bearer_token_provider( - DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default" +def main(): + # Create a credential using DefaultAzureCredential + credential = DefaultAzureCredential() + refresher = None + + # Get the App Configuration endpoint from environment variables + app_config_endpoint = os.environ.get("AZURE_APPCONFIGURATION_ENDPOINT") + if not app_config_endpoint: + raise ValueError( + "The environment variable 'AZURE_APPCONFIGURATION_ENDPOINT' is not set or is empty." ) - return AzureOpenAI( - azure_endpoint=endpoint, - api_version=api_version, - azure_ad_token_provider=token_provider, + + try: + azure_openai_config = None + azure_client = None + + def on_refresh_success(): + azure_openai_config, azure_client = _create_ai_client(provider) + + chat_app_selector = SettingSelector(key_filter="ChatApp:*") + + global provider + # Create the configuration provider with refresh settings + provider = load( + endpoint=app_config_endpoint, + selects=[chat_app_selector], + credential=credential, + keyvault_credential=credential, # Use the same credential for Key Vault references + trim_prefixes=["ChatApp:"], + refresh_on=[WatchKey(key="ChatApp:ChatCompletion:Messages")], + on_refresh_success=on_refresh_success, ) - # For API key auth - return AzureOpenAI( - azure_endpoint=endpoint, api_key=api_key, api_version=api_version + azure_openai_config, azure_client = _create_ai_client(provider, credential) + + # Initialize chat conversation + chat_conversation = [] + print("Chat started! What's on your mind?") + + while True: + # Refresh the configuration from Azure App Configuration + provider.refresh() + + # Configure chat completion with AI configuration + chat_completion_config = _extract_chat_completion_config(provider) + + # Get user input + user_input = input("You: ") + + # Exit if user input is empty + if not user_input.strip(): + print("Exiting chat. Goodbye!") + break + + # Add user message to chat conversation + chat_conversation.append({"role": "user", "content": user_input}) + + # Get latest system message from AI configuration + chat_messages = _get_chat_messages(chat_completion_config) + chat_messages.extend(chat_conversation) + + # Get AI response and add it to chat conversation + response = azure_client.chat.completions.create( + model=azure_openai_config.deployment_name, + messages=chat_messages, + max_tokens=chat_completion_config.max_tokens, + temperature=chat_completion_config.temperature, + top_p=chat_completion_config.top_p, + ) + + ai_response = response.choices[0].message.content + print(f"AI: {ai_response}") + + except Exception as e: + print(f"Error: {e}") + finally: + # Stop the refresher when done + if refresher: + refresher.stop() + + +def _create_ai_client( + azure_openai_config: AzureOpenAIConfiguration, credential: TokenCredential = None +) -> AzureOpenAI: + # Extract Azure OpenAI configuration + azure_openai_config = _extract_openai_config(provider) + # Create an Azure OpenAI client + if azure_openai_config.api_key: + return azure_openai_config, AzureOpenAI( + azure_endpoint=azure_openai_config.endpoint, + api_key=azure_openai_config.api_key, + api_version=azure_openai_config.api_version, + ) + token_provider = get_bearer_token_provider( + credential or DefaultAzureCredential(), + "https://cognitiveservices.azure.com/.default", + ) + return azure_openai_config, AzureOpenAI( + azure_endpoint=azure_openai_config.endpoint, + azure_ad_token_provider=token_provider, + api_version=azure_openai_config.api_version, ) -def get_chat_messages(messages: List[Message]): - """Convert from model Message objects to OpenAI messages format""" - return [{"role": msg.role, "content": msg.content} for msg in messages] +def _extract_openai_config(config_data: Dict[str, Any]) -> AzureOpenAIConfiguration: + """ + Extract Azure OpenAI configuration from the configuration data. + :param config_data: The configuration data from Azure App Configuration + :return: An AzureOpenAIConfiguration object + """ + prefix = "AzureOpenAI:" + return AzureOpenAIConfiguration( + api_key=config_data.get(f"{prefix}ApiKey", ""), + endpoint=config_data.get(f"{prefix}Endpoint", ""), + deployment_name=config_data.get(f"{prefix}DeploymentName", ""), + api_version=config_data.get(f"{prefix}ApiVersion", "2023-05-15"), + ) -def main(): - """Main entry point for the console app""" - # Get OpenAI client - client = get_openai_client() - - # Get deployment name - deployment_name = config.get("AzureOpenAI:DeploymentName") - - # Initialize conversation history with the configuration messages - conversation_history = [] - first_run = True - - print("Chat Application - type 'exit' to quit\n") - - while True: - # Refresh configuration from Azure App Configuration - config.refresh() - - # Get model configuration using data binding - model_config = ModelConfiguration.from_dict(config.get("Model")) - - # On first run, initialize conversation history with configuration messages - # and display the initial messages - if first_run: - conversation_history = model_config.messages.copy() - for msg in conversation_history: - print(f"{msg.role}: {msg.content}") - first_run = False - - # Get chat messages for the API - messages = get_chat_messages(conversation_history) - - # Get response from OpenAI - response = client.chat.completions.create( - model=deployment_name, - messages=messages, - max_tokens=model_config.max_tokens, - temperature=model_config.temperature, - top_p=model_config.top_p, - ) - # Extract assistant message - assistant_message = response.choices[0].message.content +def _extract_chat_completion_config(config_data: Dict[str, Any]) -> ModelConfiguration: + """ + Extract chat completion configuration from the configuration data. - # Display the response - print(f"assistant: {assistant_message}") + :param config_data: The configuration data from Azure App Configuration + """ + prefix = "ChatApp:ChatCompletion:" - # Add assistant response to conversation history - conversation_history.append( - Message(role="assistant", content=assistant_message) - ) + # Extract messages from configuration + messages_data = [] + for i in range(10): # Assuming a reasonable maximum of 10 messages + role_key = f"{prefix}Messages:{i}:Role" + content_key = f"{prefix}Messages:{i}:Content" + + if role_key in config_data and content_key in config_data: + messages_data.append( + {"role": config_data[role_key], "content": config_data[content_key]} + ) + + messages = [Message.from_dict(msg) for msg in messages_data] + + return ModelConfiguration( + model=config_data.get(f"{prefix}Model", ""), + messages=messages, + max_tokens=config_data.get(f"{prefix}MaxTokens", 1024), + temperature=config_data.get(f"{prefix}Temperature", 0.7), + top_p=config_data.get(f"{prefix}TopP", 0.95), + ) + + +def _get_chat_messages( + chat_completion_config: ModelConfiguration, +) -> List[Dict[str, str]]: + """ + Convert configuration messages to chat message dictionaries. - # Get user input for the next message - print("\nuser: ", end="") - user_input = input().strip() + :param chat_completion_config: The chat completion configuration + :return: A list of chat message dictionaries + """ + chat_messages = [] - # Check if user wants to exit - if user_input.lower() == "exit": - print("Exiting application...") - break + for message in chat_completion_config.messages: + if message.role in ["system", "user", "assistant"]: + chat_messages.append({"role": message.role, "content": message.content}) + else: + raise ValueError(f"Unknown role: {message.role}") - # Add user input to conversation history - conversation_history.append(Message(role="user", content=user_input)) + return chat_messages if __name__ == "__main__": diff --git a/examples/Python/ChatApp/models.py b/examples/Python/ChatApp/models.py index f688fd53..8a010d50 100644 --- a/examples/Python/ChatApp/models.py +++ b/examples/Python/ChatApp/models.py @@ -11,6 +11,26 @@ from typing import List, Optional, Dict, Any +class AzureOpenAIConfiguration: + """ + Represents the configuration for Azure OpenAI service. + Maps to configuration values with keys 'api_key', 'endpoint', and 'deployment_name'. + """ + + def __init__( + self, + api_key: str, + endpoint: str, + deployment_name: Optional[str] = None, + api_version: Optional[str] = None, + ): + """Initialize Azure OpenAI configuration with API key and endpoint.""" + self.api_key = api_key + self.endpoint = endpoint + self.deployment_name = deployment_name + self.api_version = api_version + + class Message: """ Represents a chat message with a role and content. From 2afb1370c461c4fe3cf1459b6560f6c69617aeae Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 7 Jul 2025 10:16:16 -0700 Subject: [PATCH 23/36] review items --- examples/Python/ChatApp/app.py | 58 ++++++++++-------------- examples/Python/ChatApp/models.py | 28 ++---------- examples/Python/ChatApp/requirements.txt | 2 +- 3 files changed, 31 insertions(+), 57 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index a0bdf16f..5b9b826d 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -15,13 +15,12 @@ from azure.identity import DefaultAzureCredential, get_bearer_token_provider from azure.appconfiguration.provider import load, SettingSelector, WatchKey from openai import AzureOpenAI -from models import AzureOpenAIConfiguration, Message, ModelConfiguration +from models import AzureOpenAIConfiguration, ChatCompletionConfiguration def main(): # Create a credential using DefaultAzureCredential credential = DefaultAzureCredential() - refresher = None # Get the App Configuration endpoint from environment variables app_config_endpoint = os.environ.get("AZURE_APPCONFIGURATION_ENDPOINT") @@ -35,15 +34,13 @@ def main(): azure_client = None def on_refresh_success(): - azure_openai_config, azure_client = _create_ai_client(provider) + azure_openai_config, azure_client = _create_ai_client(appconfig) - chat_app_selector = SettingSelector(key_filter="ChatApp:*") - - global provider + global appconfig # Create the configuration provider with refresh settings - provider = load( + appconfig = load( endpoint=app_config_endpoint, - selects=[chat_app_selector], + selects=[SettingSelector(key_filter="ChatApp:*")], credential=credential, keyvault_credential=credential, # Use the same credential for Key Vault references trim_prefixes=["ChatApp:"], @@ -51,7 +48,7 @@ def on_refresh_success(): on_refresh_success=on_refresh_success, ) - azure_openai_config, azure_client = _create_ai_client(provider, credential) + azure_openai_config, azure_client = _create_ai_client(appconfig, credential) # Initialize chat conversation chat_conversation = [] @@ -59,10 +56,10 @@ def on_refresh_success(): while True: # Refresh the configuration from Azure App Configuration - provider.refresh() + appconfig.refresh() # Configure chat completion with AI configuration - chat_completion_config = _extract_chat_completion_config(provider) + chat_completion_config = _extract_chat_completion_config(appconfig) # Get user input user_input = input("You: ") @@ -89,21 +86,18 @@ def on_refresh_success(): ) ai_response = response.choices[0].message.content + chat_conversation.append({"role": "assistant", "content": ai_response}) print(f"AI: {ai_response}") except Exception as e: print(f"Error: {e}") - finally: - # Stop the refresher when done - if refresher: - refresher.stop() def _create_ai_client( azure_openai_config: AzureOpenAIConfiguration, credential: TokenCredential = None ) -> AzureOpenAI: # Extract Azure OpenAI configuration - azure_openai_config = _extract_openai_config(provider) + azure_openai_config = _extract_openai_config(appconfig) # Create an Azure OpenAI client if azure_openai_config.api_key: return azure_openai_config, AzureOpenAI( @@ -111,15 +105,15 @@ def _create_ai_client( api_key=azure_openai_config.api_key, api_version=azure_openai_config.api_version, ) - token_provider = get_bearer_token_provider( - credential or DefaultAzureCredential(), - "https://cognitiveservices.azure.com/.default", - ) - return azure_openai_config, AzureOpenAI( - azure_endpoint=azure_openai_config.endpoint, - azure_ad_token_provider=token_provider, - api_version=azure_openai_config.api_version, - ) + else: + return azure_openai_config, AzureOpenAI( + azure_endpoint=azure_openai_config.endpoint, + azure_ad_token_provider=get_bearer_token_provider( + credential or DefaultAzureCredential(), + "https://cognitiveservices.azure.com/.default", + ), + api_version=azure_openai_config.api_version, + ) def _extract_openai_config(config_data: Dict[str, Any]) -> AzureOpenAIConfiguration: @@ -138,28 +132,26 @@ def _extract_openai_config(config_data: Dict[str, Any]) -> AzureOpenAIConfigurat ) -def _extract_chat_completion_config(config_data: Dict[str, Any]) -> ModelConfiguration: +def _extract_chat_completion_config(config_data: Dict[str, Any]) -> ChatCompletionConfiguration: """ Extract chat completion configuration from the configuration data. :param config_data: The configuration data from Azure App Configuration """ - prefix = "ChatApp:ChatCompletion:" + prefix = "ChatCompletion:" # Extract messages from configuration - messages_data = [] + messages = [] for i in range(10): # Assuming a reasonable maximum of 10 messages role_key = f"{prefix}Messages:{i}:Role" content_key = f"{prefix}Messages:{i}:Content" if role_key in config_data and content_key in config_data: - messages_data.append( + messages.append( {"role": config_data[role_key], "content": config_data[content_key]} ) - messages = [Message.from_dict(msg) for msg in messages_data] - - return ModelConfiguration( + return ChatCompletionConfiguration( model=config_data.get(f"{prefix}Model", ""), messages=messages, max_tokens=config_data.get(f"{prefix}MaxTokens", 1024), @@ -169,7 +161,7 @@ def _extract_chat_completion_config(config_data: Dict[str, Any]) -> ModelConfigu def _get_chat_messages( - chat_completion_config: ModelConfiguration, + chat_completion_config: ChatCompletionConfiguration, ) -> List[Dict[str, str]]: """ Convert configuration messages to chat message dictionaries. diff --git a/examples/Python/ChatApp/models.py b/examples/Python/ChatApp/models.py index 8a010d50..8da9a015 100644 --- a/examples/Python/ChatApp/models.py +++ b/examples/Python/ChatApp/models.py @@ -30,25 +30,7 @@ def __init__( self.deployment_name = deployment_name self.api_version = api_version - -class Message: - """ - Represents a chat message with a role and content. - Maps to configuration values with keys 'role' and 'content'. - """ - - def __init__(self, role: Optional[str] = None, content: Optional[str] = None): - """Initialize a Message instance with role and content.""" - self.role = role - self.content = content - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "Message": - """Create a Message instance from a dictionary.""" - return cls(role=data.get("role"), content=data.get("content")) - - -class ModelConfiguration: +class ChatCompletionConfiguration: """ Represents the configuration for an AI model including messages and parameters. Maps to configuration values with keys 'model', 'messages', 'max_tokens', 'temperature', and 'top_p'. @@ -57,7 +39,7 @@ class ModelConfiguration: def __init__( self, model: Optional[str] = None, - messages: Optional[List[Message]] = None, + messages: Optional[List[Dict[str, str]]] = None, max_tokens: int = 1024, temperature: float = 0.7, top_p: float = 0.95, @@ -70,9 +52,9 @@ def __init__( self.top_p = float(top_p) if top_p is not None else 0.95 @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "ModelConfiguration": - """Create a ModelConfiguration instance from a dictionary.""" - messages = [Message.from_dict(msg) for msg in data.get("messages", [])] + def from_dict(cls, data: Dict[str, Any]) -> "ChatCompletionConfiguration": + """Create a ChatCompletionConfiguration instance from a dictionary.""" + messages = data.get("messages", []) return cls( model=data.get("model"), diff --git a/examples/Python/ChatApp/requirements.txt b/examples/Python/ChatApp/requirements.txt index 2c073155..e131693e 100644 --- a/examples/Python/ChatApp/requirements.txt +++ b/examples/Python/ChatApp/requirements.txt @@ -1,3 +1,3 @@ azure-identity azure-appconfiguration-provider -openai \ No newline at end of file +openai From 20e9d6dfbada240aaeb3dfeb9c24d9076e6fc590 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 7 Jul 2025 10:32:01 -0700 Subject: [PATCH 24/36] review changes --- examples/Python/ChatApp/app.py | 4 ++- examples/Python/ChatApp/models.py | 54 ++++++++----------------------- 2 files changed, 17 insertions(+), 41 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index 5b9b826d..d45b233e 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -132,7 +132,9 @@ def _extract_openai_config(config_data: Dict[str, Any]) -> AzureOpenAIConfigurat ) -def _extract_chat_completion_config(config_data: Dict[str, Any]) -> ChatCompletionConfiguration: +def _extract_chat_completion_config( + config_data: Dict[str, Any], +) -> ChatCompletionConfiguration: """ Extract chat completion configuration from the configuration data. diff --git a/examples/Python/ChatApp/models.py b/examples/Python/ChatApp/models.py index 8da9a015..a92a3b86 100644 --- a/examples/Python/ChatApp/models.py +++ b/examples/Python/ChatApp/models.py @@ -8,58 +8,32 @@ This module provides data classes for representing chat messages and model configurations used in the Azure OpenAI-powered chat application. """ -from typing import List, Optional, Dict, Any +from dataclasses import dataclass +from typing import List, Optional, Dict +@dataclass class AzureOpenAIConfiguration: """ Represents the configuration for Azure OpenAI service. Maps to configuration values with keys 'api_key', 'endpoint', and 'deployment_name'. """ - def __init__( - self, - api_key: str, - endpoint: str, - deployment_name: Optional[str] = None, - api_version: Optional[str] = None, - ): - """Initialize Azure OpenAI configuration with API key and endpoint.""" - self.api_key = api_key - self.endpoint = endpoint - self.deployment_name = deployment_name - self.api_version = api_version + api_key: str + endpoint: str + deployment_name: Optional[str] = None + api_version: Optional[str] = None + +@dataclass class ChatCompletionConfiguration: """ Represents the configuration for an AI model including messages and parameters. Maps to configuration values with keys 'model', 'messages', 'max_tokens', 'temperature', and 'top_p'. """ - def __init__( - self, - model: Optional[str] = None, - messages: Optional[List[Dict[str, str]]] = None, - max_tokens: int = 1024, - temperature: float = 0.7, - top_p: float = 0.95, - ): - """Initialize model configuration with parameters for OpenAI API calls.""" - self.model = model - self.messages = messages or [] - self.max_tokens = int(max_tokens) if max_tokens is not None else 1024 - self.temperature = float(temperature) if temperature is not None else 0.7 - self.top_p = float(top_p) if top_p is not None else 0.95 - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "ChatCompletionConfiguration": - """Create a ChatCompletionConfiguration instance from a dictionary.""" - messages = data.get("messages", []) - - return cls( - model=data.get("model"), - messages=messages, - max_tokens=data.get("max_tokens", 1024), - temperature=data.get("temperature", 0.7), - top_p=data.get("top_p", 0.95), - ) + model: Optional[str] = None + messages: Optional[List[Dict[str, str]]] = None + max_tokens: int = 1024 + temperature: float = 0.7 + top_p: float = 0.95 From b8e8a5512ab11d0ebed872f267dbb7999e42b324 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 7 Jul 2025 11:02:23 -0700 Subject: [PATCH 25/36] review comments --- examples/Python/ChatApp/app.py | 37 +++------------------------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index d45b233e..adf797ca 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -59,7 +59,7 @@ def on_refresh_success(): appconfig.refresh() # Configure chat completion with AI configuration - chat_completion_config = _extract_chat_completion_config(appconfig) + chat_completion_config = ChatCompletionConfiguration(**appconfig["ChatCompletion"]) # Get user input user_input = input("You: ") @@ -131,37 +131,6 @@ def _extract_openai_config(config_data: Dict[str, Any]) -> AzureOpenAIConfigurat api_version=config_data.get(f"{prefix}ApiVersion", "2023-05-15"), ) - -def _extract_chat_completion_config( - config_data: Dict[str, Any], -) -> ChatCompletionConfiguration: - """ - Extract chat completion configuration from the configuration data. - - :param config_data: The configuration data from Azure App Configuration - """ - prefix = "ChatCompletion:" - - # Extract messages from configuration - messages = [] - for i in range(10): # Assuming a reasonable maximum of 10 messages - role_key = f"{prefix}Messages:{i}:Role" - content_key = f"{prefix}Messages:{i}:Content" - - if role_key in config_data and content_key in config_data: - messages.append( - {"role": config_data[role_key], "content": config_data[content_key]} - ) - - return ChatCompletionConfiguration( - model=config_data.get(f"{prefix}Model", ""), - messages=messages, - max_tokens=config_data.get(f"{prefix}MaxTokens", 1024), - temperature=config_data.get(f"{prefix}Temperature", 0.7), - top_p=config_data.get(f"{prefix}TopP", 0.95), - ) - - def _get_chat_messages( chat_completion_config: ChatCompletionConfiguration, ) -> List[Dict[str, str]]: @@ -174,8 +143,8 @@ def _get_chat_messages( chat_messages = [] for message in chat_completion_config.messages: - if message.role in ["system", "user", "assistant"]: - chat_messages.append({"role": message.role, "content": message.content}) + if message["role"] in ["system", "user", "assistant"]: + chat_messages.append({"role": message["role"], "content": message["content"]}) else: raise ValueError(f"Unknown role: {message.role}") From 18fbda08c0a0527ba195a689e1aec3ba95c9c318 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 7 Jul 2025 11:23:46 -0700 Subject: [PATCH 26/36] Update app.py --- examples/Python/ChatApp/app.py | 129 ++++++++++++++------------------- 1 file changed, 53 insertions(+), 76 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index adf797ca..45eb0437 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -10,7 +10,7 @@ """ import os -from typing import List, Dict, Any +from typing import Dict, Any from azure.core.credentials import TokenCredential from azure.identity import DefaultAzureCredential, get_bearer_token_provider from azure.appconfiguration.provider import load, SettingSelector, WatchKey @@ -19,6 +19,7 @@ def main(): + global appconfig, azure_openai_config, chat_completion_config, azure_client, chat_messages, chat_conversation, credential # Create a credential using DefaultAzureCredential credential = DefaultAzureCredential() @@ -29,84 +30,79 @@ def main(): "The environment variable 'AZURE_APPCONFIGURATION_ENDPOINT' is not set or is empty." ) - try: - azure_openai_config = None - azure_client = None - - def on_refresh_success(): - azure_openai_config, azure_client = _create_ai_client(appconfig) - - global appconfig - # Create the configuration provider with refresh settings - appconfig = load( - endpoint=app_config_endpoint, - selects=[SettingSelector(key_filter="ChatApp:*")], - credential=credential, - keyvault_credential=credential, # Use the same credential for Key Vault references - trim_prefixes=["ChatApp:"], - refresh_on=[WatchKey(key="ChatApp:ChatCompletion:Messages")], - on_refresh_success=on_refresh_success, - ) - - azure_openai_config, azure_client = _create_ai_client(appconfig, credential) + # Initialize chat conversation + chat_messages = [] + chat_conversation = [] - # Initialize chat conversation - chat_conversation = [] - print("Chat started! What's on your mind?") + def configure_app(): + global appconfig, azure_openai_config, chat_completion_config, azure_client, chat_messages, credential + azure_openai_config = _extract_openai_config(appconfig) + azure_client = _create_ai_client(azure_openai_config, credential) - while True: - # Refresh the configuration from Azure App Configuration - appconfig.refresh() + # Configure chat completion with AI configuration + chat_completion_config = ChatCompletionConfiguration( + **appconfig["ChatCompletion"] + ) + chat_messages = chat_completion_config.messages + + # Create the configuration provider with refresh settings + appconfig = load( + endpoint=app_config_endpoint, + selects=[SettingSelector(key_filter="ChatApp:*")], + credential=credential, + keyvault_credential=credential, # Use the same credential for Key Vault references + trim_prefixes=["ChatApp:"], + refresh_on=[WatchKey(key="ChatApp:ChatCompletion")], + on_refresh_success=configure_app, + ) + configure_app() - # Configure chat completion with AI configuration - chat_completion_config = ChatCompletionConfiguration(**appconfig["ChatCompletion"]) + print("Chat started! What's on your mind?") - # Get user input - user_input = input("You: ") + while True: + # Refresh the configuration from Azure App Configuration + appconfig.refresh() - # Exit if user input is empty - if not user_input.strip(): - print("Exiting chat. Goodbye!") - break + # Get user input + user_input = input("You: ") - # Add user message to chat conversation - chat_conversation.append({"role": "user", "content": user_input}) + # Exit if user input is empty + if not user_input.strip(): + print("Exiting chat. Goodbye!") + break - # Get latest system message from AI configuration - chat_messages = _get_chat_messages(chat_completion_config) - chat_messages.extend(chat_conversation) + # Add user message to chat conversation + chat_conversation.append({"role": "user", "content": user_input}) - # Get AI response and add it to chat conversation - response = azure_client.chat.completions.create( - model=azure_openai_config.deployment_name, - messages=chat_messages, - max_tokens=chat_completion_config.max_tokens, - temperature=chat_completion_config.temperature, - top_p=chat_completion_config.top_p, - ) + # Get latest system message from AI configuration + chat_messages.extend(chat_conversation) - ai_response = response.choices[0].message.content - chat_conversation.append({"role": "assistant", "content": ai_response}) - print(f"AI: {ai_response}") + # Get AI response and add it to chat conversation + response = azure_client.chat.completions.create( + model=azure_openai_config.deployment_name, + messages=chat_messages, + max_tokens=chat_completion_config.max_tokens, + temperature=chat_completion_config.temperature, + top_p=chat_completion_config.top_p, + ) - except Exception as e: - print(f"Error: {e}") + ai_response = response.choices[0].message.content + chat_conversation.append({"role": "assistant", "content": ai_response}) + print(f"AI: {ai_response}") def _create_ai_client( azure_openai_config: AzureOpenAIConfiguration, credential: TokenCredential = None ) -> AzureOpenAI: - # Extract Azure OpenAI configuration - azure_openai_config = _extract_openai_config(appconfig) # Create an Azure OpenAI client if azure_openai_config.api_key: - return azure_openai_config, AzureOpenAI( + return AzureOpenAI( azure_endpoint=azure_openai_config.endpoint, api_key=azure_openai_config.api_key, api_version=azure_openai_config.api_version, ) else: - return azure_openai_config, AzureOpenAI( + return AzureOpenAI( azure_endpoint=azure_openai_config.endpoint, azure_ad_token_provider=get_bearer_token_provider( credential or DefaultAzureCredential(), @@ -131,25 +127,6 @@ def _extract_openai_config(config_data: Dict[str, Any]) -> AzureOpenAIConfigurat api_version=config_data.get(f"{prefix}ApiVersion", "2023-05-15"), ) -def _get_chat_messages( - chat_completion_config: ChatCompletionConfiguration, -) -> List[Dict[str, str]]: - """ - Convert configuration messages to chat message dictionaries. - - :param chat_completion_config: The chat completion configuration - :return: A list of chat message dictionaries - """ - chat_messages = [] - - for message in chat_completion_config.messages: - if message["role"] in ["system", "user", "assistant"]: - chat_messages.append({"role": message["role"], "content": message["content"]}) - else: - raise ValueError(f"Unknown role: {message.role}") - - return chat_messages - if __name__ == "__main__": main() From 320a3720b4654800d8199be654b2045968a3cbe5 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 7 Jul 2025 11:40:38 -0700 Subject: [PATCH 27/36] rework to remove global need --- examples/Python/ChatApp/app.py | 172 +++++++++++++++++---------------- 1 file changed, 90 insertions(+), 82 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index 45eb0437..ffc7f2ad 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -18,114 +18,122 @@ from models import AzureOpenAIConfiguration, ChatCompletionConfiguration -def main(): - global appconfig, azure_openai_config, chat_completion_config, azure_client, chat_messages, chat_conversation, credential - # Create a credential using DefaultAzureCredential - credential = DefaultAzureCredential() - - # Get the App Configuration endpoint from environment variables - app_config_endpoint = os.environ.get("AZURE_APPCONFIGURATION_ENDPOINT") - if not app_config_endpoint: - raise ValueError( - "The environment variable 'AZURE_APPCONFIGURATION_ENDPOINT' is not set or is empty." +class ChatApp: + + def __init__(self): + # Initialize credential and config + self.credential = DefaultAzureCredential() + self.app_config_endpoint = self._get_app_config_endpoint() + + # Initialize chat state + self.chat_messages = [] + self.chat_conversation = [] + + # Load configuration + self.appconfig = self._load_config() + self.configure_app() + + def _get_app_config_endpoint(self): + app_config_endpoint = os.environ.get("AZURE_APPCONFIGURATION_ENDPOINT") + if not app_config_endpoint: + raise ValueError( + "The environment variable 'AZURE_APPCONFIGURATION_ENDPOINT' is not set or is empty." + ) + return app_config_endpoint + + def _load_config(self): + return load( + endpoint=self.app_config_endpoint, + selects=[SettingSelector(key_filter="ChatApp:*")], + credential=self.credential, + keyvault_credential=self.credential, + trim_prefixes=["ChatApp:"], + refresh_on=[WatchKey(key="ChatApp:ChatCompletion")], + on_refresh_success=self.configure_app, ) - # Initialize chat conversation - chat_messages = [] - chat_conversation = [] - - def configure_app(): - global appconfig, azure_openai_config, chat_completion_config, azure_client, chat_messages, credential - azure_openai_config = _extract_openai_config(appconfig) - azure_client = _create_ai_client(azure_openai_config, credential) + def configure_app(self): + self.azure_openai_config = self._extract_openai_config() + self.azure_client = self._create_ai_client() # Configure chat completion with AI configuration - chat_completion_config = ChatCompletionConfiguration( - **appconfig["ChatCompletion"] + self.chat_completion_config = ChatCompletionConfiguration( + **self.appconfig["ChatCompletion"] + ) + self.chat_messages = self.chat_completion_config.messages + + def _create_ai_client(self) -> AzureOpenAI: + # Create an Azure OpenAI client + if self.azure_openai_config.api_key: + return AzureOpenAI( + azure_endpoint=self.azure_openai_config.endpoint, + api_key=self.azure_openai_config.api_key, + api_version=self.azure_openai_config.api_version, + ) + else: + return AzureOpenAI( + azure_endpoint=self.azure_openai_config.endpoint, + azure_ad_token_provider=get_bearer_token_provider( + self.credential or DefaultAzureCredential(), + "https://cognitiveservices.azure.com/.default", + ), + api_version=self.azure_openai_config.api_version, + ) + + def _extract_openai_config(self) -> AzureOpenAIConfiguration: + """ + Extract Azure OpenAI configuration from the configuration data. + + :param config_data: The configuration data from Azure App Configuration + :return: An AzureOpenAIConfiguration object + """ + prefix = "AzureOpenAI:" + return AzureOpenAIConfiguration( + api_key=self.appconfig.get(f"{prefix}ApiKey", ""), + endpoint=self.appconfig.get(f"{prefix}Endpoint", ""), + deployment_name=self.appconfig.get(f"{prefix}DeploymentName", ""), + api_version=self.appconfig.get(f"{prefix}ApiVersion", "2023-05-15"), ) - chat_messages = chat_completion_config.messages - - # Create the configuration provider with refresh settings - appconfig = load( - endpoint=app_config_endpoint, - selects=[SettingSelector(key_filter="ChatApp:*")], - credential=credential, - keyvault_credential=credential, # Use the same credential for Key Vault references - trim_prefixes=["ChatApp:"], - refresh_on=[WatchKey(key="ChatApp:ChatCompletion")], - on_refresh_success=configure_app, - ) - configure_app() - - print("Chat started! What's on your mind?") - - while True: - # Refresh the configuration from Azure App Configuration - appconfig.refresh() + def ask(self): # Get user input user_input = input("You: ") # Exit if user input is empty if not user_input.strip(): print("Exiting chat. Goodbye!") - break + exit() # Add user message to chat conversation - chat_conversation.append({"role": "user", "content": user_input}) + self.chat_conversation.append({"role": "user", "content": user_input}) # Get latest system message from AI configuration - chat_messages.extend(chat_conversation) + self.chat_messages.extend(self.chat_conversation) # Get AI response and add it to chat conversation - response = azure_client.chat.completions.create( - model=azure_openai_config.deployment_name, - messages=chat_messages, - max_tokens=chat_completion_config.max_tokens, - temperature=chat_completion_config.temperature, - top_p=chat_completion_config.top_p, + response = self.azure_client.chat.completions.create( + model=self.azure_openai_config.deployment_name, + messages=self.chat_messages, + max_tokens=self.chat_completion_config.max_tokens, + temperature=self.chat_completion_config.temperature, + top_p=self.chat_completion_config.top_p, ) ai_response = response.choices[0].message.content - chat_conversation.append({"role": "assistant", "content": ai_response}) + self.chat_conversation.append({"role": "assistant", "content": ai_response}) print(f"AI: {ai_response}") -def _create_ai_client( - azure_openai_config: AzureOpenAIConfiguration, credential: TokenCredential = None -) -> AzureOpenAI: - # Create an Azure OpenAI client - if azure_openai_config.api_key: - return AzureOpenAI( - azure_endpoint=azure_openai_config.endpoint, - api_key=azure_openai_config.api_key, - api_version=azure_openai_config.api_version, - ) - else: - return AzureOpenAI( - azure_endpoint=azure_openai_config.endpoint, - azure_ad_token_provider=get_bearer_token_provider( - credential or DefaultAzureCredential(), - "https://cognitiveservices.azure.com/.default", - ), - api_version=azure_openai_config.api_version, - ) +def main(): + chat_app = ChatApp() + print("Chat started! What's on your mind?") + + while True: + # Refresh the configuration from Azure App Configuration + chat_app.appconfig.refresh() -def _extract_openai_config(config_data: Dict[str, Any]) -> AzureOpenAIConfiguration: - """ - Extract Azure OpenAI configuration from the configuration data. - - :param config_data: The configuration data from Azure App Configuration - :return: An AzureOpenAIConfiguration object - """ - prefix = "AzureOpenAI:" - return AzureOpenAIConfiguration( - api_key=config_data.get(f"{prefix}ApiKey", ""), - endpoint=config_data.get(f"{prefix}Endpoint", ""), - deployment_name=config_data.get(f"{prefix}DeploymentName", ""), - api_version=config_data.get(f"{prefix}ApiVersion", "2023-05-15"), - ) + chat_app.ask() if __name__ == "__main__": From 5fa4fff1081698355cdb1c0a9a092ca0023d60c6 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 7 Jul 2025 11:56:46 -0700 Subject: [PATCH 28/36] code cleanup --- examples/Python/ChatApp/app.py | 56 ++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index ffc7f2ad..1519821e 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -10,45 +10,48 @@ """ import os -from typing import Dict, Any -from azure.core.credentials import TokenCredential from azure.identity import DefaultAzureCredential, get_bearer_token_provider from azure.appconfiguration.provider import load, SettingSelector, WatchKey from openai import AzureOpenAI from models import AzureOpenAIConfiguration, ChatCompletionConfiguration +app_config_endpoint_key = "AZURE_APPCONFIGURATION_ENDPOINT" +chat_app_key = "ChatApp:" +azure_openai_key = "AzureOpenAI:" +chat_completion_key = "ChatCompletion" +bearer_scope = "https://cognitiveservices.azure.com/.default" + class ChatApp: def __init__(self): # Initialize credential and config - self.credential = DefaultAzureCredential() - self.app_config_endpoint = self._get_app_config_endpoint() + self._credential = DefaultAzureCredential() # Initialize chat state - self.chat_messages = [] - self.chat_conversation = [] + self._chat_messages = [] + self._chat_conversation = [] # Load configuration self.appconfig = self._load_config() self.configure_app() def _get_app_config_endpoint(self): - app_config_endpoint = os.environ.get("AZURE_APPCONFIGURATION_ENDPOINT") + app_config_endpoint = os.environ.get(app_config_endpoint_key) if not app_config_endpoint: raise ValueError( - "The environment variable 'AZURE_APPCONFIGURATION_ENDPOINT' is not set or is empty." + f"The environment variable '{app_config_endpoint_key}' is not set or is empty." ) return app_config_endpoint def _load_config(self): return load( - endpoint=self.app_config_endpoint, - selects=[SettingSelector(key_filter="ChatApp:*")], - credential=self.credential, - keyvault_credential=self.credential, - trim_prefixes=["ChatApp:"], - refresh_on=[WatchKey(key="ChatApp:ChatCompletion")], + endpoint=self._get_app_config_endpoint(), + selects=[SettingSelector(key_filter=f"{chat_app_key}:*")], + credential=self._credential, + keyvault_credential=self._credential, + trim_prefixes=[chat_app_key], + refresh_on=[WatchKey(key=f"{chat_app_key}{chat_completion_key}")], on_refresh_success=self.configure_app, ) @@ -58,9 +61,9 @@ def configure_app(self): # Configure chat completion with AI configuration self.chat_completion_config = ChatCompletionConfiguration( - **self.appconfig["ChatCompletion"] + **self.appconfig[chat_completion_key] ) - self.chat_messages = self.chat_completion_config.messages + self._chat_messages = self.chat_completion_config.messages def _create_ai_client(self) -> AzureOpenAI: # Create an Azure OpenAI client @@ -74,8 +77,8 @@ def _create_ai_client(self) -> AzureOpenAI: return AzureOpenAI( azure_endpoint=self.azure_openai_config.endpoint, azure_ad_token_provider=get_bearer_token_provider( - self.credential or DefaultAzureCredential(), - "https://cognitiveservices.azure.com/.default", + self._credential or DefaultAzureCredential(), + bearer_scope, ), api_version=self.azure_openai_config.api_version, ) @@ -87,12 +90,11 @@ def _extract_openai_config(self) -> AzureOpenAIConfiguration: :param config_data: The configuration data from Azure App Configuration :return: An AzureOpenAIConfiguration object """ - prefix = "AzureOpenAI:" return AzureOpenAIConfiguration( - api_key=self.appconfig.get(f"{prefix}ApiKey", ""), - endpoint=self.appconfig.get(f"{prefix}Endpoint", ""), - deployment_name=self.appconfig.get(f"{prefix}DeploymentName", ""), - api_version=self.appconfig.get(f"{prefix}ApiVersion", "2023-05-15"), + api_key=self.appconfig.get(f"{azure_openai_key}ApiKey", ""), + endpoint=self.appconfig.get(f"{azure_openai_key}Endpoint", ""), + deployment_name=self.appconfig.get(f"{azure_openai_key}DeploymentName", ""), + api_version=self.appconfig.get(f"{azure_openai_key}ApiVersion", "2023-05-15"), ) def ask(self): @@ -105,22 +107,22 @@ def ask(self): exit() # Add user message to chat conversation - self.chat_conversation.append({"role": "user", "content": user_input}) + self._chat_conversation.append({"role": "user", "content": user_input}) # Get latest system message from AI configuration - self.chat_messages.extend(self.chat_conversation) + self._chat_messages.extend(self._chat_conversation) # Get AI response and add it to chat conversation response = self.azure_client.chat.completions.create( model=self.azure_openai_config.deployment_name, - messages=self.chat_messages, + messages=self._chat_messages, max_tokens=self.chat_completion_config.max_tokens, temperature=self.chat_completion_config.temperature, top_p=self.chat_completion_config.top_p, ) ai_response = response.choices[0].message.content - self.chat_conversation.append({"role": "assistant", "content": ai_response}) + self._chat_conversation.append({"role": "assistant", "content": ai_response}) print(f"AI: {ai_response}") From 0b34fc71c1463df170a0b3f4aa8475e184ddefe1 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 7 Jul 2025 12:06:01 -0700 Subject: [PATCH 29/36] fixed formatting --- examples/Python/ChatApp/app.py | 62 +++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index 1519821e..a30efbf8 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -16,13 +16,17 @@ from models import AzureOpenAIConfiguration, ChatCompletionConfiguration -app_config_endpoint_key = "AZURE_APPCONFIGURATION_ENDPOINT" -chat_app_key = "ChatApp:" -azure_openai_key = "AzureOpenAI:" -chat_completion_key = "ChatCompletion" -bearer_scope = "https://cognitiveservices.azure.com/.default" +APP_CONFIG_ENDPOINT_KEY = "AZURE_APPCONFIGURATION_ENDPOINT" +CHAT_APP_KEY = "ChatApp:" +AZURE_OPENAI_KEY = "AzureOpenAI:" +CHAT_COMPLETION_KEY = "ChatCompletion" +BEARER_SCOPE = "https://cognitiveservices.azure.com/.default" + class ChatApp: + """ + Chat Application using Azure OpenAI and Azure App Configuration. + """ def __init__(self): # Initialize credential and config @@ -33,35 +37,38 @@ def __init__(self): self._chat_conversation = [] # Load configuration - self.appconfig = self._load_config() + self._appconfig = self._load_config() self.configure_app() def _get_app_config_endpoint(self): - app_config_endpoint = os.environ.get(app_config_endpoint_key) + app_config_endpoint = os.environ.get(APP_CONFIG_ENDPOINT_KEY) if not app_config_endpoint: raise ValueError( - f"The environment variable '{app_config_endpoint_key}' is not set or is empty." + f"The environment variable '{APP_CONFIG_ENDPOINT_KEY}' is not set or is empty." ) return app_config_endpoint def _load_config(self): return load( endpoint=self._get_app_config_endpoint(), - selects=[SettingSelector(key_filter=f"{chat_app_key}:*")], + selects=[SettingSelector(key_filter=f"{CHAT_APP_KEY}:*")], credential=self._credential, keyvault_credential=self._credential, - trim_prefixes=[chat_app_key], - refresh_on=[WatchKey(key=f"{chat_app_key}{chat_completion_key}")], + trim_prefixes=[CHAT_APP_KEY], + refresh_on=[WatchKey(key=f"{CHAT_APP_KEY}{CHAT_COMPLETION_KEY}")], on_refresh_success=self.configure_app, ) def configure_app(self): + """ + Configure the chat application with settings from Azure App Configuration. + """ self.azure_openai_config = self._extract_openai_config() self.azure_client = self._create_ai_client() # Configure chat completion with AI configuration self.chat_completion_config = ChatCompletionConfiguration( - **self.appconfig[chat_completion_key] + **self._appconfig[CHAT_COMPLETION_KEY] ) self._chat_messages = self.chat_completion_config.messages @@ -78,7 +85,7 @@ def _create_ai_client(self) -> AzureOpenAI: azure_endpoint=self.azure_openai_config.endpoint, azure_ad_token_provider=get_bearer_token_provider( self._credential or DefaultAzureCredential(), - bearer_scope, + BEARER_SCOPE, ), api_version=self.azure_openai_config.api_version, ) @@ -91,20 +98,30 @@ def _extract_openai_config(self) -> AzureOpenAIConfiguration: :return: An AzureOpenAIConfiguration object """ return AzureOpenAIConfiguration( - api_key=self.appconfig.get(f"{azure_openai_key}ApiKey", ""), - endpoint=self.appconfig.get(f"{azure_openai_key}Endpoint", ""), - deployment_name=self.appconfig.get(f"{azure_openai_key}DeploymentName", ""), - api_version=self.appconfig.get(f"{azure_openai_key}ApiVersion", "2023-05-15"), + api_key=self._appconfig.get(f"{AZURE_OPENAI_KEY}ApiKey", ""), + endpoint=self._appconfig.get(f"{AZURE_OPENAI_KEY}Endpoint", ""), + deployment_name=self._appconfig.get( + f"{AZURE_OPENAI_KEY}DeploymentName", "" + ), + api_version=self._appconfig.get( + f"{AZURE_OPENAI_KEY}ApiVersion", "2023-05-15" + ), ) def ask(self): + """ + Ask a question to the chat application. + """ + # Refresh the configuration from Azure App Configuration + self._appconfig.refresh() + # Get user input user_input = input("You: ") # Exit if user input is empty if not user_input.strip(): print("Exiting chat. Goodbye!") - exit() + return False # Stop the conversation # Add user message to chat conversation self._chat_conversation.append({"role": "user", "content": user_input}) @@ -124,6 +141,7 @@ def ask(self): ai_response = response.choices[0].message.content self._chat_conversation.append({"role": "assistant", "content": ai_response}) print(f"AI: {ai_response}") + return True def main(): @@ -131,11 +149,9 @@ def main(): print("Chat started! What's on your mind?") - while True: - # Refresh the configuration from Azure App Configuration - chat_app.appconfig.refresh() - - chat_app.ask() + continue_chat = True + while continue_chat: + continue_chat = chat_app.ask() if __name__ == "__main__": From e573db5d4c68b4faed0464a816c4b53e41654b26 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 7 Jul 2025 12:12:13 -0700 Subject: [PATCH 30/36] moved deployment type to AzureOpenAI --- examples/Python/ChatApp/app.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index a30efbf8..efa55274 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -51,7 +51,7 @@ def _get_app_config_endpoint(self): def _load_config(self): return load( endpoint=self._get_app_config_endpoint(), - selects=[SettingSelector(key_filter=f"{CHAT_APP_KEY}:*")], + selects=[SettingSelector(key_filter=f"{CHAT_APP_KEY}*")], credential=self._credential, keyvault_credential=self._credential, trim_prefixes=[CHAT_APP_KEY], @@ -79,6 +79,9 @@ def _create_ai_client(self) -> AzureOpenAI: azure_endpoint=self.azure_openai_config.endpoint, api_key=self.azure_openai_config.api_key, api_version=self.azure_openai_config.api_version, + azure_deployment=self._appconfig.get( + f"{AZURE_OPENAI_KEY}DeploymentName", "" + ), ) else: return AzureOpenAI( @@ -88,6 +91,9 @@ def _create_ai_client(self) -> AzureOpenAI: BEARER_SCOPE, ), api_version=self.azure_openai_config.api_version, + azure_deployment=self._appconfig.get( + f"{AZURE_OPENAI_KEY}DeploymentName", "" + ), ) def _extract_openai_config(self) -> AzureOpenAIConfiguration: @@ -100,9 +106,6 @@ def _extract_openai_config(self) -> AzureOpenAIConfiguration: return AzureOpenAIConfiguration( api_key=self._appconfig.get(f"{AZURE_OPENAI_KEY}ApiKey", ""), endpoint=self._appconfig.get(f"{AZURE_OPENAI_KEY}Endpoint", ""), - deployment_name=self._appconfig.get( - f"{AZURE_OPENAI_KEY}DeploymentName", "" - ), api_version=self._appconfig.get( f"{AZURE_OPENAI_KEY}ApiVersion", "2023-05-15" ), From 3ef214899be392a3410aca69d58eb8e4ef0a2554 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 7 Jul 2025 12:13:23 -0700 Subject: [PATCH 31/36] Update models.py --- examples/Python/ChatApp/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Python/ChatApp/models.py b/examples/Python/ChatApp/models.py index a92a3b86..9cdbca8b 100644 --- a/examples/Python/ChatApp/models.py +++ b/examples/Python/ChatApp/models.py @@ -21,7 +21,7 @@ class AzureOpenAIConfiguration: api_key: str endpoint: str - deployment_name: Optional[str] = None + deployment_name: str api_version: Optional[str] = None From 6cf8451c94d76063173eef542577ecdb9930d6d3 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 8 Jul 2025 12:59:26 -0700 Subject: [PATCH 32/36] formatting --- examples/Python/ChatApp/app.py | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index efa55274..a30f6ee8 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -43,9 +43,7 @@ def __init__(self): def _get_app_config_endpoint(self): app_config_endpoint = os.environ.get(APP_CONFIG_ENDPOINT_KEY) if not app_config_endpoint: - raise ValueError( - f"The environment variable '{APP_CONFIG_ENDPOINT_KEY}' is not set or is empty." - ) + raise ValueError(f"The environment variable '{APP_CONFIG_ENDPOINT_KEY}' is not set or is empty.") return app_config_endpoint def _load_config(self): @@ -67,9 +65,7 @@ def configure_app(self): self.azure_client = self._create_ai_client() # Configure chat completion with AI configuration - self.chat_completion_config = ChatCompletionConfiguration( - **self._appconfig[CHAT_COMPLETION_KEY] - ) + self.chat_completion_config = ChatCompletionConfiguration(**self._appconfig[CHAT_COMPLETION_KEY]) self._chat_messages = self.chat_completion_config.messages def _create_ai_client(self) -> AzureOpenAI: @@ -79,21 +75,14 @@ def _create_ai_client(self) -> AzureOpenAI: azure_endpoint=self.azure_openai_config.endpoint, api_key=self.azure_openai_config.api_key, api_version=self.azure_openai_config.api_version, - azure_deployment=self._appconfig.get( - f"{AZURE_OPENAI_KEY}DeploymentName", "" - ), + azure_deployment=self._appconfig.get(f"{AZURE_OPENAI_KEY}DeploymentName", ""), ) else: return AzureOpenAI( azure_endpoint=self.azure_openai_config.endpoint, - azure_ad_token_provider=get_bearer_token_provider( - self._credential or DefaultAzureCredential(), - BEARER_SCOPE, - ), + azure_ad_token_provider=get_bearer_token_provider(self._credential, BEARER_SCOPE), api_version=self.azure_openai_config.api_version, - azure_deployment=self._appconfig.get( - f"{AZURE_OPENAI_KEY}DeploymentName", "" - ), + azure_deployment=self._appconfig.get(f"{AZURE_OPENAI_KEY}DeploymentName", ""), ) def _extract_openai_config(self) -> AzureOpenAIConfiguration: @@ -106,9 +95,7 @@ def _extract_openai_config(self) -> AzureOpenAIConfiguration: return AzureOpenAIConfiguration( api_key=self._appconfig.get(f"{AZURE_OPENAI_KEY}ApiKey", ""), endpoint=self._appconfig.get(f"{AZURE_OPENAI_KEY}Endpoint", ""), - api_version=self._appconfig.get( - f"{AZURE_OPENAI_KEY}ApiVersion", "2023-05-15" - ), + api_version=self._appconfig.get(f"{AZURE_OPENAI_KEY}ApiVersion", "2023-05-15"), ) def ask(self): From 295c876a1ac32e830d77048b6fc4cdfcdbe15163 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 11 Jul 2025 11:07:42 -0700 Subject: [PATCH 33/36] review changes --- examples/Python/ChatApp/app.py | 172 +++++++++++-------------- examples/Python/ChatApp/models.py | 6 +- examples/Python/ChatApp/pyproject.toml | 8 ++ pyproject.toml | 4 + 4 files changed, 91 insertions(+), 99 deletions(-) create mode 100644 examples/Python/ChatApp/pyproject.toml create mode 100644 pyproject.toml diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index a30f6ee8..e8237bfd 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -15,7 +15,6 @@ from openai import AzureOpenAI from models import AzureOpenAIConfiguration, ChatCompletionConfiguration - APP_CONFIG_ENDPOINT_KEY = "AZURE_APPCONFIGURATION_ENDPOINT" CHAT_APP_KEY = "ChatApp:" AZURE_OPENAI_KEY = "AzureOpenAI:" @@ -23,87 +22,47 @@ BEARER_SCOPE = "https://cognitiveservices.azure.com/.default" -class ChatApp: - """ - Chat Application using Azure OpenAI and Azure App Configuration. - """ +# Initialize CREDENTIAL and config +CREDENTIAL = DefaultAzureCredential() - def __init__(self): - # Initialize credential and config - self._credential = DefaultAzureCredential() - - # Initialize chat state - self._chat_messages = [] - self._chat_conversation = [] - - # Load configuration - self._appconfig = self._load_config() - self.configure_app() - - def _get_app_config_endpoint(self): - app_config_endpoint = os.environ.get(APP_CONFIG_ENDPOINT_KEY) - if not app_config_endpoint: - raise ValueError(f"The environment variable '{APP_CONFIG_ENDPOINT_KEY}' is not set or is empty.") - return app_config_endpoint - - def _load_config(self): - return load( - endpoint=self._get_app_config_endpoint(), - selects=[SettingSelector(key_filter=f"{CHAT_APP_KEY}*")], - credential=self._credential, - keyvault_credential=self._credential, - trim_prefixes=[CHAT_APP_KEY], - refresh_on=[WatchKey(key=f"{CHAT_APP_KEY}{CHAT_COMPLETION_KEY}")], - on_refresh_success=self.configure_app, - ) +# Initialize chat state +APPCONFIG = None +CHAT_COMPLETION_CONFIG = None - def configure_app(self): - """ - Configure the chat application with settings from Azure App Configuration. - """ - self.azure_openai_config = self._extract_openai_config() - self.azure_client = self._create_ai_client() - - # Configure chat completion with AI configuration - self.chat_completion_config = ChatCompletionConfiguration(**self._appconfig[CHAT_COMPLETION_KEY]) - self._chat_messages = self.chat_completion_config.messages - - def _create_ai_client(self) -> AzureOpenAI: - # Create an Azure OpenAI client - if self.azure_openai_config.api_key: - return AzureOpenAI( - azure_endpoint=self.azure_openai_config.endpoint, - api_key=self.azure_openai_config.api_key, - api_version=self.azure_openai_config.api_version, - azure_deployment=self._appconfig.get(f"{AZURE_OPENAI_KEY}DeploymentName", ""), - ) - else: - return AzureOpenAI( - azure_endpoint=self.azure_openai_config.endpoint, - azure_ad_token_provider=get_bearer_token_provider(self._credential, BEARER_SCOPE), - api_version=self.azure_openai_config.api_version, - azure_deployment=self._appconfig.get(f"{AZURE_OPENAI_KEY}DeploymentName", ""), - ) - - def _extract_openai_config(self) -> AzureOpenAIConfiguration: - """ - Extract Azure OpenAI configuration from the configuration data. - - :param config_data: The configuration data from Azure App Configuration - :return: An AzureOpenAIConfiguration object - """ - return AzureOpenAIConfiguration( - api_key=self._appconfig.get(f"{AZURE_OPENAI_KEY}ApiKey", ""), - endpoint=self._appconfig.get(f"{AZURE_OPENAI_KEY}Endpoint", ""), - api_version=self._appconfig.get(f"{AZURE_OPENAI_KEY}ApiVersion", "2023-05-15"), - ) - def ask(self): - """ - Ask a question to the chat application. - """ +def main(): + global APPCONFIG + app_config_endpoint = os.environ.get(APP_CONFIG_ENDPOINT_KEY) + if not app_config_endpoint: + raise ValueError(f"The environment variable '{APP_CONFIG_ENDPOINT_KEY}' is not set or is empty.") + + # Load configuration + APPCONFIG = load( + endpoint=app_config_endpoint, + selects=[SettingSelector(key_filter=f"{CHAT_APP_KEY}*")], + credential=CREDENTIAL, + keyvault_credential=CREDENTIAL, + trim_prefixes=[CHAT_APP_KEY], + refresh_on=[WatchKey(key=f"{CHAT_APP_KEY}{CHAT_COMPLETION_KEY}")], + on_refresh_success=configure_app, + ) + configure_app() + + print("Chat started! What's on your mind?") + + azure_openai_config = AzureOpenAIConfiguration( + api_key=APPCONFIG.get(f"{AZURE_OPENAI_KEY}ApiKey", ""), + endpoint=APPCONFIG.get(f"{AZURE_OPENAI_KEY}Endpoint", ""), + deployment_name=APPCONFIG.get(f"{AZURE_OPENAI_KEY}DeploymentName", ""), + api_version=APPCONFIG.get(f"{AZURE_OPENAI_KEY}ApiVersion", ""), + ) + azure_client = create_ai_client(azure_openai_config) + + chat_messages = [] + + while True: # Refresh the configuration from Azure App Configuration - self._appconfig.refresh() + APPCONFIG.refresh() # Get user input user_input = input("You: ") @@ -111,37 +70,58 @@ def ask(self): # Exit if user input is empty if not user_input.strip(): print("Exiting chat. Goodbye!") - return False # Stop the conversation + break # Add user message to chat conversation - self._chat_conversation.append({"role": "user", "content": user_input}) + chat_messages.append({"role": "user", "content": user_input}) - # Get latest system message from AI configuration - self._chat_messages.extend(self._chat_conversation) + chat_conversation = CHAT_COMPLETION_CONFIG.messages + chat_conversation.extend(chat_messages) # Get AI response and add it to chat conversation - response = self.azure_client.chat.completions.create( - model=self.azure_openai_config.deployment_name, - messages=self._chat_messages, - max_tokens=self.chat_completion_config.max_tokens, - temperature=self.chat_completion_config.temperature, - top_p=self.chat_completion_config.top_p, + response = azure_client.chat.completions.create( + model=azure_openai_config.deployment_name, + messages=chat_conversation, + max_tokens=CHAT_COMPLETION_CONFIG.max_tokens, + temperature=CHAT_COMPLETION_CONFIG.temperature, + top_p=CHAT_COMPLETION_CONFIG.top_p, ) ai_response = response.choices[0].message.content - self._chat_conversation.append({"role": "assistant", "content": ai_response}) + chat_messages.append({"role": "assistant", "content": ai_response}) print(f"AI: {ai_response}") - return True -def main(): - chat_app = ChatApp() +def configure_app(): + """ + Configure the chat application with settings from Azure App Configuration. + """ + global CHAT_COMPLETION_CONFIG + # Configure chat completion with AI configuration + CHAT_COMPLETION_CONFIG = ChatCompletionConfiguration(**APPCONFIG[CHAT_COMPLETION_KEY]) - print("Chat started! What's on your mind?") - continue_chat = True - while continue_chat: - continue_chat = chat_app.ask() +def create_ai_client(azure_open_ai_config: AzureOpenAIConfiguration) -> AzureOpenAI: + """ + Create an Azure OpenAI client using the configuration from Azure App Configuration. + """ + if azure_open_ai_config.api_key: + return AzureOpenAI( + azure_endpoint=azure_open_ai_config.endpoint, + api_key=azure_open_ai_config.api_key, + api_version=azure_open_ai_config.api_version, + azure_deployment=azure_open_ai_config.deployment_name, + ) + else: + return AzureOpenAI( + azure_endpoint=azure_open_ai_config.endpoint, + azure_ad_token_provider=get_bearer_token_provider( + CREDENTIAL, + BEARER_SCOPE, + ), + api_version=azure_open_ai_config.api_version, + azure_deployment=APPCONFIG.get(f"{AZURE_OPENAI_KEY}DeploymentName", ""), + ) if __name__ == "__main__": diff --git a/examples/Python/ChatApp/models.py b/examples/Python/ChatApp/models.py index 9cdbca8b..13392c42 100644 --- a/examples/Python/ChatApp/models.py +++ b/examples/Python/ChatApp/models.py @@ -32,8 +32,8 @@ class ChatCompletionConfiguration: Maps to configuration values with keys 'model', 'messages', 'max_tokens', 'temperature', and 'top_p'. """ + max_tokens: int + temperature: float + top_p: float model: Optional[str] = None messages: Optional[List[Dict[str, str]]] = None - max_tokens: int = 1024 - temperature: float = 0.7 - top_p: float = 0.95 diff --git a/examples/Python/ChatApp/pyproject.toml b/examples/Python/ChatApp/pyproject.toml new file mode 100644 index 00000000..afe76d22 --- /dev/null +++ b/examples/Python/ChatApp/pyproject.toml @@ -0,0 +1,8 @@ +[tool.black] +line-length = 120 +target-version = ['py38'] +include = '\.pyi?$' + +[tool.pylint.format] +max-line-length = 120 + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..3d74b6cd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[tool.black] +line-length = 120 +target-version = ['py38'] +include = '\.pyi?$' From 24aecfb53ac9d2d17abfa0918b5b1f6c66fa3848 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 11 Jul 2025 11:13:35 -0700 Subject: [PATCH 34/36] refresh update --- examples/Python/ChatApp/README.md | 2 ++ examples/Python/ChatApp/app.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/Python/ChatApp/README.md b/examples/Python/ChatApp/README.md index 74b0fe36..91b72d3b 100644 --- a/examples/Python/ChatApp/README.md +++ b/examples/Python/ChatApp/README.md @@ -36,6 +36,8 @@ This sample demonstrates using Azure App Configuration to configure Azure OpenAI - temperature - Temperature parameter (e.g., 0.7) - top_p - Top p parameter (e.g., 0.95) - messages - An array of messages with role and content for each message + ChatApp:Sentinel - A sentinel key to trigger configuration refreshes + ``` Example JSON object for ChatApp:Model: ```json diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index e8237bfd..890d24e7 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -43,7 +43,7 @@ def main(): credential=CREDENTIAL, keyvault_credential=CREDENTIAL, trim_prefixes=[CHAT_APP_KEY], - refresh_on=[WatchKey(key=f"{CHAT_APP_KEY}{CHAT_COMPLETION_KEY}")], + refresh_on=[WatchKey(key=f"{CHAT_APP_KEY}Sentinel")], on_refresh_success=configure_app, ) configure_app() From d3cf9c54dacdd5a15fa689b5e14af45616ec96c6 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 15 Aug 2025 09:24:53 -0700 Subject: [PATCH 35/36] review comments --- examples/Python/ChatApp/app.py | 35 +++++++++++++++---------------- examples/Python/ChatApp/models.py | 4 ---- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index 890d24e7..2b42a674 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -25,7 +25,6 @@ # Initialize CREDENTIAL and config CREDENTIAL = DefaultAzureCredential() -# Initialize chat state APPCONFIG = None CHAT_COMPLETION_CONFIG = None @@ -48,17 +47,17 @@ def main(): ) configure_app() - print("Chat started! What's on your mind?") - azure_openai_config = AzureOpenAIConfiguration( api_key=APPCONFIG.get(f"{AZURE_OPENAI_KEY}ApiKey", ""), endpoint=APPCONFIG.get(f"{AZURE_OPENAI_KEY}Endpoint", ""), deployment_name=APPCONFIG.get(f"{AZURE_OPENAI_KEY}DeploymentName", ""), api_version=APPCONFIG.get(f"{AZURE_OPENAI_KEY}ApiVersion", ""), ) - azure_client = create_ai_client(azure_openai_config) + azure_client = create_azure_openai_client(azure_openai_config) - chat_messages = [] + chat_conversation = [] + + print("Chat started! What's on your mind?") while True: # Refresh the configuration from Azure App Configuration @@ -73,22 +72,22 @@ def main(): break # Add user message to chat conversation - chat_messages.append({"role": "user", "content": user_input}) + chat_conversation.append({"role": "user", "content": user_input}) - chat_conversation = CHAT_COMPLETION_CONFIG.messages - chat_conversation.extend(chat_messages) + chat_messages = list(CHAT_COMPLETION_CONFIG.messages) + chat_messages.extend(chat_conversation) # Get AI response and add it to chat conversation response = azure_client.chat.completions.create( model=azure_openai_config.deployment_name, - messages=chat_conversation, + messages=chat_messages, max_tokens=CHAT_COMPLETION_CONFIG.max_tokens, temperature=CHAT_COMPLETION_CONFIG.temperature, top_p=CHAT_COMPLETION_CONFIG.top_p, ) ai_response = response.choices[0].message.content - chat_messages.append({"role": "assistant", "content": ai_response}) + chat_conversation .append({"role": "assistant", "content": ai_response}) print(f"AI: {ai_response}") @@ -101,25 +100,25 @@ def configure_app(): CHAT_COMPLETION_CONFIG = ChatCompletionConfiguration(**APPCONFIG[CHAT_COMPLETION_KEY]) -def create_ai_client(azure_open_ai_config: AzureOpenAIConfiguration) -> AzureOpenAI: +def create_azure_openai_client(azure_openai_config: AzureOpenAIConfiguration) -> AzureOpenAI: """ Create an Azure OpenAI client using the configuration from Azure App Configuration. """ - if azure_open_ai_config.api_key: + if azure_openai_config.api_key: return AzureOpenAI( - azure_endpoint=azure_open_ai_config.endpoint, - api_key=azure_open_ai_config.api_key, - api_version=azure_open_ai_config.api_version, - azure_deployment=azure_open_ai_config.deployment_name, + azure_endpoint=azure_openai_config.endpoint, + api_key=azure_openai_config.api_key, + api_version=azure_openai_config.api_version, + azure_deployment=azure_openai_config.deployment_name, ) else: return AzureOpenAI( - azure_endpoint=azure_open_ai_config.endpoint, + azure_endpoint=azure_openai_config.endpoint, azure_ad_token_provider=get_bearer_token_provider( CREDENTIAL, BEARER_SCOPE, ), - api_version=azure_open_ai_config.api_version, + api_version=azure_openai_config.api_version, azure_deployment=APPCONFIG.get(f"{AZURE_OPENAI_KEY}DeploymentName", ""), ) diff --git a/examples/Python/ChatApp/models.py b/examples/Python/ChatApp/models.py index 13392c42..df34175d 100644 --- a/examples/Python/ChatApp/models.py +++ b/examples/Python/ChatApp/models.py @@ -5,8 +5,6 @@ # -------------------------------------------------------------------------- """ Model classes for Azure OpenAI Chat Application. -This module provides data classes for representing chat messages and model configurations -used in the Azure OpenAI-powered chat application. """ from dataclasses import dataclass from typing import List, Optional, Dict @@ -16,7 +14,6 @@ class AzureOpenAIConfiguration: """ Represents the configuration for Azure OpenAI service. - Maps to configuration values with keys 'api_key', 'endpoint', and 'deployment_name'. """ api_key: str @@ -29,7 +26,6 @@ class AzureOpenAIConfiguration: class ChatCompletionConfiguration: """ Represents the configuration for an AI model including messages and parameters. - Maps to configuration values with keys 'model', 'messages', 'max_tokens', 'temperature', and 'top_p'. """ max_tokens: int From c21857c0347e72ae0ebbaedf567641a4c2a23b23 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 15 Aug 2025 09:46:24 -0700 Subject: [PATCH 36/36] review comments --- examples/Python/ChatApp/README.md | 43 ++++++++++--------------------- examples/Python/ChatApp/app.py | 26 ++++++++----------- 2 files changed, 25 insertions(+), 44 deletions(-) diff --git a/examples/Python/ChatApp/README.md b/examples/Python/ChatApp/README.md index 91b72d3b..197d0192 100644 --- a/examples/Python/ChatApp/README.md +++ b/examples/Python/ChatApp/README.md @@ -14,22 +14,25 @@ This sample demonstrates using Azure App Configuration to configure Azure OpenAI - Azure App Configuration service - Azure OpenAI service - Required environment variables: - - `AZURE_APPCONFIG_ENDPOINT`: URL of your Azure App Configuration instance + - `AZURE_APPCONFIGURATION_ENDPOINT`: Endpoint URL of your Azure App Configuration instance - `AZURE_OPENAI_API_KEY`: API key for Azure OpenAI (optional if stored in Azure App Configuration) ## Setup 1. Clone the repository -2. Install the required packages: +1. Install the required packages: + ```bash pip install -r requirements.txt ``` -3. Configure your Azure App Configuration store with these settings: - ``` + +1. Configure your Azure App Configuration store with these settings: + + ```console ChatApp:AzureOpenAI:Endpoint - Your Azure OpenAI endpoint URL ChatApp:AzureOpenAI:DeploymentName - Your Azure OpenAI deployment name ChatApp:AzureOpenAI:ApiVersion - API version for Azure OpenAI (e.g., "2023-05-15") - ChatApp:AzureOpenAI:ApiKey - Your Azure OpenAI API key (preferably as a Key Vault reference) + ChatApp:AzureOpenAI:ApiKey - Your Azure OpenAI API key (Optional only required when not using AAD, preferably as a Key Vault reference) ChatApp:Model - An AI configuration entry containing the following settings: - model - Model name (e.g., "gpt-35-turbo") - max_tokens - Maximum tokens for completion (e.g., 1000) @@ -39,46 +42,28 @@ This sample demonstrates using Azure App Configuration to configure Azure OpenAI ChatApp:Sentinel - A sentinel key to trigger configuration refreshes ``` - Example JSON object for ChatApp:Model: - ```json - { - "model": "gpt-35-turbo", - "max_tokens": 1000, - "temperature": 0.7, - "top_p": 0.95, - "messages": [ - { - "role": "system", - "content": "You are a helpful assistant." - }, - { - "role": "user", - "content": "Tell me about Azure App Configuration." - } - ] - } - ``` - ``` +1. Set the required environment variables: -4. Set the required environment variables: ```bash - export AZURE_APPCONFIG_ENDPOINT="https://your-appconfig.azconfig.io" + export AZURE_APPCONFIGURATION_ENDPOINT="https://your-appconfig.azconfig.io" export AZURE_OPENAI_API_KEY="your-openai-api-key" # Optional if stored in Azure App Configuration ``` ## Running the Application Start the console application: + ```bash python app.py ``` The application will: + 1. Display the initial configured messages from Azure App Configuration 2. Generate a response from the AI -3. Prompt you to enter your message (type 'exit' to quit) +3. Prompt you to enter your message (Just select enter to quit) 4. Maintain conversation history during the session ## Configuration Refresh -The application refreshes the configuration at the beginning of each conversation cycle, so any changes made to the base configuration in Azure App Configuration will be incorporated into the model parameters (temperature, max_tokens, etc.) while maintaining your ongoing conversation history. \ No newline at end of file +The application refreshes the configuration at the beginning of each conversation cycle, so any changes made to the base configuration in Azure App Configuration will be incorporated into the model parameters (temperature, max_tokens, etc.) while maintaining your ongoing conversation history. Updating the configuration in Azure App Configuration will automatically reflect in the application without requiring a restart, once the `ChatApp:Sentinel` key is updated. diff --git a/examples/Python/ChatApp/app.py b/examples/Python/ChatApp/app.py index 2b42a674..d6285698 100644 --- a/examples/Python/ChatApp/app.py +++ b/examples/Python/ChatApp/app.py @@ -16,13 +16,9 @@ from models import AzureOpenAIConfiguration, ChatCompletionConfiguration APP_CONFIG_ENDPOINT_KEY = "AZURE_APPCONFIGURATION_ENDPOINT" -CHAT_APP_KEY = "ChatApp:" -AZURE_OPENAI_KEY = "AzureOpenAI:" -CHAT_COMPLETION_KEY = "ChatCompletion" -BEARER_SCOPE = "https://cognitiveservices.azure.com/.default" -# Initialize CREDENTIAL and config +# Initialize CREDENTIAL CREDENTIAL = DefaultAzureCredential() APPCONFIG = None @@ -38,20 +34,20 @@ def main(): # Load configuration APPCONFIG = load( endpoint=app_config_endpoint, - selects=[SettingSelector(key_filter=f"{CHAT_APP_KEY}*")], + selects=[SettingSelector(key_filter="ChatApp:*")], credential=CREDENTIAL, keyvault_credential=CREDENTIAL, - trim_prefixes=[CHAT_APP_KEY], - refresh_on=[WatchKey(key=f"{CHAT_APP_KEY}Sentinel")], + trim_prefixes=["ChatApp:"], + refresh_on=[WatchKey(key="ChatApp:Sentinel")], on_refresh_success=configure_app, ) configure_app() azure_openai_config = AzureOpenAIConfiguration( - api_key=APPCONFIG.get(f"{AZURE_OPENAI_KEY}ApiKey", ""), - endpoint=APPCONFIG.get(f"{AZURE_OPENAI_KEY}Endpoint", ""), - deployment_name=APPCONFIG.get(f"{AZURE_OPENAI_KEY}DeploymentName", ""), - api_version=APPCONFIG.get(f"{AZURE_OPENAI_KEY}ApiVersion", ""), + api_key=APPCONFIG.get("AzureOpenAI:ApiKey", ""), + endpoint=APPCONFIG.get("AzureOpenAI:Endpoint", ""), + deployment_name=APPCONFIG.get("AzureOpenAI:DeploymentName", ""), + api_version=APPCONFIG.get("AzureOpenAI:ApiVersion", ""), ) azure_client = create_azure_openai_client(azure_openai_config) @@ -97,7 +93,7 @@ def configure_app(): """ global CHAT_COMPLETION_CONFIG # Configure chat completion with AI configuration - CHAT_COMPLETION_CONFIG = ChatCompletionConfiguration(**APPCONFIG[CHAT_COMPLETION_KEY]) + CHAT_COMPLETION_CONFIG = ChatCompletionConfiguration(**APPCONFIG["ChatCompletion"]) def create_azure_openai_client(azure_openai_config: AzureOpenAIConfiguration) -> AzureOpenAI: @@ -116,10 +112,10 @@ def create_azure_openai_client(azure_openai_config: AzureOpenAIConfiguration) -> azure_endpoint=azure_openai_config.endpoint, azure_ad_token_provider=get_bearer_token_provider( CREDENTIAL, - BEARER_SCOPE, + "https://cognitiveservices.azure.com/.default", ), api_version=azure_openai_config.api_version, - azure_deployment=APPCONFIG.get(f"{AZURE_OPENAI_KEY}DeploymentName", ""), + azure_deployment=azure_openai_config.deployment_name, )