diff --git a/README.md b/README.md index b42d4516..557cd53d 100644 --- a/README.md +++ b/README.md @@ -342,7 +342,6 @@ To use Azure Cache for Redis with Vector Search, you need to enable the `USE_RED ```bash azd env set USE_REDIS true azd env set EMBEDDING_TYPE 4 -azd env set AZURE_USE_APPLICATION_INSIGHTS true azd up ``` diff --git a/app/Directory.Packages.props b/app/Directory.Packages.props index 80874bba..f13af88a 100644 --- a/app/Directory.Packages.props +++ b/app/Directory.Packages.props @@ -45,6 +45,8 @@ + + diff --git a/app/backend/MinimalApi.csproj b/app/backend/MinimalApi.csproj index d25c40a8..8b19a6c9 100644 --- a/app/backend/MinimalApi.csproj +++ b/app/backend/MinimalApi.csproj @@ -26,6 +26,8 @@ + + diff --git a/app/backend/Services/ReadRetrieveReadChatService.cs b/app/backend/Services/ReadRetrieveReadChatService.cs index 7c72dcd9..2dce3721 100644 --- a/app/backend/Services/ReadRetrieveReadChatService.cs +++ b/app/backend/Services/ReadRetrieveReadChatService.cs @@ -4,6 +4,8 @@ using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Embeddings; +using Redis.OM; +using Redis.OM.Vectorizers; namespace MinimalApi.Services; #pragma warning disable SKEXP0011 // Mark members as static @@ -15,6 +17,7 @@ public class ReadRetrieveReadChatService private readonly IConfiguration _configuration; private readonly IComputerVisionService? _visionService; private readonly TokenCredential? _tokenCredential; + private readonly Redis.OM.Contracts.ISemanticCache? _semanticCache; public ReadRetrieveReadChatService( ISearchService searchClient, @@ -54,6 +57,42 @@ public ReadRetrieveReadChatService( _configuration = configuration; _visionService = visionService; _tokenCredential = tokenCredential; + if (configuration["UseRedis"] == "true") + { + var SEredisConnectionString = configuration["AzureCacheServiceEndpoint"] ?? throw new ArgumentNullException("AzureCacheServiceEndpoint"); + var redisConnectionString = ConvertStackExchangeRedisConnectionStringToRedisCloudConnectionURL(SEredisConnectionString); + var provider = new RedisConnectionProvider(redisConnectionString); + + var deploymentId = configuration["AzureOpenAiEmbeddingDeployment"] ?? throw new ArgumentNullException("AzureOpenAiEmbeddingDeployment"); + var url = configuration["AzureOpenAiServiceEndpoint"] ?? throw new ArgumentNullException("AzureOpenAiServiceEndpoint"); + var apiKey = configuration["AzureOpenAiServiceKey"] ?? throw new ArgumentNullException("AzureOpenAiServiceKey"); + int startIndex = url.IndexOf("https://") + "https://".Length; + int endIndex = url.IndexOf(".openai.azure.com/"); + + var resourceName = string.Empty; + if (startIndex >= 0 && endIndex >= 0) + { + // Extract the desired string + resourceName = url.Substring(startIndex, endIndex - startIndex); + + } + else + { + throw new ArgumentNullException("AzureOpenAiServiceEndpoint is not in the correct format"); + } + + var dim = 1536; + _semanticCache = provider.AzureOpenAISemanticCache(apiKey, resourceName, deploymentId, dim); + } + } + + private string ConvertStackExchangeRedisConnectionStringToRedisCloudConnectionURL(string stackExchangeRedisConnectionString) + { + var split = stackExchangeRedisConnectionString.Split(','); + var host = split[0].Split(':')[0]; + var port = split[0].Split(':')[1]; + var password = split[1].Split('=')[1]; + return $"redis://:{password}=@{host}:{port}"; } public async Task ReplyAsync( @@ -110,7 +149,7 @@ standard plan AND dental AND employee benefit. } else { - documentContents = string.Join("\r", documentContentList.Select(x =>$"{x.Title}:{x.Content}")); + documentContents = string.Join("\r", documentContentList.Select(x => $"{x.Title}:{x.Content}")); } // step 2.5 @@ -140,7 +179,7 @@ standard plan AND dental AND employee benefit. } } - + if (images != null) { var prompt = @$"## Source ## @@ -185,12 +224,31 @@ You answer needs to be a json object with the following format. StopSequences = [], }; - // get answer - var answer = await chat.GetChatMessageContentAsync( - answerChat, - promptExecutingSetting, - cancellationToken: cancellationToken); - var answerJson = answer.Content ?? throw new InvalidOperationException("Failed to get search query"); + string answerJson = string.Empty; + if (_semanticCache is not null) + { + var res = _semanticCache.GetSimilar(question); + if (res.Length > 0) + { + answerJson = res[0].Response; + } + } + + if (string.IsNullOrEmpty(answerJson)) + { + // get answer + var answer = await chat.GetChatMessageContentAsync( + answerChat, + promptExecutingSetting, + cancellationToken: cancellationToken); + answerJson = answer.Content ?? throw new InvalidOperationException("Failed to get search query"); + + if (_semanticCache is not null) + { + _semanticCache.Store(question, answerJson); + } + } + var answerObject = JsonSerializer.Deserialize(answerJson); var ans = answerObject.GetProperty("answer").GetString() ?? throw new InvalidOperationException("Failed to get answer"); var thoughts = answerObject.GetProperty("thoughts").GetString() ?? throw new InvalidOperationException("Failed to get thoughts"); diff --git a/app/prepdocs/PrepareDocs/AppOptions.cs b/app/prepdocs/PrepareDocs/AppOptions.cs index 44bfb282..0a1887da 100644 --- a/app/prepdocs/PrepareDocs/AppOptions.cs +++ b/app/prepdocs/PrepareDocs/AppOptions.cs @@ -11,6 +11,7 @@ internal record class AppOptions( string? TenantId, string? SearchServiceEndpoint, string? AzureOpenAIServiceEndpoint, + string? AzureOpenAiServiceKey, string? SearchIndexName, string? EmbeddingModelName, bool Remove, diff --git a/app/prepdocs/PrepareDocs/Program.Options.cs b/app/prepdocs/PrepareDocs/Program.Options.cs index a04ca670..b24215b8 100644 --- a/app/prepdocs/PrepareDocs/Program.Options.cs +++ b/app/prepdocs/PrepareDocs/Program.Options.cs @@ -29,6 +29,10 @@ internal static partial class Program private static readonly Option s_azureOpenAIService = new(name: "--openaiendpoint", description: "Optional. The Azure OpenAI service endpoint which will be used to extract text, tables and layout from the documents (must exist already)"); + private static readonly Option s_azureOpenAIServiceKey = + new(name: "--openaiendpointkey", description: "Optional. The Azure OpenAI service endpoint key"); + + private static readonly Option s_embeddingModelName = new(name: "--embeddingmodel", description: "Optional. Name of the Azure Cognitive Search embedding model to use for embedding content in the search index (will be created if it doesn't exist)"); @@ -68,6 +72,7 @@ internal static partial class Program s_searchService, s_searchIndexName, s_azureOpenAIService, + s_azureOpenAIServiceKey, s_embeddingModelName, s_remove, s_removeAll, @@ -88,6 +93,7 @@ internal static partial class Program SearchServiceEndpoint: context.ParseResult.GetValueForOption(s_searchService), SearchIndexName: context.ParseResult.GetValueForOption(s_searchIndexName), AzureOpenAIServiceEndpoint: context.ParseResult.GetValueForOption(s_azureOpenAIService), + AzureOpenAiServiceKey: context.ParseResult.GetValueForOption(s_azureOpenAIServiceKey), EmbeddingModelName: context.ParseResult.GetValueForOption(s_embeddingModelName), Remove: context.ParseResult.GetValueForOption(s_remove), RemoveAll: context.ParseResult.GetValueForOption(s_removeAll), diff --git a/infra/core/ai/cognitiveservices.bicep b/infra/core/ai/cognitiveservices.bicep index 18ab1c97..74b0e237 100644 --- a/infra/core/ai/cognitiveservices.bicep +++ b/infra/core/ai/cognitiveservices.bicep @@ -53,3 +53,4 @@ resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01 output endpoint string = account.properties.endpoint output id string = account.id output name string = account.name +output apiKey string = account.listKeys().key1 diff --git a/infra/main.bicep b/infra/main.bicep index 1540853f..d7ba6248 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -288,6 +288,10 @@ module keyVaultSecrets 'core/security/keyvault-secrets.bicep' = { name: 'AzureOpenAiEmbeddingDeployment' value: azureEmbeddingDeploymentName } + { + name: 'AzureOpenAiServiceKey' + value: azureOpenAi.outputs.apiKey + } ] : [ { name: 'OpenAIAPIKey' @@ -797,6 +801,7 @@ output AZURE_LOCATION string = location output AZURE_OPENAI_RESOURCE_LOCATION string = openAiResourceGroupLocation output AZURE_OPENAI_CHATGPT_DEPLOYMENT string = azureChatGptDeploymentName output AZURE_OPENAI_EMBEDDING_DEPLOYMENT string = azureEmbeddingDeploymentName +output AZURE_OPENAI_EMBEDDING_API_KEY string = useAOAI? azureOpenAi.outputs.apiKey : '' output AZURE_OPENAI_ENDPOINT string = useAOAI? azureOpenAi.outputs.endpoint : '' output AZURE_OPENAI_RESOURCE_GROUP string = useAOAI ? azureOpenAiResourceGroup.name : '' output AZURE_OPENAI_SERVICE string = useAOAI ? azureOpenAi.outputs.name : '' @@ -805,10 +810,10 @@ output AZURE_SEARCH_INDEX string = searchIndexName output AZURE_SEARCH_SERVICE string = searchService.outputs.name output AZURE_SEARCH_SERVICE_ENDPOINT string = searchService.outputs.endpoint output AZURE_SEARCH_SERVICE_RESOURCE_GROUP string = searchServiceResourceGroup.name -output AZURE_CACHE_INDEX string = azureCacheIndexName -output AZURE_CACHE_SERVICE string = azureCache.outputs.name -output AZURE_CACHE_SERVICE_ENDPOINT string = azureCache.outputs.endpoint -output AZURE_CACHE_SERVICE_RESOURCE_GROUP string = azureCacheResourceGroup.name +output AZURE_CACHE_INDEX string = useRedis ? azureCacheIndexName : '' +output AZURE_CACHE_SERVICE string = useRedis ? azureCache.outputs.name : '' +output AZURE_CACHE_SERVICE_ENDPOINT string = useRedis ? azureCache.outputs.endpoint : '' +output AZURE_CACHE_SERVICE_RESOURCE_GROUP string = useRedis ? azureCacheResourceGroup.name : '' output USE_REDIS bool = useRedis output AZURE_STORAGE_ACCOUNT string = storage.outputs.name output AZURE_STORAGE_BLOB_ENDPOINT string = storage.outputs.primaryEndpoints.blob diff --git a/scripts/prepdocs.ps1 b/scripts/prepdocs.ps1 index f6b7a0dc..c650f3c2 100644 --- a/scripts/prepdocs.ps1 +++ b/scripts/prepdocs.ps1 @@ -69,6 +69,7 @@ if ([string]::IsNullOrEmpty($env:AZD_PREPDOCS_RAN) -or $env:AZD_PREPDOCS_RAN -eq if ($env:USE_AOAI -eq "true") { Write-Host "Using Azure OpenAI" $dotnetArguments += " --openaiendpoint $($env:AZURE_OPENAI_ENDPOINT) " + $dotnetArguments += " --openaiendpointkey $($env:AZURE_OPENAI_EMBEDDING_API_KEY) " $dotnetArguments += " --embeddingmodel $($env:AZURE_OPENAI_EMBEDDING_DEPLOYMENT) " } else{ diff --git a/scripts/prepdocs.sh b/scripts/prepdocs.sh index 76855da1..17ba1af3 100755 --- a/scripts/prepdocs.sh +++ b/scripts/prepdocs.sh @@ -37,6 +37,7 @@ if [ -z "$AZD_PREPDOCS_RAN" ] || [ "$AZD_PREPDOCS_RAN" = "false" ]; then echo "use azure openai" args="$args --openaiendpoint $AZURE_OPENAI_ENDPOINT" args="$args --embeddingmodel $AZURE_OPENAI_EMBEDDING_DEPLOYMENT" + args="$args --openaiendpointkey $AZURE_OPENAI_EMBEDDING_API_KEY" else echo "use openai" args="$args --embeddingmodel $OPENAI_EMBEDDING_DEPLOYMENT"