Skip to content

Commit

Permalink
implement chat protocal (#299)
Browse files Browse the repository at this point in the history
## Purpose
<!-- Describe the intention of the changes being proposed. What problem
does it solve or functionality does it add? -->
* ...

This PR implements chat protocal (non-streaming) part.

## Does this introduce a breaking change?
<!-- Mark one with an "x". -->
```
[ ] Yes
[x] No
```

fix #254 

Link to chat protocal:
https://github.com/Azure-Samples/ai-chat-app-protocol

## Pull Request Type
What kind of change does this Pull Request introduce?

<!-- Please check the one that applies to this PR using "x". -->
```
[ ] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Documentation content changes
[ ] Other... Please describe:
```

## How to Test
*  Get the code

```
git clone [repo-address]
cd [repo-name]
git checkout [branch-name]
npm install
```

* Test the code
<!-- Add steps to run the tests suite and/or manually test -->
```
```

## What to Check
Verify that the following are valid
* ...

## Other Information
<!-- Add any other helpful information that may be needed here. -->

---------

Co-authored-by: David Pine <[email protected]>
  • Loading branch information
LittleLittleCloud and IEvangelist authored Apr 15, 2024
1 parent eccf87e commit e193da4
Show file tree
Hide file tree
Showing 22 changed files with 198 additions and 100 deletions.
2 changes: 1 addition & 1 deletion app/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<AzureFormRecognizerVersion>4.1.0</AzureFormRecognizerVersion>
<AzureIdentityVersion>1.10.1</AzureIdentityVersion>
<AzureIdentityVersion>1.11.0</AzureIdentityVersion>
<AzureSearchDocumentsVersion>11.5.0-beta.4</AzureSearchDocumentsVersion>
<AzureStorageBlobsVersion>12.17.0</AzureStorageBlobsVersion>
<AzureOpenAIVersion>1.0.0-beta.7</AzureOpenAIVersion>
Expand Down
2 changes: 1 addition & 1 deletion app/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<PackageVersion Include="Azure.AI.FormRecognizer" Version="4.1.0" />
<PackageVersion Include="Azure.AI.OpenAI" Version="1.0.0-beta.12" />
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.0" />
<PackageVersion Include="Azure.Identity" Version="1.10.4" />
<PackageVersion Include="Azure.Identity" Version="1.11.0" />
<PackageVersion Include="Azure.Search.Documents" Version="11.5.1" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.19.1" />
<PackageVersion Include="Azure.Storage.Files.Shares" Version="12.17.1" />
Expand Down
8 changes: 4 additions & 4 deletions app/SharedWebComponents/Components/Answer.razor
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,20 @@
</MudTabPanel>
<MudTabPanel Icon="@Icons.Material.Filled.Lightbulb" Text="Thought process"
ToolTip="Show thought process."
Disabled="@(Retort is { Thoughts: null })">
Disabled="@(Retort is { Context.Thoughts: null } or { Context.Thoughts.Length: 0})">
<ChildContent>
<MudPaper Class="pa-6" Elevation="3">
<pre style="white-space: normal; font-size: 1.2em;">
@(RemoveLeadingAndTrailingLineBreaks(Retort.Thoughts!))
@(RemoveLeadingAndTrailingLineBreaks(Retort.Context.ThoughtsString))
</pre>
</MudPaper>
</ChildContent>
</MudTabPanel>
<MudTabPanel Icon="@Icons.Material.Filled.TextSnippet" Text="Supporting Content"
ToolTip="Show the supporting content." Disabled="@(Retort is { DataPoints: null } or { DataPoints.Length: 0 })">
ToolTip="Show the supporting content." Disabled="@(Retort is { Context.DataPoints.Text: null } or { Context.DataPoints.Text.Length: 0 })">
<ChildContent>
<MudPaper Class="pa-2" Elevation="3">
<SupportingContent DataPoints="Retort.DataPoints" Images="Retort.Images ?? []" />
<SupportingContent DataPoints="Retort.Context.DataPointsContent" Images="Retort.Context.DataPointsImages ?? []" />
</MudPaper>
</ChildContent>
</MudTabPanel>
Expand Down
4 changes: 2 additions & 2 deletions app/SharedWebComponents/Components/Answer.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace SharedWebComponents.Components;

public sealed partial class Answer
{
[Parameter, EditorRequired] public required ApproachResponse Retort { get; set; }
[Parameter, EditorRequired] public required ResponseChoice Retort { get; set; }
[Parameter, EditorRequired] public required EventCallback<string> FollowupQuestionClicked { get; set; }

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

base.OnParametersSet();
}
Expand Down
4 changes: 2 additions & 2 deletions app/SharedWebComponents/Components/AnswerError.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
<MudStack Spacing="4" Class="full-width">
<MudIcon Icon="@Icons.Material.Filled.Error" Color="Color.Error" Size="Size.Large" />
<MudText Typo="Typo.subtitle2">
@Error.Answer
@Error.Error
</MudText>
<MudText Typo="Typo.body1">
@Error.Error
Unable to retrieve valid response from the server.
</MudText>
<div>
<MudButton Variant="Variant.Filled" Size="Size.Small" Color="Color.Info"
Expand Down
2 changes: 1 addition & 1 deletion app/SharedWebComponents/Components/AnswerError.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace SharedWebComponents.Components;
public sealed partial class AnswerError
{
[Parameter, EditorRequired] public required string Question { get; set; }
[Parameter, EditorRequired] public required ApproachResponse Error { get; set; }
[Parameter, EditorRequired] public required ChatAppResponseOrError Error { get; set; }
[Parameter, EditorRequired] public required EventCallback<string> OnRetryClicked { get; set; }

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

public readonly record struct AnswerResult<TRequest>(
bool IsSuccessful,
ApproachResponse? Response,
ChatAppResponseOrError? Response,
Approach Approach,
TRequest Request) where TRequest : ApproachRequest;
2 changes: 1 addition & 1 deletion app/SharedWebComponents/Pages/Chat.razor
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
<MudBadge Origin="Origin.TopLeft" Overlap="true" Color="Color.Secondary"
Icon="@Icons.Material.Filled.AutoAwesome"
Style="display:inherit">
<Answer Retort="@answer" FollowupQuestionClicked="@OnAskQuestionAsync" />
<Answer Retort="@answer.Choices[0]" FollowupQuestionClicked="@OnAskQuestionAsync" />
</MudBadge>
}
</div>
Expand Down
10 changes: 5 additions & 5 deletions app/SharedWebComponents/Pages/Chat.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public sealed partial class Chat
private string _lastReferenceQuestion = "";
private bool _isReceivingResponse = false;

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

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

Expand Down Expand Up @@ -42,13 +42,13 @@ private async Task OnAskClickedAsync()
try
{
var history = _questionAndAnswerMap
.Where(x => x.Value is not null)
.Select(x => new ChatTurn(x.Key.Question, x.Value!.Answer))
.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) })
.ToList();

