Skip to content

Commit

Permalink
Improved streaming, general stability and readability of Campaign pag…
Browse files Browse the repository at this point in the history
…e code
  • Loading branch information
mirakst committed Nov 8, 2023
1 parent 9e67094 commit 520c940
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 45 deletions.
8 changes: 4 additions & 4 deletions ChatRPG/API/HttpClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

public class HttpClientFactory : IHttpClientFactory
{
private HttpMessageHandler _httpMessageHandler;
private HttpMessageHandlerFactory _httpMessageHandlerFactory;

public HttpClientFactory(HttpMessageHandler httpMessageHandler)
public HttpClientFactory(HttpMessageHandlerFactory httpMessageHandlerFactory)
{
_httpMessageHandler = httpMessageHandler;
_httpMessageHandlerFactory = httpMessageHandlerFactory;
}

public HttpClient CreateClient(string name)
{
return new HttpClient(_httpMessageHandler);
return new HttpClient(_httpMessageHandlerFactory.CreateHandler());
}
}
9 changes: 6 additions & 3 deletions ChatRPG/API/IOpenAiLlmClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ namespace ChatRPG.API;

public interface IOpenAiLlmClient
{
Task<string> GetChatCompletion(List<OpenAiGptInputMessage> inputs);
Task<string> GetChatCompletion(OpenAiGptMessage input);
Task<string> GetChatCompletion(List<OpenAiGptMessage> inputs);
IAsyncEnumerable<string> GetStreamedChatCompletion(OpenAiGptMessage input);
IAsyncEnumerable<string> GetStreamedChatCompletion(List<OpenAiGptMessage> inputs);
}

public record ChatCompletionObject(string Id, string Object, int Created, string Model, Choice[] Choices, Usage Usage);
Expand All @@ -13,6 +16,6 @@ public record Message(string Role, string Content);

public record Usage(int PromptTokens, int CompletionTokens, int TotalTokens);

public record OpenAiGptInputMessage(string Role, string Content);
public record OpenAiGptMessage(string Role, string Content);

public record OpenAiGptInput(string Model, List<OpenAiGptInputMessage> Messages, double Temperature);
public record OpenAiGptInput(string Model, List<OpenAiGptMessage> Messages, double Temperature);
28 changes: 27 additions & 1 deletion ChatRPG/API/OpenAiLlmClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@ public OpenAiLlmClient(IConfiguration configuration, IHttpClientFactory httpClie
_openAiApi.Chat.DefaultChatRequestArgs.Model = Model;
_openAiApi.Chat.DefaultChatRequestArgs.Temperature = Temperature;
}

public async Task<string> GetChatCompletion(OpenAiGptMessage input)
{
var inputList = new List<OpenAiGptMessage> {input};
return await GetChatCompletion(inputList);
}

public async Task<string> GetChatCompletion(List<OpenAiGptInputMessage> inputs)
public async Task<string> GetChatCompletion(List<OpenAiGptMessage> inputs)
{
if (inputs.IsNullOrEmpty()) throw new ArgumentNullException(nameof(inputs));

Expand All @@ -33,4 +39,24 @@ public async Task<string> GetChatCompletion(List<OpenAiGptInputMessage> inputs)

return await chat.GetResponseFromChatbotAsync();
}

public IAsyncEnumerable<string> GetStreamedChatCompletion(OpenAiGptMessage input)
{
var inputList = new List<OpenAiGptMessage> {input};
return GetStreamedChatCompletion(inputList);
}

