Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/open ai api improvements #36

Merged
merged 12 commits into from
Nov 9, 2023
16 changes: 16 additions & 0 deletions ChatRPG/API/HttpClientFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace ChatRPG.API;

public class HttpClientFactory : IHttpClientFactory
{
private HttpMessageHandlerFactory _httpMessageHandlerFactory;

public HttpClientFactory(HttpMessageHandlerFactory httpMessageHandlerFactory)
{
_httpMessageHandlerFactory = httpMessageHandlerFactory;
}

public HttpClient CreateClient(string name)
{
return new HttpClient(_httpMessageHandlerFactory.CreateHandler());
}
}
10 changes: 5 additions & 5 deletions ChatRPG/API/HttpMessageHandlerFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@
return new HttpClientHandler();
}

var mockHttpMessageHandler = new MockHttpMessageHandler();
mockHttpMessageHandler.When("*")
MockHttpMessageHandler messageHandler = new MockHttpMessageHandler();
messageHandler.When("*")
.Respond(GenerateMockResponse);

return mockHttpMessageHandler;
return messageHandler;
}

private static HttpResponseMessage GenerateMockResponse(HttpRequestMessage request)
{
Console.Write("Please enter mocked API response: ");
var input = Console.ReadLine();
var responseContent = new StringContent($$"""
string input = Console.ReadLine();

Check warning on line 31 in ChatRPG/API/HttpMessageHandlerFactory.cs

View workflow job for this annotation

GitHub Actions / build

Converting null literal or possible null value to non-nullable type.
mirakst marked this conversation as resolved.
Show resolved Hide resolved
StringContent responseContent = new StringContent($$"""
{
"id": "chatcmpl-000",
"object": "chat.completion",
Expand Down
21 changes: 0 additions & 21 deletions ChatRPG/API/IFoodWasteClient.cs

This file was deleted.

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<ChatCompletionObject> 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);
KarmaKamikaze marked this conversation as resolved.
Show resolved Hide resolved
}

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);
113 changes: 37 additions & 76 deletions ChatRPG/API/OpenAiLlmClient.cs
Original file line number Diff line number Diff line change
@@ -1,101 +1,62 @@
using Microsoft.IdentityModel.Tokens;
using RestSharp;
using RestSharp.Authenticators;
using OpenAI_API;
using OpenAI_API.Chat;

namespace ChatRPG.API;

