From 3ba62197c0711c6eb13541e514cf74aa6e0ea0c8 Mon Sep 17 00:00:00 2001 From: Max Inno Date: Wed, 11 Sep 2024 00:35:53 +0300 Subject: [PATCH] feat(ai): add more AI methods support --- src/Crowdin.Api/AI/AiApiExecutor.cs | 154 ++++++++ src/Crowdin.Api/AI/AiPromptCompletion.cs | 42 +++ src/Crowdin.Api/AI/AiPromptConfiguration.cs | 95 ++++- .../AI/AiPromptContextResources.cs | 57 +++ src/Crowdin.Api/AI/AiPromptMode.cs | 5 +- src/Crowdin.Api/AI/AiReportFormat.cs | 16 + .../AI/AiReportGenerationStatus.cs | 48 +++ src/Crowdin.Api/AI/AiSettings.cs | 19 + src/Crowdin.Api/AI/AiSettingsPatch.cs | 27 ++ src/Crowdin.Api/AI/AiSettingsShortcuts.cs | 19 + src/Crowdin.Api/AI/AiTool.cs | 18 + src/Crowdin.Api/AI/AiToolFunction.cs | 21 ++ src/Crowdin.Api/AI/AiToolObject.cs | 15 + src/Crowdin.Api/AI/AiToolType.cs | 15 + src/Crowdin.Api/AI/CloneAiPromptRequest.cs | 13 + .../AI/GenerateAiPromptCompletionRequest.cs | 22 ++ src/Crowdin.Api/AI/GenerateAiReport.cs | 48 +++ .../AI/OtherLanguageTranslationsConfig.cs | 16 + .../Crowdin.Api.Tests/AI/AiPromptsApiTests.cs | 343 +++++++++++++++++- .../Crowdin.Api.Tests/AI/AiReportsApiTests.cs | 212 +++++++++++ .../AI/AiSettingsApiTests.cs | 149 ++++++++ .../Core/Resources/AI_Prompts.Designer.cs | 158 ++------ .../Core/Resources/AI_Prompts.resx | 48 +++ .../Core/Resources/AI_Reports.Designer.cs | 66 ++++ .../Core/Resources/AI_Reports.resx | 67 ++++ .../Core/Resources/AI_Settings.Designer.cs | 60 +++ .../Core/Resources/AI_Settings.resx | 45 +++ .../Crowdin.Api.Tests.csproj | 22 +- 28 files changed, 1663 insertions(+), 157 deletions(-) create mode 100644 src/Crowdin.Api/AI/AiPromptCompletion.cs create mode 100644 src/Crowdin.Api/AI/AiPromptContextResources.cs create mode 100644 src/Crowdin.Api/AI/AiReportFormat.cs create mode 100644 src/Crowdin.Api/AI/AiReportGenerationStatus.cs create mode 100644 src/Crowdin.Api/AI/AiSettings.cs create mode 100644 src/Crowdin.Api/AI/AiSettingsPatch.cs create mode 100644 src/Crowdin.Api/AI/AiSettingsShortcuts.cs create mode 100644 src/Crowdin.Api/AI/AiTool.cs create mode 100644 src/Crowdin.Api/AI/AiToolFunction.cs create mode 100644 src/Crowdin.Api/AI/AiToolObject.cs create mode 100644 src/Crowdin.Api/AI/AiToolType.cs create mode 100644 src/Crowdin.Api/AI/CloneAiPromptRequest.cs create mode 100644 src/Crowdin.Api/AI/GenerateAiPromptCompletionRequest.cs create mode 100644 src/Crowdin.Api/AI/GenerateAiReport.cs create mode 100644 src/Crowdin.Api/AI/OtherLanguageTranslationsConfig.cs create mode 100644 tests/Crowdin.Api.Tests/AI/AiReportsApiTests.cs create mode 100644 tests/Crowdin.Api.Tests/AI/AiSettingsApiTests.cs create mode 100644 tests/Crowdin.Api.Tests/Core/Resources/AI_Reports.Designer.cs create mode 100644 tests/Crowdin.Api.Tests/Core/Resources/AI_Reports.resx create mode 100644 tests/Crowdin.Api.Tests/Core/Resources/AI_Settings.Designer.cs create mode 100644 tests/Crowdin.Api.Tests/Core/Resources/AI_Settings.resx diff --git a/src/Crowdin.Api/AI/AiApiExecutor.cs b/src/Crowdin.Api/AI/AiApiExecutor.cs index 056cc35e..5b043603 100644 --- a/src/Crowdin.Api/AI/AiApiExecutor.cs +++ b/src/Crowdin.Api/AI/AiApiExecutor.cs @@ -26,6 +26,20 @@ public AiApiExecutor(ICrowdinApiClient apiClient, IJsonParser jsonParser) } #region Prompts + + /// + /// Clone AI Prompt. Documentation: + /// Crowdin File Based API + /// Crowdin String Based API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task CloneAiPrompt(int? userId, int aiPromptId, CloneAiPromptRequest request) + { + string url = AddUserIdIfAvailable(userId, $"/ai/prompts/{aiPromptId}/clones"); + CrowdinApiResult result = await _apiClient.SendPostRequest(url, request); + return _jsonParser.ParseResponseObject(result.JsonObject); + } /// /// List AI Prompts. Documentation: @@ -63,6 +77,68 @@ public async Task AddAiPrompt(int? userId, AddAiPromptRequest CrowdinApiResult result = await _apiClient.SendPostRequest(url, request); return _jsonParser.ParseResponseObject(result.JsonObject); } + + /// + /// Generate AI Prompt Completion. Documentation: + /// Crowdin File Based API + /// Crowdin String Based API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task GenerateAiPromptCompletion( + int? userId, + int aiPromptId, + GenerateAiPromptCompletionRequest request) + { + string url = AddUserIdIfAvailable(userId, $"/ai/prompts/{aiPromptId}/completions"); + CrowdinApiResult result = await _apiClient.SendPostRequest(url, request); + return _jsonParser.ParseResponseObject(result.JsonObject); + } + + /// + /// Get AI Prompt Completion Status. Documentation: + /// Crowdin File Based API + /// Crowdin String Based API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task GetAiPromptCompletionStatus( + int? userId, + int aiPromptId, + string completionId) + { + string url = AddUserIdIfAvailable(userId, $"/ai/prompts/{aiPromptId}/completions/{completionId}"); + CrowdinApiResult result = await _apiClient.SendGetRequest(url); + return _jsonParser.ParseResponseObject(result.JsonObject); + } + + /// + /// Cancel AI Prompt Completion. Documentation: + /// Crowdin File Based API + /// Crowdin String Based API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task CancelAiPromptCompletion(int? userId, int aiPromptId, string completionId) + { + string url = AddUserIdIfAvailable(userId, $"/ai/prompts/{aiPromptId}/completions/{completionId}"); + HttpStatusCode statusCode = await _apiClient.SendDeleteRequest(url); + Utils.ThrowIfStatusNot204(statusCode, $"AI prompt completion {completionId} cancellation failed"); + } + + /// + /// Download AI Prompt Completion. Documentation: + /// Crowdin File Based API + /// Crowdin String Based API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task DownloadAiPromptCompletion(int? userId, int aiPromptId, string completionId) + { + string url = AddUserIdIfAvailable(userId, $"/ai/prompts/{aiPromptId}/completions/{completionId}/download"); + CrowdinApiResult result = await _apiClient.SendGetRequest(url); + return _jsonParser.ParseResponseObject(result.JsonObject); + } /// /// Get AI Prompt. Documentation: @@ -226,6 +302,84 @@ private static string FormUrl_AiProviderId(int? userId, int aiProviderId) #endregion + #endregion + + #region Reports + + /// + /// Generate AI Report. Documentation: + /// Crowdin File Based API + /// Crowdin String Based API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task GenerateAiReport(int? userId, GenerateAiReport request) + { + string url = AddUserIdIfAvailable(userId, "/ai/reports"); + CrowdinApiResult result = await _apiClient.SendPostRequest(url, request); + return _jsonParser.ParseResponseObject(result.JsonObject); + } + + /// + /// Check AI Report Generation Status. Documentation: + /// Crowdin File Based API + /// Crowdin String Based API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task CheckAiReportGenerationStatus(int? userId, string aiReportId) + { + string url = AddUserIdIfAvailable(userId, $"/ai/reports/{aiReportId}"); + CrowdinApiResult result = await _apiClient.SendGetRequest(url); + return _jsonParser.ParseResponseObject(result.JsonObject); + } + + /// + /// Download AI Report. Documentation: + /// Crowdin File Based API + /// Crowdin String Based API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task DownloadAiReport(int? userId, string aiReportId) + { + string url = AddUserIdIfAvailable(userId, $"/ai/reports/{aiReportId}/download"); + CrowdinApiResult result = await _apiClient.SendGetRequest(url); + return _jsonParser.ParseResponseObject(result.JsonObject); + } + + #endregion + + #region Settings + + /// + /// Get AI Settings. Documentation: + /// Crowdin File Based API + /// Crowdin String Based API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task GetAiSettings(int? userId) + { + string url = AddUserIdIfAvailable(userId, "/ai/settings"); + CrowdinApiResult result = await _apiClient.SendGetRequest(url); + return _jsonParser.ParseResponseObject(result.JsonObject); + } + + /// + /// Edit AI Settings. Documentation: + /// Crowdin File Based API + /// Crowdin String Based API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task EditAiSettings(int? userId, IEnumerable patches) + { + string url = AddUserIdIfAvailable(userId, "/ai/settings"); + CrowdinApiResult result = await _apiClient.SendPatchRequest(url, patches); + return _jsonParser.ParseResponseObject(result.JsonObject); + } + #endregion /// diff --git a/src/Crowdin.Api/AI/AiPromptCompletion.cs b/src/Crowdin.Api/AI/AiPromptCompletion.cs new file mode 100644 index 00000000..5c14da57 --- /dev/null +++ b/src/Crowdin.Api/AI/AiPromptCompletion.cs @@ -0,0 +1,42 @@ + +using System; +using JetBrains.Annotations; +using Newtonsoft.Json; + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public class AiPromptCompletion + { + [JsonProperty("identifier")] + public string Identifier { get; set; } + + [JsonProperty("status")] + public OperationStatus Status { get; set; } + + [JsonProperty("progress")] + public int Progress { get; set; } + + [JsonProperty("attributes")] + public AttributesObject Attributes { get; set; } + + [JsonProperty("createdAt")] + public DateTimeOffset CreatedAt { get; set; } + + [JsonProperty("updatedAt")] + public DateTimeOffset UpdatedAt { get; set; } + + [JsonProperty("startedAt")] + public DateTimeOffset StartedAt { get; set; } + + [JsonProperty("finishedAt")] + public DateTimeOffset FinishedAt { get; set; } + + [PublicAPI] + public class AttributesObject + { + [JsonProperty("aiPromptId")] + public int AiPromptId { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/AiPromptConfiguration.cs b/src/Crowdin.Api/AI/AiPromptConfiguration.cs index 27268a84..8a538e2c 100644 --- a/src/Crowdin.Api/AI/AiPromptConfiguration.cs +++ b/src/Crowdin.Api/AI/AiPromptConfiguration.cs @@ -1,7 +1,10 @@  +using System.Collections.Generic; using JetBrains.Annotations; using Newtonsoft.Json; +#nullable enable + namespace Crowdin.Api.AI { [PublicAPI] @@ -18,41 +21,71 @@ public class BasicModeAiPromptConfiguration : AiPromptConfiguration public override AiPromptMode Mode => AiPromptMode.Basic; [JsonProperty("companyDescription")] - public string CompanyDescription { get; set; } + public string? CompanyDescription { get; set; } [JsonProperty("projectDescription")] - public string ProjectDescription { get; set; } + public string? ProjectDescription { get; set; } [JsonProperty("audienceDescription")] - public string AudienceDescription { get; set; } + public string? AudienceDescription { get; set; } [JsonProperty("otherLanguageTranslations")] - public OtherLanguageTranslationsConfig OtherLanguageTranslations { get; set; } + public OtherLanguageTranslationsConfig? OtherLanguageTranslations { get; set; } [JsonProperty("glossaryTerms")] - public bool GlossaryTerms { get; set; } + public bool? GlossaryTerms { get; set; } [JsonProperty("tmSuggestions")] - public bool TmSuggestions { get; set; } + public bool? TmSuggestions { get; set; } [JsonProperty("fileContent")] - public bool FileContent { get; set; } + public bool? FileContent { get; set; } [JsonProperty("fileContext")] - public bool FileContext { get; set; } + public bool? FileContext { get; set; } [JsonProperty("publicProjectDescription")] - public bool PublicProjectDescription { get; set; } - - [PublicAPI] - public class OtherLanguageTranslationsConfig - { - [JsonProperty("isEnabled")] - public bool IsEnabled { get; set; } - - [JsonProperty("languageIds")] - public string[] LanguageIds { get; set; } - } + public bool? PublicProjectDescription { get; set; } + } + + [PublicAPI] + public class BasicModeAssistActionAiPromptConfiguration : AiPromptConfiguration + { + [JsonProperty("mode")] + public override AiPromptMode Mode => AiPromptMode.Basic; + + [JsonProperty("companyDescription")] + public string? CompanyDescription { get; set; } + + [JsonProperty("projectDescription")] + public string? ProjectDescription { get; set; } + + [JsonProperty("audienceDescription")] + public string? AudienceDescription { get; set; } + + [JsonProperty("otherLanguageTranslations")] + public OtherLanguageTranslationsConfig? OtherLanguageTranslations { get; set; } + + [JsonProperty("glossaryTerms")] + public bool? GlossaryTerms { get; set; } + + [JsonProperty("tmSuggestions")] + public bool? TmSuggestions { get; set; } + + [JsonProperty("fileContext")] + public bool? FileContext { get; set; } + + [JsonProperty("screenshots")] + public bool? Screenshots { get; set; } + + [JsonProperty("publicProjectDescription")] + public bool? PublicProjectDescription { get; set; } + + [JsonProperty("siblingsStrings")] + public bool? SiblingsStrings { get; set; } + + [JsonProperty("filteredStrings")] + public bool? FilteredStrings { get; set; } } [PublicAPI] @@ -61,7 +94,29 @@ public class AdvancedModeAiPromptConfiguration : AiPromptConfiguration [JsonProperty("mode")] public override AiPromptMode Mode => AiPromptMode.Advanced; + [JsonProperty("screenshots")] + public bool? Screenshots { get; set; } + [JsonProperty("prompt")] - public string Prompt { get; set; } + public string Prompt { get; set; } = null!; + + [JsonProperty("otherLanguageTranslations")] + public OtherLanguageTranslationsConfig? OtherLanguageTranslations { get; set; } + } + + [PublicAPI] + public class ExternalModeAiPromptConfiguration : AiPromptConfiguration + { + [JsonProperty("mode")] + public override AiPromptMode Mode => AiPromptMode.External; + + [JsonProperty("identifier")] + public string Identifier { get; set; } = null!; + + [JsonProperty("key")] + public string Key { get; set; } = null!; + + [JsonProperty("options")] + public IDictionary? Options { get; set; } } } \ No newline at end of file diff --git a/src/Crowdin.Api/AI/AiPromptContextResources.cs b/src/Crowdin.Api/AI/AiPromptContextResources.cs new file mode 100644 index 00000000..cc65a558 --- /dev/null +++ b/src/Crowdin.Api/AI/AiPromptContextResources.cs @@ -0,0 +1,57 @@ + +using System.Collections.Generic; +using JetBrains.Annotations; +using Newtonsoft.Json; + +#nullable enable + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public abstract class AiPromptContextResources + { + + } + + [PublicAPI] + public class PreTranslateActionAiPromptContextResources : AiPromptContextResources + { + [JsonProperty("projectId")] + public int ProjectId { get; set; } + + [JsonProperty("targetLanguageId")] + public string TargetLanguageId { get; set; } = null!; + + [JsonProperty("stringIds")] + public ICollection StringIds { get; set; } = null!; + } + + [PublicAPI] + public class AssistActionAiPromptContextResources : AiPromptContextResources + { + [JsonProperty("projectId")] + public int ProjectId { get; set; } + + [JsonProperty("targetLanguageId")] + public string TargetLanguageId { get; set; } = null!; + + [JsonProperty("stringIds")] + public ICollection StringIds { get; set; } = null!; + + [JsonProperty("filteredStringsIds")] + public ICollection? FilteredStringsIds { get; set; } + } + + [PublicAPI] + public class CustomActionAiPromptContextResources : AiPromptContextResources + { + [JsonProperty("projectId")] + public int ProjectId { get; set; } + + [JsonProperty("targetLanguageId")] + public string TargetLanguageId { get; set; } = null!; + + [JsonProperty("stringIds")] + public ICollection StringIds { get; set; } = null!; + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/AiPromptMode.cs b/src/Crowdin.Api/AI/AiPromptMode.cs index 59a41fff..b5c4ac54 100644 --- a/src/Crowdin.Api/AI/AiPromptMode.cs +++ b/src/Crowdin.Api/AI/AiPromptMode.cs @@ -11,6 +11,9 @@ public enum AiPromptMode Basic, [Description("advanced")] - Advanced + Advanced, + + [Description("external")] + External } } \ No newline at end of file diff --git a/src/Crowdin.Api/AI/AiReportFormat.cs b/src/Crowdin.Api/AI/AiReportFormat.cs new file mode 100644 index 00000000..bdc7e56f --- /dev/null +++ b/src/Crowdin.Api/AI/AiReportFormat.cs @@ -0,0 +1,16 @@ + +using System.ComponentModel; +using JetBrains.Annotations; + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public enum AiReportFormat + { + [Description("json")] + Json, + + [Description("csv")] + Csv + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/AiReportGenerationStatus.cs b/src/Crowdin.Api/AI/AiReportGenerationStatus.cs new file mode 100644 index 00000000..cfaad4b9 --- /dev/null +++ b/src/Crowdin.Api/AI/AiReportGenerationStatus.cs @@ -0,0 +1,48 @@ + +using System; +using JetBrains.Annotations; +using Newtonsoft.Json; + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public class AiReportGenerationStatus + { + [JsonProperty("identifier")] + public string Identifier { get; set; } + + [JsonProperty("status")] + public OperationStatus Status { get; set; } + + [JsonProperty("progress")] + public int Progress { get; set; } + + [JsonProperty("attributes")] + public AttributesObject Attributes { get; set; } + + [JsonProperty("createdAt")] + public DateTimeOffset CreatedAt { get; set; } + + [JsonProperty("updatedAt")] + public DateTimeOffset UpdatedAt { get; set; } + + [JsonProperty("startedAt")] + public DateTimeOffset StartedAt { get; set; } + + [JsonProperty("finishedAt")] + public DateTimeOffset FinishedAt { get; set; } + + [PublicAPI] + public class AttributesObject + { + [JsonProperty("format")] + public string Format { get; set; } // TODO: maybe enum? + + [JsonProperty("reportType")] + public string ReportType { get; set; } // TODO: maybe enum? + + [JsonProperty("schema")] + public object Schema { get; set; } // TODO: model? + } + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/AiSettings.cs b/src/Crowdin.Api/AI/AiSettings.cs new file mode 100644 index 00000000..c20a2c64 --- /dev/null +++ b/src/Crowdin.Api/AI/AiSettings.cs @@ -0,0 +1,19 @@ + +using JetBrains.Annotations; +using Newtonsoft.Json; + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public class AiSettings + { + [JsonProperty("assistActionAiPromptId")] + public int AssistActionAiPromptId { get; set; } + + [JsonProperty("showSuggestion")] + public bool ShowSuggestion { get; set; } + + [JsonProperty("shortcuts")] + public AiSettingsShortcuts[] Shortcuts { get; set; } + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/AiSettingsPatch.cs b/src/Crowdin.Api/AI/AiSettingsPatch.cs new file mode 100644 index 00000000..12c07f3e --- /dev/null +++ b/src/Crowdin.Api/AI/AiSettingsPatch.cs @@ -0,0 +1,27 @@ + +using System.ComponentModel; +using JetBrains.Annotations; +using Newtonsoft.Json; + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public class AiSettingsPatch : PatchEntry + { + [JsonProperty("path")] + public AiSettingsPatchPath Path { get; set; } + } + + [PublicAPI] + public enum AiSettingsPatchPath + { + [Description("/assistActionAiPromptId")] + AssistActionAiPromptId, + + [Description("/shortcuts")] + Shortcuts, + + [Description("/showSuggestion")] + ShowSuggestion + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/AiSettingsShortcuts.cs b/src/Crowdin.Api/AI/AiSettingsShortcuts.cs new file mode 100644 index 00000000..2e3c90e9 --- /dev/null +++ b/src/Crowdin.Api/AI/AiSettingsShortcuts.cs @@ -0,0 +1,19 @@ + +using JetBrains.Annotations; +using Newtonsoft.Json; + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public class AiSettingsShortcuts + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("prompt")] + public string Prompt { get; set; } + + [JsonProperty("enabled")] + public bool Enabled { get; set; } + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/AiTool.cs b/src/Crowdin.Api/AI/AiTool.cs new file mode 100644 index 00000000..a261d997 --- /dev/null +++ b/src/Crowdin.Api/AI/AiTool.cs @@ -0,0 +1,18 @@ + +using JetBrains.Annotations; +using Newtonsoft.Json; + +#nullable enable + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public class AiTool + { + [JsonProperty("type")] + public AiToolType Type { get; set; } + + [JsonProperty("function")] + public AiToolFunction Function { get; set; } = null!; + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/AiToolFunction.cs b/src/Crowdin.Api/AI/AiToolFunction.cs new file mode 100644 index 00000000..6356e4a4 --- /dev/null +++ b/src/Crowdin.Api/AI/AiToolFunction.cs @@ -0,0 +1,21 @@ + +using JetBrains.Annotations; +using Newtonsoft.Json; + +#nullable enable + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public class AiToolFunction + { + [JsonProperty("name")] + public string Name { get; set; } = null!; + + [JsonProperty("description")] + public string? Description { get; set; } + + [JsonProperty("parameters")] + public object? Parameters { get; set; } + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/AiToolObject.cs b/src/Crowdin.Api/AI/AiToolObject.cs new file mode 100644 index 00000000..b8c21814 --- /dev/null +++ b/src/Crowdin.Api/AI/AiToolObject.cs @@ -0,0 +1,15 @@ + +using JetBrains.Annotations; +using Newtonsoft.Json; + +#nullable enable + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public class AiToolObject + { + [JsonProperty("tool")] + public AiTool? Tool { get; set; } + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/AiToolType.cs b/src/Crowdin.Api/AI/AiToolType.cs new file mode 100644 index 00000000..5c7d151a --- /dev/null +++ b/src/Crowdin.Api/AI/AiToolType.cs @@ -0,0 +1,15 @@ + +using System.ComponentModel; +using JetBrains.Annotations; + +#nullable enable + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public enum AiToolType + { + [Description("function")] + Function + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/CloneAiPromptRequest.cs b/src/Crowdin.Api/AI/CloneAiPromptRequest.cs new file mode 100644 index 00000000..02f0ec85 --- /dev/null +++ b/src/Crowdin.Api/AI/CloneAiPromptRequest.cs @@ -0,0 +1,13 @@ + +using JetBrains.Annotations; +using Newtonsoft.Json; + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public class CloneAiPromptRequest + { + [JsonProperty("name")] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/GenerateAiPromptCompletionRequest.cs b/src/Crowdin.Api/AI/GenerateAiPromptCompletionRequest.cs new file mode 100644 index 00000000..fe5cff5c --- /dev/null +++ b/src/Crowdin.Api/AI/GenerateAiPromptCompletionRequest.cs @@ -0,0 +1,22 @@ + +using System.Collections.Generic; +using JetBrains.Annotations; +using Newtonsoft.Json; + +#nullable enable + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public class GenerateAiPromptCompletionRequest + { + [JsonProperty("resources")] + public AiPromptContextResources Resources { get; set; } = null!; + + [JsonProperty("tools")] + public ICollection? Tools { get; set; } + + [JsonProperty("tool_choice")] + public object? ToolChoice { get; set; } + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/GenerateAiReport.cs b/src/Crowdin.Api/AI/GenerateAiReport.cs new file mode 100644 index 00000000..42f27868 --- /dev/null +++ b/src/Crowdin.Api/AI/GenerateAiReport.cs @@ -0,0 +1,48 @@ + +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using Newtonsoft.Json; + +#nullable enable + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public abstract class GenerateAiReport + { + [JsonProperty("type")] + public abstract string Type { get; } + } + + [PublicAPI] + public class TokensUsageRawDataGenerateAiReport : GenerateAiReport + { + public override string Type => "tokens-usage-raw-data"; + + [JsonProperty("schema")] + public GeneralSchema Schema { get; set; } = null!; // TODO: multiple schemes in future? + + [PublicAPI] + public class GeneralSchema + { + [JsonProperty("dateFrom")] + public DateTimeOffset DateFrom { get; set; } + + [JsonProperty("dateTo")] + public DateTimeOffset DateTo { get; set; } + + [JsonProperty("format")] + public AiReportFormat? Format { get; set; } + + [JsonProperty("projectIds")] + public ICollection? ProjectIds { get; set; } + + [JsonProperty("promptIds")] + public ICollection? PromptIds { get; set; } + + [JsonProperty("userIds")] + public ICollection? UserIds { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Crowdin.Api/AI/OtherLanguageTranslationsConfig.cs b/src/Crowdin.Api/AI/OtherLanguageTranslationsConfig.cs new file mode 100644 index 00000000..5de60649 --- /dev/null +++ b/src/Crowdin.Api/AI/OtherLanguageTranslationsConfig.cs @@ -0,0 +1,16 @@ + +using JetBrains.Annotations; +using Newtonsoft.Json; + +namespace Crowdin.Api.AI +{ + [PublicAPI] + public class OtherLanguageTranslationsConfig + { + [JsonProperty("isEnabled")] + public bool IsEnabled { get; set; } + + [JsonProperty("languageIds")] + public string[] LanguageIds { get; set; } + } +} \ No newline at end of file diff --git a/tests/Crowdin.Api.Tests/AI/AiPromptsApiTests.cs b/tests/Crowdin.Api.Tests/AI/AiPromptsApiTests.cs index 142eda8c..f90ceeed 100644 --- a/tests/Crowdin.Api.Tests/AI/AiPromptsApiTests.cs +++ b/tests/Crowdin.Api.Tests/AI/AiPromptsApiTests.cs @@ -12,12 +12,312 @@ using Crowdin.Api.AI; using Crowdin.Api.Core; using Crowdin.Api.Tests.Core; +using Crowdin.Api.Tests.Core.Resources; namespace Crowdin.Api.Tests.AI { public class AiPromptsApiTests { private static readonly JsonSerializerSettings JsonSettings = TestUtils.CreateJsonSerializerOptions(); + + [Fact] + public async Task CloneAiPrompt() + { + const int userId = 1; + const int aiPromptId = 2; + + var request = new CloneAiPromptRequest + { + Name = "clone" + }; + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/users/{userId}/ai/prompts/{aiPromptId}/clones"; + + mockClient + .Setup(client => client.SendPostRequest(url, request, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.Created, + JsonObject = JObject.Parse(AI_Prompts.CommonResponses_Single) + }); + + var executor = new AiApiExecutor(mockClient.Object); + AiPromptResource? response = await executor.CloneAiPrompt(userId, aiPromptId, request); + + Assert_AiPrompt(response); + } + + [Fact] + public async Task CloneAiPrompt_Enterprise() + { + const int aiPromptId = 1; + + var request = new CloneAiPromptRequest + { + Name = "clone" + }; + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/ai/prompts/{aiPromptId}/clones"; + + mockClient + .Setup(client => client.SendPostRequest(url, request, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.Created, + JsonObject = JObject.Parse(AI_Prompts.CommonResponses_Single) + }); + + var executor = new AiApiExecutor(mockClient.Object); + AiPromptResource? response = await executor.CloneAiPrompt(userId: null, aiPromptId, request); + + Assert_AiPrompt(response); + } + + [Fact] + public async Task GenerateAiPromptCompletion() + { + const int userId = 1; + const int aiPromptId = 2; + + var request = new GenerateAiPromptCompletionRequest + { + Resources = new PreTranslateActionAiPromptContextResources + { + ProjectId = 1, + TargetLanguageId = "uk", + StringIds = new[] { 1, 2, 3 } + }, + Tools = new[] + { + new AiToolObject + { + Tool = new AiTool + { + Type = AiToolType.Function, + Function = new AiToolFunction + { + Name = "func", + Description = "func desc" + } + } + } + }, + ToolChoice = "string" + }; + + string actualRequestJson = JsonConvert.SerializeObject(request, JsonSettings); + string expectedRequestJson = TestUtils.CompactJson(AI_Prompts.GenerateAiPromptCompletion_Request); + Assert.Equal(expectedRequestJson, actualRequestJson); + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/users/{userId}/ai/prompts/{aiPromptId}/completions"; + + mockClient + .Setup(client => client.SendPostRequest(url, request, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.Created, + JsonObject = JObject.Parse(AI_Prompts.CommonResponses_AiPromptCompletion) + }); + + var executor = new AiApiExecutor(mockClient.Object); + AiPromptCompletion? response = await executor.GenerateAiPromptCompletion(userId, aiPromptId, request); + + Assert_AiPromptCompletion(response); + } + + [Fact] + public async Task GenerateAiPromptCompletion_Enterprise() + { + const int aiPromptId = 1; + + var request = new GenerateAiPromptCompletionRequest + { + Resources = new PreTranslateActionAiPromptContextResources + { + ProjectId = 1, + TargetLanguageId = "uk", + StringIds = new[] { 1, 2, 3 } + }, + Tools = new[] + { + new AiToolObject + { + Tool = new AiTool + { + Type = AiToolType.Function, + Function = new AiToolFunction + { + Name = "func", + Description = "func desc" + } + } + } + }, + ToolChoice = "string" + }; + + string actualRequestJson = JsonConvert.SerializeObject(request, JsonSettings); + string expectedRequestJson = TestUtils.CompactJson(AI_Prompts.GenerateAiPromptCompletion_Request); + Assert.Equal(expectedRequestJson, actualRequestJson); + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/ai/prompts/{aiPromptId}/completions"; + + mockClient + .Setup(client => client.SendPostRequest(url, request, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.Created, + JsonObject = JObject.Parse(AI_Prompts.CommonResponses_AiPromptCompletion) + }); + + var executor = new AiApiExecutor(mockClient.Object); + AiPromptCompletion? response = await executor.GenerateAiPromptCompletion(userId: null, aiPromptId, request); + + Assert_AiPromptCompletion(response); + } + + [Fact] + public async Task GetAiPromptCompletionStatus() + { + const int userId = 1; + const int aiPromptId = 2; + const string completionId = "123"; + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/users/{userId}/ai/prompts/{aiPromptId}/completions/{completionId}"; + + mockClient + .Setup(client => client.SendGetRequest(url, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(AI_Prompts.CommonResponses_AiPromptCompletion) + }); + + var executor = new AiApiExecutor(mockClient.Object); + AiPromptCompletion? response = await executor.GetAiPromptCompletionStatus(userId, aiPromptId, completionId); + + Assert_AiPromptCompletion(response); + } + + [Fact] + public async Task GetAiPromptCompletionStatus_Enterprise() + { + const int aiPromptId = 1; + const string completionId = "123"; + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/ai/prompts/{aiPromptId}/completions/{completionId}"; + + mockClient + .Setup(client => client.SendGetRequest(url, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(AI_Prompts.CommonResponses_AiPromptCompletion) + }); + + var executor = new AiApiExecutor(mockClient.Object); + AiPromptCompletion? response = await executor.GetAiPromptCompletionStatus(userId: null, aiPromptId, completionId); + + Assert_AiPromptCompletion(response); + } + + [Fact] + public async Task CancelAiPromptCompletion() + { + const int userId = 1; + const int aiPromptId = 2; + const string completionId = "123"; + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/users/{userId}/ai/prompts/{aiPromptId}/completions/{completionId}"; + + mockClient + .Setup(client => client.SendDeleteRequest(url, null)) + .ReturnsAsync(HttpStatusCode.NoContent); + + var executor = new AiApiExecutor(mockClient.Object); + await executor.CancelAiPromptCompletion(userId, aiPromptId, completionId); + } + + [Fact] + public async Task CancelAiPromptCompletion_Enterprise() + { + const int aiPromptId = 1; + const string completionId = "123"; + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/ai/prompts/{aiPromptId}/completions/{completionId}"; + + mockClient + .Setup(client => client.SendDeleteRequest(url, null)) + .ReturnsAsync(HttpStatusCode.NoContent); + + var executor = new AiApiExecutor(mockClient.Object); + await executor.CancelAiPromptCompletion(userId: null, aiPromptId, completionId); + } + + [Fact] + public async Task DownloadAiPromptCompletion() + { + const int userId = 1; + const int aiPromptId = 2; + const string completionId = "123"; + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/users/{userId}/ai/prompts/{aiPromptId}/completions/{completionId}/download"; + + mockClient + .Setup(client => client.SendGetRequest(url, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(AI_Prompts.DownloadAiPromptCompletion_Response) + }); + + var executor = new AiApiExecutor(mockClient.Object); + DownloadLink? response = await executor.DownloadAiPromptCompletion(userId, aiPromptId, completionId); + + Assert.NotNull(response); + } + + [Fact] + public async Task DownloadAiPromptCompletion_Enterprise() + { + const int aiPromptId = 1; + const string completionId = "123"; + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/ai/prompts/{aiPromptId}/completions/{completionId}/download"; + + mockClient + .Setup(client => client.SendGetRequest(url, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(AI_Prompts.DownloadAiPromptCompletion_Response) + }); + + var executor = new AiApiExecutor(mockClient.Object); + DownloadLink? response = await executor.DownloadAiPromptCompletion(userId: null, aiPromptId, completionId); + + Assert.NotNull(response); + } [Fact] public async Task ListAiPrompts() @@ -39,7 +339,7 @@ public async Task ListAiPrompts() .ReturnsAsync(new CrowdinApiResult { StatusCode = HttpStatusCode.OK, - JsonObject = JObject.Parse(Core.Resources.AI_Prompts.CommonResponses_Multi) + JsonObject = JObject.Parse(AI_Prompts.CommonResponses_Multi) }); var executor = new AiApiExecutor(mockClient.Object); @@ -63,7 +363,7 @@ public async Task ListAiPrompts_Enterprise() .ReturnsAsync(new CrowdinApiResult { StatusCode = HttpStatusCode.OK, - JsonObject = JObject.Parse(Core.Resources.AI_Prompts.CommonResponses_Multi) + JsonObject = JObject.Parse(AI_Prompts.CommonResponses_Multi) }); var executor = new AiApiExecutor(mockClient.Object); @@ -104,7 +404,7 @@ public async Task AddAiPrompt() }; string actualRequestJson = JsonConvert.SerializeObject(request, JsonSettings); - string expectedRequestJson = TestUtils.CompactJson(Core.Resources.AI_Prompts.AddAiPrompt_Request); + string expectedRequestJson = TestUtils.CompactJson(AI_Prompts.AddAiPrompt_Request); Assert.Equal(expectedRequestJson, actualRequestJson); Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); @@ -116,7 +416,7 @@ public async Task AddAiPrompt() .ReturnsAsync(new CrowdinApiResult { StatusCode = HttpStatusCode.OK, - JsonObject = JObject.Parse(Core.Resources.AI_Prompts.CommonResponses_Single) + JsonObject = JObject.Parse(AI_Prompts.CommonResponses_Single) }); var executor = new AiApiExecutor(mockClient.Object); @@ -137,7 +437,7 @@ public async Task AddAiPrompt_Enterprise() .ReturnsAsync(new CrowdinApiResult { StatusCode = HttpStatusCode.OK, - JsonObject = JObject.Parse(Core.Resources.AI_Prompts.CommonResponses_Single) + JsonObject = JObject.Parse(AI_Prompts.CommonResponses_Single) }); var executor = new AiApiExecutor(mockClient.Object); @@ -161,7 +461,7 @@ public async Task GetAiPrompt() .ReturnsAsync(new CrowdinApiResult { StatusCode = HttpStatusCode.OK, - JsonObject = JObject.Parse(Core.Resources.AI_Prompts.CommonResponses_Single) + JsonObject = JObject.Parse(AI_Prompts.CommonResponses_Single) }); var executor = new AiApiExecutor(mockClient.Object); @@ -184,7 +484,7 @@ public async Task GetAiPrompt_Enterprise() .ReturnsAsync(new CrowdinApiResult { StatusCode = HttpStatusCode.OK, - JsonObject = JObject.Parse(Core.Resources.AI_Prompts.CommonResponses_Single) + JsonObject = JObject.Parse(AI_Prompts.CommonResponses_Single) }); var executor = new AiApiExecutor(mockClient.Object); @@ -251,7 +551,7 @@ public async Task EditAiPrompt() }; string actualRequestJson = JsonConvert.SerializeObject(patches, JsonSettings); - string expectedRequestJson = TestUtils.CompactJson(Core.Resources.AI_Prompts.EditAiPrompt_Request); + string expectedRequestJson = TestUtils.CompactJson(AI_Prompts.EditAiPrompt_Request); Assert.Equal(expectedRequestJson, actualRequestJson); Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); @@ -263,7 +563,7 @@ public async Task EditAiPrompt() .ReturnsAsync(new CrowdinApiResult { StatusCode = HttpStatusCode.OK, - JsonObject = JObject.Parse(Core.Resources.AI_Prompts.CommonResponses_Single) + JsonObject = JObject.Parse(AI_Prompts.CommonResponses_Single) }); var executor = new AiApiExecutor(mockClient.Object); @@ -288,7 +588,7 @@ public async Task EditAiPrompt_Enterprise() .ReturnsAsync(new CrowdinApiResult { StatusCode = HttpStatusCode.OK, - JsonObject = JObject.Parse(Core.Resources.AI_Prompts.CommonResponses_Single) + JsonObject = JObject.Parse(AI_Prompts.CommonResponses_Single) }); var executor = new AiApiExecutor(mockClient.Object); @@ -321,13 +621,32 @@ private static void Assert_AiPrompt(AiPromptResource? prompt) Assert.True(config.FileContent); Assert.True(config.PublicProjectDescription); - BasicModeAiPromptConfiguration.OtherLanguageTranslationsConfig? otherLanguageTranslations = config.OtherLanguageTranslations; - Assert.NotNull(otherLanguageTranslations); + OtherLanguageTranslationsConfig? otherLanguageTranslations = config.OtherLanguageTranslations; + ArgumentNullException.ThrowIfNull(otherLanguageTranslations); Assert.True(otherLanguageTranslations.IsEnabled); Assert.Equal(new[] { "uk" }, otherLanguageTranslations.LanguageIds); Assert.Equal(DateTimeOffset.Parse("2019-09-20T11:11:05+00:00"), prompt.CreatedAt); Assert.Equal(DateTimeOffset.Parse("2019-09-20T12:22:20+00:00"), prompt.UpdatedAt); } + + private static void Assert_AiPromptCompletion(AiPromptCompletion? completion) + { + ArgumentNullException.ThrowIfNull(completion); + + Assert.Equal("50fb3506-4127-4ba8-8296-f97dc7e3e0c3", completion.Identifier); + Assert.Equal(OperationStatus.Finished, completion.Status); + Assert.Equal(100, completion.Progress); + + AiPromptCompletion.AttributesObject? attributes = completion.Attributes; + ArgumentNullException.ThrowIfNull(attributes); + Assert.Equal(38, attributes.AiPromptId); + + DateTimeOffset date = DateTimeOffset.Parse("2019-09-23T11:26:54+00:00"); + Assert.Equal(date, completion.CreatedAt); + Assert.Equal(date, completion.UpdatedAt); + Assert.Equal(date, completion.StartedAt); + Assert.Equal(date, completion.FinishedAt); + } } } \ No newline at end of file diff --git a/tests/Crowdin.Api.Tests/AI/AiReportsApiTests.cs b/tests/Crowdin.Api.Tests/AI/AiReportsApiTests.cs new file mode 100644 index 00000000..2aa1249f --- /dev/null +++ b/tests/Crowdin.Api.Tests/AI/AiReportsApiTests.cs @@ -0,0 +1,212 @@ + +using System; +using System.Net; +using System.Threading.Tasks; + +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +using Crowdin.Api.AI; +using Crowdin.Api.Core; +using Crowdin.Api.Tests.Core; +using Crowdin.Api.Tests.Core.Resources; + +namespace Crowdin.Api.Tests.AI +{ + public class AiReportsApiTests + { + private static readonly JsonSerializerSettings JsonSettings = TestUtils.CreateJsonSerializerOptions(); + + [Fact] + public async Task GenerateAiReport() + { + const int userId = 1; + + var request = new TokensUsageRawDataGenerateAiReport + { + Schema = new TokensUsageRawDataGenerateAiReport.GeneralSchema + { + DateFrom = DateTimeOffset.Parse("2024-01-23T07:00:14+00:00").ToLocalTime(), + DateTo = DateTimeOffset.Parse("2024-09-27T07:00:14+00:00").ToLocalTime(), + Format = AiReportFormat.Json, + ProjectIds = new[] { 22 }, + PromptIds = new[] { 18 }, + UserIds = new[] { 1 } + } + }; + + string actualRequestJson = JsonConvert.SerializeObject(request, JsonSettings); + string expectedRequestJson = TestUtils.CompactJson(AI_Reports.GenerateAiReport_Request); + Assert.Equal(expectedRequestJson, actualRequestJson); + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/users/{userId}/ai/reports"; + + mockClient + .Setup(client => client.SendPostRequest(url, request, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(AI_Reports.CommonResponses_AiReportGenerationStatus) + }); + + var executor = new AiApiExecutor(mockClient.Object); + AiReportGenerationStatus? response = await executor.GenerateAiReport(userId, request); + + Assert_AiReportGenerationStatus(response); + } + + [Fact] + public async Task GenerateAiReport_Enterprise() + { + var request = new TokensUsageRawDataGenerateAiReport + { + Schema = new TokensUsageRawDataGenerateAiReport.GeneralSchema + { + DateFrom = DateTimeOffset.Parse("2024-01-23T07:00:14+00:00").ToLocalTime(), + DateTo = DateTimeOffset.Parse("2024-09-27T07:00:14+00:00").ToLocalTime(), + Format = AiReportFormat.Json, + ProjectIds = new[] { 22 }, + PromptIds = new[] { 18 }, + UserIds = new[] { 1 } + } + }; + + string actualRequestJson = JsonConvert.SerializeObject(request, JsonSettings); + string expectedRequestJson = TestUtils.CompactJson(AI_Reports.GenerateAiReport_Request); + Assert.Equal(expectedRequestJson, actualRequestJson); + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + mockClient + .Setup(client => client.SendPostRequest("/ai/reports", request, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(AI_Reports.CommonResponses_AiReportGenerationStatus) + }); + + var executor = new AiApiExecutor(mockClient.Object); + AiReportGenerationStatus? response = await executor.GenerateAiReport(userId: null, request); + + Assert_AiReportGenerationStatus(response); + } + + [Fact] + public async Task CheckAiReportGenerationStatus() + { + const int userId = 1; + const string aiReportId = "123"; + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/users/{userId}/ai/reports/{aiReportId}"; + + mockClient + .Setup(client => client.SendGetRequest(url, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(AI_Reports.CommonResponses_AiReportGenerationStatus) + }); + + var executor = new AiApiExecutor(mockClient.Object); + AiReportGenerationStatus? response = await executor.CheckAiReportGenerationStatus(userId, aiReportId); + + Assert_AiReportGenerationStatus(response); + } + + [Fact] + public async Task CheckAiReportGenerationStatus_Enterprise() + { + const string aiReportId = "123"; + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + const string url = $"/ai/reports/{aiReportId}"; + + mockClient + .Setup(client => client.SendGetRequest(url, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(AI_Reports.CommonResponses_AiReportGenerationStatus) + }); + + var executor = new AiApiExecutor(mockClient.Object); + AiReportGenerationStatus? response = await executor.CheckAiReportGenerationStatus(userId: null, aiReportId); + + Assert_AiReportGenerationStatus(response); + } + + [Fact] + public async Task DownloadAiReport() + { + const int userId = 1; + const string aiReportId = "123"; + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/users/{userId}/ai/reports/{aiReportId}/download"; + + mockClient + .Setup(client => client.SendGetRequest(url, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(AI_Reports.DownloadAiReport_Response) + }); + + var executor = new AiApiExecutor(mockClient.Object); + DownloadLink? response = await executor.DownloadAiReport(userId, aiReportId); + + Assert.NotNull(response); + } + + [Fact] + public async Task DownloadAiReport_Enterprise() + { + const string aiReportId = "123"; + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + const string url = $"/ai/reports/{aiReportId}/download"; + + mockClient + .Setup(client => client.SendGetRequest(url, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(AI_Reports.DownloadAiReport_Response) + }); + + var executor = new AiApiExecutor(mockClient.Object); + DownloadLink? response = await executor.DownloadAiReport(userId: null, aiReportId); + + Assert.NotNull(response); + } + + private static void Assert_AiReportGenerationStatus(AiReportGenerationStatus? status) + { + ArgumentNullException.ThrowIfNull(status); + + Assert.Equal("50fb3506-4127-4ba8-8296-f97dc7e3e0c3", status.Identifier); + Assert.Equal(OperationStatus.Finished, status.Status); + Assert.Equal(100, status.Progress); + + AiReportGenerationStatus.AttributesObject? attributes = status.Attributes; + ArgumentNullException.ThrowIfNull(attributes); + Assert.Equal("json", attributes.Format); + Assert.Equal("tokens-usage-raw-data", attributes.ReportType); + + DateTimeOffset date = DateTimeOffset.Parse("2024-01-23T11:26:54+00:00"); + Assert.Equal(date, status.CreatedAt.ToUniversalTime()); + Assert.Equal(date, status.UpdatedAt.ToUniversalTime()); + Assert.Equal(date, status.StartedAt.ToUniversalTime()); + Assert.Equal(date, status.FinishedAt.ToUniversalTime()); + } + } +} \ No newline at end of file diff --git a/tests/Crowdin.Api.Tests/AI/AiSettingsApiTests.cs b/tests/Crowdin.Api.Tests/AI/AiSettingsApiTests.cs new file mode 100644 index 00000000..de38a191 --- /dev/null +++ b/tests/Crowdin.Api.Tests/AI/AiSettingsApiTests.cs @@ -0,0 +1,149 @@ + +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +using Crowdin.Api.AI; +using Crowdin.Api.Core; +using Crowdin.Api.Tests.Core; +using Crowdin.Api.Tests.Core.Resources; + +namespace Crowdin.Api.Tests.AI +{ + public class AiSettingsApiTests + { + private static readonly JsonSerializerSettings JsonSettings = TestUtils.CreateJsonSerializerOptions(); + + [Fact] + public async Task GetAiSettings() + { + const int userId = 1; + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/users/{userId}/ai/settings"; + + mockClient + .Setup(client => client.SendGetRequest(url, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(AI_Settings.CommonResponses_AiSettings) + }); + + var executor = new AiApiExecutor(mockClient.Object); + AiSettings? response = await executor.GetAiSettings(userId); + + Assert_AiSettings(response); + } + + [Fact] + public async Task GetAiSettings_Enterprise() + { + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + mockClient + .Setup(client => client.SendGetRequest("/ai/settings", null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(AI_Settings.CommonResponses_AiSettings) + }); + + var executor = new AiApiExecutor(mockClient.Object); + AiSettings? response = await executor.GetAiSettings(userId: null); + + Assert_AiSettings(response); + } + + [Fact] + public async Task EditAiSettings() + { + const int userId = 1; + + var request = new[] + { + new AiSettingsPatch + { + Operation = PatchOperation.Replace, + Path = AiSettingsPatchPath.AssistActionAiPromptId, + Value = true + } + }; + + string actualRequestJson = JsonConvert.SerializeObject(request, JsonSettings); + string expectedRequestJson = TestUtils.CompactJson(AI_Settings.EditAiSettings_Request); + Assert.Equal(expectedRequestJson, actualRequestJson); + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + var url = $"/users/{userId}/ai/settings"; + + mockClient + .Setup(client => client.SendPatchRequest(url, request, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(AI_Settings.CommonResponses_AiSettings) + }); + + var executor = new AiApiExecutor(mockClient.Object); + AiSettings? response = await executor.EditAiSettings(userId, request); + + Assert_AiSettings(response); + } + + [Fact] + public async Task EditAiSettings_Enterprise() + { + var request = new[] + { + new AiSettingsPatch + { + Operation = PatchOperation.Replace, + Path = AiSettingsPatchPath.AssistActionAiPromptId, + Value = true + } + }; + + string actualRequestJson = JsonConvert.SerializeObject(request, JsonSettings); + string expectedRequestJson = TestUtils.CompactJson(AI_Settings.EditAiSettings_Request); + Assert.Equal(expectedRequestJson, actualRequestJson); + + Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + mockClient + .Setup(client => client.SendPatchRequest("/ai/settings", request, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(AI_Settings.CommonResponses_AiSettings) + }); + + var executor = new AiApiExecutor(mockClient.Object); + AiSettings? response = await executor.EditAiSettings(userId: null, request); + + Assert_AiSettings(response); + } + + private static void Assert_AiSettings(AiSettings? settings) + { + ArgumentNullException.ThrowIfNull(settings); + + Assert.Equal(2, settings.AssistActionAiPromptId); + Assert.True(settings.ShowSuggestion); + + AiSettingsShortcuts? shortcut = settings.Shortcuts?.FirstOrDefault(); + ArgumentNullException.ThrowIfNull(shortcut); + Assert.Equal("My first shortcut", shortcut.Name); + Assert.Equal("Make translation shorter", shortcut.Prompt); + Assert.True(shortcut.Enabled); + } + } +} \ No newline at end of file diff --git a/tests/Crowdin.Api.Tests/Core/Resources/AI_Prompts.Designer.cs b/tests/Crowdin.Api.Tests/Core/Resources/AI_Prompts.Designer.cs index 7effbfd9..497da7c0 100644 --- a/tests/Crowdin.Api.Tests/Core/Resources/AI_Prompts.Designer.cs +++ b/tests/Crowdin.Api.Tests/Core/Resources/AI_Prompts.Designer.cs @@ -11,46 +11,32 @@ namespace Crowdin.Api.Tests.Core.Resources { using System; - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class AI_Prompts { - private static global::System.Resources.ResourceManager resourceMan; + private static System.Resources.ResourceManager resourceMan; - private static global::System.Globalization.CultureInfo resourceCulture; + private static System.Globalization.CultureInfo resourceCulture; - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal AI_Prompts() { } - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Crowdin.Api.Tests.Core.Resources.AI_Prompts", typeof(AI_Prompts).Assembly); + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Crowdin.Api.Tests.Core.Resources.AI_Prompts", typeof(AI_Prompts).Assembly); resourceMan = temp; } return resourceMan; } } - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -59,118 +45,46 @@ internal AI_Prompts() { } } - /// - /// Looks up a localized string similar to { - /// "name": "Pre-translate prompt", - /// "action": "pre_translate", - /// "aiProviderId": 1, - /// "aiModelId": "gpt-3.5-turbo-instruct", - /// "config": { - /// "mode": "basic", - /// "companyDescription": "string", - /// "projectDescription": "string", - /// "audienceDescription": "string", - /// "otherLanguageTranslations": { - /// "isEnabled": true, - /// "languageIds": [ - /// "uk" - /// ] - /// }, - /// "glossaryTerms": true, - /// "tmSuggestions": true, - /// "fileContent": true, - /// "fileContext": true, - /// "p [rest of string was truncated]";. - /// - internal static string AddAiPrompt_Request { - get { - return ResourceManager.GetString("AddAiPrompt_Request", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to { - /// "data": [{ - /// "id": 2, - /// "name": "Pre-translate prompt", - /// "action": "pre_translate", - /// "aiProviderId": 2, - /// "aiModelId": "gpt-3.5-turbo-instruct", - /// "isEnabled": true, - /// "enabledProjectIds": [ - /// 1 - /// ], - /// "config": { - /// "mode": "basic", - /// "companyDescription": "string", - /// "projectDescription": "string", - /// "audienceDescription": "string", - /// "otherLanguageTranslations": { - /// "isEnabled": true, - /// "languageIds": [ - /// "uk" - /// [rest of string was truncated]";. - /// internal static string CommonResponses_Multi { get { return ResourceManager.GetString("CommonResponses_Multi", resourceCulture); } } - /// - /// Looks up a localized string similar to { - /// "data": { - /// "id": 2, - /// "name": "Pre-translate prompt", - /// "action": "pre_translate", - /// "aiProviderId": 2, - /// "aiModelId": "gpt-3.5-turbo-instruct", - /// "isEnabled": true, - /// "enabledProjectIds": [ - /// 1 - /// ], - /// "config": { - /// "mode": "basic", - /// "companyDescription": "string", - /// "projectDescription": "string", - /// "audienceDescription": "string", - /// "otherLanguageTranslations": { - /// "isEnabled": true, - /// "languageIds": [ - /// "uk" - /// ] [rest of string was truncated]";. - /// internal static string CommonResponses_Single { get { return ResourceManager.GetString("CommonResponses_Single", resourceCulture); } } - /// - /// Looks up a localized string similar to [ - /// { - /// - /// "path": "/name", - /// "op": "replace", - /// - /// "value": "new name" - /// }, - /// - /// { - /// - /// "path": "/aiProviderId", - /// - /// "op": "replace", - /// - /// "value": 1 - /// - /// } - ///]. - /// + internal static string AddAiPrompt_Request { + get { + return ResourceManager.GetString("AddAiPrompt_Request", resourceCulture); + } + } + internal static string EditAiPrompt_Request { get { return ResourceManager.GetString("EditAiPrompt_Request", resourceCulture); } } + + internal static string CommonResponses_AiPromptCompletion { + get { + return ResourceManager.GetString("CommonResponses_AiPromptCompletion", resourceCulture); + } + } + + internal static string GenerateAiPromptCompletion_Request { + get { + return ResourceManager.GetString("GenerateAiPromptCompletion_Request", resourceCulture); + } + } + + internal static string DownloadAiPromptCompletion_Response { + get { + return ResourceManager.GetString("DownloadAiPromptCompletion_Response", resourceCulture); + } + } } } diff --git a/tests/Crowdin.Api.Tests/Core/Resources/AI_Prompts.resx b/tests/Crowdin.Api.Tests/Core/Resources/AI_Prompts.resx index 4fbee24e..ee47b6e9 100644 --- a/tests/Crowdin.Api.Tests/Core/Resources/AI_Prompts.resx +++ b/tests/Crowdin.Api.Tests/Core/Resources/AI_Prompts.resx @@ -141,4 +141,52 @@ } ] + + { + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "aiPromptId": 38 + }, + "createdAt": "2019-09-23T11:26:54+00:00", + "updatedAt": "2019-09-23T11:26:54+00:00", + "startedAt": "2019-09-23T11:26:54+00:00", + "finishedAt": "2019-09-23T11:26:54+00:00", + "eta": "1 second" + } +} + + + { + "resources": { + "projectId": 1, + "targetLanguageId": "uk", + "stringIds": [ + 1, 2, 3 + ] + }, + "tools": [ + { + "tool": { + "type": "function", + "function": { + "name": "func", + "description": "func desc" + } + } + } + ], + "tool_choice": "string" +} + + + { + "data": { + "url": "https://test.com", + "expireIn": "2019-09-20T10:31:21+00:00" + } +} + \ No newline at end of file diff --git a/tests/Crowdin.Api.Tests/Core/Resources/AI_Reports.Designer.cs b/tests/Crowdin.Api.Tests/Core/Resources/AI_Reports.Designer.cs new file mode 100644 index 00000000..f84823cb --- /dev/null +++ b/tests/Crowdin.Api.Tests/Core/Resources/AI_Reports.Designer.cs @@ -0,0 +1,66 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Crowdin.Api.Tests.Core.Resources { + using System; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class AI_Reports { + + private static System.Resources.ResourceManager resourceMan; + + private static System.Globalization.CultureInfo resourceCulture; + + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal AI_Reports() { + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { + get { + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Crowdin.Api.Tests.Core.Resources.AI_Reports", typeof(AI_Reports).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + internal static string GenerateAiReport_Request { + get { + return ResourceManager.GetString("GenerateAiReport_Request", resourceCulture); + } + } + + internal static string CommonResponses_AiReportGenerationStatus { + get { + return ResourceManager.GetString("CommonResponses_AiReportGenerationStatus", resourceCulture); + } + } + + internal static string DownloadAiReport_Response { + get { + return ResourceManager.GetString("DownloadAiReport_Response", resourceCulture); + } + } + } +} diff --git a/tests/Crowdin.Api.Tests/Core/Resources/AI_Reports.resx b/tests/Crowdin.Api.Tests/Core/Resources/AI_Reports.resx new file mode 100644 index 00000000..9d4c0e4e --- /dev/null +++ b/tests/Crowdin.Api.Tests/Core/Resources/AI_Reports.resx @@ -0,0 +1,67 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + { + "type": "tokens-usage-raw-data", + "schema": { + "dateFrom": "2024-01-23T07:00:14+00:00", + "dateTo": "2024-09-27T07:00:14+00:00", + "format": "json", + "projectIds": [ + 22 + ], + "promptIds": [ + 18 + ], + "userIds": [ + 1 + ] + } +} + + + { + "data": { + "identifier": "50fb3506-4127-4ba8-8296-f97dc7e3e0c3", + "status": "finished", + "progress": 100, + "attributes": { + "format": "json", + "reportType": "tokens-usage-raw-data", + "schema": {} + }, + "createdAt": "2024-01-23T11:26:54+00:00", + "updatedAt": "2024-01-23T11:26:54+00:00", + "startedAt": "2024-01-23T11:26:54+00:00", + "finishedAt": "2024-01-23T11:26:54+00:00", + "eta": "1 second" + } +} + + + { + "data": { + "url": "https://test.com", + "expireIn": "2019-09-20T10:31:21+00:00" + } +} + + \ No newline at end of file diff --git a/tests/Crowdin.Api.Tests/Core/Resources/AI_Settings.Designer.cs b/tests/Crowdin.Api.Tests/Core/Resources/AI_Settings.Designer.cs new file mode 100644 index 00000000..cc6a4d74 --- /dev/null +++ b/tests/Crowdin.Api.Tests/Core/Resources/AI_Settings.Designer.cs @@ -0,0 +1,60 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Crowdin.Api.Tests.Core.Resources { + using System; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class AI_Settings { + + private static System.Resources.ResourceManager resourceMan; + + private static System.Globalization.CultureInfo resourceCulture; + + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal AI_Settings() { + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { + get { + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Crowdin.Api.Tests.Core.Resources.AI_Settings", typeof(AI_Settings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + internal static string CommonResponses_AiSettings { + get { + return ResourceManager.GetString("CommonResponses_AiSettings", resourceCulture); + } + } + + internal static string EditAiSettings_Request { + get { + return ResourceManager.GetString("EditAiSettings_Request", resourceCulture); + } + } + } +} diff --git a/tests/Crowdin.Api.Tests/Core/Resources/AI_Settings.resx b/tests/Crowdin.Api.Tests/Core/Resources/AI_Settings.resx new file mode 100644 index 00000000..b242756a --- /dev/null +++ b/tests/Crowdin.Api.Tests/Core/Resources/AI_Settings.resx @@ -0,0 +1,45 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + { + "data": { + "assistActionAiPromptId": 2, + "showSuggestion": true, + "shortcuts": [ + { + "name": "My first shortcut", + "prompt": "Make translation shorter", + "enabled": true + } + ] + } +} + + + [ + { + "path": "/assistActionAiPromptId", + "op": "replace", + "value": true + } +] + + \ No newline at end of file diff --git a/tests/Crowdin.Api.Tests/Crowdin.Api.Tests.csproj b/tests/Crowdin.Api.Tests/Crowdin.Api.Tests.csproj index 748dafa8..4dab57b8 100644 --- a/tests/Crowdin.Api.Tests/Crowdin.Api.Tests.csproj +++ b/tests/Crowdin.Api.Tests/Crowdin.Api.Tests.csproj @@ -188,7 +188,7 @@ ResXFileCodeGenerator - AI.Designer.cs + AI_Prompts.Designer.cs ResXFileCodeGenerator @@ -208,7 +208,7 @@ ResXFileCodeGenerator - AI.Designer.cs + AI_Prompts.Designer.cs ResXFileCodeGenerator @@ -222,6 +222,14 @@ ResXFileCodeGenerator Reports_Archives.Designer.cs + + ResXFileCodeGenerator + AI_Reports.Designer.cs + + + ResXFileCodeGenerator + AI_Settings.Designer.cs + @@ -460,6 +468,16 @@ True Reports_Archives.resx + + True + True + AI_Reports.resx + + + True + True + AI_Settings.resx +