From 71bec9e101bce59880ca0295072105b35869ff4c Mon Sep 17 00:00:00 2001 From: Zhenlan Wang Date: Wed, 25 Jun 2025 21:25:59 -0700 Subject: [PATCH 1/4] Update the .NET ChatApp sample --- .../ChatApp/ChatApp/ModelConfiguration.cs | 25 ------- .../DotNetCore/ChatApp/ChatApp/Program.cs | 71 +++++++++++++------ 2 files changed, 51 insertions(+), 45 deletions(-) delete mode 100644 examples/DotNetCore/ChatApp/ChatApp/ModelConfiguration.cs diff --git a/examples/DotNetCore/ChatApp/ChatApp/ModelConfiguration.cs b/examples/DotNetCore/ChatApp/ChatApp/ModelConfiguration.cs deleted file mode 100644 index 75d37255..00000000 --- a/examples/DotNetCore/ChatApp/ChatApp/ModelConfiguration.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Microsoft.Extensions.Configuration; - -namespace ChatApp -{ - internal class ModelConfiguration - { - [ConfigurationKeyName("model")] - public string? Model { get; set; } - - [ConfigurationKeyName("messages")] - public List? Messages { get; set; } - - [ConfigurationKeyName("max_tokens")] - public int MaxTokens { get; set; } - - [ConfigurationKeyName("temperature")] - public float Temperature { get; set; } - - [ConfigurationKeyName("top_p")] - public float TopP { get; set; } - } -} diff --git a/examples/DotNetCore/ChatApp/ChatApp/Program.cs b/examples/DotNetCore/ChatApp/ChatApp/Program.cs index 612b8214..8e7353d4 100644 --- a/examples/DotNetCore/ChatApp/ChatApp/Program.cs +++ b/examples/DotNetCore/ChatApp/ChatApp/Program.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using Azure; using Azure.AI.OpenAI; using Azure.Core; using Azure.Identity; @@ -16,8 +17,8 @@ IConfiguration configuration = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - Uri endpoint = new(Environment.GetEnvironmentVariable("AZURE_APPCONFIG_ENDPOINT") ?? - throw new InvalidOperationException("The environment variable 'AZURE_APPCONFIG_ENDPOINT' is not set or is empty.")); + Uri endpoint = new(Environment.GetEnvironmentVariable("AZURE_APPCONFIGURATION_ENDPOINT") ?? + throw new InvalidOperationException("The environment variable 'AZURE_APPCONFIGURATION_ENDPOINT' is not set or is empty.")); options.Connect(endpoint, credential) // Load all keys that start with "ChatApp:" and have no label. .Select("ChatApp:*") @@ -26,6 +27,11 @@ .ConfigureRefresh(refreshOptions => { refreshOptions.RegisterAll(); + }) + .ConfigureKeyVault(keyVaultOptions => + { + // Use the DefaultAzureCredential to access Key Vault secrets. + keyVaultOptions.SetCredential(credential); }); refresher = options.GetRefresher(); @@ -33,43 +39,68 @@ .Build(); // Retrieve the OpenAI connection information from the configuration -Uri openaiEndpoint = new (configuration["ChatApp:AzureOpenAI:Endpoint"]); -string deploymentName = configuration["ChatApp:AzureOpenAI:DeploymentName"]; +var openAIConfiguration = configuration.GetSection("ChatApp:AzureOpenAI").Get(); -// Create a chat client -AzureOpenAIClient azureClient = new(openaiEndpoint, credential); -ChatClient chatClient = azureClient.GetChatClient(deploymentName); +// Create a chat client using API key if available, otherwise use the DefaultAzureCredential +AzureOpenAIClient azureClient; +if (!string.IsNullOrEmpty(openAIConfiguration.ApiKey)) +{ + azureClient = new AzureOpenAIClient(new Uri(openAIConfiguration.Endpoint), new Azure.AzureKeyCredential(openAIConfiguration.ApiKey)); +} +else +{ + azureClient = new AzureOpenAIClient(new Uri(openAIConfiguration.Endpoint), credential); +} +ChatClient chatClient = azureClient.GetChatClient(openAIConfiguration.DeploymentName); +// Initialize chat conversation +var chatConversation = new List(); +Console.WriteLine("Chat started! What's on your mind?"); while (true) { // Refresh the configuration from Azure App Configuration await refresher.TryRefreshAsync(); // Configure chat completion with AI configuration - var modelConfiguration = configuration.GetSection("ChatApp:Model").Get(); + var completionConfiguration = configuration.GetSection("ChatApp:Completion").Get(); var requestOptions = new ChatCompletionOptions() { - MaxOutputTokenCount = modelConfiguration.MaxTokens, - Temperature = modelConfiguration.Temperature, - TopP = modelConfiguration.TopP + MaxOutputTokenCount = completionConfiguration.MaxTokens, + Temperature = completionConfiguration.Temperature, + TopP = completionConfiguration.TopP }; - foreach (var message in modelConfiguration.Messages) + // Get user input + Console.Write("You: "); + string? userInput = Console.ReadLine(); + + // Exit if user input is empty + if (string.IsNullOrEmpty(userInput)) { - Console.WriteLine($"{message.Role}: {message.Content}"); + Console.WriteLine("Exiting chat. Goodbye!"); + break; } - // Get chat response from AI - var response = await chatClient.CompleteChatAsync(GetChatMessages(modelConfiguration), requestOptions); - System.Console.WriteLine($"AI response: {response.Value.Content[0].Text}"); + // Add user message to chat conversation + chatConversation.Add(ChatMessage.CreateUserMessage(userInput)); + + // Get latest system message from AI configuration + var chatMessages = new List(GetChatMessages(completionConfiguration)); + chatMessages.AddRange(chatConversation); + + // Get AI response and update chat conversation + var response = await chatClient.CompleteChatAsync(chatMessages, requestOptions); + var aiResponse = response.Value.Content[0].Text; + System.Console.WriteLine($"AI: {aiResponse}"); + chatConversation.Add(ChatMessage.CreateAssistantMessage(aiResponse)); - Console.WriteLine("Press Enter to continue..."); - Console.ReadLine(); + Console.WriteLine(); } -static IEnumerable GetChatMessages(ModelConfiguration modelConfiguration) +// Helper method to convert configuration messages to ChatMessage objects +static IEnumerable GetChatMessages(CompletionConfiguration completionConfiguration) { - return modelConfiguration.Messages.Select(message => message.Role switch + return completionConfiguration.Messages.Select(message => message.Role switch { "system" => ChatMessage.CreateSystemMessage(message.Content), "user" => ChatMessage.CreateUserMessage(message.Content), From 10a38d9095404a5ec56e9aac6a7fd5961e57e013 Mon Sep 17 00:00:00 2001 From: Zhenlan Wang Date: Wed, 25 Jun 2025 21:29:52 -0700 Subject: [PATCH 2/4] Add readme --- .../ChatApp/CompletionConfiguration.cs | 25 ++++++ .../ChatApp/ChatApp/OpenAIConfiguration.cs | 15 ++++ examples/DotNetCore/ChatApp/README.md | 90 +++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 examples/DotNetCore/ChatApp/ChatApp/CompletionConfiguration.cs create mode 100644 examples/DotNetCore/ChatApp/ChatApp/OpenAIConfiguration.cs create mode 100644 examples/DotNetCore/ChatApp/README.md diff --git a/examples/DotNetCore/ChatApp/ChatApp/CompletionConfiguration.cs b/examples/DotNetCore/ChatApp/ChatApp/CompletionConfiguration.cs new file mode 100644 index 00000000..bdce065a --- /dev/null +++ b/examples/DotNetCore/ChatApp/ChatApp/CompletionConfiguration.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.Extensions.Configuration; + +namespace ChatApp +{ + internal class CompletionConfiguration + { + [ConfigurationKeyName("model")] + public string? Model { get; set; } + + [ConfigurationKeyName("messages")] + public List? Messages { get; set; } + + [ConfigurationKeyName("max_tokens")] + public int MaxTokens { get; set; } + + [ConfigurationKeyName("temperature")] + public float Temperature { get; set; } + + [ConfigurationKeyName("top_p")] + public float TopP { get; set; } + } +} diff --git a/examples/DotNetCore/ChatApp/ChatApp/OpenAIConfiguration.cs b/examples/DotNetCore/ChatApp/ChatApp/OpenAIConfiguration.cs new file mode 100644 index 00000000..1b70605c --- /dev/null +++ b/examples/DotNetCore/ChatApp/ChatApp/OpenAIConfiguration.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// + +namespace ChatApp +{ + internal class OpenAIConfiguration + { + public required string Endpoint { get; set; } + + public required string DeploymentName { get; set; } + + public string? ApiKey { get; set; } + } +} diff --git a/examples/DotNetCore/ChatApp/README.md b/examples/DotNetCore/ChatApp/README.md new file mode 100644 index 00000000..43d3d48d --- /dev/null +++ b/examples/DotNetCore/ChatApp/README.md @@ -0,0 +1,90 @@ +# Azure App Configuration - .NET ChatApp Sample + +An interactive console chat application that integrates with Azure OpenAI services using Azure App Configuration for dynamic AI Configuration management. + +## Overview + +This .NET console application provides a seamless chat experience with Azure OpenAI, featuring: + +- Integration with Azure OpenAI for chat completions +- Dynamic AI configuration refresh from Azure App Configuration +- Secure authentication options using API key or Microsoft Entra ID + +## Prerequisites + +- .NET 8.0 SDK +- Azure subscription +- Azure OpenAI service instance +- Azure App Configuration service instance + +## Setup + +### Environment Variables + +Set the following environment variable: + +- `AZURE_APPCONFIGURATION_ENDPOINT`: Endpoint URL of your Azure App Configuration instance + +### Azure App Configuration Keys + +Configure the following keys in your Azure App Configuration: + +#### OpenAI Connection Settings + +- `ChatApp:AzureOpenAI:Endpoint` - Your Azure OpenAI endpoint URL +- `ChatApp:AzureOpenAI:DeploymentName` - Your Azure OpenAI deployment name +- `ChatApp:AzureOpenAI:ApiKey` - Key Vault reference to the API key for Azure OpenAI (optional) + +#### Chat Completion Configuration + +- `ChatApp:Completion` - An AI Configuration entry containing the following settings: + - `model` - Model name (e.g., "gpt-4o") + - `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 + +## Authentication + +The application supports the following authentication methods: + +- **Azure App Configuration**: Uses `DefaultAzureCredential` for authentication via Microsoft Entra ID. +- **Azure OpenAI**: Supports authentication using either an API key or `DefaultAzureCredential` via Microsoft Entra ID. +- **Azure Key Vault** *(optional, if using Key Vault references for API keys)*: Authenticates using `DefaultAzureCredential` via Microsoft Entra ID. + +## Usage + +1. **Start the Application**: Run the application using `dotnet run` +2. **Begin Chatting**: Type your messages when prompted with "You: " +3. **Continue Conversation**: The AI will respond and maintain conversation context +4. **Exit**: Press Enter without typing a message to exit gracefully + +### Example Session +``` +Chat started! What's on your mind? +You: Hello, how are you? +AI: Hello! I'm doing well, thank you for asking. How can I help you today? + +You: What can you tell me about machine learning? +AI: Machine learning is a subset of artificial intelligence that focuses on... + +You: [Press Enter to exit] +Exiting chat. Goodbye! +``` + +## Troubleshooting + +**"AZURE_APPCONFIG_ENDPOINT environment variable not set"** +- Ensure the environment variable is properly set +- Verify the endpoint URL is correct + +**Authentication Failures** +- Ensure you have the `App Configuration Data Reader` role on the Azure App Configuration instance +- For Microsoft Entra ID authentication: Verify you have the `Cognitive Services OpenAI User` role on the Azure OpenAI instance +- For API key authentication: + - Confirm you have secret read access to the Key Vault storing the API key + - Verify that a Key Vault reference for the API key is properly configured in Azure App Configuration + +**No AI Response** +- Verify deployment name matches your Azure OpenAI deployment +- Check token limits and quotas From 0b51f971bc2b410da8f43872dec987cd7942d799 Mon Sep 17 00:00:00 2001 From: Zhenlan Wang Date: Thu, 26 Jun 2025 13:09:52 -0700 Subject: [PATCH 3/4] Address feedback --- ...uration.cs => AzureOpenAIConfiguration.cs} | 2 +- ...tion.cs => ChatCompletionConfiguration.cs} | 2 +- .../DotNetCore/ChatApp/ChatApp/Program.cs | 32 +++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) rename examples/DotNetCore/ChatApp/ChatApp/{OpenAIConfiguration.cs => AzureOpenAIConfiguration.cs} (82%) rename examples/DotNetCore/ChatApp/ChatApp/{CompletionConfiguration.cs => ChatCompletionConfiguration.cs} (89%) diff --git a/examples/DotNetCore/ChatApp/ChatApp/OpenAIConfiguration.cs b/examples/DotNetCore/ChatApp/ChatApp/AzureOpenAIConfiguration.cs similarity index 82% rename from examples/DotNetCore/ChatApp/ChatApp/OpenAIConfiguration.cs rename to examples/DotNetCore/ChatApp/ChatApp/AzureOpenAIConfiguration.cs index 1b70605c..39e4a139 100644 --- a/examples/DotNetCore/ChatApp/ChatApp/OpenAIConfiguration.cs +++ b/examples/DotNetCore/ChatApp/ChatApp/AzureOpenAIConfiguration.cs @@ -4,7 +4,7 @@ namespace ChatApp { - internal class OpenAIConfiguration + internal class AzureOpenAIConfiguration { public required string Endpoint { get; set; } diff --git a/examples/DotNetCore/ChatApp/ChatApp/CompletionConfiguration.cs b/examples/DotNetCore/ChatApp/ChatApp/ChatCompletionConfiguration.cs similarity index 89% rename from examples/DotNetCore/ChatApp/ChatApp/CompletionConfiguration.cs rename to examples/DotNetCore/ChatApp/ChatApp/ChatCompletionConfiguration.cs index bdce065a..b94f8626 100644 --- a/examples/DotNetCore/ChatApp/ChatApp/CompletionConfiguration.cs +++ b/examples/DotNetCore/ChatApp/ChatApp/ChatCompletionConfiguration.cs @@ -5,7 +5,7 @@ namespace ChatApp { - internal class CompletionConfiguration + internal class ChatCompletionConfiguration { [ConfigurationKeyName("model")] public string? Model { get; set; } diff --git a/examples/DotNetCore/ChatApp/ChatApp/Program.cs b/examples/DotNetCore/ChatApp/ChatApp/Program.cs index 8e7353d4..7379c83f 100644 --- a/examples/DotNetCore/ChatApp/ChatApp/Program.cs +++ b/examples/DotNetCore/ChatApp/ChatApp/Program.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using Azure; + using Azure.AI.OpenAI; using Azure.Core; using Azure.Identity; @@ -39,19 +39,19 @@ .Build(); // Retrieve the OpenAI connection information from the configuration -var openAIConfiguration = configuration.GetSection("ChatApp:AzureOpenAI").Get(); +var azureOpenAIConfiguration = configuration.GetSection("ChatApp:AzureOpenAI").Get(); // Create a chat client using API key if available, otherwise use the DefaultAzureCredential AzureOpenAIClient azureClient; -if (!string.IsNullOrEmpty(openAIConfiguration.ApiKey)) +if (!string.IsNullOrEmpty(azureOpenAIConfiguration.ApiKey)) { - azureClient = new AzureOpenAIClient(new Uri(openAIConfiguration.Endpoint), new Azure.AzureKeyCredential(openAIConfiguration.ApiKey)); + azureClient = new AzureOpenAIClient(new Uri(azureOpenAIConfiguration.Endpoint), new Azure.AzureKeyCredential(azureOpenAIConfiguration.ApiKey)); } else { - azureClient = new AzureOpenAIClient(new Uri(openAIConfiguration.Endpoint), credential); + azureClient = new AzureOpenAIClient(new Uri(azureOpenAIConfiguration.Endpoint), credential); } -ChatClient chatClient = azureClient.GetChatClient(openAIConfiguration.DeploymentName); +ChatClient chatClient = azureClient.GetChatClient(azureOpenAIConfiguration.DeploymentName); // Initialize chat conversation var chatConversation = new List(); @@ -62,12 +62,12 @@ await refresher.TryRefreshAsync(); // Configure chat completion with AI configuration - var completionConfiguration = configuration.GetSection("ChatApp:Completion").Get(); + var chatCompletionConfiguration = configuration.GetSection("ChatApp:ChatCompletion").Get(); var requestOptions = new ChatCompletionOptions() { - MaxOutputTokenCount = completionConfiguration.MaxTokens, - Temperature = completionConfiguration.Temperature, - TopP = completionConfiguration.TopP + MaxOutputTokenCount = chatCompletionConfiguration.MaxTokens, + Temperature = chatCompletionConfiguration.Temperature, + TopP = chatCompletionConfiguration.TopP }; // Get user input @@ -85,22 +85,22 @@ chatConversation.Add(ChatMessage.CreateUserMessage(userInput)); // Get latest system message from AI configuration - var chatMessages = new List(GetChatMessages(completionConfiguration)); + var chatMessages = new List(GetChatMessages(chatCompletionConfiguration)); chatMessages.AddRange(chatConversation); - // Get AI response and update chat conversation + // Get AI response and add it to chat conversation var response = await chatClient.CompleteChatAsync(chatMessages, requestOptions); - var aiResponse = response.Value.Content[0].Text; - System.Console.WriteLine($"AI: {aiResponse}"); + string aiResponse = response.Value.Content[0].Text; + Console.WriteLine($"AI: {aiResponse}"); chatConversation.Add(ChatMessage.CreateAssistantMessage(aiResponse)); Console.WriteLine(); } // Helper method to convert configuration messages to ChatMessage objects -static IEnumerable GetChatMessages(CompletionConfiguration completionConfiguration) +static IEnumerable GetChatMessages(ChatCompletionConfiguration chatCompletionConfiguration) { - return completionConfiguration.Messages.Select(message => message.Role switch + return chatCompletionConfiguration.Messages.Select(message => message.Role switch { "system" => ChatMessage.CreateSystemMessage(message.Content), "user" => ChatMessage.CreateUserMessage(message.Content), From 780cc9bb462992218ef88e63674e19d446bfa61c Mon Sep 17 00:00:00 2001 From: Zhenlan Wang Date: Thu, 26 Jun 2025 20:52:59 -0700 Subject: [PATCH 4/4] Address feedback --- examples/DotNetCore/ChatApp/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/DotNetCore/ChatApp/README.md b/examples/DotNetCore/ChatApp/README.md index 43d3d48d..de090978 100644 --- a/examples/DotNetCore/ChatApp/README.md +++ b/examples/DotNetCore/ChatApp/README.md @@ -29,7 +29,7 @@ Set the following environment variable: Configure the following keys in your Azure App Configuration: -#### OpenAI Connection Settings +#### Azure OpenAI Connection Settings - `ChatApp:AzureOpenAI:Endpoint` - Your Azure OpenAI endpoint URL - `ChatApp:AzureOpenAI:DeploymentName` - Your Azure OpenAI deployment name @@ -37,7 +37,7 @@ Configure the following keys in your Azure App Configuration: #### Chat Completion Configuration -- `ChatApp:Completion` - An AI Configuration entry containing the following settings: +- `ChatApp:ChatCompletion` - An AI Configuration for chat completion containing the following settings: - `model` - Model name (e.g., "gpt-4o") - `max_tokens` - Maximum tokens for completion (e.g., 1000) - `temperature` - Temperature parameter (e.g., 0.7) @@ -74,7 +74,7 @@ Exiting chat. Goodbye! ## Troubleshooting -**"AZURE_APPCONFIG_ENDPOINT environment variable not set"** +**"AZURE_APPCONFIGURATION_ENDPOINT environment variable not set"** - Ensure the environment variable is properly set - Verify the endpoint URL is correct