public class OpenAiLlmClient : IOpenAiLlmClient, IDisposable
public class OpenAiLlmClient : IOpenAiLlmClient
{
private const string OpenAiBaseUrl = "https://api.openai.com/v1/";
private const string Model = "gpt-3.5-turbo";
private const double Temperature = 0.7;
private const double PromptToken1KCost = 0.0015;
private const double CompletionToken1KCost = 0.002;

private readonly ILogger<OpenAiLlmClient> _logger;
private readonly RestClient _client;
private readonly IHttpClientFactory _httpClientFactory;
private readonly OpenAIAPI _openAiApi;

public OpenAiLlmClient(ILogger<OpenAiLlmClient> logger, IConfiguration configuration,
HttpMessageHandler httpMessageHandler)
public OpenAiLlmClient(IConfiguration configuration, IHttpClientFactory httpClientFactory)
{
_logger = logger;

var options = new RestClientOptions(OpenAiBaseUrl)
{
Authenticator = new JwtAuthenticator(configuration.GetSection("ApiKeys").GetValue<string>("OpenAI") ?? string.Empty),
FailOnDeserializationError = false,
ConfigureMessageHandler = _ => httpMessageHandler
};
_client = new RestClient(options);
_httpClientFactory = httpClientFactory;
_openAiApi = new OpenAIAPI(configuration.GetSection("ApiKeys").GetValue<string>("OpenAI") ?? string.Empty);
damniko marked this conversation as resolved.
Show resolved Hide resolved
_openAiApi.Chat.DefaultChatRequestArgs.Model = Model;
_openAiApi.Chat.DefaultChatRequestArgs.Temperature = Temperature;
}

public async Task<ChatCompletionObject> GetChatCompletion(List<OpenAiGptInputMessage> inputs)
public async Task<string> GetChatCompletion(OpenAiGptMessage input)
{
if (inputs.IsNullOrEmpty()) throw new ArgumentNullException(nameof(inputs));
List<OpenAiGptMessage> inputList = new List<OpenAiGptMessage> { input };
return await GetChatCompletion(inputList);
}

var openAiGptInput = new OpenAiGptInput(Model, inputs, Temperature);
public async Task<string> GetChatCompletion(List<OpenAiGptMessage> inputs)
damniko marked this conversation as resolved.
Show resolved Hide resolved
{
Conversation chat = CreateConversation(inputs);

var request = new RestRequest("chat/completions", Method.Post);
request.AddJsonBody(openAiGptInput, ContentType.Json);
return await chat.GetResponseFromChatbotAsync();
}

_logger.LogInformation("""
Request URL: {Url}
Method: {Method}
Parameters: {Parameters}
Messages: {Messages}
""",
OpenAiBaseUrl + request.Resource,
request.Method,
string.Join(", ", request.Parameters.Select(p => $"{p.Name}={p.Value}")),
string.Join(", ", inputs.Select(input => input.Content))
);
public IAsyncEnumerable<string> GetStreamedChatCompletion(OpenAiGptMessage input)
{
List<OpenAiGptMessage> inputList = new List<OpenAiGptMessage> { input };
return GetStreamedChatCompletion(inputList);
}

var response = await _client.ExecuteAsync<ChatCompletionObject>(request);
public IAsyncEnumerable<string> GetStreamedChatCompletion(List<OpenAiGptMessage> inputs)
{
Conversation chat = CreateConversation(inputs);

if (response.ErrorException != null)
{
_logger.LogError("Error retrieving data from API: {ErrorExceptionMessage}", response.ErrorException.Message);
}
return chat.StreamResponseEnumerableFromChatbotAsync();
}

var data = response!.Data;
private Conversation CreateConversation(List<OpenAiGptMessage> inputs)
{
if (inputs.IsNullOrEmpty()) throw new ArgumentNullException(nameof(inputs));

if (data is null)
Conversation chat = _openAiApi.Chat.CreateConversation();
_openAiApi.HttpClientFactory = _httpClientFactory;
damniko marked this conversation as resolved.
Show resolved Hide resolved
foreach (OpenAiGptMessage openAiGptInputMessage in inputs)
{
throw new EmptyResponseException(response, "The API response has no data.");
chat.AppendMessage(ChatMessageRole.FromString(openAiGptInputMessage.Role), openAiGptInputMessage.Content);
}

var promptTokens = data.Usage.PromptTokens;
var completionTokens = data.Usage.CompletionTokens;

var promptCost = (promptTokens / 1000.0) * PromptToken1KCost;
var completionCost = (completionTokens / 1000.0) * CompletionToken1KCost;
var estimatedCost = promptCost + completionCost;

_logger.LogInformation("""
Prompt tokens: {PTokens}
Completion tokens: {CTokens}
Estimated cost: {EstCost}
""",
promptTokens,
completionTokens,
"$" + estimatedCost
);

return data;
}

public void Dispose()
{
_client.Dispose();
GC.SuppressFinalize(this);
}
}

public class EmptyResponseException : Exception
{
public RestResponse Response;
public EmptyResponseException(RestResponse response, string message) : base(message)
{
Response = response;
return chat;
}
}
55 changes: 0 additions & 55 deletions ChatRPG/API/SallingClient.cs

This file was deleted.

1 change: 0 additions & 1 deletion ChatRPG/Areas/Identity/Pages/Account/LogOut.cshtml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
@page
@using Microsoft.AspNetCore.Identity
@attribute [IgnoreAntiforgeryToken]
@inject SignInManager<IdentityUser> SignInManager
@functions {
Expand Down
3 changes: 2 additions & 1 deletion ChatRPG/Areas/Identity/Pages/Account/Login.cshtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@page
@using Microsoft.AspNetCore.Authentication
@model LoginModel

@{
Expand Down Expand Up @@ -65,7 +66,7 @@
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
<div>
<p>
@foreach (var provider in Model.ExternalLogins!)
@foreach (AuthenticationScheme provider in Model.ExternalLogins!)
{
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
Expand Down
10 changes: 2 additions & 8 deletions ChatRPG/Areas/Identity/Pages/Account/Login.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using ChatRPG.Data.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;

namespace ChatRPG.Areas.Identity.Pages.Account
{
Expand Down Expand Up @@ -112,7 +106,7 @@ public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
SignInResult result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
Expand Down
3 changes: 2 additions & 1 deletion ChatRPG/Areas/Identity/Pages/Account/Register.cshtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@page
@using Microsoft.AspNetCore.Authentication
@model RegisterModel
@{
ViewData["Title"] = "Register";
Expand Down Expand Up @@ -49,7 +50,7 @@
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
<div>
<p>
@foreach (var provider in Model.ExternalLogins!)
@foreach (AuthenticationScheme provider in Model.ExternalLogins!)
{
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
Expand Down
Loading