diff --git a/OpenAI.ChatGpt.AspNetCore/ChatGPTFactory.cs b/OpenAI.ChatGpt.AspNetCore/ChatGPTFactory.cs index 95f4ef2..268e24d 100644 --- a/OpenAI.ChatGpt.AspNetCore/ChatGPTFactory.cs +++ b/OpenAI.ChatGpt.AspNetCore/ChatGPTFactory.cs @@ -18,7 +18,7 @@ namespace OpenAI.ChatGpt.AspNetCore; // ReSharper disable once InconsistentNaming public class ChatGPTFactory : IDisposable { - private readonly OpenAiClient _client; + private readonly IOpenAiClient _client; private readonly ChatGPTConfig _config; private readonly IChatHistoryStorage _chatHistoryStorage; private readonly ITimeProvider _clock; diff --git a/OpenAI.ChatGpt.AspNetCore/OpenAI.ChatGpt.AspNetCore.csproj b/OpenAI.ChatGpt.AspNetCore/OpenAI.ChatGpt.AspNetCore.csproj index b09c8fb..f9b9de9 100644 --- a/OpenAI.ChatGpt.AspNetCore/OpenAI.ChatGpt.AspNetCore.csproj +++ b/OpenAI.ChatGpt.AspNetCore/OpenAI.ChatGpt.AspNetCore.csproj @@ -8,7 +8,7 @@ OpenAI.ChatGPT.AspNetCore https://github.com/rodion-m/ChatGPT_API_dotnet OpenAI ChatGPT integration for .NET with DI - 2.5.0 + 2.6.0 OpenAI Chat Completions API (ChatGPT) integration with easy DI supporting (Microsoft.Extensions.DependencyInjection). It allows you to use the API in your .NET applications. Also, the client supports streaming responses (like ChatGPT) via async streams. https://github.com/rodion-m/ChatGPT_API_dotnet net6.0;net7.0 diff --git a/OpenAI.ChatGpt.EntityFrameworkCore/OpenAI.ChatGpt.EntityFrameworkCore.csproj b/OpenAI.ChatGpt.EntityFrameworkCore/OpenAI.ChatGpt.EntityFrameworkCore.csproj index 6069f22..f62760d 100644 --- a/OpenAI.ChatGpt.EntityFrameworkCore/OpenAI.ChatGpt.EntityFrameworkCore.csproj +++ b/OpenAI.ChatGpt.EntityFrameworkCore/OpenAI.ChatGpt.EntityFrameworkCore.csproj @@ -9,7 +9,7 @@ OpenAI.ChatGPT.EntityFrameworkCore https://github.com/rodion-m/ChatGPT_API_dotnet OpenAI ChatGPT integration for .NET with EF Core storage - 2.5.0 + 2.6.0 OpenAI Chat Completions API (ChatGPT) integration with DI and EF Core supporting. It allows you to use the API in your .NET applications. Also, the client supports streaming responses (like ChatGPT) via async streams. https://github.com/rodion-m/ChatGPT_API_dotnet net6.0;net7.0 diff --git a/OpenAI.ChatGpt.Extensions/OpenAI.ChatGpt.Extensions.csproj b/OpenAI.ChatGpt.Extensions/OpenAI.ChatGpt.Extensions.csproj deleted file mode 100644 index c494020..0000000 --- a/OpenAI.ChatGpt.Extensions/OpenAI.ChatGpt.Extensions.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - Exe - net7.0 - enable - enable - - - - - - - - - - - diff --git a/OpenAI.ChatGpt.Extensions/Program.cs b/OpenAI.ChatGpt.Extensions/Program.cs deleted file mode 100644 index 415b639..0000000 --- a/OpenAI.ChatGpt.Extensions/Program.cs +++ /dev/null @@ -1,14 +0,0 @@ -using OpenAI.ChatGpt; -using OpenAI.ChatGpt.Extensions; - -var client = new OpenAiClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")!); - -var testsCode = await client.GenerateTestsForCodeInFolder( -@"C:\Users\rodio\RiderProjects\OpenAI_DotNet\OpenAI", -"C#", "xunit and FluentAssertions" -); -//var testsCode = await client.GenerateTestsForCode("int Add(int a, int b) => a + b;", "C#", "xunit and FluentAssertions"); - -File.WriteAllText("test.txt", testsCode); - -Console.WriteLine(testsCode); \ No newline at end of file diff --git a/OpenAI.ChatGpt.Extensions/SourceFiles.cs b/OpenAI.ChatGpt.Extensions/SourceFiles.cs deleted file mode 100644 index 80e0f1f..0000000 --- a/OpenAI.ChatGpt.Extensions/SourceFiles.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System.Collections; -using System.Collections.Concurrent; -using CSharpMinifier; - -namespace OpenAI.ChatGpt.Extensions; - -public class SourceFiles : IEnumerable -{ - private IEnumerable _sources; - - public SourceFiles(IEnumerable sources) - { - _sources = sources ?? throw new ArgumentNullException(nameof(sources)); - } - - public static SourceFiles ReadFilesFromDirectory(string directoryPath) - { - var files = Directory.GetFiles(directoryPath, "*.cs", SearchOption.AllDirectories); - return new SourceFiles(files.Select( - file => new SourceFile(file, File.ReadAllText(file))) - ); - } - - public static async Task ReadFilesFromDirectoryAsyncInParallel( - string directoryPath, int? degreeOfParallelism = null, CancellationToken cancellationToken = default) - { - degreeOfParallelism ??= Environment.ProcessorCount / 2; - var files = Directory.GetFiles(directoryPath, "*.cs", SearchOption.AllDirectories); - var sourceFiles = new ConcurrentBag(); - - var options = new ParallelOptions() - { - MaxDegreeOfParallelism = degreeOfParallelism.Value, - CancellationToken = cancellationToken - }; - await Parallel.ForEachAsync(files, options, - async (file, _) => - { - var content = await File.ReadAllTextAsync(file, _); - sourceFiles.Add(new SourceFile(file, content)); - }); - - return new SourceFiles(sourceFiles); - } - - public void MakeList() - { - _sources = _sources.ToList(); - } - - public void Filter( - bool excludeAutoGeneratedFiles, - bool removeConfigureAwait, - bool minify) - { - if (excludeAutoGeneratedFiles) - { - ExcludeAutoGeneratedFiles(); - } - - if (minify) - { - Minify(); - } - - if (removeConfigureAwait) - { - RemoveConfigureAwait(); - } - } - - public void RemoveConfigureAwait() - { - _sources = _sources.Select(file - => file with { Code = file.Code.Replace(".ConfigureAwait(false)", "") }); - } - - public void Minify() - { - // TODO join all usings to one, regex: ^\s*(using\s+[a-zA-Z0-9_.]+)\s*;\s* - _sources = _sources.Select(file - => file with { Code = string.Join(' ', Minifier.Minify(file.Code)) }); - } - - public void ExcludeAutoGeneratedFiles() - { - _sources = _sources.Where(file => !file.IsAutoGenerated()); - } - - public string GetCode() - { - var codeWithFileNames = _sources.Select(file => - { - var header = $"// File: {Path.GetFileName(file.FileName)}"; - return header + Environment.NewLine + file.Code - + Environment.NewLine + Environment.NewLine; - }); - var code = string.Join(Environment.NewLine, codeWithFileNames); - return code; - } - - public IEnumerator GetEnumerator() => _sources.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public record SourceFile(string FileName, string Code) - { - public string Code { get; set; } = Code; - - public bool IsAutoGenerated() - { - return Code.Contains(" !file.FileName.Split(Path.DirectorySeparatorChar).Contains("Migrations")); - } - - public static SourceFiles FromText(string input) - { - return new SourceFiles(new[] { new SourceFile("Program.cs", input) }); - } -} \ No newline at end of file diff --git a/OpenAI.ChatGpt.Extensions/TestsGeneratorExtension.cs b/OpenAI.ChatGpt.Extensions/TestsGeneratorExtension.cs deleted file mode 100644 index a7e9b8c..0000000 --- a/OpenAI.ChatGpt.Extensions/TestsGeneratorExtension.cs +++ /dev/null @@ -1,62 +0,0 @@ -using OpenAI.ChatGpt.Models.ChatCompletion; - -namespace OpenAI.ChatGpt.Extensions; - -public static class TestsGeneratorExtension -{ - public static async Task GenerateTestsForCodeInFolder( - this OpenAiClient client, - string directoryPath, - string language = "C#", - string framework = "xunit", - string model = ChatCompletionModels.Default, - bool excludeAutoGeneratedFiles = true, - CancellationToken cancellationToken = default) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (directoryPath == null) throw new ArgumentNullException(nameof(directoryPath)); - if (language == null) throw new ArgumentNullException(nameof(language)); - if (framework == null) throw new ArgumentNullException(nameof(framework)); - if (model == null) throw new ArgumentNullException(nameof(model)); - if (!Directory.Exists(directoryPath)) - { - throw new ArgumentException($"Directory {directoryPath} does not exist"); - } - - var files = SourceFiles.ReadFilesFromDirectory(directoryPath); - files.Filter(excludeAutoGeneratedFiles, true, true); - var result = await client.GenerateTestsForCode(files.GetCode(), language, framework, model, cancellationToken); - return result; - } - - public static async Task GenerateTestsForCode( - this OpenAiClient client, - string code, - string language = "C#", - string framework = "xunit", - string model = ChatCompletionModels.Default, - CancellationToken cancellationToken = default) - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (code == null) throw new ArgumentNullException(nameof(code)); - if (language == null) throw new ArgumentNullException(nameof(language)); - if (framework == null) throw new ArgumentNullException(nameof(framework)); - if (model == null) throw new ArgumentNullException(nameof(model)); - - var dialog = Dialog.StartAsSystem( - "You are a test generator assistant. The user send the whole program code " + - $"and you generate tests on {language} using {framework}" + - "with > 90% coverage for given files. In your answers write ONLY the code.") - .ThenUser(code); - - var result = await client.GetChatCompletions( - dialog, - model: model, - maxTokens: ChatCompletionModels.GetMaxTokensLimitForModel(model), - cancellationToken: cancellationToken - ); - - return result; - } - -} \ No newline at end of file diff --git a/OpenAI.ChatGpt.Modules.Translator/ChatGPTTranslatorService.cs b/OpenAI.ChatGpt.Modules.Translator/ChatGPTTranslatorService.cs new file mode 100644 index 0000000..12010f4 --- /dev/null +++ b/OpenAI.ChatGpt.Modules.Translator/ChatGPTTranslatorService.cs @@ -0,0 +1,75 @@ +using OpenAI.ChatGpt.Models.ChatCompletion; + +namespace OpenAI.ChatGpt.Modules.Translator; + +public class ChatGPTTranslatorService : IDisposable +{ + private readonly IOpenAiClient _client; + private readonly string? _defaultSourceLanguage; + private readonly string? _defaultTargetLanguage; + private readonly string? _extraPrompt; + private readonly bool _isHttpClientInjected; + + public ChatGPTTranslatorService( + IOpenAiClient client, + string? defaultSourceLanguage = null, + string? defaultTargetLanguage = null, + string? extraPrompt = null) + { + _client = client ?? throw new ArgumentNullException(nameof(client)); + _isHttpClientInjected = true; + _defaultSourceLanguage = defaultSourceLanguage; + _defaultTargetLanguage = defaultTargetLanguage; + _extraPrompt = extraPrompt; + } + + public ChatGPTTranslatorService( + string apiKey, + string? host, + string? defaultSourceLanguage = null, + string? defaultTargetLanguage = null, + string? extraPrompt = null) + { + ArgumentNullException.ThrowIfNull(apiKey); + _client = new OpenAiClient(apiKey, host); + _defaultSourceLanguage = defaultSourceLanguage; + _defaultTargetLanguage = defaultTargetLanguage; + _extraPrompt = extraPrompt; + } + + public void Dispose() + { + if (!_isHttpClientInjected) + { + _client.Dispose(); + } + } + + public async Task Translate( + string text, + string? sourceLanguage = null, + string? targetLanguage = null, + Action? requestModifier = null, + CancellationToken cancellationToken = default) + { + if (text == null) throw new ArgumentNullException(nameof(text)); + var sourceLanguageOrDefault = sourceLanguage ?? _defaultSourceLanguage; + var targetLanguageOrDefault = targetLanguage ?? _defaultTargetLanguage; + var prompt = GetPrompt(sourceLanguageOrDefault, targetLanguageOrDefault); + var response = await _client.GetChatCompletions( + Dialog.StartAsSystem(prompt).ThenUser(text), + user: null, + requestModifier: requestModifier, + cancellationToken: cancellationToken + ); + return response; + } + + private string GetPrompt(string sourceLanguage, string targetLanguage) + { + return $"I want you to act as a translator from {sourceLanguage} to {targetLanguage}. " + + "I will provide you with an English sentence and you will translate it into Russian. " + + "In the response write ONLY translated text." + + (_extraPrompt is not null ? "\n" + _extraPrompt : ""); + } +} \ No newline at end of file diff --git a/OpenAI.ChatGpt.Modules.Translator/OpenAI.ChatGpt.Modules.Translator.csproj b/OpenAI.ChatGpt.Modules.Translator/OpenAI.ChatGpt.Modules.Translator.csproj new file mode 100644 index 0000000..71baa42 --- /dev/null +++ b/OpenAI.ChatGpt.Modules.Translator/OpenAI.ChatGpt.Modules.Translator.csproj @@ -0,0 +1,32 @@ + + + + enable + enable + 11 + Rodion Mostovoi + true + OpenAI.ChatGPT.Modules.Translator + https://github.com/rodion-m/ChatGPT_API_dotnet + OpenAI ChatGPT based language translator + 2.6.0 + OpenAI ChatGPT based language translator. + https://github.com/rodion-m/ChatGPT_API_dotnet + net6.0;net7.0 + chatgpt, openai, sdk, api, chatcompletions, gpt3, gpt4, translator + MIT + OpenAI ChatGPT based language translator + Rodion Mostovoi + + + + enable + enable + net7.0;net6.0 + + + + + + + diff --git a/OpenAI.ChatGpt/ChatGPT.cs b/OpenAI.ChatGpt/ChatGPT.cs index e4281b6..202840a 100644 --- a/OpenAI.ChatGpt/ChatGPT.cs +++ b/OpenAI.ChatGpt/ChatGPT.cs @@ -14,7 +14,7 @@ public class ChatGPT : IDisposable private readonly IChatHistoryStorage _storage; private readonly ITimeProvider _clock; private readonly ChatGPTConfig? _config; - private readonly OpenAiClient _client; + private readonly IOpenAiClient _client; private ChatService? _currentChat; private static readonly string NoUser = Guid.Empty.ToString(); @@ -24,7 +24,7 @@ public class ChatGPT : IDisposable /// Use this constructor to create chat conversation provider for the specific user. /// public ChatGPT( - OpenAiClient client, + IOpenAiClient client, IChatHistoryStorage chatHistoryStorage, ITimeProvider clock, string userId, @@ -42,7 +42,7 @@ public ChatGPT( /// If you don't have users use this ChatGPT constructor. /// public ChatGPT( - OpenAiClient client, + IOpenAiClient client, IChatHistoryStorage chatHistoryStorage, ITimeProvider clock, ChatGPTConfig? config) diff --git a/OpenAI.ChatGpt/ChatService.cs b/OpenAI.ChatGpt/ChatService.cs index 605d59e..5435b9b 100644 --- a/OpenAI.ChatGpt/ChatService.cs +++ b/OpenAI.ChatGpt/ChatService.cs @@ -26,15 +26,15 @@ public class ChatService : IDisposable, IAsyncDisposable private readonly IChatHistoryStorage _chatHistoryStorage; private readonly ITimeProvider _clock; - private readonly OpenAiClient _client; - private bool _isNew; + private readonly IOpenAiClient _client; private readonly bool _clearOnDisposal; private CancellationTokenSource? _cts; + private bool _isNew; internal ChatService( IChatHistoryStorage chatHistoryStorage, ITimeProvider clock, - OpenAiClient client, + IOpenAiClient client, string userId, Topic topic, bool isNew, @@ -84,7 +84,8 @@ private async Task GetNextMessageResponse( { if (IsWriting) { - throw new InvalidOperationException("Cannot start a new chat session while the previous one is still in progress."); + throw new InvalidOperationException( + "Cannot start a new chat session while the previous one is still in progress."); } var originalCancellation = cancellationToken; _cts?.Dispose(); diff --git a/OpenAI.ChatGpt/IOpenAiClient.cs b/OpenAI.ChatGpt/IOpenAiClient.cs new file mode 100644 index 0000000..36b0403 --- /dev/null +++ b/OpenAI.ChatGpt/IOpenAiClient.cs @@ -0,0 +1,89 @@ +using System.Runtime.CompilerServices; +using OpenAI.ChatGpt.Models.ChatCompletion; +using OpenAI.ChatGpt.Models.ChatCompletion.Messaging; + +namespace OpenAI.ChatGpt; + +public interface IOpenAiClient : IDisposable +{ + Task GetChatCompletions( + UserOrSystemMessage dialog, + int maxTokens = ChatCompletionRequest.MaxTokensDefault, + string model = ChatCompletionModels.Default, + float temperature = ChatCompletionTemperatures.Default, + string? user = null, + Action? requestModifier = null, + CancellationToken cancellationToken = default); + + Task GetChatCompletions( + IEnumerable messages, + int maxTokens = ChatCompletionRequest.MaxTokensDefault, + string model = ChatCompletionModels.Default, + float temperature = ChatCompletionTemperatures.Default, + string? user = null, + Action? requestModifier = null, + CancellationToken cancellationToken = default); + + Task GetChatCompletionsRaw( + IEnumerable messages, + int maxTokens = ChatCompletionRequest.MaxTokensDefault, + string model = ChatCompletionModels.Default, + float temperature = ChatCompletionTemperatures.Default, + string? user = null, + Action? requestModifier = null, + CancellationToken cancellationToken = default); + + /// + /// Start streaming chat completions like ChatGPT + /// + /// The history of messaging + /// The length of the response + /// One of + /// + /// What sampling temperature to use, between 0 and 2. + /// Higher values like 0.8 will make the output more random, + /// while lower values like 0.2 will make it more focused and deterministic. + /// + /// + /// A unique identifier representing your end-user, which can help OpenAI to monitor + /// and detect abuse. + /// + /// A modifier of the raw request. Allows to specify any custom properties. + /// Cancellation token. + /// Chunks of ChatGPT's response, one by one. + IAsyncEnumerable StreamChatCompletions( + IEnumerable messages, + int maxTokens = ChatCompletionRequest.MaxTokensDefault, + string model = ChatCompletionModels.Default, + float temperature = ChatCompletionTemperatures.Default, + string? user = null, + Action? requestModifier = null, + CancellationToken cancellationToken = default); + + /// + /// Start streaming chat completions like ChatGPT + /// + /// The history of messaging + /// The length of the response + /// One of + /// > + /// + /// Request modifier + /// Cancellation token + /// Chunks of ChatGPT's response, one by one + IAsyncEnumerable StreamChatCompletions( + UserOrSystemMessage messages, + int maxTokens = ChatCompletionRequest.MaxTokensDefault, + string model = ChatCompletionModels.Default, + float temperature = ChatCompletionTemperatures.Default, + string? user = null, + Action? requestModifier = null, + CancellationToken cancellationToken = default); + + IAsyncEnumerable StreamChatCompletions( + ChatCompletionRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken = default); + + IAsyncEnumerable StreamChatCompletionsRaw( + ChatCompletionRequest request, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/OpenAI.ChatGpt/Models/ChatCompletion/ChatCompletionModels.cs b/OpenAI.ChatGpt/Models/ChatCompletion/ChatCompletionModels.cs index a27ced1..be486bc 100644 --- a/OpenAI.ChatGpt/Models/ChatCompletion/ChatCompletionModels.cs +++ b/OpenAI.ChatGpt/Models/ChatCompletion/ChatCompletionModels.cs @@ -119,7 +119,6 @@ public static class ChatCompletionModels /// private static readonly Dictionary MaxTokensLimits = new() { - { Gpt4, 8192 }, { Gpt4, 8192 }, { Gpt4_0613, 8192 }, { Gpt4_32k, 32768 }, diff --git a/OpenAI.ChatGpt/OpenAI.ChatGpt.csproj b/OpenAI.ChatGpt/OpenAI.ChatGpt.csproj index 093a989..230398b 100644 --- a/OpenAI.ChatGpt/OpenAI.ChatGpt.csproj +++ b/OpenAI.ChatGpt/OpenAI.ChatGpt.csproj @@ -10,7 +10,7 @@ OpenAI.ChatGPT https://github.com/rodion-m/ChatGPT_API_dotnet OpenAI ChatGPT integration for .NET - 2.5.0 + 2.6.0 .NET integration for ChatGPT with streaming responses supporting (like ChatGPT) via async streams. https://github.com/rodion-m/ChatGPT_API_dotnet MIT @@ -22,12 +22,12 @@ - + - - + + diff --git a/OpenAI.ChatGpt/OpenAIClient.cs b/OpenAI.ChatGpt/OpenAIClient.cs index 7b5a596..bef7f93 100644 --- a/OpenAI.ChatGpt/OpenAIClient.cs +++ b/OpenAI.ChatGpt/OpenAIClient.cs @@ -12,11 +12,13 @@ namespace OpenAI.ChatGpt; /// Thread-safe OpenAI client. [Fody.ConfigureAwait(false)] -public class OpenAiClient : IDisposable +public class OpenAiClient : IDisposable, IOpenAiClient { private const string DefaultHost = "https://api.openai.com/v1/"; private const string ImagesEndpoint = "images/generations"; private const string ChatCompletionsEndpoint = "chat/completions"; + + private static readonly Uri DefaultHostUri = new(DefaultHost); private readonly HttpClient _httpClient; private readonly bool _isHttpClientInjected; @@ -31,22 +33,33 @@ public class OpenAiClient : IDisposable /// /// OpenAI API key. Can be issued here: https://platform.openai.com/account/api-keys /// Open AI API host. Default is: - public OpenAiClient(string apiKey, string host = DefaultHost) + public OpenAiClient(string apiKey, string? host = DefaultHost) { if (string.IsNullOrWhiteSpace(apiKey)) - throw new ArgumentException("Value cannot be null or whitespace.", nameof(apiKey)); - ArgumentNullException.ThrowIfNull(host); - if(!Uri.TryCreate(host, UriKind.Absolute, out _) || !host.EndsWith('/')) - throw new ArgumentException("Host must be a valid absolute URI and end with a slash." + - $"For example: {DefaultHost}", nameof(host)); + throw new ArgumentException("API key cannot be null or whitespace.", nameof(apiKey)); + var uri = ValidateHost(host); + _httpClient = new HttpClient() { - BaseAddress = new Uri(host) + BaseAddress = uri }; var header = new AuthenticationHeaderValue("Bearer", apiKey); _httpClient.DefaultRequestHeaders.Authorization = header; } + private static Uri ValidateHost(string? host) + { + if (host is null) return DefaultHostUri; + if (!Uri.TryCreate(host, UriKind.Absolute, out var uri)) + { + throw new ArgumentException("Host must be a valid absolute URI and end with a slash." + + $"For example: {DefaultHost}", nameof(host)); + } + if(!host.EndsWith("/")) uri = new Uri(host + "/"); + + return uri; + } + /// /// Creates a new OpenAI client from DI with given . /// @@ -66,6 +79,7 @@ public OpenAiClient(HttpClient httpClient) private static void ValidateHttpClient(HttpClient httpClient) { + ArgumentNullException.ThrowIfNull(httpClient); if (httpClient.DefaultRequestHeaders.Authorization is null) { throw new ArgumentException( @@ -83,6 +97,14 @@ private static void ValidateHttpClient(HttpClient httpClient) nameof(httpClient) ); } + if(!httpClient.BaseAddress.AbsoluteUri.EndsWith("/")) + { + throw new ArgumentException( + "HttpClient's BaseAddress must end with a slash." + + "It should be set to OpenAI's API endpoint.", + nameof(httpClient) + ); + } } public void Dispose() diff --git a/OpenAI_DotNet.sln b/OpenAI_DotNet.sln index ed7a0c1..7483eb6 100644 --- a/OpenAI_DotNet.sln +++ b/OpenAI_DotNet.sln @@ -25,6 +25,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatGpt.BlazorExample", "sa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatGpt.TelegramBotExample", "samples\ChatGpt.TelegramBotExample\ChatGpt.TelegramBotExample.csproj", "{8C86E60A-77C2-4204-AF36-F4B845474016}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI.ChatGpt.Modules.Translator", "OpenAI.ChatGpt.Modules.Translator\OpenAI.ChatGpt.Modules.Translator.csproj", "{E155D31C-0061-40A3-AD54-93B5DD08836B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI.ChatGpt.Modules.Translator.UnitTests", "tests\OpenAI.ChatGpt.Modules.Translator.UnitTests\OpenAI.ChatGpt.Modules.Translator.UnitTests.csproj", "{49F18714-F5F9-4FFC-A674-39CE166466A6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI.ChatGpt.Modules.Translator.IntegrationTests", "tests\OpenAI.ChatGpt.Modules.Translator.IntegrationTests\OpenAI.ChatGpt.Modules.Translator.IntegrationTests.csproj", "{600195A2-6E93-46AF-87A7-EA2E48E5AE24}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI.Tests.Shared", "tests\OpenAI.Tests.Shared\OpenAI.Tests.Shared.csproj", "{E303F270-6091-47DE-9260-DAD6122005A7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -67,6 +75,22 @@ Global {8C86E60A-77C2-4204-AF36-F4B845474016}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C86E60A-77C2-4204-AF36-F4B845474016}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C86E60A-77C2-4204-AF36-F4B845474016}.Release|Any CPU.Build.0 = Release|Any CPU + {E155D31C-0061-40A3-AD54-93B5DD08836B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E155D31C-0061-40A3-AD54-93B5DD08836B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E155D31C-0061-40A3-AD54-93B5DD08836B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E155D31C-0061-40A3-AD54-93B5DD08836B}.Release|Any CPU.Build.0 = Release|Any CPU + {49F18714-F5F9-4FFC-A674-39CE166466A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49F18714-F5F9-4FFC-A674-39CE166466A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49F18714-F5F9-4FFC-A674-39CE166466A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49F18714-F5F9-4FFC-A674-39CE166466A6}.Release|Any CPU.Build.0 = Release|Any CPU + {600195A2-6E93-46AF-87A7-EA2E48E5AE24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {600195A2-6E93-46AF-87A7-EA2E48E5AE24}.Debug|Any CPU.Build.0 = Debug|Any CPU + {600195A2-6E93-46AF-87A7-EA2E48E5AE24}.Release|Any CPU.ActiveCfg = Release|Any CPU + {600195A2-6E93-46AF-87A7-EA2E48E5AE24}.Release|Any CPU.Build.0 = Release|Any CPU + {E303F270-6091-47DE-9260-DAD6122005A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E303F270-6091-47DE-9260-DAD6122005A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E303F270-6091-47DE-9260-DAD6122005A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E303F270-6091-47DE-9260-DAD6122005A7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -78,5 +102,8 @@ Global {983A565C-5AE2-4F76-8B7B-7C5C051072C7} = {926D63B6-9F6A-45A1-B5B7-5F36352C23AB} {88213D5C-AADC-4F03-ACA3-7ADDCCE87DF4} = {80DB142C-5A1B-431C-BD85-19C83C6FC0A3} {8C86E60A-77C2-4204-AF36-F4B845474016} = {80DB142C-5A1B-431C-BD85-19C83C6FC0A3} + {49F18714-F5F9-4FFC-A674-39CE166466A6} = {926D63B6-9F6A-45A1-B5B7-5F36352C23AB} + {600195A2-6E93-46AF-87A7-EA2E48E5AE24} = {926D63B6-9F6A-45A1-B5B7-5F36352C23AB} + {E303F270-6091-47DE-9260-DAD6122005A7} = {926D63B6-9F6A-45A1-B5B7-5F36352C23AB} EndGlobalSection EndGlobal diff --git a/samples/ChatGpt.TelegramBotExample/Program.cs b/samples/ChatGpt.TelegramBotExample/Program.cs index 32dc159..5e5868b 100644 --- a/samples/ChatGpt.TelegramBotExample/Program.cs +++ b/samples/ChatGpt.TelegramBotExample/Program.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using OpenAI.ChatGpt.AspNetCore; +using OpenAI.ChatGpt.AspNetCore.Extensions; using OpenAI.ChatGpt.AspNetCore.Models; using OpenAI.ChatGpt.EntityFrameworkCore.Extensions; using OpenAI.ChatGpt.Models; diff --git a/tests/OpenAI.ChatGpt.IntegrationTests/ChatGptTests.cs b/tests/OpenAI.ChatGpt.IntegrationTests/ChatGptTests.cs index c514557..cb897ec 100644 --- a/tests/OpenAI.ChatGpt.IntegrationTests/ChatGptTests.cs +++ b/tests/OpenAI.ChatGpt.IntegrationTests/ChatGptTests.cs @@ -1,4 +1,6 @@ -namespace OpenAI.ChatGpt.IntegrationTests; +using OpenAI.Tests.Shared; + +namespace OpenAI.ChatGpt.IntegrationTests; public class ChatGptTests { @@ -36,7 +38,7 @@ await FluentActions.Invoking( private static async Task CreateInMemoryChat() { - return await ChatGPT.CreateInMemoryChat(Helpers.GetKeyFromEnvironment("OPENAI_API_KEY"), + return await ChatGPT.CreateInMemoryChat(Helpers.GetOpenAiKey(), new ChatGPTConfig() { MaxTokens = 100 diff --git a/tests/OpenAI.ChatGpt.IntegrationTests/OpenAI.ChatGpt.IntegrationTests.csproj b/tests/OpenAI.ChatGpt.IntegrationTests/OpenAI.ChatGpt.IntegrationTests.csproj index a121708..c1f8065 100644 --- a/tests/OpenAI.ChatGpt.IntegrationTests/OpenAI.ChatGpt.IntegrationTests.csproj +++ b/tests/OpenAI.ChatGpt.IntegrationTests/OpenAI.ChatGpt.IntegrationTests.csproj @@ -30,6 +30,7 @@ + diff --git a/tests/OpenAI.ChatGpt.IntegrationTests/OpenAiClientTests/ChatCompletionsApiTests.cs b/tests/OpenAI.ChatGpt.IntegrationTests/OpenAiClientTests/ChatCompletionsApiTests.cs index 259f4a1..27555f8 100644 --- a/tests/OpenAI.ChatGpt.IntegrationTests/OpenAiClientTests/ChatCompletionsApiTests.cs +++ b/tests/OpenAI.ChatGpt.IntegrationTests/OpenAiClientTests/ChatCompletionsApiTests.cs @@ -1,4 +1,6 @@ -namespace OpenAI.ChatGpt.IntegrationTests.OpenAiClientTests; +using OpenAI.Tests.Shared; + +namespace OpenAI.ChatGpt.IntegrationTests.OpenAiClientTests; [Collection("OpenAiTestCollection")] //to prevent parallel execution public class ChatCompletionsApiTests @@ -9,7 +11,7 @@ public class ChatCompletionsApiTests public ChatCompletionsApiTests(ITestOutputHelper outputHelper) { _outputHelper = outputHelper; - _client = new OpenAiClient(Helpers.GetKeyFromEnvironment("OPENAI_API_KEY")); + _client = new OpenAiClient(Helpers.GetOpenAiKey()); } [Fact] diff --git a/tests/OpenAI.ChatGpt.IntegrationTests/OpenAiClientTests/ImagesApiTest.cs b/tests/OpenAI.ChatGpt.IntegrationTests/OpenAiClientTests/ImagesApiTest.cs index bf71a1b..fc7dc0f 100644 --- a/tests/OpenAI.ChatGpt.IntegrationTests/OpenAiClientTests/ImagesApiTest.cs +++ b/tests/OpenAI.ChatGpt.IntegrationTests/OpenAiClientTests/ImagesApiTest.cs @@ -1,4 +1,5 @@ using OpenAI.ChatGpt.Models.Images; +using OpenAI.Tests.Shared; namespace OpenAI.ChatGpt.IntegrationTests.OpenAiClientTests; @@ -11,7 +12,7 @@ public class ImagesApiTest public ImagesApiTest(ITestOutputHelper outputHelper) { _outputHelper = outputHelper; - _client = new OpenAiClient(Helpers.GetKeyFromEnvironment("OPENAI_API_KEY")); + _client = new OpenAiClient(Helpers.GetOpenAiKey()); } [Fact(Skip = "Images API will be removed")] diff --git a/tests/OpenAI.ChatGpt.Modules.Translator.IntegrationTests/ChatGptTranslatorServiceTests.cs b/tests/OpenAI.ChatGpt.Modules.Translator.IntegrationTests/ChatGptTranslatorServiceTests.cs new file mode 100644 index 0000000..90d59a1 --- /dev/null +++ b/tests/OpenAI.ChatGpt.Modules.Translator.IntegrationTests/ChatGptTranslatorServiceTests.cs @@ -0,0 +1,35 @@ +using FluentAssertions; +using OpenAI.Tests.Shared; + +namespace OpenAI.ChatGpt.Modules.Translator.IntegrationTests; + +public class ChatGptTranslatorServiceTests +{ + [Fact] + public async Task Translate_without_source_and_target_languages_uses_default_languages() + { + // Arrange + var expectedSourceLanguage = "English"; + var expectedTargetLanguage = "Russian"; + var textToTranslate = "Hello, world!"; + var translatorService = new ChatGPTTranslatorService( + Helpers.GetOpenAiKey(), + null, + defaultSourceLanguage: expectedSourceLanguage, + defaultTargetLanguage: expectedTargetLanguage); + + // Act + var translatedText = await translatorService.Translate(textToTranslate); + + // Assert + translatedText.Should().NotBeNullOrEmpty(); + translatedText.Should().NotBe(textToTranslate); + + var englishCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + translatedText.Should().NotContainAny(englishCharacters.Select(c => new string(new []{ c }))); + + // Check at least some characters are in the Cyrillic script + translatedText.Any(ch => ch >= 0x0400 && ch <= 0x04FF).Should().BeTrue(); + } + +} \ No newline at end of file diff --git a/tests/OpenAI.ChatGpt.Modules.Translator.IntegrationTests/OpenAI.ChatGpt.Modules.Translator.IntegrationTests.csproj b/tests/OpenAI.ChatGpt.Modules.Translator.IntegrationTests/OpenAI.ChatGpt.Modules.Translator.IntegrationTests.csproj new file mode 100644 index 0000000..c30d465 --- /dev/null +++ b/tests/OpenAI.ChatGpt.Modules.Translator.IntegrationTests/OpenAI.ChatGpt.Modules.Translator.IntegrationTests.csproj @@ -0,0 +1,32 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/OpenAI.ChatGpt.Modules.Translator.IntegrationTests/Usings.cs b/tests/OpenAI.ChatGpt.Modules.Translator.IntegrationTests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/tests/OpenAI.ChatGpt.Modules.Translator.IntegrationTests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/tests/OpenAI.ChatGpt.Modules.Translator.UnitTests/ChatGptTranslatorServiceTests.cs b/tests/OpenAI.ChatGpt.Modules.Translator.UnitTests/ChatGptTranslatorServiceTests.cs new file mode 100644 index 0000000..c594ab4 --- /dev/null +++ b/tests/OpenAI.ChatGpt.Modules.Translator.UnitTests/ChatGptTranslatorServiceTests.cs @@ -0,0 +1,88 @@ +using FluentAssertions; +using Moq; +using OpenAI.ChatGpt.Models.ChatCompletion; +using OpenAI.ChatGpt.Models.ChatCompletion.Messaging; + +namespace OpenAI.ChatGpt.Modules.Translator.UnitTests; + +public class ChatGptTranslatorServiceTests +{ + [Fact] + public void Initialization_with_null_api_key_should_throw_exception() + { + // Arrange & Act + Action act = () => new ChatGPTTranslatorService( + apiKey: null!, + host: "host"); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Initialization_with_null_client_should_throw_exception() + { + // Arrange & Act + Action act = () => new ChatGPTTranslatorService(client: null); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Dispose_with_injected_client_should_not_dispose_client() + { + // Arrange + var clientMock = new Mock(); + clientMock.Setup(client => client.Dispose()).Verifiable(); + var translatorService = new ChatGPTTranslatorService(clientMock.Object); + + // Act + translatorService.Dispose(); + + // Assert + clientMock.Verify(client => client.Dispose(), Times.Never); + } + + [Fact] + public async Task Translate_without_source_and_target_languages_uses_default_languages() + { + // Arrange + var expectedSourceLanguage = "English"; + var expectedTargetLanguage = "Russian"; + var textToTranslate = "Hello, world!"; + var clientMock = new Mock(); + clientMock.Setup(client => client.GetChatCompletions( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync("Привет, мир!") + .Verifiable(); + var translatorService = new ChatGPTTranslatorService( + clientMock.Object, + defaultSourceLanguage: expectedSourceLanguage, + defaultTargetLanguage: expectedTargetLanguage); + + // Act + var translatedText = await translatorService.Translate(textToTranslate); + + // Assert + clientMock.Verify(client => client.GetChatCompletions( + It.Is(dialog => + dialog.GetMessages().Any( + message => message.Role == "system" && + message.Content.Contains($"I want you to act as a translator from {expectedSourceLanguage} to {expectedTargetLanguage}"))), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), + Times.Once); + translatedText.Should().Be("Привет, мир!"); + } +} \ No newline at end of file diff --git a/tests/OpenAI.ChatGpt.Modules.Translator.UnitTests/OpenAI.ChatGpt.Modules.Translator.UnitTests.csproj b/tests/OpenAI.ChatGpt.Modules.Translator.UnitTests/OpenAI.ChatGpt.Modules.Translator.UnitTests.csproj new file mode 100644 index 0000000..c2875b9 --- /dev/null +++ b/tests/OpenAI.ChatGpt.Modules.Translator.UnitTests/OpenAI.ChatGpt.Modules.Translator.UnitTests.csproj @@ -0,0 +1,31 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/OpenAI.ChatGpt.Modules.Translator.UnitTests/Usings.cs b/tests/OpenAI.ChatGpt.Modules.Translator.UnitTests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/tests/OpenAI.ChatGpt.Modules.Translator.UnitTests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/tests/OpenAI.ChatGpt.IntegrationTests/Helpers.cs b/tests/OpenAI.Tests.Shared/Helpers.cs similarity index 62% rename from tests/OpenAI.ChatGpt.IntegrationTests/Helpers.cs rename to tests/OpenAI.Tests.Shared/Helpers.cs index f22b6c6..1c1d6b3 100644 --- a/tests/OpenAI.ChatGpt.IntegrationTests/Helpers.cs +++ b/tests/OpenAI.Tests.Shared/Helpers.cs @@ -1,7 +1,14 @@ -namespace OpenAI.ChatGpt.IntegrationTests; +namespace OpenAI.Tests.Shared; public static class Helpers { + public static string GetOpenAiKey() => GetKeyFromEnvironment("OPENAI_API_KEY"); + + public static string? NullIfEmpty(this string? str) + { + return string.IsNullOrEmpty(str) ? null : str; + } + public static string GetKeyFromEnvironment(string keyName) { if (keyName == null) throw new ArgumentNullException(nameof(keyName)); diff --git a/tests/OpenAI.Tests.Shared/OpenAI.Tests.Shared.csproj b/tests/OpenAI.Tests.Shared/OpenAI.Tests.Shared.csproj new file mode 100644 index 0000000..e8c6a11 --- /dev/null +++ b/tests/OpenAI.Tests.Shared/OpenAI.Tests.Shared.csproj @@ -0,0 +1,11 @@ + + + + net7.0 + enable + enable + + false + + +