diff --git a/examples/Go/ChatApp/README.md b/examples/Go/ChatApp/README.md new file mode 100644 index 00000000..01ea6a4a --- /dev/null +++ b/examples/Go/ChatApp/README.md @@ -0,0 +1,92 @@ +# Azure App Configuration - Go ChatApp Sample + +An interactive console chat application that integrates with Azure OpenAI services using Azure App Configuration for dynamic AI Configuration management. + +## Overview + +This Go 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 + +- Go 1.23 or later +- 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:APIVersion` - the Azure OpenAI API version to target. See [Azure OpenAI apiversions](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning) for current API versions. +- `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. **Initialize a new Go module**: `go mod init chatapp-quickstart` +1. **Install dependencies**: `go mod tidy` +1. **Start the Application**: Run the application using `go run main.go` +1. **Begin Chatting**: Type your messages when prompted with "You: " +1. **Continue Conversation**: The AI will respond and maintain conversation context +1. **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 \ No newline at end of file diff --git a/examples/Go/ChatApp/main.go b/examples/Go/ChatApp/main.go new file mode 100644 index 00000000..538055e1 --- /dev/null +++ b/examples/Go/ChatApp/main.go @@ -0,0 +1,160 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "log" + "os" + + "github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + openai "github.com/openai/openai-go" + "github.com/openai/openai-go/azure" +) + +type AIConfig struct { + ChatCompletion ChatCompletion + AzureOpenAI AzureOpenAI +} + +type ChatCompletion struct { + Model string `json:"model"` + Messages []Message `json:"messages"` + MaxTokens int64 `json:"max_tokens"` + Temperature float64 `json:"temperature"` + TopP float64 `json:"top_p"` +} + +type AzureOpenAI struct { + Endpoint string + APIVersion string + APIKey string +} + +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + +var aiConfig AIConfig +var tokenCredential, _ = azidentity.NewDefaultAzureCredential(nil) + +func main() { + configProvider, err := loadAzureAppConfiguration(context.Background()) + if err != nil { + log.Fatal("Error loading Azure App Configuration:", err) + } + + // Configure chat completion with AI configuration + configProvider.Unmarshal(&aiConfig, &azureappconfiguration.ConstructionOptions{Separator: ":"}) + + // Register a callback to refresh AI configuration on changes + configProvider.OnRefreshSuccess(func() { + configProvider.Unmarshal(&aiConfig, &azureappconfiguration.ConstructionOptions{Separator: ":"}) + }) + + // Create a chat client using API key if available, otherwise use the DefaultAzureCredential + var openAIClient openai.Client + if aiConfig.AzureOpenAI.APIKey != "" { + openAIClient = openai.NewClient(azure.WithAPIKey(aiConfig.AzureOpenAI.APIKey), azure.WithEndpoint(aiConfig.AzureOpenAI.Endpoint, aiConfig.AzureOpenAI.APIVersion)) + } else { + openAIClient = openai.NewClient(azure.WithEndpoint(aiConfig.AzureOpenAI.Endpoint, aiConfig.AzureOpenAI.APIVersion), azure.WithTokenCredential(tokenCredential)) + } + + // Initialize chat conversation + var chatConversation []openai.ChatCompletionMessageParamUnion + fmt.Println("Chat started! What's on your mind?") + reader := bufio.NewReader(os.Stdin) + + for { + // Refresh the configuration from Azure App Configuration + configProvider.Refresh(context.Background()) + + // Get user input + fmt.Print("You: ") + userInput, _ := reader.ReadString('\n') + + // Exit if user input is empty + if userInput == "" { + fmt.Println("Exiting Chat. Goodbye!") + break + } + + // Add user message to chat conversation + chatConversation = append(chatConversation, openai.UserMessage(userInput)) + + // Get AI response and add it to chat conversation + response, _ := getAIResponse(openAIClient, chatConversation) + fmt.Printf("AI: %s\n", response) + chatConversation = append(chatConversation, openai.AssistantMessage(response)) + + fmt.Println() + } +} + +// Load configuration from Azure App Configuration +func loadAzureAppConfiguration(ctx context.Context) (*azureappconfiguration.AzureAppConfiguration, error) { + endpoint := os.Getenv("AZURE_APPCONFIGURATION_ENDPOINT") + if endpoint == "" { + return nil, fmt.Errorf("AZURE_APPCONFIGURATION_ENDPOINT environment variable is not set") + } + + authOptions := azureappconfiguration.AuthenticationOptions{ + Endpoint: endpoint, + Credential: tokenCredential, + } + + options := &azureappconfiguration.Options{ + Selectors: []azureappconfiguration.Selector{ + // Load all keys that start with "ChatApp:" and have no label + { + KeyFilter: "ChatApp:*", + }, + }, + TrimKeyPrefixes: []string{"ChatApp:"}, + // Reload configuration if any selected key-values have changed. + // Use the default refresh interval of 30 seconds. It can be overridden via RefreshOptions.Interval + RefreshOptions: azureappconfiguration.KeyValueRefreshOptions{ + Enabled: true, + }, + KeyVaultOptions: azureappconfiguration.KeyVaultOptions{ + Credential: tokenCredential, + }, + } + + return azureappconfiguration.Load(ctx, authOptions, options) +} + +func getAIResponse(openAIClient openai.Client, chatConversation []openai.ChatCompletionMessageParamUnion) (string, error) { + var completionMessages []openai.ChatCompletionMessageParamUnion + + for _, msg := range aiConfig.ChatCompletion.Messages { + switch msg.Role { + case "system": + completionMessages = append(completionMessages, openai.SystemMessage(msg.Content)) + case "user": + completionMessages = append(completionMessages, openai.UserMessage(msg.Content)) + case "assistant": + completionMessages = append(completionMessages, openai.AssistantMessage(msg.Content)) + } + } + + // Add the chat conversation history + completionMessages = append(completionMessages, chatConversation...) + + // Create chat completion parameters + params := openai.ChatCompletionNewParams{ + Messages: completionMessages, + Model: aiConfig.ChatCompletion.Model, + MaxTokens: openai.Int(aiConfig.ChatCompletion.MaxTokens), + Temperature: openai.Float(aiConfig.ChatCompletion.Temperature), + TopP: openai.Float(aiConfig.ChatCompletion.TopP), + } + + if completion, err := openAIClient.Chat.Completions.New(context.Background(), params); err != nil { + return "", err + } else { + return completion.Choices[0].Message.Content, nil + } +}