From 3a6c72a8be8db53642db16d160f9306064414f3a Mon Sep 17 00:00:00 2001 From: Mykyta Dubovyi Date: Sat, 14 Oct 2023 21:16:23 +0300 Subject: [PATCH] SA-29 created openai service --- .../IServices/Identity/IOpenAiService.cs | 10 ++ .../Models/OpenAi/OpenAiChoice.cs | 12 +++ .../Models/OpenAi/OpenAiDelta.cs | 8 ++ .../Models/OpenAi/OpenAiResponse.cs | 16 +++ .../Models/OpenAi/OpenAiUsage.cs | 10 ++ .../Services/Identity/OpenAiService.cs | 101 ++++++++++++++++++ ...ShoppingAssistantApi.Infrastructure.csproj | 1 + 7 files changed, 158 insertions(+) create mode 100644 ShoppingAssistantApi.Application/IServices/Identity/IOpenAiService.cs create mode 100644 ShoppingAssistantApi.Application/Models/OpenAi/OpenAiChoice.cs create mode 100644 ShoppingAssistantApi.Application/Models/OpenAi/OpenAiDelta.cs create mode 100644 ShoppingAssistantApi.Application/Models/OpenAi/OpenAiResponse.cs create mode 100644 ShoppingAssistantApi.Application/Models/OpenAi/OpenAiUsage.cs create mode 100644 ShoppingAssistantApi.Infrastructure/Services/Identity/OpenAiService.cs diff --git a/ShoppingAssistantApi.Application/IServices/Identity/IOpenAiService.cs b/ShoppingAssistantApi.Application/IServices/Identity/IOpenAiService.cs new file mode 100644 index 0000000..0f5d5d1 --- /dev/null +++ b/ShoppingAssistantApi.Application/IServices/Identity/IOpenAiService.cs @@ -0,0 +1,10 @@ +using ShoppingAssistantApi.Application.Models.OpenAi; + +namespace ShoppingAssistantApi.Application.IServices; + +public interface IOpenAiService +{ + Task GetChatCompletion(ChatCompletionRequest chat, CancellationToken cancellationToken); + + IAsyncEnumerable GetChatCompletionStream(ChatCompletionRequest chat, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/ShoppingAssistantApi.Application/Models/OpenAi/OpenAiChoice.cs b/ShoppingAssistantApi.Application/Models/OpenAi/OpenAiChoice.cs new file mode 100644 index 0000000..3ce06dc --- /dev/null +++ b/ShoppingAssistantApi.Application/Models/OpenAi/OpenAiChoice.cs @@ -0,0 +1,12 @@ +namespace ShoppingAssistantApi.Application.Models.OpenAi; + +public class OpenAiChoice +{ + public OpenAiMessage Message { get; set; } + + public OpenAiDelta Delta { get; set; } + + public string FinishReason { get; set; } + + public int Index { get; set; } +} \ No newline at end of file diff --git a/ShoppingAssistantApi.Application/Models/OpenAi/OpenAiDelta.cs b/ShoppingAssistantApi.Application/Models/OpenAi/OpenAiDelta.cs new file mode 100644 index 0000000..c9b7dbc --- /dev/null +++ b/ShoppingAssistantApi.Application/Models/OpenAi/OpenAiDelta.cs @@ -0,0 +1,8 @@ +namespace ShoppingAssistantApi.Application.Models.OpenAi; + +public class OpenAiDelta +{ + public string Role { get; set; } + + public string Content { get; set; } +} \ No newline at end of file diff --git a/ShoppingAssistantApi.Application/Models/OpenAi/OpenAiResponse.cs b/ShoppingAssistantApi.Application/Models/OpenAi/OpenAiResponse.cs new file mode 100644 index 0000000..991b854 --- /dev/null +++ b/ShoppingAssistantApi.Application/Models/OpenAi/OpenAiResponse.cs @@ -0,0 +1,16 @@ +namespace ShoppingAssistantApi.Application.Models.OpenAi; + +public class OpenAiResponse +{ + public string Id { get; set; } + + public string Object { get; set; } + + public int Created { get; set; } + + public string Model { get; set; } + + public OpenAiUsage Usage { get; set; } + + public List Choices { get; set; } +} \ No newline at end of file diff --git a/ShoppingAssistantApi.Application/Models/OpenAi/OpenAiUsage.cs b/ShoppingAssistantApi.Application/Models/OpenAi/OpenAiUsage.cs new file mode 100644 index 0000000..6789eac --- /dev/null +++ b/ShoppingAssistantApi.Application/Models/OpenAi/OpenAiUsage.cs @@ -0,0 +1,10 @@ +namespace ShoppingAssistantApi.Application.Models.OpenAi; + +public class OpenAiUsage +{ + public int PromptTokens { get; set; } + + public int CompletionTokens { get; set; } + + public int TotalTokens { get; set; } +} \ No newline at end of file diff --git a/ShoppingAssistantApi.Infrastructure/Services/Identity/OpenAiService.cs b/ShoppingAssistantApi.Infrastructure/Services/Identity/OpenAiService.cs new file mode 100644 index 0000000..88e4b50 --- /dev/null +++ b/ShoppingAssistantApi.Infrastructure/Services/Identity/OpenAiService.cs @@ -0,0 +1,101 @@ +using System; +using System.Net.Http.Headers; +using System.Text; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using ShoppingAssistantApi.Application.IServices; +using ShoppingAssistantApi.Application.Models.OpenAi; + +namespace ShoppingAssistantApi.Infrastructure.Services; + +public class OpenaiService : IOpenAiService +{ + private readonly HttpClient _httpClient; + + private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + }, + NullValueHandling = NullValueHandling.Ignore, + }; + + private readonly IConfiguration _configuration; + + //private readonly OpenAIClient _openAiClient; + + private readonly ILogger _logger; + + public OpenaiService( + IConfiguration configuration, + IHttpClientFactory httpClientFactory, + ILogger logger + ) + { + _httpClient = httpClientFactory.CreateClient("OpenAiHttpClient"); + _configuration = configuration; + + //var openAIApiKey = _configuration.GetSection("OpenAi")?.GetValue("ApiKey"); + //_openAiClient = new OpenAIClient(openAIApiKey, new OpenAIClientOptions()); + + _logger = logger; + } + + public async IAsyncEnumerable GetChatCompletionStream(ChatCompletionRequest chat, CancellationToken cancellationToken) + { + chat.Stream = true; + var jsonBody = JsonConvert.SerializeObject(chat, _jsonSettings); + _logger.LogInformation(jsonBody); + var body = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions") + { + Content = body + }; + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream")); + + using var httpResponse = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + var allData = string.Empty; + + using var streamReader = new StreamReader(await httpResponse.Content.ReadAsStringAsync(cancellationToken)); + while(!streamReader.EndOfStream) + { + var line = await streamReader.ReadLineAsync(); + allData += line + "\n\n"; + if (string.IsNullOrEmpty(line)) continue; + + var json = line?.Substring(6, line.Length - 6); + if (json == "[DONE]") yield break; + + var OpenAiResponse = JsonConvert.DeserializeObject(json, _jsonSettings); + yield return OpenAiResponse; + } + } + + public async Task GetChatCompletion(ChatCompletionRequest chat, CancellationToken cancellationToken) + { + chat.Stream = false; + var jsonBody = JsonConvert.SerializeObject(chat, _jsonSettings); + var body = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + + _logger.LogInformation(jsonBody); + using var httpResponse = await _httpClient.PostAsync("chat/completions", body, cancellationToken); + + var responseBody = await httpResponse.Content.ReadAsStringAsync(cancellationToken); + + var responses = new List(); + foreach (var line in responseBody.Split(new[] {"\n\n"}, StringSplitOptions.RemoveEmptyEntries)) + { + if (line.Trim() == "[DONE]") break; + + var json = line.Substring(6); + var OpenAiResponse = JsonConvert.DeserializeObject(json, _jsonSettings); + responses.Add(OpenAiResponse); + } + + return responses.Count > 0 ? responses.Last() : null; + } +} \ No newline at end of file diff --git a/ShoppingAssistantApi.Infrastructure/ShoppingAssistantApi.Infrastructure.csproj b/ShoppingAssistantApi.Infrastructure/ShoppingAssistantApi.Infrastructure.csproj index 6b6f722..ab6f623 100644 --- a/ShoppingAssistantApi.Infrastructure/ShoppingAssistantApi.Infrastructure.csproj +++ b/ShoppingAssistantApi.Infrastructure/ShoppingAssistantApi.Infrastructure.csproj @@ -11,6 +11,7 @@ +