diff --git a/examples/DotNetCore/ChatApp/ChatApp/AzureOpenAIConfiguration.cs b/examples/DotNetCore/ChatApp/ChatApp/AzureOpenAIConfiguration.cs new file mode 100644 index 00000000..39e4a139 --- /dev/null +++ b/examples/DotNetCore/ChatApp/ChatApp/AzureOpenAIConfiguration.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// + +namespace ChatApp +{ + internal class AzureOpenAIConfiguration + { + public required string Endpoint { get; set; } + + public required string DeploymentName { get; set; } + + public string? ApiKey { get; set; } + } +} diff --git a/examples/DotNetCore/ChatApp/ChatApp/ModelConfiguration.cs b/examples/DotNetCore/ChatApp/ChatApp/ChatCompletionConfiguration.cs similarity index 89% rename from examples/DotNetCore/ChatApp/ChatApp/ModelConfiguration.cs rename to examples/DotNetCore/ChatApp/ChatApp/ChatCompletionConfiguration.cs index 75d37255..b94f8626 100644 --- a/examples/DotNetCore/ChatApp/ChatApp/ModelConfiguration.cs +++ b/examples/DotNetCore/ChatApp/ChatApp/ChatCompletionConfiguration.cs @@ -5,7 +5,7 @@ namespace ChatApp { - internal class ModelConfiguration + 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 612b8214..7379c83f 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.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 azureOpenAIConfiguration = 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(azureOpenAIConfiguration.ApiKey)) +{ + azureClient = new AzureOpenAIClient(new Uri(azureOpenAIConfiguration.Endpoint), new Azure.AzureKeyCredential(azureOpenAIConfiguration.ApiKey)); +} +else +{ + azureClient = new AzureOpenAIClient(new Uri(azureOpenAIConfiguration.Endpoint), credential); +} +ChatClient chatClient = azureClient.GetChatClient(azureOpenAIConfiguration.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 chatCompletionConfiguration = configuration.GetSection("ChatApp:ChatCompletion").Get(); var requestOptions = new ChatCompletionOptions() { - MaxOutputTokenCount = modelConfiguration.MaxTokens, - Temperature = modelConfiguration.Temperature, - TopP = modelConfiguration.TopP + MaxOutputTokenCount = chatCompletionConfiguration.MaxTokens, + Temperature = chatCompletionConfiguration.Temperature, + TopP = chatCompletionConfiguration.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(chatCompletionConfiguration)); + chatMessages.AddRange(chatConversation); + + // Get AI response and add it to chat conversation + var response = await chatClient.CompleteChatAsync(chatMessages, requestOptions); + string aiResponse = response.Value.Content[0].Text; + 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(ChatCompletionConfiguration chatCompletionConfiguration) { - return modelConfiguration.Messages.Select(message => message.Role switch + return chatCompletionConfiguration.Messages.Select(message => message.Role switch { "system" => ChatMessage.CreateSystemMessage(message.Content), "user" => ChatMessage.CreateUserMessage(message.Content), diff --git a/examples/DotNetCore/ChatApp/README.md b/examples/DotNetCore/ChatApp/README.md new file mode 100644 index 00000000..de090978 --- /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: + +#### Azure 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: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) + - `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_APPCONFIGURATION_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