diff --git a/cli/src/main/java/com/box/l10n/mojito/cli/command/RepositoryAiReviewCommand.java b/cli/src/main/java/com/box/l10n/mojito/cli/command/RepositoryAiReviewCommand.java new file mode 100644 index 0000000000..4140bce32d --- /dev/null +++ b/cli/src/main/java/com/box/l10n/mojito/cli/command/RepositoryAiReviewCommand.java @@ -0,0 +1,100 @@ +package com.box.l10n.mojito.cli.command; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.box.l10n.mojito.cli.command.param.Param; +import com.box.l10n.mojito.cli.console.ConsoleWriter; +import com.box.l10n.mojito.rest.client.RepositoryAiReviewClient; +import com.box.l10n.mojito.rest.entity.PollableTask; +import java.util.List; +import java.util.stream.Collectors; +import org.fusesource.jansi.Ansi.Color; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +/** + * Command to machine review strings in a repository. + * + * @author jaurambault + */ +@Component +@Scope("prototype") +@Parameters( + commandNames = {"repository-ai-review"}, + commandDescription = "Ai review translated strings in a repository") +public class RepositoryAiReviewCommand extends Command { + + /** logger */ + static Logger logger = LoggerFactory.getLogger(RepositoryAiReviewCommand.class); + + @Autowired ConsoleWriter consoleWriter; + + @Parameter( + names = {Param.REPOSITORY_LONG, Param.REPOSITORY_SHORT}, + arity = 1, + required = true, + description = Param.REPOSITORY_DESCRIPTION) + String repositoryParam; + + @Parameter( + names = {Param.REPOSITORY_LOCALES_LONG, Param.REPOSITORY_LOCALES_SHORT}, + variableArity = true, + description = + "List of locales (bcp47 tags) to translate, if not provided translate all locales in the repository") + List locales; + + @Parameter( + names = {"--source-text-max-count"}, + arity = 1, + description = + "Source text max count per locale sent to MT (this param is used to avoid " + + "sending too many strings to MT)") + int sourceTextMaxCount = 100; + + @Parameter( + names = {"--text-unit-ids"}, + arity = 1, + description = "The list of TmTextUnitIds to translate") + List textUnitIds; + + @Parameter( + names = {"--use-batch"}, + arity = 1, + description = "To use the batch API or not") + boolean useBatch = false; + + @Autowired CommandHelper commandHelper; + + @Autowired RepositoryAiReviewClient repositoryAiReviewClient; + + @Override + public boolean shouldShowInCommandList() { + return false; + } + + @Override + public void execute() throws CommandException { + + consoleWriter + .newLine() + .a("Ai review repository: ") + .fg(Color.CYAN) + .a(repositoryParam) + .reset() + .a(" for locales: ") + .fg(Color.CYAN) + .a(locales == null ? "" : locales.stream().collect(Collectors.joining(", ", "[", "]"))) + .println(2); + + RepositoryAiReviewClient.ProtoAiReviewResponse protoAiTranslateResponse = + repositoryAiReviewClient.reviewRepository( + new RepositoryAiReviewClient.ProtoAiReviewRequest( + repositoryParam, locales, sourceTextMaxCount, textUnitIds, useBatch)); + + PollableTask pollableTask = protoAiTranslateResponse.pollableTask(); + commandHelper.waitForPollableTask(pollableTask.getId()); + } +} diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/client/RepositoryAiReviewClient.java b/restclient/src/main/java/com/box/l10n/mojito/rest/client/RepositoryAiReviewClient.java new file mode 100644 index 0000000000..e235103888 --- /dev/null +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/client/RepositoryAiReviewClient.java @@ -0,0 +1,38 @@ +package com.box.l10n.mojito.rest.client; + +import com.box.l10n.mojito.rest.entity.PollableTask; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * @author jaurambault + */ +@Component +public class RepositoryAiReviewClient extends BaseClient { + + /** logger */ + static Logger logger = LoggerFactory.getLogger(RepositoryAiReviewClient.class); + + @Override + public String getEntityName() { + return "proto-ai-review"; + } + + /** Ai review strings in a repository for a given list of locales */ + public ProtoAiReviewResponse reviewRepository(ProtoAiReviewRequest protoAiReviewRequest) { + + return authenticatedRestTemplate.postForObject( + getBasePathForEntity(), protoAiReviewRequest, ProtoAiReviewResponse.class); + } + + public record ProtoAiReviewRequest( + String repositoryName, + List targetBcp47tags, + int sourceTextMaxCountPerLocale, + List tmTextUnitIds, + boolean useBatch) {} + + public record ProtoAiReviewResponse(PollableTask pollableTask) {} +} diff --git a/webapp/src/main/java/com/box/l10n/mojito/entity/AiReviewProto.java b/webapp/src/main/java/com/box/l10n/mojito/entity/AiReviewProto.java new file mode 100644 index 0000000000..f9b8e2a529 --- /dev/null +++ b/webapp/src/main/java/com/box/l10n/mojito/entity/AiReviewProto.java @@ -0,0 +1,53 @@ +package com.box.l10n.mojito.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +/** + * We keep a single review per text unit variant for now. Meaning it has to be updated / cleaned up + * to re-review. The review is stored a JSON blob defined in the {@link + * com.box.l10n.mojito.service.oaireview.AiReviewService.AiReviewSingleTextUnitOutput} but that + * format could change any time. + */ +@Entity +@Table( + name = "ai_review_proto", + indexes = { + @Index( + name = "UK__AI_REVIEW_PROTO__TM_TEXT_UNIT_VARIANT_ID", + columnList = "tm_text_unit_variant_id", + unique = true) + }) +public class AiReviewProto extends AuditableEntity { + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn( + name = "tm_text_unit_variant_id", + foreignKey = @ForeignKey(name = "FK__AI_REVIEW_PROTO__TM_TEXT_UNIT_VARIANT__ID")) + TMTextUnitVariant tmTextUnitVariant; + + @Column(name = "json_review", length = Integer.MAX_VALUE) + String jsonReview; + + public TMTextUnitVariant getTmTextUnitVariant() { + return tmTextUnitVariant; + } + + public void setTmTextUnitVariant(TMTextUnitVariant tmTextUnitVariant) { + this.tmTextUnitVariant = tmTextUnitVariant; + } + + public String getJsonReview() { + return jsonReview; + } + + public void setJsonReview(String jsonReview) { + this.jsonReview = jsonReview; + } +} diff --git a/webapp/src/main/java/com/box/l10n/mojito/rest/textunit/AiReviewWS.java b/webapp/src/main/java/com/box/l10n/mojito/rest/textunit/AiReviewWS.java index b974cd4593..7941b738f4 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/rest/textunit/AiReviewWS.java +++ b/webapp/src/main/java/com/box/l10n/mojito/rest/textunit/AiReviewWS.java @@ -1,15 +1,20 @@ package com.box.l10n.mojito.rest.textunit; +import com.box.l10n.mojito.entity.AiReviewProto; +import com.box.l10n.mojito.entity.PollableTask; +import com.box.l10n.mojito.json.ObjectMapper; import com.box.l10n.mojito.service.oaireview.AiReviewService; +import com.box.l10n.mojito.service.pollableTask.PollableFuture; +import com.box.l10n.mojito.service.tm.AiReviewProtoRepository; import com.box.l10n.mojito.service.tm.search.TextUnitDTO; import com.box.l10n.mojito.service.tm.search.TextUnitSearcher; import com.box.l10n.mojito.service.tm.search.TextUnitSearcherParameters; - import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseStatus; @@ -21,24 +26,62 @@ public class AiReviewWS { /** logger */ static Logger logger = LoggerFactory.getLogger(AiReviewWS.class); + private final AiReviewProtoRepository aiReviewProtoRepository; + private final ObjectMapper objectMapper; + TextUnitSearcher textUnitSearcher; - @Autowired AiReviewService aiReviewService; + AiReviewProtoRepository AiReviewProtoRepository; + public AiReviewWS( TextUnitSearcher textUnitSearcher, - AiReviewService aiReviewService) { + AiReviewService aiReviewService, + AiReviewProtoRepository AiReviewProtoRepository, + AiReviewProtoRepository aiReviewProtoRepository, + @Qualifier("AiTranslate") ObjectMapper objectMapper) { this.textUnitSearcher = textUnitSearcher; this.aiReviewService = aiReviewService; + this.AiReviewProtoRepository = AiReviewProtoRepository; + this.aiReviewProtoRepository = aiReviewProtoRepository; + this.objectMapper = objectMapper; + } + + @RequestMapping(method = RequestMethod.POST, value = "/api/proto-ai-review") + @ResponseStatus(HttpStatus.OK) + public ProtoAiReviewResponse aiReview(@RequestBody ProtoAiReviewRequest protoAiReviewRequest) { + + PollableFuture pollableFuture = + aiReviewService.aiReviewAsync( + new AiReviewService.AiReviewInput( + protoAiReviewRequest.repositoryName(), + protoAiReviewRequest.targetBcp47tags(), + protoAiReviewRequest.sourceTextMaxCountPerLocale(), + protoAiReviewRequest.tmTextUnitIds(), + protoAiReviewRequest.useBatch())); + + return new ProtoAiReviewResponse(pollableFuture.getPollableTask()); } - @RequestMapping(method = RequestMethod.GET, value = "/api/proto-ai-review") + public record ProtoAiReviewRequest( + String repositoryName, + List targetBcp47tags, + int sourceTextMaxCountPerLocale, + boolean useBatch, + List tmTextUnitIds, + boolean allLocales) {} + + public record ProtoAiReviewResponse(PollableTask pollableTask) {} + + @RequestMapping(method = RequestMethod.GET, value = "/api/proto-ai-review-single-text-unit") @ResponseStatus(HttpStatus.OK) - public ProtoAiReviewResponse getTextUnitsWithGet(ProtoAiReviewRequest protoAiReviewRequest) { + public ProtoAiReviewSingleTextUnitResponse getTextUnitsWithGet( + ProtoAiReviewSingleTextUnitRequest protoAiReviewSingleTextUnitRequest) { TextUnitSearcherParameters textUnitSearcherParameters = new TextUnitSearcherParameters(); - textUnitSearcherParameters.setTmTextUnitVariantId(protoAiReviewRequest.tmTextUnitVariantId); + textUnitSearcherParameters.setTmTextUnitVariantId( + protoAiReviewSingleTextUnitRequest.tmTextUnitVariantId); List search = textUnitSearcher.search(textUnitSearcherParameters); if (search.isEmpty()) { @@ -47,20 +90,43 @@ public ProtoAiReviewResponse getTextUnitsWithGet(ProtoAiReviewRequest protoAiRev TextUnitDTO textUnit = search.getFirst(); - AiReviewService.AiReviewSingleTextUnitInput input = - new AiReviewService.AiReviewSingleTextUnitInput( - textUnit.getTargetLocale(), - textUnit.getSource(), - textUnit.getComment(), - new AiReviewService.AiReviewSingleTextUnitInput.ExistingTarget( - textUnit.getTarget(), !textUnit.isIncludedInLocalizedFile())); + logger.info("Check for pre-computed review"); - AiReviewService.AiReviewSingleTextUnitOutput aiReviewSingleTextUnitOutput = aiReviewService.getAiReviewSingleTextUnit(input, textUnit); + AiReviewProto alreadyReviewed = + aiReviewProtoRepository.findByTmTextUnitVariantId( + protoAiReviewSingleTextUnitRequest.tmTextUnitVariantId()); + + AiReviewService.AiReviewSingleTextUnitOutput aiReviewSingleTextUnitOutput = null; + + if (alreadyReviewed != null) { + try { + aiReviewSingleTextUnitOutput = + objectMapper.readValueUnchecked( + alreadyReviewed.getJsonReview(), + AiReviewService.AiReviewSingleTextUnitOutput.class); + } catch (RuntimeException e) { + logger.warn("Can't deserialize the existing review, we will recompute"); + } + } + + if (aiReviewSingleTextUnitOutput == null) { + + AiReviewService.AiReviewSingleTextUnitInput input = + new AiReviewService.AiReviewSingleTextUnitInput( + textUnit.getTargetLocale(), + textUnit.getSource(), + textUnit.getComment(), + new AiReviewService.AiReviewSingleTextUnitInput.ExistingTarget( + textUnit.getTarget(), !textUnit.isIncludedInLocalizedFile())); + + aiReviewSingleTextUnitOutput = aiReviewService.getAiReviewSingleTextUnit(input); + } - return new ProtoAiReviewResponse(textUnit, aiReviewSingleTextUnitOutput); + return new ProtoAiReviewSingleTextUnitResponse(textUnit, aiReviewSingleTextUnitOutput); } - public record ProtoAiReviewRequest(long tmTextUnitVariantId) {} + public record ProtoAiReviewSingleTextUnitRequest(long tmTextUnitVariantId) {} - public record ProtoAiReviewResponse(TextUnitDTO textUnitDTO, AiReviewService.AiReviewSingleTextUnitOutput aiReviewOutput) {} + public record ProtoAiReviewSingleTextUnitResponse( + TextUnitDTO textUnitDTO, AiReviewService.AiReviewSingleTextUnitOutput aiReviewOutput) {} } diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewConfig.java b/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewConfig.java index 08dee330be..8513000721 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewConfig.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewConfig.java @@ -3,19 +3,12 @@ import com.box.l10n.mojito.json.ObjectMapper; import com.box.l10n.mojito.openai.OpenAIClient; import com.box.l10n.mojito.openai.OpenAIClientPool; -import com.box.l10n.mojito.quartz.QuartzPollableTaskScheduler; -import com.box.l10n.mojito.service.blobstorage.StructuredBlobStorage; -import com.box.l10n.mojito.service.repository.RepositoryRepository; -import com.box.l10n.mojito.service.repository.RepositoryService; -import com.box.l10n.mojito.service.tm.importer.TextUnitBatchImporterService; -import com.box.l10n.mojito.service.tm.search.TextUnitSearcher; +import java.time.Duration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import reactor.util.retry.Retry; import reactor.util.retry.RetryBackoffSpec; -import java.time.Duration; - @Configuration public class AiReviewConfig { @@ -25,28 +18,6 @@ public AiReviewConfig(AiReviewConfigurationProperties aiReviewConfigurationPrope this.aiReviewConfigurationProperties = aiReviewConfigurationProperties; } - // TextUnitSearcher textUnitSearcher; -// RepositoryRepository repositoryRepository; -// RepositoryService repositoryService; -// TextUnitBatchImporterService textUnitBatchImporterService; -// StructuredBlobStorage structuredBlobStorage; -// QuartzPollableTaskScheduler quartzPollableTaskScheduler; -// -// public AiReviewConfig(AiReviewConfigurationProperties aiReviewConfigurationProperties, TextUnitSearcher textUnitSearcher, RepositoryRepository repositoryRepository, RepositoryService repositoryService, TextUnitBatchImporterService textUnitBatchImporterService, StructuredBlobStorage structuredBlobStorage, QuartzPollableTaskScheduler quartzPollableTaskScheduler) { -// this.aiReviewConfigurationProperties = aiReviewConfigurationProperties; -// this.textUnitSearcher = textUnitSearcher; -// this.repositoryRepository = repositoryRepository; -// this.repositoryService = repositoryService; -// this.textUnitBatchImporterService = textUnitBatchImporterService; -// this.structuredBlobStorage = structuredBlobStorage; -// this.quartzPollableTaskScheduler = quartzPollableTaskScheduler; -// } -// -// @Bean -// AiReviewService aiReviewService() { -// return new AiReviewService(textUnitSearcher, repositoryRepository, repositoryService, textUnitBatchImporterService, structuredBlobStorage, aiReviewConfigurationProperties, openAIClient(), openAIClientPool(), objectMapper(), retryBackoffSpec(), quartzPollableTaskScheduler); -// } - @Bean("openAIClientReview") OpenAIClient openAIClient() { String openaiClientToken = aiReviewConfigurationProperties.getOpenaiClientToken(); @@ -62,8 +33,7 @@ OpenAIClientPool openAIClientPool() { if (openaiClientToken == null) { return null; } - return new OpenAIClientPool( - 10, 50, 5, aiReviewConfigurationProperties.getOpenaiClientToken()); + return new OpenAIClientPool(10, 50, 5, aiReviewConfigurationProperties.getOpenaiClientToken()); } @Bean("objectMapperReview") diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewConfigurationProperties.java b/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewConfigurationProperties.java index 1cd02d90b4..98128233e4 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewConfigurationProperties.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewConfigurationProperties.java @@ -9,6 +9,7 @@ public class AiReviewConfigurationProperties { String openaiClientToken; String schedulerName = QuartzSchedulerManager.DEFAULT_SCHEDULER_NAME; + String modelName = "gpt-4o-2024-08-06"; public String getOpenaiClientToken() { return openaiClientToken; @@ -25,4 +26,12 @@ public String getSchedulerName() { public void setSchedulerName(String schedulerName) { this.schedulerName = schedulerName; } + + public String getModelName() { + return modelName; + } + + public void setModelName(String modelName) { + this.modelName = modelName; + } } diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewJob.java b/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewJob.java index ce3486dd44..41cf60e02b 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewJob.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewJob.java @@ -12,8 +12,7 @@ public class AiReviewJob extends QuartzPollableJob { static Logger logger = LoggerFactory.getLogger(AiReviewJob.class); - @Autowired - AiReviewService aiReviewService; + @Autowired AiReviewService aiReviewService; @Override public Void call(AiReviewInput aiReviewJobInput) throws Exception { diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewService.java b/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewService.java index 267afc8942..fdc1eb4d59 100644 --- a/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewService.java +++ b/webapp/src/main/java/com/box/l10n/mojito/service/oaireview/AiReviewService.java @@ -1,22 +1,27 @@ package com.box.l10n.mojito.service.oaireview; +import static com.box.l10n.mojito.openai.OpenAIClient.ChatCompletionsRequest; +import static com.box.l10n.mojito.openai.OpenAIClient.ChatCompletionsRequest.JsonFormat.JsonSchema.createJsonSchema; +import static com.box.l10n.mojito.openai.OpenAIClient.ChatCompletionsRequest.SystemMessage.systemMessageBuilder; +import static com.box.l10n.mojito.openai.OpenAIClient.ChatCompletionsRequest.UserMessage.userMessageBuilder; +import static com.box.l10n.mojito.openai.OpenAIClient.ChatCompletionsRequest.chatCompletionsRequest; + +import com.box.l10n.mojito.entity.AiReviewProto; import com.box.l10n.mojito.entity.Repository; import com.box.l10n.mojito.entity.RepositoryLocale; import com.box.l10n.mojito.json.ObjectMapper; import com.box.l10n.mojito.openai.OpenAIClient; import com.box.l10n.mojito.openai.OpenAIClient.ChatCompletionsResponse; -import com.box.l10n.mojito.openai.OpenAIClient.CreateBatchResponse; -import com.box.l10n.mojito.openai.OpenAIClient.RequestBatchFileLine; import com.box.l10n.mojito.openai.OpenAIClientPool; import com.box.l10n.mojito.quartz.QuartzJobInfo; import com.box.l10n.mojito.quartz.QuartzPollableTaskScheduler; -import com.box.l10n.mojito.service.blobstorage.Retention; import com.box.l10n.mojito.service.blobstorage.StructuredBlobStorage; -import com.box.l10n.mojito.service.oaireview.AiReviewService.CompletionInput.ExistingTarget; import com.box.l10n.mojito.service.pollableTask.PollableFuture; import com.box.l10n.mojito.service.repository.RepositoryNameNotFoundException; import com.box.l10n.mojito.service.repository.RepositoryRepository; import com.box.l10n.mojito.service.repository.RepositoryService; +import com.box.l10n.mojito.service.tm.AiReviewProtoRepository; +import com.box.l10n.mojito.service.tm.TMTextUnitVariantRepository; import com.box.l10n.mojito.service.tm.importer.TextUnitBatchImporterService; import com.box.l10n.mojito.service.tm.search.StatusFilter; import com.box.l10n.mojito.service.tm.search.TextUnitDTO; @@ -28,46 +33,25 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.stereotype.Service; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; - import java.io.IOException; import java.time.Duration; -import java.util.ArrayDeque; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.TimeoutException; -import java.util.function.Function; import java.util.stream.Collectors; - -import static com.box.l10n.mojito.openai.OpenAIClient.ChatCompletionResponseBatchFileLine; -import static com.box.l10n.mojito.openai.OpenAIClient.ChatCompletionsRequest; -import static com.box.l10n.mojito.openai.OpenAIClient.ChatCompletionsRequest.JsonFormat.JsonSchema.createJsonSchema; -import static com.box.l10n.mojito.openai.OpenAIClient.ChatCompletionsRequest.SystemMessage.systemMessageBuilder; -import static com.box.l10n.mojito.openai.OpenAIClient.ChatCompletionsRequest.UserMessage.userMessageBuilder; -import static com.box.l10n.mojito.openai.OpenAIClient.ChatCompletionsRequest.chatCompletionsRequest; -import static com.box.l10n.mojito.openai.OpenAIClient.CreateBatchRequest.forChatCompletion; -import static com.box.l10n.mojito.openai.OpenAIClient.DownloadFileContentRequest; -import static com.box.l10n.mojito.openai.OpenAIClient.DownloadFileContentResponse; -import static com.box.l10n.mojito.openai.OpenAIClient.RetrieveBatchRequest; -import static com.box.l10n.mojito.openai.OpenAIClient.RetrieveBatchResponse; -import static com.box.l10n.mojito.openai.OpenAIClient.UploadFileRequest; -import static com.box.l10n.mojito.openai.OpenAIClient.UploadFileResponse; -import static com.box.l10n.mojito.service.blobstorage.StructuredBlobStorage.Prefix.AI_REVIEW_WS; -import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; @Service public class AiReviewService { @@ -81,6 +65,10 @@ public class AiReviewService { RepositoryRepository repositoryRepository; + TMTextUnitVariantRepository textUnitVariantRepository; + + AiReviewProtoRepository aiReviewProtoRepository; + RepositoryService repositoryService; AiReviewConfigurationProperties aiReviewConfigurationProperties; @@ -100,35 +88,36 @@ public class AiReviewService { QuartzPollableTaskScheduler quartzPollableTaskScheduler; /** - * openAIClient and openAIClientPool are nullable. The public API will check for the client if they are not - * configured will throw an exception (keeping code minimal for now, could split into interface + 2 implementations: - * 1 for the non-configured case, and 1 for the configured) + * openAIClient and openAIClientPool are nullable. The public API will check for the client if + * they are not configured will throw an exception (keeping code minimal for now, could split into + * interface + 2 implementations: 1 for the non-configured case, and 1 for the configured) */ public AiReviewService( TextUnitSearcher textUnitSearcher, RepositoryRepository repositoryRepository, + TMTextUnitVariantRepository textUnitVariantRepository, + AiReviewProtoRepository aiReviewProtoRepository, RepositoryService repositoryService, TextUnitBatchImporterService textUnitBatchImporterService, StructuredBlobStorage structuredBlobStorage, AiReviewConfigurationProperties aiReviewConfigurationProperties, - @Autowired(required = false) @Qualifier("openAIClientReview") - OpenAIClient openAIClient, - @Autowired(required = false) @Qualifier("openAIClientPoolReview") - OpenAIClientPool openAIClientPool, - @Qualifier("objectMapperReview") - ObjectMapper objectMapper, - @Qualifier("retryBackoffSpecReview") - RetryBackoffSpec retryBackoffSpec, + @Autowired(required = false) @Qualifier("openAIClientReview") OpenAIClient openAIClient, + @Autowired(required = false) @Qualifier("openAIClientPoolReview") + OpenAIClientPool openAIClientPool, + @Qualifier("objectMapperReview") ObjectMapper objectMapper, + @Qualifier("retryBackoffSpecReview") RetryBackoffSpec retryBackoffSpec, QuartzPollableTaskScheduler quartzPollableTaskScheduler) { this.textUnitSearcher = Objects.requireNonNull(textUnitSearcher); this.repositoryRepository = Objects.requireNonNull(repositoryRepository); + this.textUnitVariantRepository = Objects.requireNonNull(textUnitVariantRepository); + this.aiReviewProtoRepository = Objects.requireNonNull(aiReviewProtoRepository); this.repositoryService = Objects.requireNonNull(repositoryService); this.textUnitBatchImporterService = Objects.requireNonNull(textUnitBatchImporterService); this.structuredBlobStorage = Objects.requireNonNull(structuredBlobStorage); this.aiReviewConfigurationProperties = Objects.requireNonNull(aiReviewConfigurationProperties); this.objectMapper = Objects.requireNonNull(objectMapper); this.openAIClient = openAIClient; // nullable - this.openAIClientPool = openAIClientPool; // nullable + this.openAIClientPool = openAIClientPool; // nullable this.retryBackoffSpec = Objects.requireNonNull(retryBackoffSpec); this.quartzPollableTaskScheduler = Objects.requireNonNull(quartzPollableTaskScheduler); } @@ -141,12 +130,12 @@ public record AiReviewInput( boolean useBatch) {} public record AiReviewSingleTextUnitOutput( - String source, - Target target, - DescriptionRating descriptionRating, - AltTarget altTarget, - ExistingTargetRating existingTargetRating, - ReviewRequired reviewRequired) { + String source, + Target target, + DescriptionRating descriptionRating, + AltTarget altTarget, + ExistingTargetRating existingTargetRating, + ReviewRequired reviewRequired) { record Target(String content, String explanation, int confidenceLevel) {} record AltTarget(String content, String explanation, int confidenceLevel) {} @@ -159,45 +148,47 @@ record ReviewRequired(boolean required, String reason) {} } public record AiReviewSingleTextUnitInput( - String locale, String source, String sourceDescription, ExistingTarget existingTarget) { + String locale, String source, String sourceDescription, ExistingTarget existingTarget) { public record ExistingTarget(String content, boolean hasBrokenPlaceholders) {} } - public AiReviewSingleTextUnitOutput getAiReviewSingleTextUnit(AiReviewSingleTextUnitInput input, TextUnitDTO textUnit) { - ObjectMapper objectMapper = ObjectMapper.withIndentedOutput(); // why do we have another one here? + public AiReviewSingleTextUnitOutput getAiReviewSingleTextUnit(AiReviewSingleTextUnitInput input) { + + ObjectMapper objectMapper = ObjectMapper.withIndentedOutput(); String inputAsJsonString = objectMapper.writeValueAsStringUnchecked(input); ObjectNode jsonSchema = createJsonSchema(AiReviewSingleTextUnitOutput.class); OpenAIClient.ChatCompletionsRequest chatCompletionsRequest = - chatCompletionsRequest() - .model("gpt-4o-2024-08-06") - .maxTokens(16384) - .messages( - List.of( - systemMessageBuilder().content(AiReviewService.PROMPT).build(), - userMessageBuilder().content(inputAsJsonString).build())) - .responseFormat( - new OpenAIClient.ChatCompletionsRequest.JsonFormat( - "json_schema", - new OpenAIClient.ChatCompletionsRequest.JsonFormat.JsonSchema( - true, "request_json_format", jsonSchema))) - .build(); + chatCompletionsRequest() + .model(aiReviewConfigurationProperties.getModelName()) + .maxTokens(16384) + .messages( + List.of( + systemMessageBuilder().content(AiReviewService.PROMPT).build(), + userMessageBuilder().content(inputAsJsonString).build())) + .responseFormat( + new OpenAIClient.ChatCompletionsRequest.JsonFormat( + "json_schema", + new OpenAIClient.ChatCompletionsRequest.JsonFormat.JsonSchema( + true, "request_json_format", jsonSchema))) + .build(); logger.info(objectMapper.writeValueAsStringUnchecked(chatCompletionsRequest)); OpenAIClient openAIClient = - OpenAIClient.builder() - .apiKey(aiReviewConfigurationProperties.getOpenaiClientToken()) - .build(); + OpenAIClient.builder() + .apiKey(aiReviewConfigurationProperties.getOpenaiClientToken()) + .build(); OpenAIClient.ChatCompletionsResponse chatCompletionsResponse = - openAIClient.getChatCompletions(chatCompletionsRequest).join(); + openAIClient.getChatCompletions(chatCompletionsRequest).join(); logger.info(objectMapper.writeValueAsStringUnchecked(chatCompletionsResponse)); String jsonResponse = chatCompletionsResponse.choices().getFirst().message().content(); - AiReviewSingleTextUnitOutput aiReviewSingleTextUnitOutput = objectMapper.readValueUnchecked(jsonResponse, AiReviewSingleTextUnitOutput.class); + AiReviewSingleTextUnitOutput aiReviewSingleTextUnitOutput = + objectMapper.readValueUnchecked(jsonResponse, AiReviewSingleTextUnitOutput.class); return aiReviewSingleTextUnitOutput; } @@ -215,7 +206,7 @@ public PollableFuture aiReviewAsync(AiReviewInput aiReviewInput) { public void aiReview(AiReviewInput aiReviewInput) throws AiReviewException { if (aiReviewInput.useBatch()) { - aiReviewBatch(aiReviewInput); + throw new UnsupportedOperationException("Only non batch for review"); } else { aiReviewNoBatch(aiReviewInput); } @@ -255,8 +246,13 @@ Mono asyncReviewNoBatchLocale( Repository repository = repositoryLocale.getRepository(); + logger.info("Get already reviewed tm text unit variants"); + Set alreadyReviewedTmTextUnitVariantIds = + aiReviewProtoRepository.findIdsByLocaleIdAndRepositoryId( + repositoryLocale.getLocale().getId(), repositoryLocale.getRepository().getId()); + logger.info( - "Get unreviewd strings for locale: '{}' in repository: '{}'", + "Get translated strings for locale: '{}' in repository: '{}'", repositoryLocale.getLocale().getBcp47Tag(), repository.getName()); @@ -275,11 +271,15 @@ Mono asyncReviewNoBatchLocale( textUnitSearcherParameters.setLimit(sourceTextMaxCountPerLocale); } - List textUnitDTOS = textUnitSearcher.search(textUnitSearcherParameters); + List allTextUnitDTOS = textUnitSearcher.search(textUnitSearcherParameters); + List textUnitDTOS = + allTextUnitDTOS.stream() + .filter(t -> !alreadyReviewedTmTextUnitVariantIds.contains(t.getTmTextUnitVariantId())) + .toList(); + logger.info("All text unit dtos: {}, filtered: {}", textUnitDTOS.size(), textUnitDTOS.size()); if (textUnitDTOS.isEmpty()) { - logger.debug( - "Nothing to review for locale: {}", repositoryLocale.getLocale().getBcp47Tag()); + logger.debug("Nothing to review for locale: {}", repositoryLocale.getLocale().getBcp47Tag()); return Mono.empty(); } @@ -315,16 +315,16 @@ Mono asyncReviewNoBatchLocale( return Mono.empty(); })) .collectList() - .flatMap(this::submitForImport) + .flatMap(this::submitForSave) .doOnTerminate(() -> logger.info("Done submitting for processing"))) .then(); } record MyRecord(TextUnitDTO textUnitDTO, ChatCompletionsResponse chatCompletionsResponse) {} - private Mono submitForImport(List results) { - logger.info("Submit for import for locale {}", results.get(0).textUnitDTO().getTargetLocale()); - List forImport = + private Mono submitForSave(List results) { + logger.info("Submit for save for locale {}", results.get(0).textUnitDTO().getTargetLocale()); + List forSave = results.stream() .map( myRecord -> { @@ -332,45 +332,56 @@ private Mono submitForImport(List results) { ChatCompletionsResponse chatCompletionsResponse = myRecord.chatCompletionsResponse(); - String completionOutputAsJson = + String jsonReview = chatCompletionsResponse.choices().getFirst().message().content(); + String completionOutputAsJson = jsonReview; - CompletionOutput completionOutput = + // this is just to check the format right now since we save the json anyway. + AiReviewSingleTextUnitOutput aiReviewSingleTextUnitOutput = objectMapper.readValueUnchecked( - completionOutputAsJson, CompletionOutput.class); - - textUnitDTO.setTarget(completionOutput.target().content()); - textUnitDTO.setTargetComment("ai-review"); - return textUnitDTO; + completionOutputAsJson, AiReviewSingleTextUnitOutput.class); + + logger.debug( + "Review for text unit variant id: {}, is\n:{}", + textUnitDTO.getTmTextUnitVariantId(), + aiReviewSingleTextUnitOutput); + + AiReviewProto aiReviewProto = new AiReviewProto(); + aiReviewProto.setTmTextUnitVariant( + textUnitVariantRepository.getReferenceById( + textUnitDTO.getTmTextUnitVariantId())); + aiReviewProto.setJsonReview(jsonReview); + return aiReviewProto; }) - .collect(Collectors.toList()); - - textUnitBatchImporterService.importTextUnits( - forImport, - TextUnitBatchImporterService.IntegrityChecksType.ALWAYS_USE_INTEGRITY_CHECKER_STATUS); + .toList(); + saveAiReviewProtosInTx(forSave); return Mono.empty(); } + @Transactional + public void saveAiReviewProtosInTx(List aiReviewProtos) { + aiReviewProtoRepository.saveAll(aiReviewProtos); + } + private Mono getChatCompletionForTextUnitDTO( TextUnitDTO textUnitDTO, OpenAIClientPool openAIClientPool) { - CompletionInput completionInput = - new CompletionInput( + AiReviewSingleTextUnitInput aiReviewSingleTextUnitInput = + new AiReviewService.AiReviewSingleTextUnitInput( textUnitDTO.getTargetLocale(), textUnitDTO.getSource(), textUnitDTO.getComment(), - textUnitDTO.getTarget() == null - ? null - : new ExistingTarget( - textUnitDTO.getTarget(), !textUnitDTO.isIncludedInLocalizedFile())); + new AiReviewService.AiReviewSingleTextUnitInput.ExistingTarget( + textUnitDTO.getTarget(), !textUnitDTO.isIncludedInLocalizedFile())); - String inputAsJsonString = objectMapper.writeValueAsStringUnchecked(completionInput); - ObjectNode jsonSchema = createJsonSchema(CompletionOutput.class); + String inputAsJsonString = + objectMapper.writeValueAsStringUnchecked(aiReviewSingleTextUnitInput); + ObjectNode jsonSchema = createJsonSchema(AiReviewSingleTextUnitOutput.class); ChatCompletionsRequest chatCompletionsRequest = chatCompletionsRequest() - .model("gpt-4o-2024-08-06") + .model(aiReviewConfigurationProperties.getModelName()) .maxTokens(16384) .messages( List.of( @@ -395,38 +406,6 @@ private boolean isRetriableException(Throwable throwable) { return cause instanceof IOException || cause instanceof TimeoutException; } - public void aiReviewBatch(AiReviewInput aiReviewInput) throws AiReviewException { - - Repository repository = getRepository(aiReviewInput); - - logger.debug("Start AI Review for repository: {}", repository.getName()); - - try { - Set repositoryLocalesWithoutRootLocale = - getFilteredRepositoryLocales(aiReviewInput, repository); - - logger.debug("Create batches for repository: {}", repository.getName()); - ArrayDeque batches = - repositoryLocalesWithoutRootLocale.stream() - .map( - createBatchForRepositoryLocale( - repository, aiReviewInput.sourceTextMaxCountPerLocale())) - .filter(Objects::nonNull) - .collect(Collectors.toCollection(ArrayDeque::new)); - - logger.debug("Import batches for repository: {}", repository.getName()); - while (!batches.isEmpty()) { - RetrieveBatchResponse retrieveBatchResponse = getNextFinishedBatch(batches); - importBatch(retrieveBatchResponse); - } - } catch (OpenAIClient.OpenAIClientResponseException openAIClientResponseException) { - logger.error( - "Failed to ai review: %s".formatted(openAIClientResponseException), - openAIClientResponseException); - throw new AiReviewException(openAIClientResponseException); - } - } - private Set getFilteredRepositoryLocales( AiReviewInput aiReviewInput, Repository repository) { return repositoryService.getRepositoryLocalesWithoutRootLocale(repository).stream() @@ -448,244 +427,8 @@ private Repository getRepository(AiReviewInput aiReviewInput) { return repository; } - void importBatch(RetrieveBatchResponse retrieveBatchResponse) { - - logger.info("Importing batch: {}", retrieveBatchResponse.id()); - - String textUnitDTOsBlobId = - retrieveBatchResponse.metadata().get(METADATA__TEXT_UNIT_DTOS__BLOB_ID); - - logger.info("Trying to load textUnitDTOs from blob: {}", textUnitDTOsBlobId); - AiReviewBlobStorage aiReviewBlobStorage = - structuredBlobStorage - .getString(AI_REVIEW_WS, textUnitDTOsBlobId) - .map(s -> objectMapper.readValueUnchecked(s, AiReviewBlobStorage.class)) - .orElseThrow( - () -> - new RuntimeException( - "There must be an entry for textUnitDTOsBlobId: " + textUnitDTOsBlobId)); - - Map tmTextUnitIdToTextUnitDTOs = - aiReviewBlobStorage.textUnitDTOS().stream() - .collect(toMap(TextUnitDTO::getTmTextUnitId, Function.identity())); - - DownloadFileContentResponse downloadFileContentResponse = - getOpenAIClient() - .downloadFileContent( - new DownloadFileContentRequest(retrieveBatchResponse.outputFileId())); - - List forImport = - downloadFileContentResponse - .content() - .lines() - .map( - line -> { - ChatCompletionResponseBatchFileLine chatCompletionResponseBatchFileLine = - objectMapper.readValueUnchecked( - line, ChatCompletionResponseBatchFileLine.class); - - if (chatCompletionResponseBatchFileLine.response().statusCode() != 200) { - throw new RuntimeException( - "Response batch file line failed: " + chatCompletionResponseBatchFileLine); - } - - String completionOutputAsJson = - chatCompletionResponseBatchFileLine - .response() - .chatCompletionsResponse() - .choices() - .getFirst() - .message() - .content(); - - CompletionOutput completionOutput = - objectMapper.readValueUnchecked( - completionOutputAsJson, CompletionOutput.class); - - TextUnitDTO textUnitDTO = - tmTextUnitIdToTextUnitDTOs.get( - Long.valueOf(chatCompletionResponseBatchFileLine.customId())); - textUnitDTO.setTarget(completionOutput.target().content()); - textUnitDTO.setTargetComment("ai-review"); - return textUnitDTO; - }) - .toList(); - - textUnitBatchImporterService.importTextUnits( - forImport, - TextUnitBatchImporterService.IntegrityChecksType.ALWAYS_USE_INTEGRITY_CHECKER_STATUS); - } - - Function createBatchForRepositoryLocale( - Repository repository, int sourceTextMaxCountPerLocale) { - - return repositoryLocale -> { - logger.debug( - "Get unreviewd string for locale: '{}' in repository: '{}'", - repositoryLocale.getLocale().getBcp47Tag(), - repository.getName()); - TextUnitSearcherParameters textUnitSearcherParameters = new TextUnitSearcherParameters(); - textUnitSearcherParameters.setRepositoryIds(repository.getId()); - textUnitSearcherParameters.setStatusFilter(StatusFilter.UNTRANSLATED); - textUnitSearcherParameters.setLocaleId(repositoryLocale.getLocale().getId()); - textUnitSearcherParameters.setLimit(sourceTextMaxCountPerLocale); - textUnitSearcherParameters.setUsedFilter(UsedFilter.USED); - List textUnitDTOS = textUnitSearcher.search(textUnitSearcherParameters); - - CreateBatchResponse createBatchResponse = null; - if (textUnitDTOS.isEmpty()) { - logger.debug("Nothing to review, don't create a batch"); - } else { - logger.debug("Save the TextUnitDTOs in blob storage for later batch import"); - String batchId = - "%s_%s".formatted(repositoryLocale.getLocale().getBcp47Tag(), UUID.randomUUID()); - structuredBlobStorage.put( - AI_REVIEW_WS, - batchId, - objectMapper.writeValueAsStringUnchecked(new AiReviewBlobStorage(textUnitDTOS)), - Retention.MIN_1_DAY); - - logger.debug("Generate the batch file content"); - String batchFileContent = generateBatchFileContent(textUnitDTOS); - - UploadFileResponse uploadFileResponse = - getOpenAIClient() - .uploadFile( - UploadFileRequest.forBatch("%s.jsonl".formatted(batchId), batchFileContent)); - - logger.debug("Create the batch using file: {}", uploadFileResponse); - createBatchResponse = - getOpenAIClient() - .createBatch( - forChatCompletion( - uploadFileResponse.id(), - Map.of(METADATA__TEXT_UNIT_DTOS__BLOB_ID, batchId))); - } - - logger.info( - "Created batch for locale: {} with {} text units", - repositoryLocale.getLocale().getBcp47Tag(), - textUnitDTOS.size()); - return createBatchResponse; - }; - } - - String generateBatchFileContent(List textUnitDTOS) { - return textUnitDTOS.stream() - .map( - textUnitDTO -> { - CompletionInput completionInput = - new CompletionInput( - textUnitDTO.getTargetLocale(), - textUnitDTO.getSource(), - textUnitDTO.getComment(), - new ExistingTarget( - textUnitDTO.getTarget(), !textUnitDTO.isIncludedInLocalizedFile())); - - String inputAsJsonString = objectMapper.writeValueAsStringUnchecked(completionInput); - - ObjectNode jsonSchema = createJsonSchema(CompletionOutput.class); - - ChatCompletionsRequest chatCompletionsRequest = - chatCompletionsRequest() - .model("gpt-4o-2024-08-06") - .maxTokens(16384) - .messages( - List.of( - systemMessageBuilder().content(PROMPT).build(), - userMessageBuilder().content(inputAsJsonString).build())) - .responseFormat( - new ChatCompletionsRequest.JsonFormat( - "json_schema", - new ChatCompletionsRequest.JsonFormat.JsonSchema( - true, "request_json_format", jsonSchema))) - .build(); - - return RequestBatchFileLine.forChatCompletion( - textUnitDTO.getTmTextUnitId().toString(), chatCompletionsRequest); - }) - .map(objectMapper::writeValueAsStringUnchecked) - .collect(joining("\n")); - } - - /** - * Use a queue to not stay stuck on a slow job, and try to import faster. Batch are imported - * sequentially. - * - *

Note: This is an active blocking pooling which blocks the thread but is isolated in a thread - * pool. - */ - RetrieveBatchResponse getNextFinishedBatch(ArrayDeque batches) { - while (true) { - int size = batches.size(); - - for (int i = 0; i < size; i++) { - CreateBatchResponse batch = batches.removeFirst(); - - logger.debug("Retrieve current status of batch: {}", batch.id()); - RetrieveBatchResponse retrieveBatchResponse = retrieveBatchWithRetry(batch); - - if ("completed".equals(retrieveBatchResponse.status())) { - logger.info("Next completed batch is: {}", retrieveBatchResponse.id()); - return retrieveBatchResponse; - } else if ("failed".equals(retrieveBatchResponse.status())) { - logger.error("Batch failed, skipping it: {}", retrieveBatchResponse); - } else { - logger.debug( - "Batch is still processing append to the end of the queue: {}", - retrieveBatchResponse); - batches.offerLast(batch); - } - } - try { - Thread.sleep(10000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - } - - RetrieveBatchResponse retrieveBatchWithRetry(CreateBatchResponse batch) { - - return Mono.fromCallable( - () -> getOpenAIClient().retrieveBatch(new RetrieveBatchRequest(batch.id()))) - .retryWhen( - retryBackoffSpec.doBeforeRetry( - doBeforeRetry -> { - logger.info("Retrying retrieving batch: {}", batch.id()); - })) - .doOnError( - throwable -> new RuntimeException("Failed to retrieve batch: " + batch.id(), throwable)) - .block(); - } - - record CompletionInput( - String locale, String source, String sourceDescription, ExistingTarget existingTarget) { - record ExistingTarget(String content, boolean hasBrokenPlaceholders) {} - } - - record CompletionOutput( - String source, - Target target, - DescriptionRating descriptionRating, - AltTarget altTarget, - ExistingTargetRating existingTargetRating, - ReviewRequired reviewRequired) { - record Target(String content, String explanation, int confidenceLevel) {} - - record AltTarget(String content, String explanation, int confidenceLevel) {} - - record DescriptionRating(String explanation, int score) {} - - record ExistingTargetRating(String explanation, int score) {} - - record ReviewRequired(boolean required, String reason) {} - } - - record AiReviewBlobStorage(List textUnitDTOS) {} - public static final String PROMPT = - """ + """ Your role is to act as a translator. You are tasked with translating provided source strings while preserving both the tone and the technical structure of the string. This includes protecting any tags, placeholders, or code elements that should not be translated. diff --git a/webapp/src/main/java/com/box/l10n/mojito/service/tm/AiReviewProtoRepository.java b/webapp/src/main/java/com/box/l10n/mojito/service/tm/AiReviewProtoRepository.java new file mode 100644 index 0000000000..06b32c069b --- /dev/null +++ b/webapp/src/main/java/com/box/l10n/mojito/service/tm/AiReviewProtoRepository.java @@ -0,0 +1,25 @@ +package com.box.l10n.mojito.service.tm; + +import com.box.l10n.mojito.entity.AiReviewProto; +import java.util.Set; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +/** + * @author jaurambault + */ +@RepositoryRestResource(exported = false) +public interface AiReviewProtoRepository extends JpaRepository { + + @Query( + "select a.id " + + "from AiReviewProto a " + + "where a.tmTextUnitVariant.locale.id = :localeId " + + " and a.tmTextUnitVariant.tmTextUnit.asset.repository.id = :repositoryId") + Set findIdsByLocaleIdAndRepositoryId( + @Param("localeId") Long localeId, @Param("repositoryId") Long repositoryId); + + AiReviewProto findByTmTextUnitVariantId(Long tmTextUnitVariantId); +} diff --git a/webapp/src/main/resources/db/migration/V68__Add_ai_review_proto.sql b/webapp/src/main/resources/db/migration/V68__Add_ai_review_proto.sql new file mode 100644 index 0000000000..9f9a32e0dd --- /dev/null +++ b/webapp/src/main/resources/db/migration/V68__Add_ai_review_proto.sql @@ -0,0 +1,3 @@ +create table ai_review_proto (id bigint not null auto_increment, created_date datetime, last_modified_date datetime, json_review JSON, tm_text_unit_variant_id bigint, primary key (id)); +alter table ai_review_proto add constraint UK__AI_REVIEW_PROTO__TM_TEXT_UNIT_VARIANT_ID unique (tm_text_unit_variant_id); +alter table ai_review_proto add constraint FK__AI_REVIEW_PROTO__TM_TEXT_UNIT_VARIANT__ID foreign key (tm_text_unit_variant_id) references tm_text_unit_variant (id); diff --git a/webapp/src/main/resources/public/js/sdk/TextUnitClient.js b/webapp/src/main/resources/public/js/sdk/TextUnitClient.js index eaea836a59..27470ee853 100644 --- a/webapp/src/main/resources/public/js/sdk/TextUnitClient.js +++ b/webapp/src/main/resources/public/js/sdk/TextUnitClient.js @@ -111,7 +111,7 @@ class TextUnitClient extends BaseClient { } getAiReview(textUnit) { - return this.get(this.baseUrl + "proto-ai-review", { + return this.get(this.baseUrl + "proto-ai-review-single-text-unit", { "tmTextUnitVariantId": textUnit.getTmTextUnitVariantId() }); }