history.Add(new ChatTurn(_userQuestion));
history.Add(new ChatMessage("user", _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
14 changes: 6 additions & 8 deletions app/SharedWebComponents/Services/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,21 +112,19 @@ private async Task<AnswerResult<TRequest>> PostRequestAsync<TRequest>(

if (response.IsSuccessStatusCode)
{
var answer = await response.Content.ReadFromJsonAsync<ApproachResponse>();
var answer = await response.Content.ReadFromJsonAsync<ChatAppResponseOrError>();
return result with
{
IsSuccessful = answer is not null,
Response = answer
Response = answer,
};
}
else
{
var answer = new ApproachResponse(
$"HTTP {(int)response.StatusCode} : {response.ReasonPhrase ?? "☹️ Unknown error..."}",
null,
[],
null,
"Unable to retrieve valid response from the server.");
var errorTitle = $"HTTP {(int)response.StatusCode} : {response.ReasonPhrase ?? "☹️ Unknown error..."}";
var answer = new ChatAppResponseOrError(
Array.Empty<ResponseChoice>(),
errorTitle);

return result with
{
Expand Down
48 changes: 24 additions & 24 deletions app/backend/Extensions/ChatTurnExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,33 @@ namespace MinimalApi.Extensions;

internal static class ChatTurnExtensions
{
internal static string GetChatHistoryAsText(
this ChatTurn[] history, bool includeLastTurn = true, int approximateMaxTokens = 1_000)
{
var historyTextResult = string.Empty;
var skip = includeLastTurn ? 0 : 1;
//internal static string GetChatHistoryAsText(
// this ChatMessage[] history, bool includeLastTurn = true, int approximateMaxTokens = 1_000)
//{
// var historyTextResult = string.Empty;
// var skip = includeLastTurn ? 0 : 1;

foreach (var turn in history.SkipLast(skip).Reverse())
{
var historyText = $"user: {turn.User}";
// foreach (var turn in history.SkipLast(skip).Reverse())
// {
// var historyText = $"user: {turn.User}";

if (turn.Bot is not null)
{
historyText += $"""
<|im_start|>assistant
{turn.Bot}
<|im_end|>
""";
}
// if (turn.Content is not null)
// {
// historyText += $"""
// <|im_start|>assistant
// {turn.Content}
// <|im_end|>
// """;
// }

historyTextResult = historyText + historyTextResult;
// historyTextResult = historyText + historyTextResult;

if (historyTextResult.Length > approximateMaxTokens * 4)
{
return historyTextResult;
}
}
// if (historyTextResult.Length > approximateMaxTokens * 4)
// {
// return historyTextResult;
// }
// }

return historyTextResult;
}
// return historyTextResult;
//}
}
2 changes: 1 addition & 1 deletion app/backend/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7181;http://localhost:5167"
"applicationUrl": "https://localhost:7181"
},
"Docker": {
"commandName": "Docker",
Expand Down
42 changes: 29 additions & 13 deletions app/backend/Services/ReadRetrieveReadChatService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ public ReadRetrieveReadChatService(
_tokenCredential = tokenCredential;
}

public async Task<ApproachResponse> ReplyAsync(
ChatTurn[] history,
public async Task<ChatAppResponse> ReplyAsync(
ChatMessage[] history,
RequestOverrides? overrides,
CancellationToken cancellationToken = default)
{
Expand All @@ -69,9 +69,11 @@ public async Task<ApproachResponse> ReplyAsync(
var chat = _kernel.GetRequiredService<IChatCompletionService>();
var embedding = _kernel.GetRequiredService<ITextEmbeddingGenerationService>();
float[]? embeddings = null;
var question = history.LastOrDefault()?.User is { } userQuestion
var question = history.LastOrDefault(m => m.IsUser)?.Content is { } userQuestion
? userQuestion
: throw new InvalidOperationException("Use question is null");

string[]? followUpQuestionList = null;
if (overrides?.RetrievalMode != RetrievalMode.Text && embedding is not null)
{
embeddings = (await embedding.GenerateEmbeddingAsync(question, cancellationToken: cancellationToken)).ToArray();
Expand Down Expand Up @@ -126,12 +128,15 @@ standard plan AND dental AND employee benefit.
"You are a system assistant who helps the company employees with their questions. Be brief in your answers");

// add chat history
foreach (var turn in history)
foreach (var message in history)
{
answerChat.AddUserMessage(turn.User);
if (turn.Bot is { } botMessage)
if (message.IsUser)
{
answerChat.AddUserMessage(message.Content);
}
else
{
answerChat.AddAssistantMessage(botMessage);
answerChat.AddAssistantMessage(message.Content);
}
}

Expand Down Expand Up @@ -215,17 +220,28 @@ You answer needs to be a json object with the following format.

var followUpQuestionsJson = followUpQuestions.Content ?? throw new InvalidOperationException("Failed to get search query");
var followUpQuestionsObject = JsonSerializer.Deserialize<JsonElement>(followUpQuestionsJson);
var followUpQuestionsList = followUpQuestionsObject.EnumerateArray().Select(x => x.GetString()).ToList();
var followUpQuestionsList = followUpQuestionsObject.EnumerateArray().Select(x => x.GetString()!).ToList();
foreach (var followUpQuestion in followUpQuestionsList)
{
ans += $" <<{followUpQuestion}>> ";
}

followUpQuestionList = followUpQuestionsList.ToArray();
}
return new ApproachResponse(
DataPoints: documentContentList,
Images: images,
Answer: ans,
Thoughts: thoughts,

var responseMessage = new ResponseMessage("assistant", ans);
var responseContext = new ResponseContext(
DataPointsContent: documentContentList.Select(x => new SupportingContentRecord(x.Title, x.Content)).ToArray(),
DataPointsImages: images?.Select(x => new SupportingImageRecord(x.Title, x.Url)).ToArray(),
FollowupQuestions: followUpQuestionList ?? Array.Empty<string>(),
Thoughts: new[] { new Thoughts("Thoughts", thoughts) });

var choice = new ResponseChoice(
Index: 0,
Message: responseMessage,
Context: responseContext,
CitationBaseUrl: _configuration.ToCitationBaseUrl());

return new ChatAppResponse(new[] { choice });
}
}
2 changes: 1 addition & 1 deletion app/shared/Shared/Models/Approach.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ public enum Approach
{
RetrieveThenRead,
ReadRetrieveRead,
ReadDecomposeAsk
ReadDecomposeAsk,
};
15 changes: 0 additions & 15 deletions app/shared/Shared/Models/ApproachResponse.cs

This file was deleted.

12 changes: 12 additions & 0 deletions app/shared/Shared/Models/ChatMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json.Serialization;

namespace Shared.Models;

public record ChatMessage(
[property:JsonPropertyName("role")] string Role,
[property: JsonPropertyName("content")] string Content)
{
public bool IsUser => Role == "user";
}
10 changes: 6 additions & 4 deletions app/shared/Shared/Models/ChatRequest.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json.Serialization;

namespace Shared.Models;

public record class ChatRequest(
ChatTurn[] History,
Approach Approach,
RequestOverrides? Overrides = null) : ApproachRequest(Approach)
[property: JsonPropertyName("messages")] ChatMessage[] History,
[property: JsonPropertyName("overrides")] RequestOverrides? Overrides
) : ApproachRequest(Approach.RetrieveThenRead)
{
public string? LastUserQuestion => History?.LastOrDefault()?.User;
public string? LastUserQuestion => History?.Last(m => m.Role == "user")?.Content;
}
5 changes: 0 additions & 5 deletions app/shared/Shared/Models/ChatTurn.cs

This file was deleted.

Loading

0 comments on commit e193da4

Please sign in to comment.