public IAsyncEnumerable<string> GetStreamedChatCompletion(List<OpenAiGptMessage> inputs)
{
if (inputs.IsNullOrEmpty()) throw new ArgumentNullException(nameof(inputs));

var chat = _openAiApi.Chat.CreateConversation();
_openAiApi.HttpClientFactory = _httpClientFactory;
foreach (var openAiGptInputMessage in inputs)
{
chat.AppendMessage(ChatMessageRole.FromString(openAiGptInputMessage.Role), openAiGptInputMessage.Content);
}

return chat.StreamResponseEnumerableFromChatbotAsync();
}
}
15 changes: 15 additions & 0 deletions ChatRPG/ChatRPG.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,23 @@
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0-rc.2.23479.6" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.10" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.11" />
<PackageReference Include="OpenAI" Version="1.7.2" />
<PackageReference Include="RestSharp" Version="110.2.1-alpha.0.13" />
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
<_ContentIncludedByDefault Remove="ChatRPG\bin\Debug\net7.0\appsettings.Development.json" />
<_ContentIncludedByDefault Remove="ChatRPG\bin\Debug\net7.0\appsettings.json" />
<_ContentIncludedByDefault Remove="ChatRPG\bin\Debug\net7.0\ChatRPG.deps.json" />
<_ContentIncludedByDefault Remove="ChatRPG\bin\Debug\net7.0\ChatRPG.runtimeconfig.json" />
<_ContentIncludedByDefault Remove="ChatRPG\bin\Debug\net7.0\ChatRPG.staticwebassets.runtime.json" />
<_ContentIncludedByDefault Remove="ChatRPG\obj\ChatRPG.csproj.nuget.dgspec.json" />
<_ContentIncludedByDefault Remove="ChatRPG\obj\Debug\net7.0\staticwebassets.build.json" />
<_ContentIncludedByDefault Remove="ChatRPG\obj\Debug\net7.0\staticwebassets.development.json" />
<_ContentIncludedByDefault Remove="ChatRPG\obj\Debug\net7.0\staticwebassets.pack.json" />
<_ContentIncludedByDefault Remove="ChatRPG\obj\project.assets.json" />
<_ContentIncludedByDefault Remove="ChatRPG\obj\project.packagespec.json" />
</ItemGroup>

</Project>
24 changes: 24 additions & 0 deletions ChatRPG/OpenAiGptMessageComponent.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@using ChatRPG.API
@inherits ComponentBase

<div class="@($"{OpenAiGptMessage.Role}-message-container")">
<p class="@($"{OpenAiGptMessage.Role}-message")">@MessagePrefix@OpenAiGptMessage.Content</p>
</div>

@code {
[Parameter]
public OpenAiGptMessage OpenAiGptMessage { get; set; }

private string MessagePrefix
{
get
{
return OpenAiGptMessage.Role switch
{
"assistant" => "Assistant: ",
"user" => "Player: ",
_ => ""
};
}
}
}
3 changes: 2 additions & 1 deletion ChatRPG/Pages/ApiAccess.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@page "/ApiAccess"
@using ChatRPG.API
@using Microsoft.IdentityModel.Tokens
@using OpenAiGptMessage = ChatRPG.API.OpenAiGptMessage
@inject ILogger<ApiAccess> Logger
@inject IOpenAiLlmClient OpenAiLlmClient;
@inject IFoodWasteClient FoodWasteClient;
Expand Down Expand Up @@ -60,7 +61,7 @@

private async Task CallOpenAiApiWithInput()
{
var input = new List<OpenAiGptInputMessage> { new ("user", _input) };
var input = new List<OpenAiGptMessage> { new ("user", _input) };

_openAiResponse = await OpenAiLlmClient.GetChatCompletion(input);
}
Expand Down
100 changes: 66 additions & 34 deletions ChatRPG/Pages/Campaign.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
@page "/Campaign"
@using ChatRPG.Data
@using ChatRPG.API
@using Microsoft.IdentityModel.Tokens
@using OpenAiGptMessage = ChatRPG.API.OpenAiGptMessage
@inject IConfiguration Configuration
@inject IOpenAiLlmClient OpenAiLlmClient;
@inject IJSRuntime JsRuntime
Expand All @@ -14,11 +16,14 @@

<div class="conversation mx-auto col-8 mt-4 rounded p-5 text-black">
<div class="conversation-text">
@foreach (string message in _conversation)
@foreach (OpenAiGptMessage message in _conversation)
{
string prefix = message.StartsWith("Player: ") ? "user" : "assistant";
<div class="@($"{prefix}-message-container")">
<p class="@($"{prefix}-message")">@message</p>
<OpenAiGptMessageComponent OpenAiGptMessage="@message"/>
}
@if (!_tempMessage.IsNullOrEmpty())
{
<div class="@("assistant-message-container")">
<p class="@("assistant-message")">Assistant: @_tempMessage</p>
</div>
}
</div>
Expand All @@ -27,9 +32,9 @@
<div class="container">
<div class="input-group mx-auto col-8 mt-2 mb-5 input-group-border">
<input type="text" style="resize:vertical;" class="form-control user-prompt custom-text-field"
@bind="_userInput" placeholder="What do you do?" @onkeyup="@EnterKeyHandler"/>
@bind="_userInput" placeholder="What do you do?" @onkeyup="@EnterKeyHandler" disabled="@_isWaitingForResponse"/>
<div class="input-group-append">
<button class="btn btn-primary ml-2" type="button" @onclick="SendPrompt">
<button class="btn btn-primary ml-2" type="button" @onclick="SendPrompt" disabled="@_isWaitingForResponse">
Send
</button>
</div>
Expand All @@ -43,16 +48,23 @@
private bool _shouldSave;
private IJSObjectReference? _scrollJsScript;
private FileUtility? _fileUtil;
readonly List<string> _conversation = new List<string>();
readonly List<OpenAiGptMessage> _conversation = new();
private string _userInput = "";
private string _tempMessage = "";
private bool _shouldStream;
private bool _isWaitingForResponse;

