Skip to content

Commit

Permalink
Merge branch 'redis' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
stanleysmall-microsoft authored Apr 26, 2024
2 parents 5fd9b3a + 895c5f8 commit 82fefc8
Show file tree
Hide file tree
Showing 13 changed files with 65 additions and 30 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ products:
- azure-blob-storage
- azure-container-apps
- azure-cognitive-search
- azure-redis-cache
- azure-openai
- aspnet-core
- blazor
Expand Down Expand Up @@ -56,11 +57,11 @@ description: A csharp sample app that chats with your data using OpenAI and AI S
[![Open in GitHub - Codespaces](https://img.shields.io/static/v1?style=for-the-badge&label=GitHub+Codespaces&message=Open&color=brightgreen&logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=624102171&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestUs2)
[![Open in Remote - Containers](https://img.shields.io/static/v1?style=for-the-badge&label=Remote%20-%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/azure-samples/azure-search-openai-demo-csharp)

This sample demonstrates a few approaches for creating ChatGPT-like experiences over your own data using the Retrieval Augmented Generation pattern. It uses Azure OpenAI Service to access the ChatGPT model (`gpt-35-turbo`), and Azure Cognitive Search for data indexing and retrieval.
This sample demonstrates a few approaches for creating ChatGPT-like experiences over your own data using the Retrieval Augmented Generation pattern. It uses Azure OpenAI Service to access the ChatGPT model (`gpt-35-turbo`), and Azure Cache for Redis for Vector Similariy Search.

The repo includes sample data so it's ready to try end-to-end. In this sample application, we use a fictitious company called Contoso Electronics, and the experience allows its employees to ask questions about the benefits, internal policies, as well as job descriptions and roles.

![RAG Architecture](docs/appcomponents-version-4.png)
![RAG Architecture](docs/appcomponents-version-5.png)

For more details on how this application was built, check out:

Expand All @@ -84,7 +85,7 @@ We want to hear from you! Are you interested in building or currently building i

- **User interface** - The application’s chat interface is a [Blazor WebAssembly](https://learn.microsoft.com/aspnet/core/blazor/) application. This interface is what accepts user queries, routes request to the application backend, and displays generated responses.
- **Backend** - The application backend is an [ASP.NET Core Minimal API](https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/overview). The backend hosts the Blazor static web application and what orchestrates the interactions among the different services. Services used in this application include:
- [**Azure Cognitive Search**](https://learn.microsoft.com/azure/search/search-what-is-azure-search) – indexes documents from the data stored in an Azure Storage Account. This makes the documents searchable using [vector search](https://learn.microsoft.com/azure/search/search-get-started-vector) capabilities.
- [**Azure Cache for Redis**](https://learn.microsoft.com/azure/azure-cache-for-redis/) – indexes documents from the data stored in an Azure Storage Account. This makes the documents searchable using [vector search](https://redis.io/docs/interact/search-and-query/advanced-concepts/vectors/) capabilities.
- [**Azure OpenAI Service**](https://learn.microsoft.com/azure/ai-services/openai/overview) – provides the Large Language Models to generate responses. [Semantic Kernel](https://learn.microsoft.com/semantic-kernel/whatissk) is used in conjunction with the Azure OpenAI Service to orchestrate the more complex AI workflows.

## Getting Started
Expand All @@ -94,7 +95,7 @@ We want to hear from you! Are you interested in building or currently building i
In order to deploy and run this example, you'll need

- **Azure Account** - If you're new to Azure, get an [Azure account for free](https://aka.ms/free) and you'll get some free Azure credits to get started.
- **Azure subscription with access enabled for the Azure OpenAI service** - [You can request access](https://aka.ms/oaiapply). You can also visit [the Cognitive Search docs](https://azure.microsoft.com/free/cognitive-search/) to get some free Azure credits to get you started.
- **Azure subscription with access enabled for the Azure OpenAI service** - [You can request access](https://aka.ms/oaiapply).
- **Azure account permissions** - Your Azure Account must have `Microsoft.Authorization/roleAssignments/write` permissions, such as [User Access Administrator](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#user-access-administrator) or [Owner](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#owner).


Expand All @@ -108,7 +109,7 @@ Pricing varies per region and usage, so it isn't possible to predict exact costs
- [**Azure Container Apps**](https://azure.microsoft.com/pricing/details/container-apps/)
- [**Azure OpenAI Service**](https://azure.microsoft.com/pricing/details/cognitive-services/openai-service/)
- [**Azure Form Recognizer**](https://azure.microsoft.com/pricing/details/form-recognizer/)
- [**Azure Cognitive Search**](https://azure.microsoft.com/pricing/details/search/)
- [**Azure Cache for Redis**](https://azure.microsoft.com/pricing/details/cache/)
- [**Azure Blob Storage**](https://azure.microsoft.com/pricing/details/storage/blobs/)
- [**Azure Monitor**](https://azure.microsoft.com/pricing/details/monitor/)

Expand Down Expand Up @@ -362,7 +363,7 @@ to production. Here are some things to consider:
## Resources

- [Revolutionize your Enterprise Data with ChatGPT: Next-gen Apps w/ Azure OpenAI and Cognitive Search](https://aka.ms/entgptsearchblog)
- [Azure Cognitive Search](https://learn.microsoft.com/azure/search/search-what-is-azure-search)
- [Azure Cache for Redis](https://learn.microsoft.com/azure/azure-cache-for-redis/cache-overview)
- [Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/overview)
- [`Azure.AI.OpenAI` NuGet package](https://www.nuget.org/packages/Azure.AI.OpenAI)
- [Original Blazor App](https://github.com/IEvangelist/blazor-azure-openai)
Expand Down
11 changes: 7 additions & 4 deletions app/SharedWebComponents/Components/Answer.razor
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,23 @@
</MudTabPanel>
<MudTabPanel Icon="@Icons.Material.Filled.Lightbulb" Text="Thought process"
ToolTip="Show thought process."
Disabled="@(Retort is { Context.Thoughts: null } or { Context.Thoughts.Length: 0})">

Disabled="@(Retort is { Thoughts: null })">
<ChildContent>
<MudPaper Class="pa-6" Elevation="3">
<pre style="white-space: normal; font-size: 1.2em;">
@(RemoveLeadingAndTrailingLineBreaks(Retort.Context.ThoughtsString))
@(RemoveLeadingAndTrailingLineBreaks(Retort.Thoughts!))
</pre>
</MudPaper>
</ChildContent>
</MudTabPanel>
<MudTabPanel Icon="@Icons.Material.Filled.TextSnippet" Text="Supporting Content"
ToolTip="Show the supporting content." Disabled="@(Retort is { Context.DataPoints.Text: null } or { Context.DataPoints.Text.Length: 0 })">

ToolTip="Show the supporting content." Disabled="@(Retort is { DataPoints: null } or { DataPoints.Length: 0 })">
<ChildContent>
<MudPaper Class="pa-2" Elevation="3">
<SupportingContent DataPoints="Retort.Context.DataPointsContent" Images="Retort.Context.DataPointsImages ?? []" />
<SupportingContent DataPoints="Retort.DataPoints" Images="Retort.Images ?? []" />

</MudPaper>
</ChildContent>
</MudTabPanel>
Expand Down
8 changes: 6 additions & 2 deletions app/SharedWebComponents/Components/Answer.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ namespace SharedWebComponents.Components;

public sealed partial class Answer
{
[Parameter, EditorRequired] public required ResponseChoice Retort { get; set; }

[Parameter, EditorRequired] public required ApproachResponse Retort { get; set; }

[Parameter, EditorRequired] public required EventCallback<string> FollowupQuestionClicked { get; set; }

[Inject] public required IPdfViewer PdfViewer { get; set; }
Expand All @@ -14,7 +16,9 @@ public sealed partial class Answer
protected override void OnParametersSet()
{
_parsedAnswer = ParseAnswerToHtml(
Retort.Message.Content, Retort.CitationBaseUrl);

Retort.Answer, Retort.CitationBaseUrl);


base.OnParametersSet();
}
Expand Down
6 changes: 4 additions & 2 deletions app/SharedWebComponents/Components/AnswerError.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
<MudStack Spacing="4" Class="full-width">
<MudIcon Icon="@Icons.Material.Filled.Error" Color="Color.Error" Size="Size.Large" />
<MudText Typo="Typo.subtitle2">
@Error.Error

@Error.Answer
</MudText>
<MudText Typo="Typo.body1">
Unable to retrieve valid response from the server.
@Error.Error

</MudText>
<div>
<MudButton Variant="Variant.Filled" Size="Size.Small" Color="Color.Info"
Expand Down
4 changes: 3 additions & 1 deletion app/SharedWebComponents/Components/AnswerError.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ namespace SharedWebComponents.Components;
public sealed partial class AnswerError
{
[Parameter, EditorRequired] public required string Question { get; set; }
[Parameter, EditorRequired] public required ChatAppResponseOrError Error { get; set; }

[Parameter, EditorRequired] public required ApproachResponse Error { get; set; }

[Parameter, EditorRequired] public required EventCallback<string> OnRetryClicked { get; set; }

private async Task OnRetryClickedAsync()
Expand Down
4 changes: 3 additions & 1 deletion app/SharedWebComponents/Models/AnswerResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ namespace SharedWebComponents.Models;

public readonly record struct AnswerResult<TRequest>(
bool IsSuccessful,
ChatAppResponseOrError? Response,

ApproachResponse? Response,

Approach Approach,
TRequest Request) where TRequest : ApproachRequest;
4 changes: 3 additions & 1 deletion app/SharedWebComponents/Pages/Chat.razor
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@
<MudBadge Origin="Origin.TopLeft" Overlap="true" Color="Color.Secondary"
Icon="@Icons.Material.Filled.AutoAwesome"
Style="display:inherit">
<Answer Retort="@answer.Choices[0]" FollowupQuestionClicked="@OnAskQuestionAsync" />

<Answer Retort="@answer" FollowupQuestionClicked="@OnAskQuestionAsync" />

</MudBadge>
}
</div>
Expand Down
14 changes: 9 additions & 5 deletions app/SharedWebComponents/Pages/Chat.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ public sealed partial class Chat
private string _lastReferenceQuestion = "";
private bool _isReceivingResponse = false;

private readonly Dictionary<UserQuestion, ChatAppResponseOrError?> _questionAndAnswerMap = [];

private readonly Dictionary<UserQuestion, ApproachResponse?> _questionAndAnswerMap = [];


[Inject] public required ISessionStorageService SessionStorage { get; set; }

Expand Down Expand Up @@ -42,13 +44,15 @@ private async Task OnAskClickedAsync()
try
{
var history = _questionAndAnswerMap
.Where(x => x.Value?.Choices is { Length: > 0})
.SelectMany(x => new ChatMessage[] { new ChatMessage("user", x.Key.Question), new ChatMessage("assistant", x.Value!.Choices[0].Message.Content) })

.Where(x => x.Value is not null)
.Select(x => new ChatTurn(x.Key.Question, x.Value!.Answer))
.ToList();

history.Add(new ChatMessage("user", _userQuestion));
history.Add(new ChatTurn(_userQuestion));

var request = new ChatRequest([.. history], Settings.Approach, Settings.Overrides);

var request = new ChatRequest([.. history], Settings.Overrides);
var result = await ApiClient.ChatConversationAsync(request);

_questionAndAnswerMap[_currentQuestion] = result.Response;
Expand Down
18 changes: 12 additions & 6 deletions app/SharedWebComponents/Services/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,19 +112,25 @@ private async Task<AnswerResult<TRequest>> PostRequestAsync<TRequest>(

if (response.IsSuccessStatusCode)
{
var answer = await response.Content.ReadFromJsonAsync<ChatAppResponseOrError>();

var answer = await response.Content.ReadFromJsonAsync<ApproachResponse>();
return result with
{
IsSuccessful = answer is not null,
Response = answer,
Response = answer

};
}
else
{
var errorTitle = $"HTTP {(int)response.StatusCode} : {response.ReasonPhrase ?? "☹️ Unknown error..."}";
var answer = new ChatAppResponseOrError(
Array.Empty<ResponseChoice>(),
errorTitle);

var answer = new ApproachResponse(
$"HTTP {(int)response.StatusCode} : {response.ReasonPhrase ?? "☹️ Unknown error..."}",
null,
[],
null,
"Unable to retrieve valid response from the server.");


return result with
{
Expand Down
4 changes: 4 additions & 0 deletions app/functions/EmbedFunctions/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,12 @@ uri is not null
AzureComputerVisionService? visionClient = null;
bool includeImageEmbeddingsField = false;


if (useVision)
{
var visionEndpoint = Environment.GetEnvironmentVariable("AZURE_COMPUTER_VISION_ENDPOINT") ?? throw new ArgumentNullException("AZURE_COMPUTER_VISION_ENDPOINT is null");
var httpClient = new HttpClient();

visionClient = new AzureComputerVisionService(httpClient, visionEndpoint, new DefaultAzureCredential());
includeImageEmbeddingsField = true;
}
Expand Down Expand Up @@ -124,8 +126,10 @@ uri is not null
searchIndexClient: searchIndexClient,
documentAnalysisClient: documentClient,
corpusContainerClient: corpusContainer,

computerVisionService: visionClient,
includeImageEmbeddingsField: includeImageEmbeddingsField,

logger: logger);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ await corpusClient.SetMetadataAsync(new Dictionary<string, string>
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to embed: {Name}, error: {Message} stackTrace: {StackTrace}", blobName, ex.Message, ex.StackTrace);

logger.LogError(ex, "Failed to embed: {Name}, error: {Message}", blobName, ex.Message);

throw;
}
}
Expand Down
Binary file added docs/appcomponents-version-5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ param location string
param tags string = ''

@description('Location for the OpenAI resource group')
@allowed([ 'canadaeast', 'westus', 'eastus', 'eastus2', 'francecentral', 'swedencentral', 'switzerlandnorth', 'uksouth', 'japaneast', 'northcentralus', 'australiaeast' ])

@allowed([ 'canadaeast', 'westus', 'eastus', 'eastus2', 'francecentral', 'switzerlandnorth', 'uksouth', 'japaneast', 'northcentralus', 'australiaeast' ])

@metadata({
azd: {
type: 'location'
Expand Down Expand Up @@ -185,6 +187,7 @@ param useVision bool = false
@description('Use Azure Cache. default: false')
param useRedis bool = false


var abbrs = loadJsonContent('./abbreviations.json')
var resourceToken = toLower(uniqueString(subscription().id, environmentName, location))

Expand Down

0 comments on commit 82fefc8

Please sign in to comment.