protected override async Task OnInitializedAsync()
protected override async Task OnAfterRenderAsync(bool firstRender)
{
AuthenticationState authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
_loggedInUsername = authenticationState.User.Identity?.Name;
if (_loggedInUsername != null) _fileUtil = new FileUtility(_loggedInUsername);
_shouldSave = Configuration.GetValue<bool>("SaveConversationsToFile");
_scrollJsScript = await JsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/scroll.js");
if (firstRender)
{
AuthenticationState authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
_loggedInUsername = authenticationState.User.Identity?.Name;
if (_loggedInUsername != null) _fileUtil = new FileUtility(_loggedInUsername);
_shouldSave = Configuration.GetValue<bool>("SaveConversationsToFile");
_scrollJsScript ??= await JsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/scroll.js");
}
_shouldStream = !Configuration.GetValue<bool>("UseMocks") && Configuration.GetValue<bool>("StreamChatCompletions");
}

async Task EnterKeyHandler(KeyboardEventArgs e)
Expand All @@ -65,33 +77,53 @@

private async Task SendPrompt()
{
if (!string.IsNullOrWhiteSpace(_userInput))
if (string.IsNullOrWhiteSpace(_userInput))
{
// Use the user input to generate a response
_conversation.Add($"Player: {_userInput}");
string response = await ProcessUserInput(_userInput);
_conversation.Add($"Assistant: {response}");
if (_shouldSave && _fileUtil != null)
{
await _fileUtil.UpdateSaveFileAsync(new MessagePair(_userInput, response));
}
return;
}
_isWaitingForResponse = true;

// Clear the user input
_userInput = "";
var userInput = new OpenAiGptMessage("user", _userInput);
_conversation.Add(userInput);

if (!_shouldStream)
{
string response = await OpenAiLlmClient.GetChatCompletion(userInput);
HandleResponse(response);
}
else
{
await HandleStreamedResponse(OpenAiLlmClient.GetStreamedChatCompletion(userInput));
}

if (_shouldSave && _fileUtil != null)
{
string assistantOutput = _conversation.Last().Content;
await _fileUtil.UpdateSaveFileAsync(new MessagePair(_userInput, assistantOutput));
}

// Notify the component to re-render because of async methods
_userInput = "";
StateHasChanged();
await ScrollToElement("bottom-id");
_isWaitingForResponse = false;
}

private void HandleResponse(string response)
{
var assistantOutput = new OpenAiGptMessage("assistant", response);
_conversation.Add(assistantOutput);
}

private async Task HandleStreamedResponse(IAsyncEnumerable<string> streamedResponse)
{
await foreach (var res in streamedResponse)
{
_tempMessage += res;
StateHasChanged();

// Scroll to bottom of page
await ScrollToElement("bottom-id");
}
}

async Task<string> ProcessUserInput(string input)
{
List<OpenAiGptInputMessage> inputMessage = new List<OpenAiGptInputMessage> { new OpenAiGptInputMessage("user", input) };

return await OpenAiLlmClient.GetChatCompletion(inputMessage);
HandleResponse(_tempMessage);
_tempMessage = "";
}

private async Task ScrollToElement(string elementId)
Expand Down
2 changes: 1 addition & 1 deletion ChatRPG/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

var httpMessageHandlerFactory = new HttpMessageHandlerFactory(configuration);
builder.Services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<User>>()
.AddSingleton<HttpMessageHandler>(_ => httpMessageHandlerFactory.CreateHandler())
.AddSingleton(httpMessageHandlerFactory)
.AddSingleton<IHttpClientFactory, HttpClientFactory>()
.AddSingleton<IOpenAiLlmClient, OpenAiLlmClient>()
.AddSingleton<IFoodWasteClient, SallingClient>()
Expand Down
3 changes: 2 additions & 1 deletion ChatRPG/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
}
},
"AllowedHosts": "*",
"SaveConversationsToFile": true
"SaveConversationsToFile": true,
"StreamChatCompletions": true
}

0 comments on commit 520c940

Please sign in to comment.