From 1315442160b2f1b51c4cad3fbf8fb8d3471ef1d0 Mon Sep 17 00:00:00 2001 From: Daniel Augusto Date: Fri, 20 Dec 2024 13:56:05 +0000 Subject: [PATCH 01/10] ## Details Data model, DAO, service layer and endpoints for Automation Rule Evaluators. I've started with a more complicated approach, but in the end decided for a much simpler one which hopefully is easy enough to extend later into non-evaluator automation rules. Also as we've decided for a non-validation approach in the code, we're just going with a text field. ## Issues OPIK-590 OPIK-591 --- .../opik/api/AutomationRuleEvaluator.java | 66 + .../opik/api/AutomationRuleEvaluatorType.java | 17 + .../api/AutomationRuleEvaluatorUpdate.java | 15 + .../AutomationRuleEvaluatorsResource.java | 167 +++ .../comet/opik/domain/AutomationRuleDAO.java | 79 + .../opik/domain/AutomationRuleService.java | 241 +++ .../000008_add_automation_rule_tables.sql | 20 + .../AutomationRuleEvaluatorsResourceTest.java | 1307 +++++++++++++++++ 8 files changed, 1912 insertions(+) create mode 100644 apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java create mode 100644 apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluatorType.java create mode 100644 apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluatorUpdate.java create mode 100644 apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java create mode 100644 apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java create mode 100644 apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleService.java create mode 100644 apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000008_add_automation_rule_tables.sql create mode 100644 apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java new file mode 100644 index 000000000..097ac86ae --- /dev/null +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java @@ -0,0 +1,66 @@ +package com.comet.opik.api; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Pattern; +import lombok.Builder; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +import static com.comet.opik.utils.ValidationUtils.NULL_OR_NOT_BLANK; + +//@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true) +//@JsonSubTypes({ +// @JsonSubTypes.Type(value = AutomationRule.LlmAsJudgeEvaluator.class, name = "llm_as_judge"), +// @JsonSubTypes.Type(value = AutomationRule.PythonAsJudgeEvaluator.class, name = "python") +//}) +//@Schema(name = "Feedback", discriminatorProperty = "type", discriminatorMapping = { +// @DiscriminatorMapping(value = "llm_as_judge", schema = AutomationRule.LlmAsJudgeEvaluator.class), +// @DiscriminatorMapping(value = "python", schema = AutomationRule.PythonAsJudgeEvaluator.class) +//}) +@Builder(toBuilder = true) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record AutomationRuleEvaluator( + // Fields and methods + @JsonView({View.Public.class, View.Write.class}) UUID id, + @JsonView({View.Public.class, View.Write.class}) UUID projectId, + @JsonView({View.Public.class, View.Write.class}) AutomationRuleEvaluatorType evaluatorType, + + @JsonView({View.Public.class, + View.Write.class}) @Pattern(regexp = NULL_OR_NOT_BLANK, message = "must not be blank") String code, + + @JsonView({View.Public.class, View.Write.class}) float samplingRate, + + @JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) Instant createdAt, + @JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) String createdBy, + @JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) Instant lastUpdatedAt, + @JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) String lastUpdatedBy){ + + public static class View { + public static class Write { + } + public static class Public { + } + } + + @Builder(toBuilder = true) + public record AutomationRuleEvaluatorPage( + @JsonView( { + View.Public.class}) int page, + @JsonView({View.Public.class}) int size, + @JsonView({View.Public.class}) long total, + @JsonView({View.Public.class}) List content) + implements + Page{ + + public static AutomationRuleEvaluator.AutomationRuleEvaluatorPage empty(int page) { + return new AutomationRuleEvaluator.AutomationRuleEvaluatorPage(page, 0, 0, List.of()); + } + } +} \ No newline at end of file diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluatorType.java b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluatorType.java new file mode 100644 index 000000000..a6dec61d2 --- /dev/null +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluatorType.java @@ -0,0 +1,17 @@ +package com.comet.opik.api; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public enum AutomationRuleEvaluatorType { + + LLM_AS_JUDGE("llm-as-judge"), + PYTHON("python"); + + @JsonValue + private final String type; +} diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluatorUpdate.java b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluatorUpdate.java new file mode 100644 index 000000000..a98de19db --- /dev/null +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluatorUpdate.java @@ -0,0 +1,15 @@ +package com.comet.opik.api; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; + +@Builder(toBuilder = true) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record AutomationRuleEvaluatorUpdate( + @NotBlank String code, + float samplingRate) { +} \ No newline at end of file diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java new file mode 100644 index 000000000..2706fcc9a --- /dev/null +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java @@ -0,0 +1,167 @@ +package com.comet.opik.api.resources.v1.priv; + +import com.codahale.metrics.annotation.Timed; +import com.comet.opik.api.AutomationRuleEvaluator; +import com.comet.opik.api.AutomationRuleEvaluatorUpdate; +import com.comet.opik.api.Page; +import com.comet.opik.domain.AutomationRuleService; +import com.comet.opik.infrastructure.auth.RequestContext; +import com.comet.opik.infrastructure.ratelimit.RateLimited; +import com.fasterxml.jackson.annotation.JsonView; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.net.URI; +import java.util.UUID; + +@Path("/v1/private/automation/evaluator") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Timed +@Slf4j +@RequiredArgsConstructor(onConstructor_ = @Inject) +@Tag(name = "Automation rule evaluators", description = "Automation rule evaluators resource") +public class AutomationRuleEvaluatorsResource { + + private final @NonNull AutomationRuleService service; + private final @NonNull Provider requestContext; + + @GET + @Path("/projectId/{projectId}") + @Operation(operationId = "findEvaluators", summary = "Find Evaluators", description = "Find Evaluators", responses = { + @ApiResponse(responseCode = "200", description = "Evaluators resource", content = @Content(schema = @Schema(implementation = AutomationRuleEvaluator.AutomationRuleEvaluatorPage.class))) + }) + @JsonView(AutomationRuleEvaluator.View.Public.class) + public Response find(@PathParam("projectId") UUID projectId, + @QueryParam("page") @Min(1) @DefaultValue("1") int page, + @QueryParam("size") @Min(1) @DefaultValue("10") int size) { + + String workspaceId = requestContext.get().getWorkspaceId(); + log.info("Looking for automated evaluators for project id '{}' on workspaceId '{}' (page {})", projectId, + workspaceId, page); + Page definitionPage = service.find(page, size, projectId); + log.info("Found {} automated evaluators for project id '{}' on workspaceId '{}' (page {}, total {})", + definitionPage.size(), projectId, workspaceId, page, definitionPage.total()); + + return Response.ok() + .entity(definitionPage) + .build(); + } + + @GET + @Path("/projectId/{projectId}/evaluatorId/{evaluatorId}") + @Operation(operationId = "getAutomationRulesByProjectId", summary = "Get automation rule evaluator by id", description = "Get dataset by id", responses = { + @ApiResponse(responseCode = "200", description = "Automation Rule resource", content = @Content(schema = @Schema(implementation = AutomationRuleEvaluator.class))) + }) + @JsonView(AutomationRuleEvaluator.View.Public.class) + public Response getEvaluator(@PathParam("projectId") UUID projectId, @PathParam("evaluatorId") UUID evaluatorId) { + String workspaceId = requestContext.get().getWorkspaceId(); + + log.info("Finding automated evaluators by id '{}' on project_id '{}'", projectId, workspaceId); + AutomationRuleEvaluator evaluator = service.findById(evaluatorId, projectId, workspaceId); + log.info("Found automated evaluators by id '{}' on project_id '{}'", projectId, workspaceId); + + return Response.ok().entity(evaluator).build(); + } + + @POST + @Operation(operationId = "createAutomationRuleEvaluator", summary = "Create automation rule evaluator", description = "Create automation rule evaluator", responses = { + @ApiResponse(responseCode = "201", description = "Created", headers = { + @Header(name = "Location", required = true, example = "${basePath}/api/v1/private/automation/evaluator/projectId/{projectId}/evaluatorId/{evaluatorId}", schema = @Schema(implementation = String.class)) + }) + }) + @RateLimited + public Response createEvaluator( + @RequestBody(content = @Content(schema = @Schema(implementation = AutomationRuleEvaluator.class))) @JsonView(AutomationRuleEvaluator.View.Write.class) @NotNull @Valid AutomationRuleEvaluator evaluator, + @Context UriInfo uriInfo) { + + String workspaceId = requestContext.get().getWorkspaceId(); + + log.info("Creating {} evaluator for project_id '{}' on workspace_id '{}'", evaluator.evaluatorType(), + evaluator.projectId(), workspaceId); + AutomationRuleEvaluator savedEvaluator = service.save(evaluator, workspaceId); + log.info("Created {} evaluator '{}' for project_id '{}' on workspace_id '{}'", evaluator.evaluatorType(), + savedEvaluator.id(), evaluator.projectId(), workspaceId); + + URI uri = uriInfo.getAbsolutePathBuilder() + .path("/projectId/%s/evaluatorId/%s".formatted(savedEvaluator.projectId().toString(), + savedEvaluator.id().toString())) + .build(); + return Response.created(uri).build(); + } + + @PUT + @Path("/projectId/{projectId}/evaluatorId/{id}") + @Operation(operationId = "updateAutomationRuleEvaluator", summary = "update Automation Rule Evaluator by id", description = "update Automation Rule Evaluator by id", responses = { + @ApiResponse(responseCode = "204", description = "No content"), + }) + @RateLimited + public Response updateEvaluator(@PathParam("id") UUID id, + @PathParam("projectId") UUID projectId, + @RequestBody(content = @Content(schema = @Schema(implementation = AutomationRuleEvaluatorUpdate.class))) @NotNull @Valid AutomationRuleEvaluatorUpdate evaluatorUpdate) { + + String workspaceId = requestContext.get().getWorkspaceId(); + log.info("Updating automation rule evaluator by id '{}' and project_id '{}' on workspace_id '{}'", id, + projectId, workspaceId); + service.update(id, projectId, workspaceId, evaluatorUpdate); + log.info("Updated automation rule evaluator by id '{}' and project_id '{}' on workspace_id '{}'", id, projectId, + workspaceId); + + return Response.noContent().build(); + } + + @DELETE + @Path("/projectId/{projectId}/evaluatorId/{id}") + @Operation(operationId = "deleteAutomationRuleEvaluatorById", summary = "Delete Automation Rule Evaluator by id", description = "Delete Automation Rule Evaluator by id", responses = { + @ApiResponse(responseCode = "204", description = "No content"), + }) + public Response deleteEvaluator(@PathParam("id") UUID id, @PathParam("projectId") UUID projectId) { + + String workspaceId = requestContext.get().getWorkspaceId(); + log.info("Deleting dataset by id '{}' on workspace_id '{}'", id, workspaceId); + service.delete(id, projectId, workspaceId); + log.info("Deleted dataset by id '{}' on workspace_id '{}'", id, workspaceId); + return Response.noContent().build(); + } + + @DELETE + @Path("/projectId/{projectId}") + @Operation(operationId = "deleteAutomationRuleEvaluatorByProject", summary = "Delete Automation Rule Evaluator by Project id", description = "Delete Automation Rule Evaluator by Project id", responses = { + @ApiResponse(responseCode = "204", description = "No content"), + }) + public Response deleteProjectEvaluators(@PathParam("projectId") UUID projectId) { + + String workspaceId = requestContext.get().getWorkspaceId(); + log.info("Deleting evaluators from project_id '{}' on workspace_id '{}'", projectId, workspaceId); + service.deleteByProject(projectId, workspaceId); + log.info("Deleted evaluators from project_id '{}' on workspace_id '{}'", projectId, workspaceId); + return Response.noContent().build(); + } +} diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java new file mode 100644 index 000000000..6fdfb665a --- /dev/null +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java @@ -0,0 +1,79 @@ +package com.comet.opik.domain; + +import com.comet.opik.api.AutomationRuleEvaluator; +import com.comet.opik.api.AutomationRuleEvaluatorUpdate; +import com.comet.opik.infrastructure.db.UUIDArgumentFactory; +import org.jdbi.v3.sqlobject.config.RegisterArgumentFactory; +import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper; +import org.jdbi.v3.sqlobject.customizer.AllowUnusedBindings; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.customizer.BindList; +import org.jdbi.v3.sqlobject.customizer.BindMethods; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import org.jdbi.v3.stringtemplate4.UseStringTemplateEngine; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +@RegisterArgumentFactory(UUIDArgumentFactory.class) +@RegisterConstructorMapper(AutomationRuleEvaluator.class) +public interface AutomationRuleDAO { + + @SqlUpdate("INSERT INTO automation_rule_evaluators(id, project_id, workspace_id, evaluator_type, sampling_rate, code, created_by, last_updated_by) "+ + "VALUES (:rule.id, :rule.projectId, :workspaceId, :rule.evaluatorType, :rule.samplingRate, :rule.code, :rule.createdBy, :rule.lastUpdatedBy)") + void save(@BindMethods("rule") AutomationRuleEvaluator rule, @Bind("workspaceId") String workspaceId); + + @SqlUpdate(""" + UPDATE automation_rule_evaluators + SET + sampling_rate = :rule.samplingRate, + code = :rule.code, + last_updated_by = :lastUpdatedBy + WHERE id = :id AND project_id = :projectId AND workspace_id = :workspaceId + """) + int update(@Bind("id") UUID id, + @Bind("projectId") UUID projectId, + @Bind("workspaceId") String workspaceId, + @BindMethods("rule") AutomationRuleEvaluatorUpdate ruleUpdate, + @Bind("lastUpdatedBy") String lastUpdatedBy); + + @SqlQuery("SELECT * FROM automation_rule_evaluators WHERE id = :id AND project_id = :projectId AND workspace_id = :workspaceId") + Optional findById(@Bind("id") UUID id, @Bind("projectId") UUID projectId, + @Bind("workspaceId") String workspaceId); + + @SqlQuery("SELECT * FROM automation_rule_evaluators WHERE id IN () AND project_id = :projectId AND workspace_id = :workspaceId") + List findByIds(@BindList("ids") Set ids, @Bind("projectId") UUID projectId, + @Bind("workspaceId") String workspaceId); + + @SqlQuery("SELECT * FROM automation_rule_evaluators WHERE project_id = :projectId AND workspace_id = :workspaceId") + List findByProjectId(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); + + @SqlUpdate("DELETE FROM automation_rule_evaluators WHERE id = :id AND project_id = :projectId AND workspace_id = :workspaceId") + void delete(@Bind("id") UUID id, @Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); + + @SqlUpdate("DELETE FROM automation_rule_evaluators WHERE project_id = :projectId AND workspace_id = :workspaceId") + void deleteByProject(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); + + @SqlUpdate("DELETE FROM automation_rule_evaluators WHERE id IN () AND project_id = :projectId AND workspace_id = :workspaceId") + void delete(@BindList("ids") Set ids, @Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); + + @SqlQuery("SELECT * FROM automation_rule_evaluators " + + " WHERE project_id = :projectId AND workspace_id = :workspaceId " + + " LIMIT :limit OFFSET :offset ") + @UseStringTemplateEngine + @AllowUnusedBindings + List find(@Bind("limit") int limit, + @Bind("offset") int offset, + @Bind("projectId") UUID projectId, + @Bind("workspaceId") String workspaceId); + + @SqlQuery("SELECT COUNT(*) FROM automation_rule_evaluators WHERE project_id = :projectId AND workspace_id = :workspaceId") + long findCount(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); + + @SqlQuery("SELECT id FROM automation_rule_evaluators WHERE id IN () and project_id = :projectId AND workspace_id = :workspaceId") + Set exists(@BindList("ids") Set ruleIds, @Bind("projectId") UUID projectId, + @Bind("workspaceId") String workspaceId); +} diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleService.java new file mode 100644 index 000000000..b3e6cc065 --- /dev/null +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleService.java @@ -0,0 +1,241 @@ +package com.comet.opik.domain; + +import com.comet.opik.api.AutomationRuleEvaluator; +import com.comet.opik.api.AutomationRuleEvaluatorUpdate; +import com.comet.opik.api.error.EntityAlreadyExistsException; +import com.comet.opik.api.error.ErrorMessage; +import com.comet.opik.domain.sorting.SortingQueryBuilder; +import com.comet.opik.infrastructure.BatchOperationsConfig; +import com.comet.opik.infrastructure.auth.RequestContext; +import com.google.inject.ImplementedBy; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.core.Response; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jdbi.v3.core.statement.UnableToExecuteStatementException; +import ru.vyarus.dropwizard.guice.module.yaml.bind.Config; +import ru.vyarus.guicey.jdbi3.tx.TransactionTemplate; + +import java.sql.SQLIntegrityConstraintViolationException; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static com.comet.opik.infrastructure.db.TransactionTemplateAsync.READ_ONLY; +import static com.comet.opik.infrastructure.db.TransactionTemplateAsync.WRITE; + +@ImplementedBy(AutomationRuleServiceImpl.class) +public interface AutomationRuleService { + + AutomationRuleEvaluator save(AutomationRuleEvaluator AutomationRuleEvaluator, @NonNull String workspaceId); + + Optional getById(UUID id, UUID projectId, @NonNull String workspaceId); + + void update(UUID id, UUID projectId, @NonNull String workspaceId, + AutomationRuleEvaluatorUpdate AutomationRuleEvaluator); + + AutomationRuleEvaluator findById(UUID id, UUID projectId, @NonNull String workspaceId); + + List findByProjectId(UUID projectId, @NonNull String workspaceId); + + List findByIds(Set ids, UUID projectId, @NonNull String workspaceId); + + void deleteByProject(UUID projectId, @NonNull String workspaceId); + + void delete(UUID id, UUID projectId, @NonNull String workspaceId); + + void delete(Set ids, UUID projectId, @NonNull String workspaceId); + + AutomationRuleEvaluator.AutomationRuleEvaluatorPage find(int page, int size, @NonNull UUID projectId); +} + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +@Slf4j +class AutomationRuleServiceImpl implements AutomationRuleService { + + private static final String EVALUATOR_ALREADY_EXISTS = "AutomationRuleEvaluator already exists"; + + private final @NonNull IdGenerator idGenerator; + private final @NonNull TransactionTemplate template; + private final @NonNull Provider requestContext; + private final @NonNull SortingQueryBuilder sortingQueryBuilder; + private final @NonNull @Config BatchOperationsConfig batchOperationsConfig; + + @Override + public AutomationRuleEvaluator save(@NonNull AutomationRuleEvaluator automationRuleEvaluator, + @NonNull String workspaceId) { + + var builder = automationRuleEvaluator.id() == null + ? automationRuleEvaluator.toBuilder().id(idGenerator.generateId()) + : automationRuleEvaluator.toBuilder(); + + String userName = requestContext.get().getUserName(); + + builder.createdBy(userName) + .lastUpdatedBy(userName); + + var newAutomationRuleEvaluator = builder.build(); + + IdGenerator.validateVersion(newAutomationRuleEvaluator.id(), "AutomationRuleEvaluator"); + + return template.inTransaction(WRITE, handle -> { + var dao = handle.attach(AutomationRuleDAO.class); + + try { + dao.save(newAutomationRuleEvaluator, workspaceId); + return dao + .findById(newAutomationRuleEvaluator.id(), newAutomationRuleEvaluator.projectId(), workspaceId) + .orElseThrow(); + } catch (UnableToExecuteStatementException e) { + if (e.getCause() instanceof SQLIntegrityConstraintViolationException) { + log.info(EVALUATOR_ALREADY_EXISTS); + throw new EntityAlreadyExistsException(new ErrorMessage(List.of(EVALUATOR_ALREADY_EXISTS))); + } else { + throw e; + } + } + }); + } + + @Override + public Optional getById(@NonNull UUID id, @NonNull UUID projectId, + @NonNull String workspaceId) { + log.info("Getting AutomationRuleEvaluator with id '{}', workspaceId '{}'", id, projectId); + return template.inTransaction(READ_ONLY, handle -> { + var dao = handle.attach(AutomationRuleDAO.class); + var AutomationRuleEvaluator = dao.findById(id, projectId, workspaceId); + log.info("Got AutomationRuleEvaluator with id '{}', workspaceId '{}'", id, projectId); + return AutomationRuleEvaluator; + }); + } + + @Override + public void update(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId, + @NonNull AutomationRuleEvaluatorUpdate automationRuleEvaluator) { + String userName = requestContext.get().getUserName(); + + template.inTransaction(WRITE, handle -> { + var dao = handle.attach(AutomationRuleDAO.class); + + try { + int result = dao.update(id, projectId, workspaceId, automationRuleEvaluator, userName); + + if (result == 0) { + throw newNotFoundException(); + } + } catch (UnableToExecuteStatementException e) { + if (e.getCause() instanceof SQLIntegrityConstraintViolationException) { + log.info(EVALUATOR_ALREADY_EXISTS); + throw new EntityAlreadyExistsException(new ErrorMessage(List.of(EVALUATOR_ALREADY_EXISTS))); + } else { + throw e; + } + } + + return null; + }); + } + + @Override + public AutomationRuleEvaluator findById(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId) { + log.info("Finding AutomationRuleEvaluator with id '{}', projectId '{}'", id, projectId); + return template.inTransaction(READ_ONLY, handle -> { + var dao = handle.attach(AutomationRuleDAO.class); + var AutomationRuleEvaluator = dao.findById(id, projectId, workspaceId) + .orElseThrow(this::newNotFoundException); + log.info("Found AutomationRuleEvaluator with id '{}', projectId '{}'", id, projectId); + return AutomationRuleEvaluator; + }); + } + + @Override + public List findByProjectId(@NonNull UUID projectId, @NonNull String workspaceId) { + log.info("Finding AutomationRuleEvaluators with for projectId '{}'", projectId); + return template.inTransaction(READ_ONLY, handle -> { + var dao = handle.attach(AutomationRuleDAO.class); + var automationRuleEvaluators = dao.findByProjectId(projectId, workspaceId); + log.info("Found {} AutomationRuleEvaluators for projectId '{}'", automationRuleEvaluators.size(), + projectId); + return automationRuleEvaluators; + }); + } + @Override + public List findByIds(@NonNull Set ids, @NonNull UUID projectId, + @NonNull String workspaceId) { + if (ids.isEmpty()) { + log.info("Returning empty AutomationRuleEvaluators for empty ids, projectId '{}'", projectId); + return List.of(); + } + log.info("Finding AutomationRuleEvaluators with ids '{}', projectId '{}'", ids, projectId); + return template.inTransaction(READ_ONLY, handle -> { + var dao = handle.attach(AutomationRuleDAO.class); + var automationRuleEvaluators = dao.findByIds(ids, projectId, workspaceId); + log.info("Found AutomationRuleEvaluators with ids '{}', projectId '{}'", ids, projectId); + return automationRuleEvaluators; + }); + } + + /** + * Deletes a AutomationRuleEvaluator. + **/ + @Override + public void delete(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId) { + template.inTransaction(WRITE, handle -> { + var dao = handle.attach(AutomationRuleDAO.class); + dao.delete(id, projectId, workspaceId); + return null; + }); + } + + @Override + public void deleteByProject(@NonNull UUID projectId, @NonNull String workspaceId) { + template.inTransaction(WRITE, handle -> { + var dao = handle.attach(AutomationRuleDAO.class); + dao.deleteByProject(projectId, workspaceId); + return null; + }); + } + + private NotFoundException newNotFoundException() { + String message = "AutomationRuleEvaluator not found"; + log.info(message); + return new NotFoundException(message, + Response.status(Response.Status.NOT_FOUND).entity(new ErrorMessage(List.of(message))).build()); + } + + @Override + public void delete(Set ids, @NonNull UUID projectId, @NonNull String workspaceId) { + if (ids.isEmpty()) { + log.info("ids list is empty, returning"); + return; + } + + template.inTransaction(WRITE, handle -> { + handle.attach(AutomationRuleDAO.class).delete(ids, projectId, workspaceId); + return null; + }); + } + + @Override + public AutomationRuleEvaluator.AutomationRuleEvaluatorPage find(int page, int size, @NonNull UUID projectId) { + String workspaceId = requestContext.get().getWorkspaceId(); + + return template.inTransaction(READ_ONLY, handle -> { + var dao = handle.attach(AutomationRuleDAO.class); + var total = dao.findCount(projectId, workspaceId); + var offset = (page - 1) * size; + var automationRuleEvaluators = dao.find(size, offset, projectId, workspaceId); + log.info("Found {} AutomationRuleEvaluators for projectId '{}'", automationRuleEvaluators.size(), + projectId); + return new AutomationRuleEvaluator.AutomationRuleEvaluatorPage(page, automationRuleEvaluators.size(), total, + automationRuleEvaluators); + }); + } + +} diff --git a/apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000008_add_automation_rule_tables.sql b/apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000008_add_automation_rule_tables.sql new file mode 100644 index 000000000..d3ff73b00 --- /dev/null +++ b/apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000008_add_automation_rule_tables.sql @@ -0,0 +1,20 @@ +--liquibase formatted sql +--changeset DanielAugusto:000007_add_automation_rule_tables +CREATE TABLE IF NOT EXISTS automation_rule_evaluators ( + id CHAR(36), + project_id CHAR(36) NOT NULL, + workspace_id VARCHAR(150) NOT NULL, + + evaluator_type CHAR(20) NOT NULL, + sampling_rate FLOAT NOT NULL CHECK (sampling_rate >= 0 AND sampling_rate <= 1), + code TEXT NOT NULL, + + created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + created_by VARCHAR(100) NOT NULL DEFAULT 'admin', + last_updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + last_updated_by VARCHAR(100) NOT NULL DEFAULT 'admin', + + CONSTRAINT `automation_rules_pk` PRIMARY KEY (workspace_id, project_id, id) + ); + + diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java new file mode 100644 index 000000000..c06f902d1 --- /dev/null +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java @@ -0,0 +1,1307 @@ +package com.comet.opik.api.resources.v1.priv; + +import com.comet.opik.api.AutomationRuleEvaluator; +import com.comet.opik.api.resources.utils.AuthTestUtils; +import com.comet.opik.api.resources.utils.ClientSupportUtils; +import com.comet.opik.api.resources.utils.MigrationUtils; +import com.comet.opik.api.resources.utils.MySQLContainerUtils; +import com.comet.opik.api.resources.utils.RedisContainerUtils; +import com.comet.opik.api.resources.utils.TestDropwizardAppExtensionUtils; +import com.comet.opik.api.resources.utils.TestUtils; +import com.comet.opik.api.resources.utils.WireMockUtils; +import com.comet.opik.podam.PodamFactoryUtils; +import com.fasterxml.uuid.Generators; +import com.fasterxml.uuid.impl.TimeBasedEpochGenerator; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.redis.testcontainers.RedisContainer; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import org.jdbi.v3.core.Jdbi; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.lifecycle.Startables; +import ru.vyarus.dropwizard.guice.test.ClientSupport; +import ru.vyarus.dropwizard.guice.test.jupiter.ext.TestDropwizardAppExtension; +import uk.co.jemos.podam.api.PodamFactory; + +import java.util.UUID; +import java.util.random.RandomGenerator; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static com.comet.opik.infrastructure.auth.RequestContext.WORKSPACE_HEADER; +import static com.comet.opik.infrastructure.auth.TestHttpClientUtils.UNAUTHORIZED_RESPONSE; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; +import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@DisplayName("Feedback Resource Test") +class AutomationRuleEvaluatorsResourceTest { + + private static final String URL_TEMPLATE = "%s/v1/private/automation/evaluator/"; + private static final String URL_TEMPLATE_BY_PROJ_ID = "%s/v1/private/automation/evaluator/projectId/%s"; + private static final String URL_TEMPLATE_BY_PROJ_ID_AND_EVAL_ID = "%s/v1/private/automation/evaluator/projectId/%s/evaluatorId/%s"; + + private static final String USER = UUID.randomUUID().toString(); + private static final String API_KEY = UUID.randomUUID().toString(); + private static final String WORKSPACE_ID = UUID.randomUUID().toString(); + private static final String TEST_WORKSPACE = UUID.randomUUID().toString(); + + private static final RedisContainer REDIS = RedisContainerUtils.newRedisContainer(); + + private static final MySQLContainer MYSQL = MySQLContainerUtils.newMySQLContainer(); + + @RegisterExtension + private static final TestDropwizardAppExtension app; + + private static final WireMockUtils.WireMockRuntime wireMock; + + static { + Startables.deepStart(REDIS, MYSQL).join(); + + wireMock = WireMockUtils.startWireMock(); + + app = TestDropwizardAppExtensionUtils.newTestDropwizardAppExtension(MYSQL.getJdbcUrl(), null, + wireMock.runtimeInfo(), REDIS.getRedisURI()); + } + + private final PodamFactory factory = PodamFactoryUtils.newPodamFactory(); + + private String baseURI; + private ClientSupport client; + + @BeforeAll + void setUpAll(ClientSupport client, Jdbi jdbi) { + + MigrationUtils.runDbMigration(jdbi, MySQLContainerUtils.migrationParameters()); + + this.baseURI = "http://localhost:%d".formatted(client.getPort()); + this.client = client; + + ClientSupportUtils.config(client); + + mockTargetWorkspace(API_KEY, TEST_WORKSPACE, WORKSPACE_ID); + } + + private static void mockTargetWorkspace(String apiKey, String workspaceName, String workspaceId) { + AuthTestUtils.mockTargetWorkspace(wireMock.server(), apiKey, workspaceName, workspaceId, USER); + } + + @AfterAll + void tearDownAll() { + wireMock.server().stop(); + } + + private UUID create(AutomationRuleEvaluator evaluator, String apiKey, String workspaceName) { + try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + .request() + .accept(MediaType.APPLICATION_JSON_TYPE) + .header(HttpHeaders.AUTHORIZATION, apiKey) + .header(WORKSPACE_HEADER, workspaceName) + .post(Entity.json(evaluator))) { + + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); + + return TestUtils.getIdFromLocation(actualResponse.getLocation()); + } + } + + @Nested + @DisplayName("Api Key Authentication:") + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class ApiKey { + + private final String fakeApikey = UUID.randomUUID().toString(); + private final String okApikey = UUID.randomUUID().toString(); + + Stream credentials() { + return Stream.of( + arguments(okApikey, true), + arguments(fakeApikey, false), + arguments("", false)); + } + + @BeforeEach + void setUp() { + + wireMock.server().stubFor( + post(urlPathEqualTo("/opik/auth")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo(fakeApikey)) + .withRequestBody(matchingJsonPath("$.workspaceName", matching(".+"))) + .willReturn(WireMock.unauthorized())); + + wireMock.server().stubFor( + post(urlPathEqualTo("/opik/auth")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("")) + .withRequestBody(matchingJsonPath("$.workspaceName", matching(".+"))) + .willReturn(WireMock.unauthorized())); + } + + @ParameterizedTest + @MethodSource("credentials") + @DisplayName("create evaluator definition: when api key is present, then return proper response") + void createAutomationRuleEvaluator__whenApiKeyIsPresent__thenReturnProperResponse(String apiKey, + boolean isAuthorized) { + + var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); + + mockTargetWorkspace(okApikey, TEST_WORKSPACE, WORKSPACE_ID); + + try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + .request() + .header(HttpHeaders.AUTHORIZATION, apiKey) + .accept(MediaType.APPLICATION_JSON_TYPE) + .header(WORKSPACE_HEADER, TEST_WORKSPACE) + .post(Entity.json(ruleEvaluator))) { + + if (isAuthorized) { + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); + assertThat(actualResponse.hasEntity()).isFalse(); + } else { + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); + assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) + .isEqualTo(UNAUTHORIZED_RESPONSE); + } + } + } + + @ParameterizedTest + @MethodSource("credentials") + @DisplayName("get evaluators by project id: when api key is present, then return proper response") + void getProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnProperResponse(String apiKey, + boolean isAuthorized) { + + final String workspaceName = UUID.randomUUID().toString(); + final String workspaceId = UUID.randomUUID().toString(); + final UUID projectId = UUID.randomUUID(); + + mockTargetWorkspace(okApikey, workspaceName, workspaceId); + + int samplesToCreate = 15; + + IntStream.range(0, samplesToCreate).forEach(i -> { + var evaluator = factory.manufacturePojo(AutomationRuleEvaluator.class) + .toBuilder().projectId(projectId).build(); + create(evaluator, okApikey, workspaceName); + }); + + try (var actualResponse = client.target(URL_TEMPLATE_BY_PROJ_ID.formatted(baseURI, projectId)) + .queryParam("size", samplesToCreate) + .request() + .header(HttpHeaders.AUTHORIZATION, apiKey) + .accept(MediaType.APPLICATION_JSON_TYPE) + .header(WORKSPACE_HEADER, workspaceName) + .get()) { + + if (isAuthorized) { + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); + assertThat(actualResponse.hasEntity()).isTrue(); + + var actualEntity = actualResponse + .readEntity(AutomationRuleEvaluator.AutomationRuleEvaluatorPage.class); + assertThat(actualEntity.content()).hasSize(samplesToCreate); + assertThat(actualEntity.total()).isEqualTo(samplesToCreate); + + } else { + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); + assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) + .isEqualTo(UNAUTHORIZED_RESPONSE); + } + } + } + + @ParameterizedTest + @MethodSource("credentials") + @DisplayName("get evaluator by id: when api key is present, then return proper response") + void getAutomationRuleEvaluatorById__whenApiKeyIsPresent__thenReturnProperResponse(String apiKey, + boolean isAuthorized) { + + var evaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); + + String workspaceName = UUID.randomUUID().toString(); + String workspaceId = UUID.randomUUID().toString(); + + mockTargetWorkspace(okApikey, workspaceName, workspaceId); + + UUID id = create(evaluator, okApikey, workspaceName); + + try (var actualResponse = client.target(URL_TEMPLATE_BY_PROJ_ID_AND_EVAL_ID.formatted(baseURI, evaluator.projectId(), id)) + //.path(id.toString()) + .request() + .header(HttpHeaders.AUTHORIZATION, apiKey) + .accept(MediaType.APPLICATION_JSON_TYPE) + .header(WORKSPACE_HEADER, workspaceName) + .get()) { + + if (isAuthorized) { + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); + assertThat(actualResponse.hasEntity()).isTrue(); + + var ruleEvaluator = actualResponse.readEntity(AutomationRuleEvaluator.class); + assertThat(ruleEvaluator.id()).isEqualTo(id); + } else { + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); + assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) + .isEqualTo(UNAUTHORIZED_RESPONSE); + } + } + } + + @ParameterizedTest + @MethodSource("credentials") + @DisplayName("update evaluator: when api key is present, then return proper response") + void updateAutomationRuleEvaluator__whenApiKeyIsPresent__thenReturnProperResponse(String apiKey, + boolean isAuthorized) { + + var evaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); + + String workspaceName = UUID.randomUUID().toString(); + String workspaceId = UUID.randomUUID().toString(); + + mockTargetWorkspace(okApikey, workspaceName, workspaceId); + + UUID id = create(evaluator, okApikey, workspaceName); + + var updatedEvaluator = evaluator.toBuilder() + .code(UUID.randomUUID().toString()) + .samplingRate(RandomGenerator.getDefault().nextFloat()) + .build(); + + try (var actualResponse = client.target(URL_TEMPLATE_BY_PROJ_ID_AND_EVAL_ID.formatted(baseURI, evaluator.projectId(), id)) + .request() + .header(HttpHeaders.AUTHORIZATION, apiKey) + .accept(MediaType.APPLICATION_JSON_TYPE) + .header(WORKSPACE_HEADER, workspaceName) + .put(Entity.json(updatedEvaluator))) { + + if (isAuthorized) { + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(204); + assertThat(actualResponse.hasEntity()).isFalse(); + } else { + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); + assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) + .isEqualTo(UNAUTHORIZED_RESPONSE); + } + } + } + + @ParameterizedTest + @MethodSource("credentials") + @DisplayName("delete evaluator by id: when api key is present, then return proper response") + void deleteAutomationRuleEvaluator__whenApiKeyIsPresent__thenReturnProperResponse(String apiKey, + boolean isAuthorized) { + + var evaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); + + String workspaceName = UUID.randomUUID().toString(); + String workspaceId = UUID.randomUUID().toString(); + + mockTargetWorkspace(okApikey, workspaceName, workspaceId); + + UUID id = create(evaluator, okApikey, workspaceName); + + try (var actualResponse = client.target(URL_TEMPLATE_BY_PROJ_ID_AND_EVAL_ID.formatted(baseURI, evaluator.projectId(), id)) + .request() + .header(HttpHeaders.AUTHORIZATION, apiKey) + .accept(MediaType.APPLICATION_JSON_TYPE) + .header(WORKSPACE_HEADER, workspaceName) + .delete()) { + + if (isAuthorized) { + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(204); + assertThat(actualResponse.hasEntity()).isFalse(); + } else { + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); + assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) + .isEqualTo(UNAUTHORIZED_RESPONSE); + } + } + } + + @ParameterizedTest + @MethodSource("credentials") + @DisplayName("delete evaluators by project id: when api key is present, then return proper response") + void deleteProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnProperResponse(String apiKey, + boolean isAuthorized) { + + var projectId = UUID.randomUUID(); + var evaluator1 = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().projectId(projectId).build(); + var evaluator2 = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().projectId(projectId).build(); + + String workspaceName = UUID.randomUUID().toString(); + String workspaceId = UUID.randomUUID().toString(); + + mockTargetWorkspace(okApikey, workspaceName, workspaceId); + + create(evaluator1, okApikey, workspaceName); + create(evaluator2, okApikey, workspaceName); + + try (var actualResponse = client.target(URL_TEMPLATE_BY_PROJ_ID.formatted(baseURI, evaluator1.projectId())) + .request() + .header(HttpHeaders.AUTHORIZATION, apiKey) + .accept(MediaType.APPLICATION_JSON_TYPE) + .header(WORKSPACE_HEADER, workspaceName) + .delete()) { + + if (isAuthorized) { + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(204); + assertThat(actualResponse.hasEntity()).isFalse(); + } else { + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); + assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) + .isEqualTo(UNAUTHORIZED_RESPONSE); + } + } + + // we shall see a single evaluators for the project now + try (var actualResponse = client.target(URL_TEMPLATE_BY_PROJ_ID.formatted(baseURI, projectId)) + .request() + .header(HttpHeaders.AUTHORIZATION, apiKey) + .accept(MediaType.APPLICATION_JSON_TYPE) + .header(WORKSPACE_HEADER, workspaceName) + .get()) { + + if (isAuthorized) { + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); + assertThat(actualResponse.hasEntity()).isTrue(); + + var actualEntity = actualResponse + .readEntity(AutomationRuleEvaluator.AutomationRuleEvaluatorPage.class); + assertThat(actualEntity.content()).hasSize(0); + assertThat(actualEntity.total()).isEqualTo(0); + + } else { + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); + assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) + .isEqualTo(UNAUTHORIZED_RESPONSE); + } + } + } + } + + // @Nested + // @DisplayName("Session Token Authentication:") + // @TestInstance(TestInstance.Lifecycle.PER_CLASS) + // class SessionTokenCookie { + // + // private final String sessionToken = UUID.randomUUID().toString(); + // private final String fakeSessionToken = UUID.randomUUID().toString(); + // + // Stream credentials() { + // return Stream.of( + // arguments(sessionToken, true, "OK_" + UUID.randomUUID()), + // arguments(fakeSessionToken, false, UUID.randomUUID().toString())); + // } + // + // @BeforeEach + // void setUp() { + // wireMock.server().stubFor( + // post(urlPathEqualTo("/opik/auth-session")) + // .withCookie(SESSION_COOKIE, equalTo(sessionToken)) + // .withRequestBody(matchingJsonPath("$.workspaceName", matching("OK_.+"))) + // .willReturn(okJson(AuthTestUtils.newWorkspaceAuthResponse(USER, WORKSPACE_ID)))); + // + // wireMock.server().stubFor( + // post(urlPathEqualTo("/opik/auth-session")) + // .withCookie(SESSION_COOKIE, equalTo(fakeSessionToken)) + // .withRequestBody(matchingJsonPath("$.workspaceName", matching(".+"))) + // .willReturn(WireMock.unauthorized())); + // } + // + // @ParameterizedTest + // @MethodSource("credentials") + // @DisplayName("create evaluator definition: when session token is present, then return proper response") + // void createAutomationRuleEvaluator__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, + // boolean success, String workspaceName) { + // + // var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .request() + // .cookie(SESSION_COOKIE, sessionToken) + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(WORKSPACE_HEADER, workspaceName) + // .post(Entity.json(ruleEvaluator))) { + // + // if (success) { + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); + // assertThat(actualResponse.hasEntity()).isFalse(); + // } else { + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); + // assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) + // .isEqualTo(UNAUTHORIZED_RESPONSE); + // } + // } + // } + // + // @ParameterizedTest + // @MethodSource("credentials") + // @DisplayName("get evaluators: when session token is present, then return proper response") + // void getAutomationRuleEvaluators__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, + // boolean success, String workspaceName) { + // + // int size = 15; + // var newWorkspaceName = UUID.randomUUID().toString(); + // var newWorkspaceId = UUID.randomUUID().toString(); + // + // wireMock.server().stubFor( + // post(urlPathEqualTo("/opik/auth-session")) + // .withCookie(SESSION_COOKIE, equalTo(sessionToken)) + // .withRequestBody(matchingJsonPath("$.workspaceName", equalTo(newWorkspaceName))) + // .willReturn(okJson(AuthTestUtils.newWorkspaceAuthResponse(USER, newWorkspaceId)))); + // + // IntStream.range(0, size).forEach(i -> { + // create(factory.manufacturePojo(AutomationRuleEvaluator.class), API_KEY, TEST_WORKSPACE); + // }); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .queryParam("workspace_name", workspaceName) + // .queryParam("size", size) + // .request() + // .cookie(SESSION_COOKIE, sessionToken) + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(WORKSPACE_HEADER, workspaceName) + // .get()) { + // + // if (success) { + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); + // assertThat(actualResponse.hasEntity()).isTrue(); + // + // var actualEntity = actualResponse.readEntity(AutomationRuleEvaluatorUpdate.class); + // assertThat(actualEntity.content()).hasSize(size); + // } else { + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); + // assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) + // .isEqualTo(UNAUTHORIZED_RESPONSE); + // } + // } + // } + // + // @ParameterizedTest + // @MethodSource("credentials") + // @DisplayName("get evaluator by id: when session token is present, then return proper response") + // void getAutomationRuleEvaluatorById__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, + // boolean success, String workspaceName) { + // + // var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class); + // + // UUID id = create(feedback, API_KEY, TEST_WORKSPACE); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .path(id.toString()) + // .request() + // .cookie(SESSION_COOKIE, sessionToken) + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(WORKSPACE_HEADER, workspaceName) + // .get()) { + // + // if (success) { + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); + // assertThat(actualResponse.hasEntity()).isTrue(); + // + // var ruleEvaluator = actualResponse + // .readEntity(AutomationRuleEvaluator.class); + // assertThat(ruleEvaluator.getId()).isEqualTo(id); + // } else { + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); + // assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) + // .isEqualTo(UNAUTHORIZED_RESPONSE); + // } + // } + // + // } + // + // @ParameterizedTest + // @MethodSource("credentials") + // @DisplayName("update evaluator: when session token is present, then return proper response") + // void updateAutomationRuleEvaluator__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, + // boolean success, String workspaceName) { + // + // var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class); + // + // UUID id = create(feedback, API_KEY, TEST_WORKSPACE); + // + // var updatedFeedback = feedback.toBuilder() + // .name(UUID.randomUUID().toString()) + // .build(); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .path(id.toString()) + // .request() + // .cookie(SESSION_COOKIE, sessionToken) + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(WORKSPACE_HEADER, workspaceName) + // .put(Entity.json(updatedFeedback))) { + // + // if (success) { + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(204); + // assertThat(actualResponse.hasEntity()).isFalse(); + // } else { + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); + // assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) + // .isEqualTo(UNAUTHORIZED_RESPONSE); + // } + // } + // } + // + // @ParameterizedTest + // @MethodSource("credentials") + // @DisplayName("delete evaluator: when session token is present, then return proper response") + // void deleteAutomationRuleEvaluator__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, + // boolean success, String workspaceName) { + // + // var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class); + // + // UUID id = create(feedback, API_KEY, TEST_WORKSPACE); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .path(id.toString()) + // .request() + // .cookie(SESSION_COOKIE, sessionToken) + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(WORKSPACE_HEADER, workspaceName) + // .delete()) { + // + // if (success) { + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(204); + // assertThat(actualResponse.hasEntity()).isFalse(); + // } else { + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); + // assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) + // .isEqualTo(UNAUTHORIZED_RESPONSE); + // } + // } + // } + // } + // + // @Nested + // @DisplayName("Get:") + // @TestInstance(TestInstance.Lifecycle.PER_CLASS) + // class GetAllAutomationRuleEvaluator { + // + // @Test + // @DisplayName("Success") + // void find() { + // + // String workspaceName = UUID.randomUUID().toString(); + // String workspaceId = UUID.randomUUID().toString(); + // String apiKey = UUID.randomUUID().toString(); + // + // mockTargetWorkspace(apiKey, workspaceName, workspaceId); + // + // IntStream.range(0, 15).forEach(i -> { + // create(i % 2 == 0 + // ? factory.manufacturePojo(AutomationRuleEvaluator.class) + // : factory.manufacturePojo(AutomationRuleEvaluator.class), + // apiKey, + // workspaceName); + // }); + // + // var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .queryParam("workspace_name", workspaceName) + // .request() + // .header(HttpHeaders.AUTHORIZATION, apiKey) + // .header(WORKSPACE_HEADER, workspaceName) + // .get(); + // + // var actualEntity = actualResponse.readEntity(AutomationRuleEvaluatorPage.class); + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); + // assertThat(actualEntity.page()).isEqualTo(1); + // assertThat(actualEntity.size()).isEqualTo(10); + // assertThat(actualEntity.content()).hasSize(10); + // assertThat(actualEntity.total()).isGreaterThanOrEqualTo(15); + // } + // + // @Test + // @DisplayName("when searching by name, then return feedbacks") + // void find__whenSearchingByName__thenReturnFeedbacks() { + // + // String workspaceName = UUID.randomUUID().toString(); + // String workspaceId = UUID.randomUUID().toString(); + // String apiKey = UUID.randomUUID().toString(); + // + // mockTargetWorkspace(apiKey, workspaceName, workspaceId); + // String name = "My Feedback:" + UUID.randomUUID(); + // + // var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class) + // .toBuilder() + // .name(name) + // .build(); + // + // create(feedback, apiKey, workspaceName); + // + // var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .queryParam("name", "eedback") + // .request() + // .header(HttpHeaders.AUTHORIZATION, apiKey) + // .header(WORKSPACE_HEADER, workspaceName) + // .get(); + // + // var actualEntity = actualResponse.readEntity(AutomationRuleEvaluatorPage.class); + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); + // assertThat(actualEntity.page()).isEqualTo(1); + // assertThat(actualEntity.size()).isEqualTo(1); + // assertThat(actualEntity.total()).isEqualTo(1); + // + // List> content = actualEntity.content(); + // assertThat(content.stream().map(AutomationRuleEvaluator::getName).toList()).contains(name); + // } + // + // @Test + // @DisplayName("when searching by type, then return feedbacks") + // void find__whenSearchingByType__thenReturnFeedbacks() { + // + // String workspaceName = UUID.randomUUID().toString(); + // String workspaceId = UUID.randomUUID().toString(); + // String apiKey = UUID.randomUUID().toString(); + // + // mockTargetWorkspace(apiKey, workspaceName, workspaceId); + // + // var feedback1 = factory.manufacturePojo(AutomationRuleEvaluator.class); + // var feedback2 = factory.manufacturePojo(AutomationRuleEvaluator.class); + // + // create(feedback1, apiKey, workspaceName); + // create(feedback2, apiKey, workspaceName); + // + // var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .queryParam("type", FeedbackType.NUMERICAL.getType()) + // .request() + // .header(HttpHeaders.AUTHORIZATION, apiKey) + // .header(WORKSPACE_HEADER, workspaceName) + // .get(); + // + // var actualEntity = actualResponse.readEntity(AutomationRuleEvaluatorPage.class); + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); + // assertThat(actualEntity.page()).isEqualTo(1); + // assertThat(actualEntity.size()).isEqualTo(1); + // assertThat(actualEntity.total()).isEqualTo(1); + // + // List> content = actualEntity.content(); + // assertThat( + // content.stream().map(AutomationRuleEvaluator::getType).allMatch(type -> FeedbackType.NUMERICAL == type)) + // .isTrue(); + // } + // + // @Test + // @DisplayName("when searching by workspace name, then return feedbacks") + // void find__whenSearchingByWorkspaceName__thenReturnFeedbacks() { + // + // String workspaceName = UUID.randomUUID().toString(); + // String workspaceId = UUID.randomUUID().toString(); + // String apiKey = UUID.randomUUID().toString(); + // + // String workspaceName2 = UUID.randomUUID().toString(); + // String workspaceId2 = UUID.randomUUID().toString(); + // String apiKey2 = UUID.randomUUID().toString(); + // + // mockTargetWorkspace(apiKey, workspaceName, workspaceId); + // mockTargetWorkspace(apiKey2, workspaceName2, workspaceId2); + // + // var feedback1 = factory.manufacturePojo(AutomationRuleEvaluator.class); + // + // var feedback2 = factory.manufacturePojo(AutomationRuleEvaluator.class); + // + // create(feedback1, apiKey, workspaceName); + // create(feedback2, apiKey2, workspaceName2); + // + // var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .request() + // .header(HttpHeaders.AUTHORIZATION, apiKey2) + // .header(WORKSPACE_HEADER, workspaceName2) + // .get(); + // + // var actualEntity = actualResponse.readEntity(AutomationRuleEvaluatorPage.class); + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); + // assertThat(actualEntity.page()).isEqualTo(1); + // assertThat(actualEntity.size()).isEqualTo(1); + // assertThat(actualEntity.total()).isEqualTo(1); + // assertThat(actualEntity.content()).hasSize(1); + // + // AutomationRuleEvaluator actual = (AutomationRuleEvaluator) actualEntity + // .content().get(0); + // + // assertThat(actual.getName()).isEqualTo(feedback2.getName()); + // assertThat(actual.getDetails().getCategories()).isEqualTo(feedback2.getDetails().getCategories()); + // assertThat(actual.getType()).isEqualTo(feedback2.getType()); + // } + // + // @Test + // @DisplayName("when searching by name and workspace, then return feedbacks") + // void find__whenSearchingByNameAndWorkspace__thenReturnFeedbacks() { + // + // var name = UUID.randomUUID().toString(); + // + // var workspaceName = UUID.randomUUID().toString(); + // var workspaceId = UUID.randomUUID().toString(); + // + // var workspaceName2 = UUID.randomUUID().toString(); + // var workspaceId2 = UUID.randomUUID().toString(); + // + // var apiKey = UUID.randomUUID().toString(); + // var apiKey2 = UUID.randomUUID().toString(); + // + // mockTargetWorkspace(apiKey, workspaceName, workspaceId); + // mockTargetWorkspace(apiKey2, workspaceName2, workspaceId2); + // + // var feedback1 = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder() + // .name(name) + // .build(); + // + // var feedback2 = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder() + // .name(name) + // .build(); + // + // create(feedback1, apiKey, workspaceName); + // create(feedback2, apiKey2, workspaceName2); + // + // var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .queryParam("name", name) + // .request() + // .header(HttpHeaders.AUTHORIZATION, apiKey2) + // .header(WORKSPACE_HEADER, workspaceName2) + // .get(); + // + // var actualEntity = actualResponse.readEntity(AutomationRuleEvaluatorPage.class); + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); + // assertThat(actualEntity.page()).isEqualTo(1); + // assertThat(actualEntity.size()).isEqualTo(1); + // assertThat(actualEntity.total()).isEqualTo(1); + // assertThat(actualEntity.content()).hasSize(1); + // + // AutomationRuleEvaluator actual = (AutomationRuleEvaluator) actualEntity + // .content().get(0); + // + // assertThat(actual.getName()).isEqualTo(feedback2.getName()); + // assertThat(actual.getDetails().getCategories()).isEqualTo(feedback2.getDetails().getCategories()); + // assertThat(actual.getType()).isEqualTo(feedback2.getType()); + // } + // + // } + // + // @Nested + // @DisplayName("Get {id}:") + // @TestInstance(TestInstance.Lifecycle.PER_CLASS) + // class GetAutomationRuleEvaluator { + // + // @Test + // @DisplayName("Success") + // void getById() { + // + // final var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class); + // + // var id = create(feedback, API_KEY, TEST_WORKSPACE); + // + // var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .path(id.toString()) + // .request() + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .get(); + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); + // var actualEntity = actualResponse.readEntity(AutomationRuleEvaluator.class); + // + // assertThat(actualEntity) + // .usingRecursiveComparison(RecursiveComparisonConfiguration.builder() + // .withIgnoredFields(IGNORED_FIELDS) + // .withComparatorForType(BigDecimal::compareTo, BigDecimal.class) + // .build()) + // .isEqualTo(feedback); + // + // assertThat(actualEntity.getType()).isEqualTo(FeedbackType.NUMERICAL); + // assertThat(actualEntity.getLastUpdatedBy()).isEqualTo(USER); + // assertThat(actualEntity.getCreatedBy()).isEqualTo(USER); + // assertThat(actualEntity.getCreatedAt()).isNotNull(); + // assertThat(actualEntity.getCreatedAt()).isInstanceOf(Instant.class); + // assertThat(actualEntity.getLastUpdatedAt()).isNotNull(); + // assertThat(actualEntity.getLastUpdatedAt()).isInstanceOf(Instant.class); + // + // assertThat(actualEntity.getCreatedAt()).isAfter(feedback.getCreatedAt()); + // assertThat(actualEntity.getLastUpdatedAt()).isAfter(feedback.getLastUpdatedAt()); + // } + // + // @Test + // @DisplayName("when feedback does not exist, then return not found") + // void getById__whenFeedbackDoesNotExist__thenReturnNotFound() { + // + // var id = generator.generate(); + // + // var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .path(id.toString()) + // .request() + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .get(); + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(404); + // var actualEntity = actualResponse.readEntity(ErrorMessage.class); + // + // assertThat(actualEntity.errors()).containsExactly("evaluator not found"); + // } + // + // } + // + // @Nested + // @DisplayName("Create:") + // @TestInstance(TestInstance.Lifecycle.PER_CLASS) + // class CreateAutomationRuleEvaluator { + // + // @Test + // @DisplayName("Success") + // void create() { + // UUID id; + // + // var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .request() + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .post(Entity.json(ruleEvaluator))) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); + // assertThat(actualResponse.hasEntity()).isFalse(); + // assertThat(actualResponse.getHeaderString("Location")).matches(Pattern.compile(URL_PATTERN)); + // + // id = TestUtils.getIdFromLocation(actualResponse.getLocation()); + // } + // + // var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .path(id.toString()) + // .request() + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .get(); + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); + // + // var actualEntity = actualResponse.readEntity(AutomationRuleEvaluator.class); + // + // assertThat(actualEntity.getId()).isEqualTo(id); + // } + // + // @Test + // @DisplayName("when feedback already exists, then return error") + // void create__whenFeedbackAlreadyExists__thenReturnError() { + // + // NumericalAutomationRuleEvaluator feedback = factory + // .manufacturePojo(AutomationRuleEvaluator.class); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .request() + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .post(Entity.json(feedback))) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); + // assertThat(actualResponse.hasEntity()).isFalse(); + // } + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .request() + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .post(Entity.json(feedback))) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(409); + // assertThat(actualResponse.hasEntity()).isTrue(); + // assertThat(actualResponse.readEntity(ErrorMessage.class).errors()) + // .containsExactly("Feedback already exists"); + // } + // } + // + // @Test + // @DisplayName("when details is null, then return bad request") + // void create__whenDetailsIsNull__thenReturnBadRequest() { + // + // var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder() + // .details(null) + // .build(); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .request() + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .post(Entity.json(ruleEvaluator))) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(422); + // assertThat(actualResponse.hasEntity()).isTrue(); + // assertThat(actualResponse.readEntity(ErrorMessage.class).errors()) + // .containsExactly("details must not be null"); + // + // } + // } + // + // @Test + // @DisplayName("when name is null, then return bad request") + // void create__whenNameIsNull__thenReturnBadRequest() { + // + // var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder() + // .name(null) + // .build(); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .request() + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .post(Entity.json(ruleEvaluator))) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(422); + // assertThat(actualResponse.hasEntity()).isTrue(); + // assertThat(actualResponse.readEntity(ErrorMessage.class).errors()) + // .containsExactly("name must not be blank"); + // } + // } + // + // @Test + // @DisplayName("when categoryName is null, then return bad request") + // void create__whenCategoryIsNull__thenReturnBadRequest() { + // + // var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder() + // .details(CategoricalFeedbackDetail + // .builder() + // .build()) + // .build(); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .request() + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .post(Entity.json(ruleEvaluator))) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(422); + // assertThat(actualResponse.hasEntity()).isTrue(); + // assertThat(actualResponse.readEntity(ErrorMessage.class).errors()) + // .containsExactly("details.categories must not be null"); + // } + // } + // + // @Test + // @DisplayName("when categoryName is empty, then return bad request") + // void create__whenCategoryIsEmpty__thenReturnBadRequest() { + // + // var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder() + // .details(CategoricalFeedbackDetail.builder().categories(Map.of()).build()) + // .build(); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .request() + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .post(Entity.json(ruleEvaluator))) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(422); + // assertThat(actualResponse.hasEntity()).isTrue(); + // assertThat(actualResponse.readEntity(ErrorMessage.class).errors()) + // .containsExactly("details.categories size must be between 2 and 2147483647"); + // } + // } + // + // @Test + // @DisplayName("when categoryName has one key pair, then return bad request") + // void create__whenCategoryHasOneKeyPair__thenReturnBadRequest() { + // + // var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder() + // .details( + // CategoricalFeedbackDetail.builder() + // .categories(Map.of("yes", 1.0)) + // .build()) + // .build(); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .request() + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .post(Entity.json(ruleEvaluator))) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(422); + // assertThat(actualResponse.hasEntity()).isTrue(); + // assertThat(actualResponse.readEntity(ErrorMessage.class).errors()) + // .containsExactly("details.categories size must be between 2 and 2147483647"); + // } + // } + // + // @Test + // @DisplayName("when numerical min is null, then return bad request") + // void create__whenNumericalMinIsNull__thenReturnBadRequest() { + // + // var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder() + // .details(NumericalAutomationRuleEvaluator.NumericalFeedbackDetail + // .builder() + // .max(BigDecimal.valueOf(10)) + // .build()) + // .build(); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .request() + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .post(Entity.json(ruleEvaluator))) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(422); + // assertThat(actualResponse.hasEntity()).isTrue(); + // assertThat(actualResponse.readEntity(ErrorMessage.class).errors()) + // .containsExactly("details.min must not be null"); + // } + // } + // + // @Test + // @DisplayName("when numerical max is null, then return bad request") + // void create__whenNumericalMaxIsNull__thenReturnBadRequest() { + // + // var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder() + // .details(NumericalAutomationRuleEvaluator.NumericalFeedbackDetail + // .builder() + // .min(BigDecimal.valueOf(10)) + // .build()) + // .build(); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .request() + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .post(Entity.json(ruleEvaluator))) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(422); + // assertThat(actualResponse.hasEntity()).isTrue(); + // assertThat(actualResponse.readEntity(ErrorMessage.class).errors()) + // .containsExactly("details.max must not be null"); + // } + // } + // + // @Test + // @DisplayName("when numerical max is smaller than min, then return bad request") + // void create__whenNumericalMaxIsSmallerThanMin__thenReturnBadRequest() { + // + // var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder() + // .details(NumericalAutomationRuleEvaluator.NumericalFeedbackDetail + // .builder() + // .min(BigDecimal.valueOf(10)) + // .max(BigDecimal.valueOf(1)) + // .build()) + // .build(); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .request() + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .post(Entity.json(ruleEvaluator))) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(422); + // assertThat(actualResponse.hasEntity()).isTrue(); + // assertThat(actualResponse.readEntity(ErrorMessage.class).errors()) + // .containsExactly("details.min has to be smaller than details.max"); + // } + // } + // + // } + // + // @Nested + // @DisplayName("Update:") + // @TestInstance(TestInstance.Lifecycle.PER_CLASS) + // class UpdateAutomationRuleEvaluator { + // + // @Test + // + // void notfound() { + // + // UUID id = generator.generate(); + // + // var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder() + // .details(CategoricalFeedbackDetail + // .builder() + // .categories(Map.of("yes", 1., "no", 0.)) + // .build()) + // .build(); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .path(id.toString()) + // .request() + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .put(Entity.json(ruleEvaluator))) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(404); + // assertThat(actualResponse.hasEntity()).isTrue(); + // + // var actualEntity = actualResponse.readEntity(ErrorMessage.class); + // assertThat(actualEntity.errors()).containsExactly("evaluator not found"); + // } + // } + // + // @Test + // void update() { + // + // String name = UUID.randomUUID().toString(); + // String name2 = UUID.randomUUID().toString(); + // + // var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class) + // .toBuilder() + // .name(name) + // .build(); + // + // UUID id = create(ruleEvaluator, API_KEY, TEST_WORKSPACE); + // + // var ruleEvaluator1 = ruleEvaluator.toBuilder() + // .name(name2) + // .build(); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .path(id.toString()) + // .request() + // .accept(MediaType.APPLICATION_JSON_TYPE) + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .put(Entity.json(ruleEvaluator1))) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(204); + // assertThat(actualResponse.hasEntity()).isFalse(); + // } + // + // var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .path(id.toString()) + // .request() + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .get(); + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); + // var actualEntity = actualResponse.readEntity(AutomationRuleEvaluator.class); + // + // assertThat(actualEntity.getName()).isEqualTo(name2); + // assertThat(actualEntity.getDetails().getCategories()) + // .isEqualTo(ruleEvaluator.getDetails().getCategories()); + // } + // + // } + // + // @Nested + // @DisplayName("Delete:") + // @TestInstance(TestInstance.Lifecycle.PER_CLASS) + // class DeleteAutomationRuleEvaluator { + // + // @Test + // @DisplayName("Success") + // void deleteById() { + // final UUID id = create(factory.manufacturePojo(AutomationRuleEvaluator.class), + // API_KEY, TEST_WORKSPACE); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .path(id.toString()) + // .request() + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .delete()) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(204); + // assertThat(actualResponse.hasEntity()).isFalse(); + // } + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .path(id.toString()) + // .request() + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .get()) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(404); + // assertThat(actualResponse.hasEntity()).isTrue(); + // assertThat(actualResponse.readEntity(ErrorMessage.class).errors()) + // .containsExactly("evaluator not found"); + // } + // } + // + // @Test + // @DisplayName("delete batch evaluators") + // void deleteBatch() { + // var apiKey = UUID.randomUUID().toString(); + // var workspaceName = UUID.randomUUID().toString(); + // var workspaceId = UUID.randomUUID().toString(); + // mockTargetWorkspace(apiKey, workspaceName, workspaceId); + // + // var ids = PodamFactoryUtils.manufacturePojoList(factory, + // AutomationRuleEvaluator.class).stream() + // .map(ruleEvaluator -> create(ruleEvaluator, apiKey, workspaceName)) + // .toList(); + // var idsToDelete = ids.subList(0, 3); + // var notDeletedIds = ids.subList(3, ids.size()); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .path("delete") + // .request() + // .header(HttpHeaders.AUTHORIZATION, apiKey) + // .header(WORKSPACE_HEADER, workspaceName) + // .post(Entity.json(new BatchDelete(new HashSet<>(idsToDelete))))) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT); + // assertThat(actualResponse.hasEntity()).isFalse(); + // } + // + // var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .queryParam("size", ids.size()) + // .queryParam("page", 1) + // .request() + // .header(HttpHeaders.AUTHORIZATION, apiKey) + // .header(WORKSPACE_HEADER, workspaceName) + // .get(); + // + // var actualEntity = actualResponse.readEntity(AutomationRuleEvaluatorPage.class); + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(HttpStatus.SC_OK); + // assertThat(actualEntity.size()).isEqualTo(notDeletedIds.size()); + // assertThat(actualEntity.content().stream().map(AutomationRuleEvaluator::getId).toList()) + // .usingRecursiveComparison() + // .ignoringCollectionOrder() + // .isEqualTo(notDeletedIds); + // } + // + // @Test + // @DisplayName("when id found, then return no content") + // void deleteById__whenIdNotFound__thenReturnNoContent() { + // UUID id = UUID.randomUUID(); + // + // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + // .path(id.toString()) + // .request() + // .header(HttpHeaders.AUTHORIZATION, API_KEY) + // .header(WORKSPACE_HEADER, TEST_WORKSPACE) + // .delete()) { + // + // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(204); + // assertThat(actualResponse.hasEntity()).isFalse(); + // } + // } + // } +} \ No newline at end of file From 59cb8c2a99c5f5ec90f90d5f78b656d06a42a66c Mon Sep 17 00:00:00 2001 From: Daniel Augusto Date: Fri, 20 Dec 2024 13:58:48 +0000 Subject: [PATCH 02/10] cleaning up --- .../comet/opik/api/AutomationRuleEvaluator.java | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java index 097ac86ae..3a8aabfb4 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java @@ -14,15 +14,6 @@ import static com.comet.opik.utils.ValidationUtils.NULL_OR_NOT_BLANK; -//@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true) -//@JsonSubTypes({ -// @JsonSubTypes.Type(value = AutomationRule.LlmAsJudgeEvaluator.class, name = "llm_as_judge"), -// @JsonSubTypes.Type(value = AutomationRule.PythonAsJudgeEvaluator.class, name = "python") -//}) -//@Schema(name = "Feedback", discriminatorProperty = "type", discriminatorMapping = { -// @DiscriminatorMapping(value = "llm_as_judge", schema = AutomationRule.LlmAsJudgeEvaluator.class), -// @DiscriminatorMapping(value = "python", schema = AutomationRule.PythonAsJudgeEvaluator.class) -//}) @Builder(toBuilder = true) @JsonIgnoreProperties(ignoreUnknown = true) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @@ -43,10 +34,8 @@ public record AutomationRuleEvaluator( @JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) String lastUpdatedBy){ public static class View { - public static class Write { - } - public static class Public { - } + public static class Write {} + public static class Public {} } @Builder(toBuilder = true) From bdf11fea4f7cde0010376ad22cfbbc99e53b5587 Mon Sep 17 00:00:00 2001 From: Daniel Augusto Date: Fri, 20 Dec 2024 23:53:23 +0000 Subject: [PATCH 03/10] Splitting Rule and Evaluator types --- .../com/comet/opik/api/AutomationRule.java | 49 +++ .../opik/api/AutomationRuleEvaluator.java | 21 +- .../AutomationRuleEvaluatorsResource.java | 8 +- .../comet/opik/domain/AutomationRuleDAO.java | 66 +-- .../domain/AutomationRuleEvaluatorDAO.java | 84 ++++ ...va => AutomationRuleEvaluatorService.java} | 78 ++-- .../opik/domain/AutomationRuleRowMapper.java | 27 ++ ... => 000009_add_automation_rule_tables.sql} | 21 +- .../AutomationRuleEvaluatorsResourceTest.java | 407 +++++++++--------- 9 files changed, 443 insertions(+), 318 deletions(-) create mode 100644 apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRule.java create mode 100644 apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java rename apps/opik-backend/src/main/java/com/comet/opik/domain/{AutomationRuleService.java => AutomationRuleEvaluatorService.java} (71%) create mode 100644 apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleRowMapper.java rename apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/{000008_add_automation_rule_tables.sql => 000009_add_automation_rule_tables.sql} (60%) diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRule.java b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRule.java new file mode 100644 index 000000000..fdf4bf2c9 --- /dev/null +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRule.java @@ -0,0 +1,49 @@ +package com.comet.opik.api; + +import com.comet.opik.domain.FeedbackDefinitionModel; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Pattern; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static com.comet.opik.utils.ValidationUtils.NULL_OR_NOT_BLANK; + +public sealed interface AutomationRule permits AutomationRuleEvaluator { + + UUID id(); + UUID projectId(); + + AutomationRuleAction action(); + + Instant createdAt(); + String createdBy(); + Instant lastUpdatedAt(); + String lastUpdatedBy(); + + @Getter + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + enum AutomationRuleAction { + + EVALUATOR("evaluator"); + + @JsonValue + private final String action; + + public static AutomationRule.AutomationRuleAction fromString(String action) { + return Arrays.stream(values()).filter(v -> v.action.equals(action)).findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unknown feedback type: " + action)); + } + } +} \ No newline at end of file diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java index 3a8aabfb4..0af2f7407 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java @@ -17,21 +17,24 @@ @Builder(toBuilder = true) @JsonIgnoreProperties(ignoreUnknown = true) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) -public record AutomationRuleEvaluator( +public record AutomationRuleEvaluator ( // Fields and methods @JsonView({View.Public.class, View.Write.class}) UUID id, @JsonView({View.Public.class, View.Write.class}) UUID projectId, - @JsonView({View.Public.class, View.Write.class}) AutomationRuleEvaluatorType evaluatorType, - - @JsonView({View.Public.class, - View.Write.class}) @Pattern(regexp = NULL_OR_NOT_BLANK, message = "must not be blank") String code, + @JsonView({View.Public.class, View.Write.class}) AutomationRuleEvaluatorType type, + @JsonView({View.Public.class, View.Write.class}) @Pattern(regexp = NULL_OR_NOT_BLANK, message = "must not be blank") String code, @JsonView({View.Public.class, View.Write.class}) float samplingRate, @JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) Instant createdAt, @JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) String createdBy, @JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) Instant lastUpdatedAt, - @JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) String lastUpdatedBy){ + @JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) String lastUpdatedBy) implements AutomationRule { + + @Override + public AutomationRuleAction action() { + return AutomationRuleAction.EVALUATOR; + } public static class View { public static class Write {} @@ -40,13 +43,11 @@ public static class Public {} @Builder(toBuilder = true) public record AutomationRuleEvaluatorPage( - @JsonView( { - View.Public.class}) int page, + @JsonView({View.Public.class}) int page, @JsonView({View.Public.class}) int size, @JsonView({View.Public.class}) long total, @JsonView({View.Public.class}) List content) - implements - Page{ + implements Page{ public static AutomationRuleEvaluator.AutomationRuleEvaluatorPage empty(int page) { return new AutomationRuleEvaluator.AutomationRuleEvaluatorPage(page, 0, 0, List.of()); diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java index 2706fcc9a..c3e366156 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java @@ -4,7 +4,7 @@ import com.comet.opik.api.AutomationRuleEvaluator; import com.comet.opik.api.AutomationRuleEvaluatorUpdate; import com.comet.opik.api.Page; -import com.comet.opik.domain.AutomationRuleService; +import com.comet.opik.domain.AutomationRuleEvaluatorService; import com.comet.opik.infrastructure.auth.RequestContext; import com.comet.opik.infrastructure.ratelimit.RateLimited; import com.fasterxml.jackson.annotation.JsonView; @@ -50,7 +50,7 @@ @Tag(name = "Automation rule evaluators", description = "Automation rule evaluators resource") public class AutomationRuleEvaluatorsResource { - private final @NonNull AutomationRuleService service; + private final @NonNull AutomationRuleEvaluatorService service; private final @NonNull Provider requestContext; @GET @@ -104,10 +104,10 @@ public Response createEvaluator( String workspaceId = requestContext.get().getWorkspaceId(); - log.info("Creating {} evaluator for project_id '{}' on workspace_id '{}'", evaluator.evaluatorType(), + log.info("Creating {} evaluator for project_id '{}' on workspace_id '{}'", evaluator.type(), evaluator.projectId(), workspaceId); AutomationRuleEvaluator savedEvaluator = service.save(evaluator, workspaceId); - log.info("Created {} evaluator '{}' for project_id '{}' on workspace_id '{}'", evaluator.evaluatorType(), + log.info("Created {} evaluator '{}' for project_id '{}' on workspace_id '{}'", evaluator.type(), savedEvaluator.id(), evaluator.projectId(), workspaceId); URI uri = uriInfo.getAbsolutePathBuilder() diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java index 6fdfb665a..f2a40f9d5 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java @@ -1,79 +1,49 @@ package com.comet.opik.domain; +import com.comet.opik.api.AutomationRule; import com.comet.opik.api.AutomationRuleEvaluator; -import com.comet.opik.api.AutomationRuleEvaluatorUpdate; import com.comet.opik.infrastructure.db.UUIDArgumentFactory; import org.jdbi.v3.sqlobject.config.RegisterArgumentFactory; import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper; +import org.jdbi.v3.sqlobject.config.RegisterRowMapper; import org.jdbi.v3.sqlobject.customizer.AllowUnusedBindings; import org.jdbi.v3.sqlobject.customizer.Bind; import org.jdbi.v3.sqlobject.customizer.BindList; import org.jdbi.v3.sqlobject.customizer.BindMethods; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; -import org.jdbi.v3.stringtemplate4.UseStringTemplateEngine; -import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.UUID; @RegisterArgumentFactory(UUIDArgumentFactory.class) +@RegisterRowMapper(AutomationRuleRowMapper.class) @RegisterConstructorMapper(AutomationRuleEvaluator.class) -public interface AutomationRuleDAO { +interface AutomationRuleDAO { - @SqlUpdate("INSERT INTO automation_rule_evaluators(id, project_id, workspace_id, evaluator_type, sampling_rate, code, created_by, last_updated_by) "+ - "VALUES (:rule.id, :rule.projectId, :workspaceId, :rule.evaluatorType, :rule.samplingRate, :rule.code, :rule.createdBy, :rule.lastUpdatedBy)") - void save(@BindMethods("rule") AutomationRuleEvaluator rule, @Bind("workspaceId") String workspaceId); + @SqlUpdate("INSERT INTO automation_rules(id, project_id, workspace_id, `action`, created_by, last_updated_by) "+ + "VALUES (:rule.id, :rule.projectId, :workspaceId, :rule.action, :rule.createdBy, :rule.lastUpdatedBy)") + void saveRule(@BindMethods("rule") AutomationRule rule, @Bind("workspaceId") String workspaceId); @SqlUpdate(""" - UPDATE automation_rule_evaluators - SET - sampling_rate = :rule.samplingRate, - code = :rule.code, - last_updated_by = :lastUpdatedBy + UPDATE automation_rules + SET last_updated_by = :lastUpdatedBy WHERE id = :id AND project_id = :projectId AND workspace_id = :workspaceId """) - int update(@Bind("id") UUID id, - @Bind("projectId") UUID projectId, - @Bind("workspaceId") String workspaceId, - @BindMethods("rule") AutomationRuleEvaluatorUpdate ruleUpdate, - @Bind("lastUpdatedBy") String lastUpdatedBy); + int updateRule(@Bind("id") UUID id, + @Bind("projectId") UUID projectId, + @Bind("workspaceId") String workspaceId, + @Bind("lastUpdatedBy") String lastUpdatedBy); - @SqlQuery("SELECT * FROM automation_rule_evaluators WHERE id = :id AND project_id = :projectId AND workspace_id = :workspaceId") - Optional findById(@Bind("id") UUID id, @Bind("projectId") UUID projectId, - @Bind("workspaceId") String workspaceId); - - @SqlQuery("SELECT * FROM automation_rule_evaluators WHERE id IN () AND project_id = :projectId AND workspace_id = :workspaceId") - List findByIds(@BindList("ids") Set ids, @Bind("projectId") UUID projectId, - @Bind("workspaceId") String workspaceId); - - @SqlQuery("SELECT * FROM automation_rule_evaluators WHERE project_id = :projectId AND workspace_id = :workspaceId") - List findByProjectId(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); - - @SqlUpdate("DELETE FROM automation_rule_evaluators WHERE id = :id AND project_id = :projectId AND workspace_id = :workspaceId") + @SqlUpdate("DELETE FROM automation_rules WHERE id = :id AND project_id = :projectId AND workspace_id = :workspaceId") void delete(@Bind("id") UUID id, @Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); - @SqlUpdate("DELETE FROM automation_rule_evaluators WHERE project_id = :projectId AND workspace_id = :workspaceId") + @SqlUpdate("DELETE FROM automation_rules WHERE project_id = :projectId AND workspace_id = :workspaceId") void deleteByProject(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); - @SqlUpdate("DELETE FROM automation_rule_evaluators WHERE id IN () AND project_id = :projectId AND workspace_id = :workspaceId") + @SqlUpdate("DELETE FROM automation_rules WHERE id IN () AND project_id = :projectId AND workspace_id = :workspaceId") void delete(@BindList("ids") Set ids, @Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); - @SqlQuery("SELECT * FROM automation_rule_evaluators " + - " WHERE project_id = :projectId AND workspace_id = :workspaceId " + - " LIMIT :limit OFFSET :offset ") - @UseStringTemplateEngine - @AllowUnusedBindings - List find(@Bind("limit") int limit, - @Bind("offset") int offset, - @Bind("projectId") UUID projectId, - @Bind("workspaceId") String workspaceId); - - @SqlQuery("SELECT COUNT(*) FROM automation_rule_evaluators WHERE project_id = :projectId AND workspace_id = :workspaceId") - long findCount(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); - - @SqlQuery("SELECT id FROM automation_rule_evaluators WHERE id IN () and project_id = :projectId AND workspace_id = :workspaceId") - Set exists(@BindList("ids") Set ruleIds, @Bind("projectId") UUID projectId, - @Bind("workspaceId") String workspaceId); + @SqlQuery("SELECT COUNT(*) FROM automation_rules WHERE project_id = :projectId AND workspace_id = :workspaceId") + long findRuleCount(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); } diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java new file mode 100644 index 000000000..6969ddd2e --- /dev/null +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java @@ -0,0 +1,84 @@ +package com.comet.opik.domain; + +import com.comet.opik.api.AutomationRuleEvaluator; +import com.comet.opik.api.AutomationRuleEvaluatorUpdate; +import com.comet.opik.infrastructure.db.UUIDArgumentFactory; +import org.jdbi.v3.sqlobject.config.RegisterArgumentFactory; +import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper; +import org.jdbi.v3.sqlobject.config.RegisterRowMapper; +import org.jdbi.v3.sqlobject.customizer.AllowUnusedBindings; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.customizer.BindList; +import org.jdbi.v3.sqlobject.customizer.BindMethods; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import org.jdbi.v3.stringtemplate4.UseStringTemplateEngine; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +@RegisterArgumentFactory(UUIDArgumentFactory.class) +@RegisterRowMapper(AutomationRuleRowMapper.class) +@RegisterConstructorMapper(AutomationRuleEvaluator.class) +public interface AutomationRuleEvaluatorDAO extends AutomationRuleDAO { + + @SqlUpdate("INSERT INTO automation_rule_evaluators(id, `type`, sampling_rate, code) "+ + "VALUES (:rule.id, :rule.type, :rule.samplingRate, :rule.code)") + void save(@BindMethods("rule") AutomationRuleEvaluator rule); + + @SqlUpdate(""" + UPDATE automation_rule_evaluators + SET sampling_rate = :rule.samplingRate, + code = :rule.code + WHERE id = :id + """) + int update(@Bind("id") UUID id, @BindMethods("rule") AutomationRuleEvaluatorUpdate ruleUpdate); + + @SqlQuery(""" + SELECT rule.id, rule.project_id, rule.action, evaluator.type, evaluator.sampling_rate, evaluator.code, rule.created_at, rule.created_by, rule.last_updated_at, rule.last_updated_by + FROM automation_rules rule + JOIN automation_rule_evaluators evaluator + ON rule.id = evaluator.id + WHERE `action` = 'evaluator' + AND workspace_id = :workspaceId AND project_id = :projectId AND rule.id = :id + """) + Optional findById(@Bind("id") UUID id, + @Bind("projectId") UUID projectId, + @Bind("workspaceId") String workspaceId); + + @SqlQuery(""" + SELECT rule.id, rule.project_id, rule.action, evaluator.type, evaluator.sampling_rate, evaluator.code, rule.created_at, rule.created_by, rule.last_updated_at, rule.last_updated_by + FROM automation_rules rule + JOIN automation_rule_evaluators evaluator + ON rule.id = evaluator.id + WHERE `action` = 'evaluator' + AND workspace_id = :workspaceId AND project_id = :projectId + """) + List findByProjectId(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); + + @SqlQuery(""" + SELECT rule.id, rule.project_id, rule.action, evaluator.type, evaluator.sampling_rate, evaluator.code, rule.created_at, rule.created_by, rule.last_updated_at, rule.last_updated_by + FROM automation_rules rule + JOIN automation_rule_evaluators evaluator + ON rule.id = evaluator.id + WHERE `action` = 'evaluator' + AND workspace_id = :workspaceId AND project_id = :projectId + LIMIT :limit OFFSET :offset + """) + @AllowUnusedBindings + List find(@Bind("limit") int limit, @Bind("offset") int offset, + @Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); + + @SqlQuery(""" + SELECT COUNT(*) + FROM automation_rules rule + JOIN automation_rule_evaluators evaluator + ON rule.id = evaluator.id + WHERE `action` = 'evaluator' + AND workspace_id = :workspaceId AND project_id = :projectId + """) + long findCount(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); + +} diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java similarity index 71% rename from apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleService.java rename to apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java index b3e6cc065..29713d115 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java @@ -4,8 +4,6 @@ import com.comet.opik.api.AutomationRuleEvaluatorUpdate; import com.comet.opik.api.error.EntityAlreadyExistsException; import com.comet.opik.api.error.ErrorMessage; -import com.comet.opik.domain.sorting.SortingQueryBuilder; -import com.comet.opik.infrastructure.BatchOperationsConfig; import com.comet.opik.infrastructure.auth.RequestContext; import com.google.inject.ImplementedBy; import jakarta.inject.Inject; @@ -17,7 +15,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.jdbi.v3.core.statement.UnableToExecuteStatementException; -import ru.vyarus.dropwizard.guice.module.yaml.bind.Config; import ru.vyarus.guicey.jdbi3.tx.TransactionTemplate; import java.sql.SQLIntegrityConstraintViolationException; @@ -29,8 +26,8 @@ import static com.comet.opik.infrastructure.db.TransactionTemplateAsync.READ_ONLY; import static com.comet.opik.infrastructure.db.TransactionTemplateAsync.WRITE; -@ImplementedBy(AutomationRuleServiceImpl.class) -public interface AutomationRuleService { +@ImplementedBy(AutomationRuleEvaluatorServiceImpl.class) +public interface AutomationRuleEvaluatorService { AutomationRuleEvaluator save(AutomationRuleEvaluator AutomationRuleEvaluator, @NonNull String workspaceId); @@ -43,8 +40,6 @@ void update(UUID id, UUID projectId, @NonNull String workspaceId, List findByProjectId(UUID projectId, @NonNull String workspaceId); - List findByIds(Set ids, UUID projectId, @NonNull String workspaceId); - void deleteByProject(UUID projectId, @NonNull String workspaceId); void delete(UUID id, UUID projectId, @NonNull String workspaceId); @@ -57,43 +52,44 @@ void update(UUID id, UUID projectId, @NonNull String workspaceId, @Singleton @RequiredArgsConstructor(onConstructor_ = @Inject) @Slf4j -class AutomationRuleServiceImpl implements AutomationRuleService { +class AutomationRuleEvaluatorServiceImpl implements AutomationRuleEvaluatorService { private static final String EVALUATOR_ALREADY_EXISTS = "AutomationRuleEvaluator already exists"; private final @NonNull IdGenerator idGenerator; private final @NonNull TransactionTemplate template; private final @NonNull Provider requestContext; - private final @NonNull SortingQueryBuilder sortingQueryBuilder; - private final @NonNull @Config BatchOperationsConfig batchOperationsConfig; @Override - public AutomationRuleEvaluator save(@NonNull AutomationRuleEvaluator automationRuleEvaluator, + public AutomationRuleEvaluator save(@NonNull AutomationRuleEvaluator ruleEvaluator, @NonNull String workspaceId) { - var builder = automationRuleEvaluator.id() == null - ? automationRuleEvaluator.toBuilder().id(idGenerator.generateId()) - : automationRuleEvaluator.toBuilder(); + var builder = ruleEvaluator.id() == null + ? ruleEvaluator.toBuilder().id(idGenerator.generateId()) + : ruleEvaluator.toBuilder(); String userName = requestContext.get().getUserName(); builder.createdBy(userName) .lastUpdatedBy(userName); - var newAutomationRuleEvaluator = builder.build(); + var evaluatorToSave = builder.build(); - IdGenerator.validateVersion(newAutomationRuleEvaluator.id(), "AutomationRuleEvaluator"); + IdGenerator.validateVersion(evaluatorToSave.id(), "AutomationRuleEvaluator"); return template.inTransaction(WRITE, handle -> { - var dao = handle.attach(AutomationRuleDAO.class); + var evaluatorsDAO = handle.attach(AutomationRuleEvaluatorDAO.class); try { - dao.save(newAutomationRuleEvaluator, workspaceId); - return dao - .findById(newAutomationRuleEvaluator.id(), newAutomationRuleEvaluator.projectId(), workspaceId) + evaluatorsDAO.saveRule(evaluatorToSave, workspaceId); + evaluatorsDAO.save(evaluatorToSave); + + return evaluatorsDAO + .findById(evaluatorToSave.id(), evaluatorToSave.projectId(), workspaceId) .orElseThrow(); } catch (UnableToExecuteStatementException e) { if (e.getCause() instanceof SQLIntegrityConstraintViolationException) { + log.info(e.getMessage()); log.info(EVALUATOR_ALREADY_EXISTS); throw new EntityAlreadyExistsException(new ErrorMessage(List.of(EVALUATOR_ALREADY_EXISTS))); } else { @@ -108,7 +104,7 @@ public Optional getById(@NonNull UUID id, @NonNull UUID @NonNull String workspaceId) { log.info("Getting AutomationRuleEvaluator with id '{}', workspaceId '{}'", id, projectId); return template.inTransaction(READ_ONLY, handle -> { - var dao = handle.attach(AutomationRuleDAO.class); + var dao = handle.attach(AutomationRuleEvaluatorDAO.class); var AutomationRuleEvaluator = dao.findById(id, projectId, workspaceId); log.info("Got AutomationRuleEvaluator with id '{}', workspaceId '{}'", id, projectId); return AutomationRuleEvaluator; @@ -117,14 +113,15 @@ public Optional getById(@NonNull UUID id, @NonNull UUID @Override public void update(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId, - @NonNull AutomationRuleEvaluatorUpdate automationRuleEvaluator) { + @NonNull AutomationRuleEvaluatorUpdate evaluatorUpdate) { String userName = requestContext.get().getUserName(); template.inTransaction(WRITE, handle -> { - var dao = handle.attach(AutomationRuleDAO.class); + var dao = handle.attach(AutomationRuleEvaluatorDAO.class); try { - int result = dao.update(id, projectId, workspaceId, automationRuleEvaluator, userName); + dao.updateRule(id, projectId, workspaceId, userName); + int result = dao.update(id, evaluatorUpdate); if (result == 0) { throw newNotFoundException(); @@ -146,7 +143,7 @@ public void update(@NonNull UUID id, @NonNull UUID projectId, @NonNull String wo public AutomationRuleEvaluator findById(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId) { log.info("Finding AutomationRuleEvaluator with id '{}', projectId '{}'", id, projectId); return template.inTransaction(READ_ONLY, handle -> { - var dao = handle.attach(AutomationRuleDAO.class); + var dao = handle.attach(AutomationRuleEvaluatorDAO.class); var AutomationRuleEvaluator = dao.findById(id, projectId, workspaceId) .orElseThrow(this::newNotFoundException); log.info("Found AutomationRuleEvaluator with id '{}', projectId '{}'", id, projectId); @@ -158,28 +155,13 @@ public AutomationRuleEvaluator findById(@NonNull UUID id, @NonNull UUID projectI public List findByProjectId(@NonNull UUID projectId, @NonNull String workspaceId) { log.info("Finding AutomationRuleEvaluators with for projectId '{}'", projectId); return template.inTransaction(READ_ONLY, handle -> { - var dao = handle.attach(AutomationRuleDAO.class); + var dao = handle.attach(AutomationRuleEvaluatorDAO.class); var automationRuleEvaluators = dao.findByProjectId(projectId, workspaceId); log.info("Found {} AutomationRuleEvaluators for projectId '{}'", automationRuleEvaluators.size(), projectId); return automationRuleEvaluators; }); } - @Override - public List findByIds(@NonNull Set ids, @NonNull UUID projectId, - @NonNull String workspaceId) { - if (ids.isEmpty()) { - log.info("Returning empty AutomationRuleEvaluators for empty ids, projectId '{}'", projectId); - return List.of(); - } - log.info("Finding AutomationRuleEvaluators with ids '{}', projectId '{}'", ids, projectId); - return template.inTransaction(READ_ONLY, handle -> { - var dao = handle.attach(AutomationRuleDAO.class); - var automationRuleEvaluators = dao.findByIds(ids, projectId, workspaceId); - log.info("Found AutomationRuleEvaluators with ids '{}', projectId '{}'", ids, projectId); - return automationRuleEvaluators; - }); - } /** * Deletes a AutomationRuleEvaluator. @@ -187,7 +169,7 @@ public List findByIds(@NonNull Set ids, @NonNull @Override public void delete(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId) { template.inTransaction(WRITE, handle -> { - var dao = handle.attach(AutomationRuleDAO.class); + var dao = handle.attach(AutomationRuleEvaluatorDAO.class); dao.delete(id, projectId, workspaceId); return null; }); @@ -196,7 +178,7 @@ public void delete(@NonNull UUID id, @NonNull UUID projectId, @NonNull String wo @Override public void deleteByProject(@NonNull UUID projectId, @NonNull String workspaceId) { template.inTransaction(WRITE, handle -> { - var dao = handle.attach(AutomationRuleDAO.class); + var dao = handle.attach(AutomationRuleEvaluatorDAO.class); dao.deleteByProject(projectId, workspaceId); return null; }); @@ -217,23 +199,23 @@ public void delete(Set ids, @NonNull UUID projectId, @NonNull String works } template.inTransaction(WRITE, handle -> { - handle.attach(AutomationRuleDAO.class).delete(ids, projectId, workspaceId); + handle.attach(AutomationRuleEvaluatorDAO.class).delete(ids, projectId, workspaceId); return null; }); } @Override - public AutomationRuleEvaluator.AutomationRuleEvaluatorPage find(int page, int size, @NonNull UUID projectId) { + public AutomationRuleEvaluator.AutomationRuleEvaluatorPage find(int pageNum, int size, @NonNull UUID projectId) { String workspaceId = requestContext.get().getWorkspaceId(); return template.inTransaction(READ_ONLY, handle -> { - var dao = handle.attach(AutomationRuleDAO.class); + var dao = handle.attach(AutomationRuleEvaluatorDAO.class); var total = dao.findCount(projectId, workspaceId); - var offset = (page - 1) * size; + var offset = (pageNum - 1) * size; var automationRuleEvaluators = dao.find(size, offset, projectId, workspaceId); log.info("Found {} AutomationRuleEvaluators for projectId '{}'", automationRuleEvaluators.size(), projectId); - return new AutomationRuleEvaluator.AutomationRuleEvaluatorPage(page, automationRuleEvaluators.size(), total, + return new AutomationRuleEvaluator.AutomationRuleEvaluatorPage(pageNum, automationRuleEvaluators.size(), total, automationRuleEvaluators); }); } diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleRowMapper.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleRowMapper.java new file mode 100644 index 000000000..8a5876dd5 --- /dev/null +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleRowMapper.java @@ -0,0 +1,27 @@ +package com.comet.opik.domain; + +import com.comet.opik.api.AutomationRule; +import com.comet.opik.api.AutomationRuleEvaluator; +import org.jdbi.v3.core.mapper.RowMapper; +import org.jdbi.v3.core.statement.StatementContext; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import static com.comet.opik.domain.FeedbackDefinitionModel.FeedbackType; + +public class AutomationRuleRowMapper implements RowMapper> { + + @Override + public AutomationRule map(ResultSet rs, StatementContext ctx) throws SQLException { + + var action = AutomationRule.AutomationRuleAction.fromString(rs.getString("action")); + + return switch (action) { + case EVALUATOR -> ctx.findMapperFor(AutomationRuleEvaluator.class) + .orElseThrow(() -> new IllegalStateException( + "No mapper found for Automation Rule Action type: %s".formatted(action))) + .map(rs, ctx); + }; + } +} diff --git a/apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000008_add_automation_rule_tables.sql b/apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000009_add_automation_rule_tables.sql similarity index 60% rename from apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000008_add_automation_rule_tables.sql rename to apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000009_add_automation_rule_tables.sql index d3ff73b00..680e33e26 100644 --- a/apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000008_add_automation_rule_tables.sql +++ b/apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000009_add_automation_rule_tables.sql @@ -1,20 +1,27 @@ --liquibase formatted sql --changeset DanielAugusto:000007_add_automation_rule_tables -CREATE TABLE IF NOT EXISTS automation_rule_evaluators ( +CREATE TABLE IF NOT EXISTS automation_rules ( id CHAR(36), project_id CHAR(36) NOT NULL, workspace_id VARCHAR(150) NOT NULL, - - evaluator_type CHAR(20) NOT NULL, - sampling_rate FLOAT NOT NULL CHECK (sampling_rate >= 0 AND sampling_rate <= 1), - code TEXT NOT NULL, + `action` ENUM('evaluator') NOT NULL, created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), created_by VARCHAR(100) NOT NULL DEFAULT 'admin', last_updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), last_updated_by VARCHAR(100) NOT NULL DEFAULT 'admin', - CONSTRAINT `automation_rules_pk` PRIMARY KEY (workspace_id, project_id, id) - ); + CONSTRAINT `automation_rules_pk` PRIMARY KEY (id), + INDEX `automation_rules_idx` (workspace_id, project_id, id) +); +CREATE TABLE IF NOT EXISTS automation_rule_evaluators ( + id CHAR(36), + + `type` ENUM('llm_as_judge', 'python') NOT NULL, + sampling_rate FLOAT NOT NULL CHECK (sampling_rate >= 0 AND sampling_rate <= 1), + code TEXT NOT NULL, + CONSTRAINT `automation_rules_evaluators_pk` PRIMARY KEY (id), + FOREIGN KEY (id) REFERENCES automation_rules(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java index c06f902d1..89ebe2958 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java @@ -1,6 +1,7 @@ package com.comet.opik.api.resources.v1.priv; import com.comet.opik.api.AutomationRuleEvaluator; +import com.comet.opik.api.AutomationRuleEvaluatorUpdate; import com.comet.opik.api.resources.utils.AuthTestUtils; import com.comet.opik.api.resources.utils.ClientSupportUtils; import com.comet.opik.api.resources.utils.MigrationUtils; @@ -39,18 +40,20 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import static com.comet.opik.infrastructure.auth.RequestContext.SESSION_COOKIE; import static com.comet.opik.infrastructure.auth.RequestContext.WORKSPACE_HEADER; import static com.comet.opik.infrastructure.auth.TestHttpClientUtils.UNAUTHORIZED_RESPONSE; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.matching; import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath; +import static com.github.tomakehurst.wiremock.client.WireMock.okJson; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.params.provider.Arguments.arguments; @TestInstance(TestInstance.Lifecycle.PER_CLASS) -@DisplayName("Feedback Resource Test") +@DisplayName("Automation Rule Evaluators Resource Test") class AutomationRuleEvaluatorsResourceTest { private static final String URL_TEMPLATE = "%s/v1/private/automation/evaluator/"; @@ -156,7 +159,7 @@ void setUp() { @MethodSource("credentials") @DisplayName("create evaluator definition: when api key is present, then return proper response") void createAutomationRuleEvaluator__whenApiKeyIsPresent__thenReturnProperResponse(String apiKey, - boolean isAuthorized) { + boolean isAuthorized) { var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); @@ -196,7 +199,7 @@ void getProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnProperRe IntStream.range(0, samplesToCreate).forEach(i -> { var evaluator = factory.manufacturePojo(AutomationRuleEvaluator.class) - .toBuilder().projectId(projectId).build(); + .toBuilder().projectId(projectId).build(); create(evaluator, okApikey, workspaceName); }); @@ -229,7 +232,7 @@ void getProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnProperRe @MethodSource("credentials") @DisplayName("get evaluator by id: when api key is present, then return proper response") void getAutomationRuleEvaluatorById__whenApiKeyIsPresent__thenReturnProperResponse(String apiKey, - boolean isAuthorized) { + boolean isAuthorized) { var evaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); @@ -266,7 +269,7 @@ void getAutomationRuleEvaluatorById__whenApiKeyIsPresent__thenReturnProperRespon @MethodSource("credentials") @DisplayName("update evaluator: when api key is present, then return proper response") void updateAutomationRuleEvaluator__whenApiKeyIsPresent__thenReturnProperResponse(String apiKey, - boolean isAuthorized) { + boolean isAuthorized) { var evaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); @@ -337,7 +340,7 @@ void deleteAutomationRuleEvaluator__whenApiKeyIsPresent__thenReturnProperRespons @MethodSource("credentials") @DisplayName("delete evaluators by project id: when api key is present, then return proper response") void deleteProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnProperResponse(String apiKey, - boolean isAuthorized) { + boolean isAuthorized) { var projectId = UUID.randomUUID(); var evaluator1 = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().projectId(projectId).build(); @@ -394,201 +397,203 @@ void deleteProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnPrope } } - // @Nested - // @DisplayName("Session Token Authentication:") - // @TestInstance(TestInstance.Lifecycle.PER_CLASS) - // class SessionTokenCookie { - // - // private final String sessionToken = UUID.randomUUID().toString(); - // private final String fakeSessionToken = UUID.randomUUID().toString(); - // - // Stream credentials() { - // return Stream.of( - // arguments(sessionToken, true, "OK_" + UUID.randomUUID()), - // arguments(fakeSessionToken, false, UUID.randomUUID().toString())); - // } - // - // @BeforeEach - // void setUp() { - // wireMock.server().stubFor( - // post(urlPathEqualTo("/opik/auth-session")) - // .withCookie(SESSION_COOKIE, equalTo(sessionToken)) - // .withRequestBody(matchingJsonPath("$.workspaceName", matching("OK_.+"))) - // .willReturn(okJson(AuthTestUtils.newWorkspaceAuthResponse(USER, WORKSPACE_ID)))); - // - // wireMock.server().stubFor( - // post(urlPathEqualTo("/opik/auth-session")) - // .withCookie(SESSION_COOKIE, equalTo(fakeSessionToken)) - // .withRequestBody(matchingJsonPath("$.workspaceName", matching(".+"))) - // .willReturn(WireMock.unauthorized())); - // } - // - // @ParameterizedTest - // @MethodSource("credentials") - // @DisplayName("create evaluator definition: when session token is present, then return proper response") - // void createAutomationRuleEvaluator__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, - // boolean success, String workspaceName) { - // - // var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); - // - // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) - // .request() - // .cookie(SESSION_COOKIE, sessionToken) - // .accept(MediaType.APPLICATION_JSON_TYPE) - // .header(WORKSPACE_HEADER, workspaceName) - // .post(Entity.json(ruleEvaluator))) { - // - // if (success) { - // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); - // assertThat(actualResponse.hasEntity()).isFalse(); - // } else { - // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); - // assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) - // .isEqualTo(UNAUTHORIZED_RESPONSE); - // } - // } - // } - // - // @ParameterizedTest - // @MethodSource("credentials") - // @DisplayName("get evaluators: when session token is present, then return proper response") - // void getAutomationRuleEvaluators__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, - // boolean success, String workspaceName) { - // - // int size = 15; - // var newWorkspaceName = UUID.randomUUID().toString(); - // var newWorkspaceId = UUID.randomUUID().toString(); - // - // wireMock.server().stubFor( - // post(urlPathEqualTo("/opik/auth-session")) - // .withCookie(SESSION_COOKIE, equalTo(sessionToken)) - // .withRequestBody(matchingJsonPath("$.workspaceName", equalTo(newWorkspaceName))) - // .willReturn(okJson(AuthTestUtils.newWorkspaceAuthResponse(USER, newWorkspaceId)))); - // - // IntStream.range(0, size).forEach(i -> { - // create(factory.manufacturePojo(AutomationRuleEvaluator.class), API_KEY, TEST_WORKSPACE); - // }); - // - // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) - // .queryParam("workspace_name", workspaceName) - // .queryParam("size", size) - // .request() - // .cookie(SESSION_COOKIE, sessionToken) - // .accept(MediaType.APPLICATION_JSON_TYPE) - // .header(WORKSPACE_HEADER, workspaceName) - // .get()) { - // - // if (success) { - // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); - // assertThat(actualResponse.hasEntity()).isTrue(); - // - // var actualEntity = actualResponse.readEntity(AutomationRuleEvaluatorUpdate.class); - // assertThat(actualEntity.content()).hasSize(size); - // } else { - // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); - // assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) - // .isEqualTo(UNAUTHORIZED_RESPONSE); - // } - // } - // } - // - // @ParameterizedTest - // @MethodSource("credentials") - // @DisplayName("get evaluator by id: when session token is present, then return proper response") - // void getAutomationRuleEvaluatorById__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, - // boolean success, String workspaceName) { - // - // var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class); - // - // UUID id = create(feedback, API_KEY, TEST_WORKSPACE); - // - // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) - // .path(id.toString()) - // .request() - // .cookie(SESSION_COOKIE, sessionToken) - // .accept(MediaType.APPLICATION_JSON_TYPE) - // .header(WORKSPACE_HEADER, workspaceName) - // .get()) { - // - // if (success) { - // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); - // assertThat(actualResponse.hasEntity()).isTrue(); - // - // var ruleEvaluator = actualResponse - // .readEntity(AutomationRuleEvaluator.class); - // assertThat(ruleEvaluator.getId()).isEqualTo(id); - // } else { - // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); - // assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) - // .isEqualTo(UNAUTHORIZED_RESPONSE); - // } - // } - // - // } - // - // @ParameterizedTest - // @MethodSource("credentials") - // @DisplayName("update evaluator: when session token is present, then return proper response") - // void updateAutomationRuleEvaluator__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, - // boolean success, String workspaceName) { - // - // var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class); - // - // UUID id = create(feedback, API_KEY, TEST_WORKSPACE); - // - // var updatedFeedback = feedback.toBuilder() - // .name(UUID.randomUUID().toString()) - // .build(); - // - // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) - // .path(id.toString()) - // .request() - // .cookie(SESSION_COOKIE, sessionToken) - // .accept(MediaType.APPLICATION_JSON_TYPE) - // .header(WORKSPACE_HEADER, workspaceName) - // .put(Entity.json(updatedFeedback))) { - // - // if (success) { - // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(204); - // assertThat(actualResponse.hasEntity()).isFalse(); - // } else { - // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); - // assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) - // .isEqualTo(UNAUTHORIZED_RESPONSE); - // } - // } - // } - // - // @ParameterizedTest - // @MethodSource("credentials") - // @DisplayName("delete evaluator: when session token is present, then return proper response") - // void deleteAutomationRuleEvaluator__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, - // boolean success, String workspaceName) { - // - // var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class); - // - // UUID id = create(feedback, API_KEY, TEST_WORKSPACE); - // - // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) - // .path(id.toString()) - // .request() - // .cookie(SESSION_COOKIE, sessionToken) - // .accept(MediaType.APPLICATION_JSON_TYPE) - // .header(WORKSPACE_HEADER, workspaceName) - // .delete()) { - // - // if (success) { - // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(204); - // assertThat(actualResponse.hasEntity()).isFalse(); - // } else { - // assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); - // assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) - // .isEqualTo(UNAUTHORIZED_RESPONSE); - // } - // } - // } - // } - // +// @Nested +// @DisplayName("Session Token Authentication:") +// @TestInstance(TestInstance.Lifecycle.PER_CLASS) +// class SessionTokenCookie { +// +// private final String sessionToken = UUID.randomUUID().toString(); +// private final String fakeSessionToken = UUID.randomUUID().toString(); +// +// Stream credentials() { +// return Stream.of( +// arguments(sessionToken, true, "OK_" + UUID.randomUUID()), +// arguments(fakeSessionToken, false, UUID.randomUUID().toString())); +// } +// +// @BeforeEach +// void setUp() { +// wireMock.server().stubFor( +// post(urlPathEqualTo("/opik/auth-session")) +// .withCookie(SESSION_COOKIE, equalTo(sessionToken)) +// .withRequestBody(matchingJsonPath("$.workspaceName", matching("OK_.+"))) +// .willReturn(okJson(AuthTestUtils.newWorkspaceAuthResponse(USER, WORKSPACE_ID)))); +// +// wireMock.server().stubFor( +// post(urlPathEqualTo("/opik/auth-session")) +// .withCookie(SESSION_COOKIE, equalTo(fakeSessionToken)) +// .withRequestBody(matchingJsonPath("$.workspaceName", matching(".+"))) +// .willReturn(WireMock.unauthorized())); +// } +// +// // .cookie(SESSION_COOKIE, sessionToken) +// +// @ParameterizedTest +// @MethodSource("credentials") +// @DisplayName("create evaluator definition: when session token is present, then return proper response") +// void createAutomationRuleEvaluator__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, +// boolean success, String workspaceName) { +// +// var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); +// +// try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) +// .request() +// .cookie(SESSION_COOKIE, sessionToken) +// .accept(MediaType.APPLICATION_JSON_TYPE) +// .header(WORKSPACE_HEADER, workspaceName) +// .post(Entity.json(ruleEvaluator))) { +// +// if (success) { +// assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); +// assertThat(actualResponse.hasEntity()).isFalse(); +// } else { +// assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); +// assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) +// .isEqualTo(UNAUTHORIZED_RESPONSE); +// } +// } +// } +// +// @ParameterizedTest +// @MethodSource("credentials") +// @DisplayName("get evaluators: when session token is present, then return proper response") +// void getAutomationRuleEvaluators__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, +// boolean success, String workspaceName) { +// +// int size = 15; +// var newWorkspaceName = UUID.randomUUID().toString(); +// var newWorkspaceId = UUID.randomUUID().toString(); +// +// wireMock.server().stubFor( +// post(urlPathEqualTo("/opik/auth-session")) +// .withCookie(SESSION_COOKIE, equalTo(sessionToken)) +// .withRequestBody(matchingJsonPath("$.workspaceName", equalTo(newWorkspaceName))) +// .willReturn(okJson(AuthTestUtils.newWorkspaceAuthResponse(USER, newWorkspaceId)))); +// +// IntStream.range(0, size).forEach(i -> { +// create(factory.manufacturePojo(AutomationRuleEvaluator.class), API_KEY, TEST_WORKSPACE); +// }); +// +// try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) +// .queryParam("workspace_name", workspaceName) +// .queryParam("size", size) +// .request() +// .cookie(SESSION_COOKIE, sessionToken) +// .accept(MediaType.APPLICATION_JSON_TYPE) +// .header(WORKSPACE_HEADER, workspaceName) +// .get()) { +// +// if (success) { +// assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); +// assertThat(actualResponse.hasEntity()).isTrue(); +// +// var actualEntity = actualResponse.readEntity(AutomationRuleEvaluatorUpdate.class); +// assertThat(actualEntity.content()).hasSize(size); +// } else { +// assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); +// assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) +// .isEqualTo(UNAUTHORIZED_RESPONSE); +// } +// } +// } +// +// @ParameterizedTest +// @MethodSource("credentials") +// @DisplayName("get evaluator by id: when session token is present, then return proper response") +// void getAutomationRuleEvaluatorById__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, +// boolean success, String workspaceName) { +// +// var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class); +// +// UUID id = create(feedback, API_KEY, TEST_WORKSPACE); +// +// try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) +// .path(id.toString()) +// .request() +// .cookie(SESSION_COOKIE, sessionToken) +// .accept(MediaType.APPLICATION_JSON_TYPE) +// .header(WORKSPACE_HEADER, workspaceName) +// .get()) { +// +// if (success) { +// assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); +// assertThat(actualResponse.hasEntity()).isTrue(); +// +// var ruleEvaluator = actualResponse +// .readEntity(AutomationRuleEvaluator.class); +// assertThat(ruleEvaluator.getId()).isEqualTo(id); +// } else { +// assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); +// assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) +// .isEqualTo(UNAUTHORIZED_RESPONSE); +// } +// } +// +// } +// +// @ParameterizedTest +// @MethodSource("credentials") +// @DisplayName("update evaluator: when session token is present, then return proper response") +// void updateAutomationRuleEvaluator__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, +// boolean success, String workspaceName) { +// +// var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class); +// +// UUID id = create(feedback, API_KEY, TEST_WORKSPACE); +// +// var updatedFeedback = feedback.toBuilder() +// .name(UUID.randomUUID().toString()) +// .build(); +// +// try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) +// .path(id.toString()) +// .request() +// .cookie(SESSION_COOKIE, sessionToken) +// .accept(MediaType.APPLICATION_JSON_TYPE) +// .header(WORKSPACE_HEADER, workspaceName) +// .put(Entity.json(updatedFeedback))) { +// +// if (success) { +// assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(204); +// assertThat(actualResponse.hasEntity()).isFalse(); +// } else { +// assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); +// assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) +// .isEqualTo(UNAUTHORIZED_RESPONSE); +// } +// } +// } +// +// @ParameterizedTest +// @MethodSource("credentials") +// @DisplayName("delete evaluator: when session token is present, then return proper response") +// void deleteAutomationRuleEvaluator__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, +// boolean success, String workspaceName) { +// +// var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class); +// +// UUID id = create(feedback, API_KEY, TEST_WORKSPACE); +// +// try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) +// .path(id.toString()) +// .request() +// .cookie(SESSION_COOKIE, sessionToken) +// .accept(MediaType.APPLICATION_JSON_TYPE) +// .header(WORKSPACE_HEADER, workspaceName) +// .delete()) { +// +// if (success) { +// assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(204); +// assertThat(actualResponse.hasEntity()).isFalse(); +// } else { +// assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(401); +// assertThat(actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class)) +// .isEqualTo(UNAUTHORIZED_RESPONSE); +// } +// } +// } +// } +// // @Nested // @DisplayName("Get:") // @TestInstance(TestInstance.Lifecycle.PER_CLASS) From 6d731f6401b76b0baa6def1a3e42d0e2b4a95380 Mon Sep 17 00:00:00 2001 From: Daniel Augusto Date: Sat, 21 Dec 2024 00:00:50 +0000 Subject: [PATCH 04/10] cleaning up --- .../com/comet/opik/api/AutomationRule.java | 11 ------- .../comet/opik/domain/AutomationRuleDAO.java | 7 +++-- .../AutomationRuleEvaluatorService.java | 31 ++++++------------- .../opik/domain/AutomationRuleRowMapper.java | 2 -- .../AutomationRuleEvaluatorsResourceTest.java | 3 -- 5 files changed, 13 insertions(+), 41 deletions(-) diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRule.java b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRule.java index fdf4bf2c9..594193ad9 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRule.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRule.java @@ -1,25 +1,14 @@ package com.comet.opik.api; -import com.comet.opik.domain.FeedbackDefinitionModel; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonValue; -import com.fasterxml.jackson.annotation.JsonView; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Pattern; import lombok.AccessLevel; -import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; import java.time.Instant; import java.util.Arrays; -import java.util.List; import java.util.UUID; -import static com.comet.opik.utils.ValidationUtils.NULL_OR_NOT_BLANK; - public sealed interface AutomationRule permits AutomationRuleEvaluator { UUID id(); diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java index f2a40f9d5..518cd4736 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java @@ -6,7 +6,6 @@ import org.jdbi.v3.sqlobject.config.RegisterArgumentFactory; import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper; import org.jdbi.v3.sqlobject.config.RegisterRowMapper; -import org.jdbi.v3.sqlobject.customizer.AllowUnusedBindings; import org.jdbi.v3.sqlobject.customizer.Bind; import org.jdbi.v3.sqlobject.customizer.BindList; import org.jdbi.v3.sqlobject.customizer.BindMethods; @@ -38,8 +37,10 @@ int updateRule(@Bind("id") UUID id, @SqlUpdate("DELETE FROM automation_rules WHERE id = :id AND project_id = :projectId AND workspace_id = :workspaceId") void delete(@Bind("id") UUID id, @Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); - @SqlUpdate("DELETE FROM automation_rules WHERE project_id = :projectId AND workspace_id = :workspaceId") - void deleteByProject(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); + @SqlUpdate("DELETE FROM automation_rules WHERE `action` = :action AND project_id = :projectId AND workspace_id = :workspaceId") + void deleteByProject(@Bind("projectId") UUID projectId, + @Bind("workspaceId") String workspaceId, + @Bind("action") AutomationRule.AutomationRuleAction action); @SqlUpdate("DELETE FROM automation_rules WHERE id IN () AND project_id = :projectId AND workspace_id = :workspaceId") void delete(@BindList("ids") Set ids, @Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java index 29713d115..684d512d9 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java @@ -1,5 +1,6 @@ package com.comet.opik.domain; +import com.comet.opik.api.AutomationRule; import com.comet.opik.api.AutomationRuleEvaluator; import com.comet.opik.api.AutomationRuleEvaluatorUpdate; import com.comet.opik.api.error.EntityAlreadyExistsException; @@ -31,20 +32,18 @@ public interface AutomationRuleEvaluatorService { AutomationRuleEvaluator save(AutomationRuleEvaluator AutomationRuleEvaluator, @NonNull String workspaceId); - Optional getById(UUID id, UUID projectId, @NonNull String workspaceId); + Optional getById(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId); - void update(UUID id, UUID projectId, @NonNull String workspaceId, - AutomationRuleEvaluatorUpdate AutomationRuleEvaluator); + void update(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId, + AutomationRuleEvaluatorUpdate AutomationRuleEvaluator); - AutomationRuleEvaluator findById(UUID id, UUID projectId, @NonNull String workspaceId); + AutomationRuleEvaluator findById(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId); - List findByProjectId(UUID projectId, @NonNull String workspaceId); + void deleteByProject(@NonNull UUID projectId, @NonNull String workspaceId); - void deleteByProject(UUID projectId, @NonNull String workspaceId); + void delete(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId); - void delete(UUID id, UUID projectId, @NonNull String workspaceId); - - void delete(Set ids, UUID projectId, @NonNull String workspaceId); + void delete(Set ids, @NonNull UUID projectId, @NonNull String workspaceId); AutomationRuleEvaluator.AutomationRuleEvaluatorPage find(int page, int size, @NonNull UUID projectId); } @@ -151,18 +150,6 @@ public AutomationRuleEvaluator findById(@NonNull UUID id, @NonNull UUID projectI }); } - @Override - public List findByProjectId(@NonNull UUID projectId, @NonNull String workspaceId) { - log.info("Finding AutomationRuleEvaluators with for projectId '{}'", projectId); - return template.inTransaction(READ_ONLY, handle -> { - var dao = handle.attach(AutomationRuleEvaluatorDAO.class); - var automationRuleEvaluators = dao.findByProjectId(projectId, workspaceId); - log.info("Found {} AutomationRuleEvaluators for projectId '{}'", automationRuleEvaluators.size(), - projectId); - return automationRuleEvaluators; - }); - } - /** * Deletes a AutomationRuleEvaluator. **/ @@ -179,7 +166,7 @@ public void delete(@NonNull UUID id, @NonNull UUID projectId, @NonNull String wo public void deleteByProject(@NonNull UUID projectId, @NonNull String workspaceId) { template.inTransaction(WRITE, handle -> { var dao = handle.attach(AutomationRuleEvaluatorDAO.class); - dao.deleteByProject(projectId, workspaceId); + dao.deleteByProject(projectId, workspaceId, AutomationRule.AutomationRuleAction.EVALUATOR); return null; }); } diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleRowMapper.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleRowMapper.java index 8a5876dd5..277d4900c 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleRowMapper.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleRowMapper.java @@ -8,8 +8,6 @@ import java.sql.ResultSet; import java.sql.SQLException; -import static com.comet.opik.domain.FeedbackDefinitionModel.FeedbackType; - public class AutomationRuleRowMapper implements RowMapper> { @Override diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java index 89ebe2958..4b9a6a002 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java @@ -1,7 +1,6 @@ package com.comet.opik.api.resources.v1.priv; import com.comet.opik.api.AutomationRuleEvaluator; -import com.comet.opik.api.AutomationRuleEvaluatorUpdate; import com.comet.opik.api.resources.utils.AuthTestUtils; import com.comet.opik.api.resources.utils.ClientSupportUtils; import com.comet.opik.api.resources.utils.MigrationUtils; @@ -11,8 +10,6 @@ import com.comet.opik.api.resources.utils.TestUtils; import com.comet.opik.api.resources.utils.WireMockUtils; import com.comet.opik.podam.PodamFactoryUtils; -import com.fasterxml.uuid.Generators; -import com.fasterxml.uuid.impl.TimeBasedEpochGenerator; import com.github.tomakehurst.wiremock.client.WireMock; import com.redis.testcontainers.RedisContainer; import jakarta.ws.rs.client.Entity; From eccaa4b83e27ebe5b47a16601e952694b81432c1 Mon Sep 17 00:00:00 2001 From: Daniel Augusto Date: Sat, 21 Dec 2024 00:10:24 +0000 Subject: [PATCH 05/10] moving 'sampling rate' to the base rule; other rule actions would also be based in a sample --- .../com/comet/opik/api/AutomationRule.java | 1 + .../comet/opik/domain/AutomationRuleDAO.java | 18 ++++++++++-------- .../domain/AutomationRuleEvaluatorDAO.java | 13 ++++++------- .../domain/AutomationRuleEvaluatorService.java | 4 ++-- .../000009_add_automation_rule_tables.sql | 2 +- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRule.java b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRule.java index 594193ad9..f89f105ea 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRule.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRule.java @@ -15,6 +15,7 @@ public sealed interface AutomationRule permits AutomationRuleEvaluator { UUID projectId(); AutomationRuleAction action(); + float samplingRate(); Instant createdAt(); String createdBy(); diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java index 518cd4736..ea96fdbb6 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java @@ -20,19 +20,21 @@ @RegisterConstructorMapper(AutomationRuleEvaluator.class) interface AutomationRuleDAO { - @SqlUpdate("INSERT INTO automation_rules(id, project_id, workspace_id, `action`, created_by, last_updated_by) "+ - "VALUES (:rule.id, :rule.projectId, :workspaceId, :rule.action, :rule.createdBy, :rule.lastUpdatedBy)") - void saveRule(@BindMethods("rule") AutomationRule rule, @Bind("workspaceId") String workspaceId); + @SqlUpdate("INSERT INTO automation_rules(id, project_id, workspace_id, `action`, sampling_rate, created_by, last_updated_by) "+ + "VALUES (:rule.id, :rule.projectId, :workspaceId, :rule.action, :rule.samplingRate, :rule.createdBy, :rule.lastUpdatedBy)") + void saveBaseRule(@BindMethods("rule") AutomationRule rule, @Bind("workspaceId") String workspaceId); @SqlUpdate(""" UPDATE automation_rules - SET last_updated_by = :lastUpdatedBy + SET sampling_rate = :samplingRate, + last_updated_by = :lastUpdatedBy WHERE id = :id AND project_id = :projectId AND workspace_id = :workspaceId """) - int updateRule(@Bind("id") UUID id, - @Bind("projectId") UUID projectId, - @Bind("workspaceId") String workspaceId, - @Bind("lastUpdatedBy") String lastUpdatedBy); + int updateBaseRule(@Bind("id") UUID id, + @Bind("projectId") UUID projectId, + @Bind("workspaceId") String workspaceId, + @Bind("samplingRate") float samplingRate, + @Bind("lastUpdatedBy") String lastUpdatedBy); @SqlUpdate("DELETE FROM automation_rules WHERE id = :id AND project_id = :projectId AND workspace_id = :workspaceId") void delete(@Bind("id") UUID id, @Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java index 6969ddd2e..5b05ba7dd 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java @@ -24,20 +24,19 @@ @RegisterConstructorMapper(AutomationRuleEvaluator.class) public interface AutomationRuleEvaluatorDAO extends AutomationRuleDAO { - @SqlUpdate("INSERT INTO automation_rule_evaluators(id, `type`, sampling_rate, code) "+ - "VALUES (:rule.id, :rule.type, :rule.samplingRate, :rule.code)") + @SqlUpdate("INSERT INTO automation_rule_evaluators(id, `type`, code) "+ + "VALUES (:rule.id, :rule.type, :rule.code)") void save(@BindMethods("rule") AutomationRuleEvaluator rule); @SqlUpdate(""" UPDATE automation_rule_evaluators - SET sampling_rate = :rule.samplingRate, - code = :rule.code + SET code = :rule.code WHERE id = :id """) int update(@Bind("id") UUID id, @BindMethods("rule") AutomationRuleEvaluatorUpdate ruleUpdate); @SqlQuery(""" - SELECT rule.id, rule.project_id, rule.action, evaluator.type, evaluator.sampling_rate, evaluator.code, rule.created_at, rule.created_by, rule.last_updated_at, rule.last_updated_by + SELECT rule.id, rule.project_id, rule.action, rule.sampling_rate, evaluator.type, evaluator.code, rule.created_at, rule.created_by, rule.last_updated_at, rule.last_updated_by FROM automation_rules rule JOIN automation_rule_evaluators evaluator ON rule.id = evaluator.id @@ -49,7 +48,7 @@ Optional findById(@Bind("id") UUID id, @Bind("workspaceId") String workspaceId); @SqlQuery(""" - SELECT rule.id, rule.project_id, rule.action, evaluator.type, evaluator.sampling_rate, evaluator.code, rule.created_at, rule.created_by, rule.last_updated_at, rule.last_updated_by + SELECT rule.id, rule.project_id, rule.action, rule.sampling_rate, evaluator.type, evaluator.code, rule.created_at, rule.created_by, rule.last_updated_at, rule.last_updated_by FROM automation_rules rule JOIN automation_rule_evaluators evaluator ON rule.id = evaluator.id @@ -59,7 +58,7 @@ Optional findById(@Bind("id") UUID id, List findByProjectId(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); @SqlQuery(""" - SELECT rule.id, rule.project_id, rule.action, evaluator.type, evaluator.sampling_rate, evaluator.code, rule.created_at, rule.created_by, rule.last_updated_at, rule.last_updated_by + SELECT rule.id, rule.project_id, rule.action, rule.sampling_rate, evaluator.type, evaluator.code, rule.created_at, rule.created_by, rule.last_updated_at, rule.last_updated_by FROM automation_rules rule JOIN automation_rule_evaluators evaluator ON rule.id = evaluator.id diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java index 684d512d9..9dd32e762 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java @@ -80,7 +80,7 @@ public AutomationRuleEvaluator save(@NonNull AutomationRuleEvaluator ruleEvaluat var evaluatorsDAO = handle.attach(AutomationRuleEvaluatorDAO.class); try { - evaluatorsDAO.saveRule(evaluatorToSave, workspaceId); + evaluatorsDAO.saveBaseRule(evaluatorToSave, workspaceId); evaluatorsDAO.save(evaluatorToSave); return evaluatorsDAO @@ -119,7 +119,7 @@ public void update(@NonNull UUID id, @NonNull UUID projectId, @NonNull String wo var dao = handle.attach(AutomationRuleEvaluatorDAO.class); try { - dao.updateRule(id, projectId, workspaceId, userName); + dao.updateBaseRule(id, projectId, workspaceId, evaluatorUpdate.samplingRate(), userName); int result = dao.update(id, evaluatorUpdate); if (result == 0) { diff --git a/apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000009_add_automation_rule_tables.sql b/apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000009_add_automation_rule_tables.sql index 680e33e26..122feb22a 100644 --- a/apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000009_add_automation_rule_tables.sql +++ b/apps/opik-backend/src/main/resources/liquibase/db-app-state/migrations/000009_add_automation_rule_tables.sql @@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS automation_rules ( project_id CHAR(36) NOT NULL, workspace_id VARCHAR(150) NOT NULL, `action` ENUM('evaluator') NOT NULL, + sampling_rate FLOAT NOT NULL CHECK (sampling_rate >= 0 AND sampling_rate <= 1), created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), created_by VARCHAR(100) NOT NULL DEFAULT 'admin', @@ -19,7 +20,6 @@ CREATE TABLE IF NOT EXISTS automation_rule_evaluators ( id CHAR(36), `type` ENUM('llm_as_judge', 'python') NOT NULL, - sampling_rate FLOAT NOT NULL CHECK (sampling_rate >= 0 AND sampling_rate <= 1), code TEXT NOT NULL, CONSTRAINT `automation_rules_evaluators_pk` PRIMARY KEY (id), From a79b400114b8e7554023aae83ad8ba4469269eef Mon Sep 17 00:00:00 2001 From: Daniel Augusto Date: Sat, 21 Dec 2024 00:20:57 +0000 Subject: [PATCH 06/10] removing RequestContext references from Service --- .../opik/api/AutomationRuleEvaluator.java | 5 +++-- .../priv/AutomationRuleEvaluatorsResource.java | 13 ++++++++----- .../domain/AutomationRuleEvaluatorService.java | 18 +++++++----------- .../AutomationRuleEvaluatorsResourceTest.java | 6 +++--- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java index 0af2f7407..fe68a2510 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import lombok.Builder; @@ -20,8 +21,8 @@ public record AutomationRuleEvaluator ( // Fields and methods @JsonView({View.Public.class, View.Write.class}) UUID id, - @JsonView({View.Public.class, View.Write.class}) UUID projectId, - @JsonView({View.Public.class, View.Write.class}) AutomationRuleEvaluatorType type, + @JsonView({View.Public.class, View.Write.class}) @NotNull UUID projectId, + @JsonView({View.Public.class, View.Write.class}) @NotNull AutomationRuleEvaluatorType type, @JsonView({View.Public.class, View.Write.class}) @Pattern(regexp = NULL_OR_NOT_BLANK, message = "must not be blank") String code, @JsonView({View.Public.class, View.Write.class}) float samplingRate, diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java index c3e366156..8c90fa99f 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java @@ -41,7 +41,7 @@ import java.net.URI; import java.util.UUID; -@Path("/v1/private/automation/evaluator") +@Path("/v1/private/automation/evaluators") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Timed @@ -66,7 +66,7 @@ public Response find(@PathParam("projectId") UUID projectId, String workspaceId = requestContext.get().getWorkspaceId(); log.info("Looking for automated evaluators for project id '{}' on workspaceId '{}' (page {})", projectId, workspaceId, page); - Page definitionPage = service.find(page, size, projectId); + Page definitionPage = service.find(page, size, projectId, workspaceId); log.info("Found {} automated evaluators for project id '{}' on workspaceId '{}' (page {}, total {})", definitionPage.size(), projectId, workspaceId, page, definitionPage.total()); @@ -94,7 +94,7 @@ public Response getEvaluator(@PathParam("projectId") UUID projectId, @PathParam( @POST @Operation(operationId = "createAutomationRuleEvaluator", summary = "Create automation rule evaluator", description = "Create automation rule evaluator", responses = { @ApiResponse(responseCode = "201", description = "Created", headers = { - @Header(name = "Location", required = true, example = "${basePath}/api/v1/private/automation/evaluator/projectId/{projectId}/evaluatorId/{evaluatorId}", schema = @Schema(implementation = String.class)) + @Header(name = "Location", required = true, example = "${basePath}/api/v1/private/automation/evaluators/projectId/{projectId}/evaluatorId/{evaluatorId}", schema = @Schema(implementation = String.class)) }) }) @RateLimited @@ -103,10 +103,11 @@ public Response createEvaluator( @Context UriInfo uriInfo) { String workspaceId = requestContext.get().getWorkspaceId(); + String userName = requestContext.get().getUserName(); log.info("Creating {} evaluator for project_id '{}' on workspace_id '{}'", evaluator.type(), evaluator.projectId(), workspaceId); - AutomationRuleEvaluator savedEvaluator = service.save(evaluator, workspaceId); + AutomationRuleEvaluator savedEvaluator = service.save(evaluator, workspaceId, userName); log.info("Created {} evaluator '{}' for project_id '{}' on workspace_id '{}'", evaluator.type(), savedEvaluator.id(), evaluator.projectId(), workspaceId); @@ -128,9 +129,11 @@ public Response updateEvaluator(@PathParam("id") UUID id, @RequestBody(content = @Content(schema = @Schema(implementation = AutomationRuleEvaluatorUpdate.class))) @NotNull @Valid AutomationRuleEvaluatorUpdate evaluatorUpdate) { String workspaceId = requestContext.get().getWorkspaceId(); + String userName = requestContext.get().getUserName(); + log.info("Updating automation rule evaluator by id '{}' and project_id '{}' on workspace_id '{}'", id, projectId, workspaceId); - service.update(id, projectId, workspaceId, evaluatorUpdate); + service.update(id, projectId, workspaceId, userName, evaluatorUpdate); log.info("Updated automation rule evaluator by id '{}' and project_id '{}' on workspace_id '{}'", id, projectId, workspaceId); diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java index 9dd32e762..b1e3cc0cb 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java @@ -30,11 +30,11 @@ @ImplementedBy(AutomationRuleEvaluatorServiceImpl.class) public interface AutomationRuleEvaluatorService { - AutomationRuleEvaluator save(AutomationRuleEvaluator AutomationRuleEvaluator, @NonNull String workspaceId); + AutomationRuleEvaluator save(AutomationRuleEvaluator AutomationRuleEvaluator, @NonNull String workspaceId, @NonNull String userName); Optional getById(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId); - void update(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId, + void update(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId, @NonNull String userName, AutomationRuleEvaluatorUpdate AutomationRuleEvaluator); AutomationRuleEvaluator findById(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId); @@ -45,7 +45,7 @@ void update(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspace void delete(Set ids, @NonNull UUID projectId, @NonNull String workspaceId); - AutomationRuleEvaluator.AutomationRuleEvaluatorPage find(int page, int size, @NonNull UUID projectId); + AutomationRuleEvaluator.AutomationRuleEvaluatorPage find(int page, int size, @NonNull UUID projectId, @NonNull String workspaceId); } @Singleton @@ -57,18 +57,16 @@ class AutomationRuleEvaluatorServiceImpl implements AutomationRuleEvaluatorServi private final @NonNull IdGenerator idGenerator; private final @NonNull TransactionTemplate template; - private final @NonNull Provider requestContext; @Override public AutomationRuleEvaluator save(@NonNull AutomationRuleEvaluator ruleEvaluator, - @NonNull String workspaceId) { + @NonNull String workspaceId, + @NonNull String userName) { var builder = ruleEvaluator.id() == null ? ruleEvaluator.toBuilder().id(idGenerator.generateId()) : ruleEvaluator.toBuilder(); - String userName = requestContext.get().getUserName(); - builder.createdBy(userName) .lastUpdatedBy(userName); @@ -111,9 +109,8 @@ public Optional getById(@NonNull UUID id, @NonNull UUID } @Override - public void update(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId, + public void update(@NonNull UUID id, @NonNull UUID projectId, @NonNull String workspaceId, @NonNull String userName, @NonNull AutomationRuleEvaluatorUpdate evaluatorUpdate) { - String userName = requestContext.get().getUserName(); template.inTransaction(WRITE, handle -> { var dao = handle.attach(AutomationRuleEvaluatorDAO.class); @@ -192,8 +189,7 @@ public void delete(Set ids, @NonNull UUID projectId, @NonNull String works } @Override - public AutomationRuleEvaluator.AutomationRuleEvaluatorPage find(int pageNum, int size, @NonNull UUID projectId) { - String workspaceId = requestContext.get().getWorkspaceId(); + public AutomationRuleEvaluator.AutomationRuleEvaluatorPage find(int pageNum, int size, @NonNull UUID projectId, @NonNull String workspaceId) { return template.inTransaction(READ_ONLY, handle -> { var dao = handle.attach(AutomationRuleEvaluatorDAO.class); diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java index 4b9a6a002..d8e741455 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java @@ -53,9 +53,9 @@ @DisplayName("Automation Rule Evaluators Resource Test") class AutomationRuleEvaluatorsResourceTest { - private static final String URL_TEMPLATE = "%s/v1/private/automation/evaluator/"; - private static final String URL_TEMPLATE_BY_PROJ_ID = "%s/v1/private/automation/evaluator/projectId/%s"; - private static final String URL_TEMPLATE_BY_PROJ_ID_AND_EVAL_ID = "%s/v1/private/automation/evaluator/projectId/%s/evaluatorId/%s"; + private static final String URL_TEMPLATE = "%s/v1/private/automation/evaluators/"; + private static final String URL_TEMPLATE_BY_PROJ_ID = "%s/v1/private/automation/evaluators/projectId/%s"; + private static final String URL_TEMPLATE_BY_PROJ_ID_AND_EVAL_ID = "%s/v1/private/automation/evaluators/projectId/%s/evaluatorId/%s"; private static final String USER = UUID.randomUUID().toString(); private static final String API_KEY = UUID.randomUUID().toString(); From a20a4520eafb3d2a29ff45d0f0d398b6ce4b2d72 Mon Sep 17 00:00:00 2001 From: Daniel Augusto Date: Sat, 21 Dec 2024 00:30:14 +0000 Subject: [PATCH 07/10] Read-only rule evaluator ids --- .../opik/api/AutomationRuleEvaluator.java | 2 +- .../AutomationRuleEvaluatorsResourceTest.java | 30 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java index fe68a2510..2de364a7c 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/AutomationRuleEvaluator.java @@ -20,7 +20,7 @@ @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public record AutomationRuleEvaluator ( // Fields and methods - @JsonView({View.Public.class, View.Write.class}) UUID id, + @JsonView({View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) UUID id, @JsonView({View.Public.class, View.Write.class}) @NotNull UUID projectId, @JsonView({View.Public.class, View.Write.class}) @NotNull AutomationRuleEvaluatorType type, diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java index d8e741455..b4123f984 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java @@ -158,7 +158,7 @@ void setUp() { void createAutomationRuleEvaluator__whenApiKeyIsPresent__thenReturnProperResponse(String apiKey, boolean isAuthorized) { - var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); + var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().id(null).build(); mockTargetWorkspace(okApikey, TEST_WORKSPACE, WORKSPACE_ID); @@ -196,7 +196,7 @@ void getProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnProperRe IntStream.range(0, samplesToCreate).forEach(i -> { var evaluator = factory.manufacturePojo(AutomationRuleEvaluator.class) - .toBuilder().projectId(projectId).build(); + .toBuilder().id(null).projectId(projectId).build(); create(evaluator, okApikey, workspaceName); }); @@ -231,7 +231,7 @@ void getProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnProperRe void getAutomationRuleEvaluatorById__whenApiKeyIsPresent__thenReturnProperResponse(String apiKey, boolean isAuthorized) { - var evaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); + var evaluator = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().id(null).build(); String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); @@ -268,7 +268,7 @@ void getAutomationRuleEvaluatorById__whenApiKeyIsPresent__thenReturnProperRespon void updateAutomationRuleEvaluator__whenApiKeyIsPresent__thenReturnProperResponse(String apiKey, boolean isAuthorized) { - var evaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); + var evaluator = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().id(null).build(); String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); @@ -306,7 +306,7 @@ void updateAutomationRuleEvaluator__whenApiKeyIsPresent__thenReturnProperRespons void deleteAutomationRuleEvaluator__whenApiKeyIsPresent__thenReturnProperResponse(String apiKey, boolean isAuthorized) { - var evaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); + var evaluator = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().id(null).build();; String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); @@ -431,7 +431,7 @@ void deleteProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnPrope // void createAutomationRuleEvaluator__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, // boolean success, String workspaceName) { // -// var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); +// var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().id(null).build(); // // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) // .request() @@ -500,7 +500,7 @@ void deleteProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnPrope // void getAutomationRuleEvaluatorById__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, // boolean success, String workspaceName) { // -// var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class); +// var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().id(null).build(); // // UUID id = create(feedback, API_KEY, TEST_WORKSPACE); // @@ -534,7 +534,7 @@ void deleteProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnPrope // void updateAutomationRuleEvaluator__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, // boolean success, String workspaceName) { // -// var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class); +// var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().id(null).build(); // // UUID id = create(feedback, API_KEY, TEST_WORKSPACE); // @@ -567,7 +567,7 @@ void deleteProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnPrope // void deleteAutomationRuleEvaluator__whenSessionTokenIsPresent__thenReturnProperResponse(String sessionToken, // boolean success, String workspaceName) { // -// var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class); +// var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().id(null).build(); // // UUID id = create(feedback, API_KEY, TEST_WORKSPACE); // @@ -676,8 +676,8 @@ void deleteProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnPrope // // mockTargetWorkspace(apiKey, workspaceName, workspaceId); // - // var feedback1 = factory.manufacturePojo(AutomationRuleEvaluator.class); - // var feedback2 = factory.manufacturePojo(AutomationRuleEvaluator.class); + // var feedback1 = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().id(null).build(); + // var feedback2 = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().id(null).build(); // // create(feedback1, apiKey, workspaceName); // create(feedback2, apiKey, workspaceName); @@ -717,9 +717,9 @@ void deleteProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnPrope // mockTargetWorkspace(apiKey, workspaceName, workspaceId); // mockTargetWorkspace(apiKey2, workspaceName2, workspaceId2); // - // var feedback1 = factory.manufacturePojo(AutomationRuleEvaluator.class); + // var feedback1 = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().id(null).build(); // - // var feedback2 = factory.manufacturePojo(AutomationRuleEvaluator.class); + // var feedback2 = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().id(null).build(); // // create(feedback1, apiKey, workspaceName); // create(feedback2, apiKey2, workspaceName2); @@ -809,7 +809,7 @@ void deleteProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnPrope // @DisplayName("Success") // void getById() { // - // final var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class); + // final var feedback = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().id(null).build(); // // var id = create(feedback, API_KEY, TEST_WORKSPACE); // @@ -873,7 +873,7 @@ void deleteProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnPrope // void create() { // UUID id; // - // var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class); + // var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluator.class).toBuilder().id(null).build(); // // try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) // .request() From a54494058725418954bae9eb98427dd10e2843c5 Mon Sep 17 00:00:00 2001 From: Daniel Augusto Date: Sat, 21 Dec 2024 00:59:39 +0000 Subject: [PATCH 08/10] cleaning up --- .../com/comet/opik/domain/AutomationRuleEvaluatorDAO.java | 5 +---- .../comet/opik/domain/AutomationRuleEvaluatorService.java | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java index 5b05ba7dd..aeaab2837 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java @@ -8,15 +8,12 @@ import org.jdbi.v3.sqlobject.config.RegisterRowMapper; import org.jdbi.v3.sqlobject.customizer.AllowUnusedBindings; import org.jdbi.v3.sqlobject.customizer.Bind; -import org.jdbi.v3.sqlobject.customizer.BindList; import org.jdbi.v3.sqlobject.customizer.BindMethods; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; -import org.jdbi.v3.stringtemplate4.UseStringTemplateEngine; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.UUID; @RegisterArgumentFactory(UUIDArgumentFactory.class) @@ -64,7 +61,7 @@ Optional findById(@Bind("id") UUID id, ON rule.id = evaluator.id WHERE `action` = 'evaluator' AND workspace_id = :workspaceId AND project_id = :projectId - LIMIT :limit OFFSET :offset + LIMIT :limit OFFSET :offset """) @AllowUnusedBindings List find(@Bind("limit") int limit, @Bind("offset") int offset, diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java index b1e3cc0cb..eb7e3396e 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java @@ -5,10 +5,8 @@ import com.comet.opik.api.AutomationRuleEvaluatorUpdate; import com.comet.opik.api.error.EntityAlreadyExistsException; import com.comet.opik.api.error.ErrorMessage; -import com.comet.opik.infrastructure.auth.RequestContext; import com.google.inject.ImplementedBy; import jakarta.inject.Inject; -import jakarta.inject.Provider; import jakarta.inject.Singleton; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.core.Response; From 84f519707bb82d1870e40260abcc36de2c5a7167 Mon Sep 17 00:00:00 2001 From: Daniel Augusto Date: Mon, 23 Dec 2024 11:44:19 +0000 Subject: [PATCH 09/10] moving project to endpoint root and cleaning up descriptions --- .../AutomationRuleEvaluatorsResource.java | 24 +++++++++---------- .../AutomationRuleEvaluatorsResourceTest.java | 23 ++++++++---------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java index 8c90fa99f..fa6de0011 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResource.java @@ -41,7 +41,7 @@ import java.net.URI; import java.util.UUID; -@Path("/v1/private/automation/evaluators") +@Path("/v1/private/automation/evaluators/project/{projectId}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Timed @@ -54,7 +54,6 @@ public class AutomationRuleEvaluatorsResource { private final @NonNull Provider requestContext; @GET - @Path("/projectId/{projectId}") @Operation(operationId = "findEvaluators", summary = "Find Evaluators", description = "Find Evaluators", responses = { @ApiResponse(responseCode = "200", description = "Evaluators resource", content = @Content(schema = @Schema(implementation = AutomationRuleEvaluator.AutomationRuleEvaluatorPage.class))) }) @@ -76,17 +75,17 @@ public Response find(@PathParam("projectId") UUID projectId, } @GET - @Path("/projectId/{projectId}/evaluatorId/{evaluatorId}") - @Operation(operationId = "getAutomationRulesByProjectId", summary = "Get automation rule evaluator by id", description = "Get dataset by id", responses = { + @Path("/evaluator/{evaluatorId}") + @Operation(operationId = "getEvaluatorById", summary = "Get automation rule evaluator by id", description = "Get automation rule by id", responses = { @ApiResponse(responseCode = "200", description = "Automation Rule resource", content = @Content(schema = @Schema(implementation = AutomationRuleEvaluator.class))) }) @JsonView(AutomationRuleEvaluator.View.Public.class) public Response getEvaluator(@PathParam("projectId") UUID projectId, @PathParam("evaluatorId") UUID evaluatorId) { String workspaceId = requestContext.get().getWorkspaceId(); - log.info("Finding automated evaluators by id '{}' on project_id '{}'", projectId, workspaceId); + log.info("Looking for automated evaluator: id '{}' on project_id '{}'", projectId, workspaceId); AutomationRuleEvaluator evaluator = service.findById(evaluatorId, projectId, workspaceId); - log.info("Found automated evaluators by id '{}' on project_id '{}'", projectId, workspaceId); + log.info("Found automated evaluator: id '{}' on project_id '{}'", projectId, workspaceId); return Response.ok().entity(evaluator).build(); } @@ -94,7 +93,7 @@ public Response getEvaluator(@PathParam("projectId") UUID projectId, @PathParam( @POST @Operation(operationId = "createAutomationRuleEvaluator", summary = "Create automation rule evaluator", description = "Create automation rule evaluator", responses = { @ApiResponse(responseCode = "201", description = "Created", headers = { - @Header(name = "Location", required = true, example = "${basePath}/api/v1/private/automation/evaluators/projectId/{projectId}/evaluatorId/{evaluatorId}", schema = @Schema(implementation = String.class)) + @Header(name = "Location", required = true, example = "${basePath}/api/v1/private/automation/evaluators/project/{projectId}/evaluator/{evaluatorId}", schema = @Schema(implementation = String.class)) }) }) @RateLimited @@ -112,14 +111,14 @@ public Response createEvaluator( savedEvaluator.id(), evaluator.projectId(), workspaceId); URI uri = uriInfo.getAbsolutePathBuilder() - .path("/projectId/%s/evaluatorId/%s".formatted(savedEvaluator.projectId().toString(), + .path("/projectId/%s/evaluator/%s".formatted(savedEvaluator.projectId().toString(), savedEvaluator.id().toString())) .build(); return Response.created(uri).build(); } @PUT - @Path("/projectId/{projectId}/evaluatorId/{id}") + @Path("/evaluator/{id}") @Operation(operationId = "updateAutomationRuleEvaluator", summary = "update Automation Rule Evaluator by id", description = "update Automation Rule Evaluator by id", responses = { @ApiResponse(responseCode = "204", description = "No content"), }) @@ -141,8 +140,8 @@ public Response updateEvaluator(@PathParam("id") UUID id, } @DELETE - @Path("/projectId/{projectId}/evaluatorId/{id}") - @Operation(operationId = "deleteAutomationRuleEvaluatorById", summary = "Delete Automation Rule Evaluator by id", description = "Delete Automation Rule Evaluator by id", responses = { + @Path("/evaluator/{id}") + @Operation(operationId = "deleteAutomationRuleEvaluatorById", summary = "Delete Automation Rule Evaluator by id", description = "Delete a single Automation Rule Evaluator by id", responses = { @ApiResponse(responseCode = "204", description = "No content"), }) public Response deleteEvaluator(@PathParam("id") UUID id, @PathParam("projectId") UUID projectId) { @@ -155,8 +154,7 @@ public Response deleteEvaluator(@PathParam("id") UUID id, @PathParam("projectId" } @DELETE - @Path("/projectId/{projectId}") - @Operation(operationId = "deleteAutomationRuleEvaluatorByProject", summary = "Delete Automation Rule Evaluator by Project id", description = "Delete Automation Rule Evaluator by Project id", responses = { + @Operation(operationId = "deleteAutomationRuleEvaluatorByProject", summary = "Delete all project Automation Rule Evaluators", description = "Delete all Automation Rule Evaluator in a project", responses = { @ApiResponse(responseCode = "204", description = "No content"), }) public Response deleteProjectEvaluators(@PathParam("projectId") UUID projectId) { diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java index b4123f984..d46fb8df5 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java @@ -37,13 +37,11 @@ import java.util.stream.IntStream; import java.util.stream.Stream; -import static com.comet.opik.infrastructure.auth.RequestContext.SESSION_COOKIE; import static com.comet.opik.infrastructure.auth.RequestContext.WORKSPACE_HEADER; import static com.comet.opik.infrastructure.auth.TestHttpClientUtils.UNAUTHORIZED_RESPONSE; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.matching; import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath; -import static com.github.tomakehurst.wiremock.client.WireMock.okJson; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static org.assertj.core.api.Assertions.assertThat; @@ -53,9 +51,8 @@ @DisplayName("Automation Rule Evaluators Resource Test") class AutomationRuleEvaluatorsResourceTest { - private static final String URL_TEMPLATE = "%s/v1/private/automation/evaluators/"; - private static final String URL_TEMPLATE_BY_PROJ_ID = "%s/v1/private/automation/evaluators/projectId/%s"; - private static final String URL_TEMPLATE_BY_PROJ_ID_AND_EVAL_ID = "%s/v1/private/automation/evaluators/projectId/%s/evaluatorId/%s"; + private static final String URL_TEMPLATE = "%s/v1/private/automation/evaluators/project/%s"; + private static final String URL_TEMPLATE_BY_EVAL_ID = "%s/v1/private/automation/evaluators/project/%s/evaluator/%s"; private static final String USER = UUID.randomUUID().toString(); private static final String API_KEY = UUID.randomUUID().toString(); @@ -108,7 +105,7 @@ void tearDownAll() { } private UUID create(AutomationRuleEvaluator evaluator, String apiKey, String workspaceName) { - try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI, evaluator.projectId())) .request() .accept(MediaType.APPLICATION_JSON_TYPE) .header(HttpHeaders.AUTHORIZATION, apiKey) @@ -162,7 +159,7 @@ void createAutomationRuleEvaluator__whenApiKeyIsPresent__thenReturnProperRespons mockTargetWorkspace(okApikey, TEST_WORKSPACE, WORKSPACE_ID); - try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI, ruleEvaluator.projectId())) .request() .header(HttpHeaders.AUTHORIZATION, apiKey) .accept(MediaType.APPLICATION_JSON_TYPE) @@ -200,7 +197,7 @@ void getProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnProperRe create(evaluator, okApikey, workspaceName); }); - try (var actualResponse = client.target(URL_TEMPLATE_BY_PROJ_ID.formatted(baseURI, projectId)) + try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI, projectId)) .queryParam("size", samplesToCreate) .request() .header(HttpHeaders.AUTHORIZATION, apiKey) @@ -240,7 +237,7 @@ void getAutomationRuleEvaluatorById__whenApiKeyIsPresent__thenReturnProperRespon UUID id = create(evaluator, okApikey, workspaceName); - try (var actualResponse = client.target(URL_TEMPLATE_BY_PROJ_ID_AND_EVAL_ID.formatted(baseURI, evaluator.projectId(), id)) + try (var actualResponse = client.target(URL_TEMPLATE_BY_EVAL_ID.formatted(baseURI, evaluator.projectId(), id)) //.path(id.toString()) .request() .header(HttpHeaders.AUTHORIZATION, apiKey) @@ -282,7 +279,7 @@ void updateAutomationRuleEvaluator__whenApiKeyIsPresent__thenReturnProperRespons .samplingRate(RandomGenerator.getDefault().nextFloat()) .build(); - try (var actualResponse = client.target(URL_TEMPLATE_BY_PROJ_ID_AND_EVAL_ID.formatted(baseURI, evaluator.projectId(), id)) + try (var actualResponse = client.target(URL_TEMPLATE_BY_EVAL_ID.formatted(baseURI, evaluator.projectId(), id)) .request() .header(HttpHeaders.AUTHORIZATION, apiKey) .accept(MediaType.APPLICATION_JSON_TYPE) @@ -315,7 +312,7 @@ void deleteAutomationRuleEvaluator__whenApiKeyIsPresent__thenReturnProperRespons UUID id = create(evaluator, okApikey, workspaceName); - try (var actualResponse = client.target(URL_TEMPLATE_BY_PROJ_ID_AND_EVAL_ID.formatted(baseURI, evaluator.projectId(), id)) + try (var actualResponse = client.target(URL_TEMPLATE_BY_EVAL_ID.formatted(baseURI, evaluator.projectId(), id)) .request() .header(HttpHeaders.AUTHORIZATION, apiKey) .accept(MediaType.APPLICATION_JSON_TYPE) @@ -351,7 +348,7 @@ void deleteProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnPrope create(evaluator1, okApikey, workspaceName); create(evaluator2, okApikey, workspaceName); - try (var actualResponse = client.target(URL_TEMPLATE_BY_PROJ_ID.formatted(baseURI, evaluator1.projectId())) + try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI, evaluator1.projectId())) .request() .header(HttpHeaders.AUTHORIZATION, apiKey) .accept(MediaType.APPLICATION_JSON_TYPE) @@ -369,7 +366,7 @@ void deleteProjectAutomationRuleEvaluators__whenApiKeyIsPresent__thenReturnPrope } // we shall see a single evaluators for the project now - try (var actualResponse = client.target(URL_TEMPLATE_BY_PROJ_ID.formatted(baseURI, projectId)) + try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI, projectId)) .request() .header(HttpHeaders.AUTHORIZATION, apiKey) .accept(MediaType.APPLICATION_JSON_TYPE) From f4310cbf4e134e5c5c6fc2a6a6460a7176c93c9d Mon Sep 17 00:00:00 2001 From: Daniel Augusto Date: Mon, 23 Dec 2024 12:03:26 +0000 Subject: [PATCH 10/10] Removing hard coded 'evaluator' from sqls to Action bind. Defensive programming in case the string changes. --- .../comet/opik/domain/AutomationRuleDAO.java | 5 +++- .../domain/AutomationRuleEvaluatorDAO.java | 24 +++++++------------ .../AutomationRuleEvaluatorService.java | 11 +++++---- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java index ea96fdbb6..a5aa7ce03 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleDAO.java @@ -48,5 +48,8 @@ void deleteByProject(@Bind("projectId") UUID projectId, void delete(@BindList("ids") Set ids, @Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); @SqlQuery("SELECT COUNT(*) FROM automation_rules WHERE project_id = :projectId AND workspace_id = :workspaceId") - long findRuleCount(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); + long findCount(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); + + @SqlQuery("SELECT COUNT(*) FROM automation_rules WHERE project_id = :projectId AND workspace_id = :workspaceId AND `action` = :action") + long findCountByActionType(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId, @Bind("action") AutomationRule.AutomationRuleAction action); } diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java index aeaab2837..9bc4bbddc 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorDAO.java @@ -1,5 +1,6 @@ package com.comet.opik.domain; +import com.comet.opik.api.AutomationRule; import com.comet.opik.api.AutomationRuleEvaluator; import com.comet.opik.api.AutomationRuleEvaluatorUpdate; import com.comet.opik.infrastructure.db.UUIDArgumentFactory; @@ -37,44 +38,35 @@ public interface AutomationRuleEvaluatorDAO extends AutomationRuleDAO { FROM automation_rules rule JOIN automation_rule_evaluators evaluator ON rule.id = evaluator.id - WHERE `action` = 'evaluator' + WHERE `action` = :action AND workspace_id = :workspaceId AND project_id = :projectId AND rule.id = :id """) Optional findById(@Bind("id") UUID id, @Bind("projectId") UUID projectId, - @Bind("workspaceId") String workspaceId); + @Bind("workspaceId") String workspaceId, + @Bind("action") AutomationRule.AutomationRuleAction action); @SqlQuery(""" SELECT rule.id, rule.project_id, rule.action, rule.sampling_rate, evaluator.type, evaluator.code, rule.created_at, rule.created_by, rule.last_updated_at, rule.last_updated_by FROM automation_rules rule JOIN automation_rule_evaluators evaluator ON rule.id = evaluator.id - WHERE `action` = 'evaluator' + WHERE `action` = :action AND workspace_id = :workspaceId AND project_id = :projectId """) - List findByProjectId(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); + List findByProjectId(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId, @Bind("action") AutomationRule.AutomationRuleAction action); @SqlQuery(""" SELECT rule.id, rule.project_id, rule.action, rule.sampling_rate, evaluator.type, evaluator.code, rule.created_at, rule.created_by, rule.last_updated_at, rule.last_updated_by FROM automation_rules rule JOIN automation_rule_evaluators evaluator ON rule.id = evaluator.id - WHERE `action` = 'evaluator' + WHERE `action` = :action AND workspace_id = :workspaceId AND project_id = :projectId LIMIT :limit OFFSET :offset """) @AllowUnusedBindings List find(@Bind("limit") int limit, @Bind("offset") int offset, - @Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); - - @SqlQuery(""" - SELECT COUNT(*) - FROM automation_rules rule - JOIN automation_rule_evaluators evaluator - ON rule.id = evaluator.id - WHERE `action` = 'evaluator' - AND workspace_id = :workspaceId AND project_id = :projectId - """) - long findCount(@Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId); + @Bind("projectId") UUID projectId, @Bind("workspaceId") String workspaceId, @Bind("action") AutomationRule.AutomationRuleAction action); } diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java index eb7e3396e..dce03bab6 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AutomationRuleEvaluatorService.java @@ -80,7 +80,7 @@ public AutomationRuleEvaluator save(@NonNull AutomationRuleEvaluator ruleEvaluat evaluatorsDAO.save(evaluatorToSave); return evaluatorsDAO - .findById(evaluatorToSave.id(), evaluatorToSave.projectId(), workspaceId) + .findById(evaluatorToSave.id(), evaluatorToSave.projectId(), workspaceId, AutomationRule.AutomationRuleAction.EVALUATOR) .orElseThrow(); } catch (UnableToExecuteStatementException e) { if (e.getCause() instanceof SQLIntegrityConstraintViolationException) { @@ -100,7 +100,7 @@ public Optional getById(@NonNull UUID id, @NonNull UUID log.info("Getting AutomationRuleEvaluator with id '{}', workspaceId '{}'", id, projectId); return template.inTransaction(READ_ONLY, handle -> { var dao = handle.attach(AutomationRuleEvaluatorDAO.class); - var AutomationRuleEvaluator = dao.findById(id, projectId, workspaceId); + var AutomationRuleEvaluator = dao.findById(id, projectId, workspaceId, AutomationRule.AutomationRuleAction.EVALUATOR); log.info("Got AutomationRuleEvaluator with id '{}', workspaceId '{}'", id, projectId); return AutomationRuleEvaluator; }); @@ -138,7 +138,7 @@ public AutomationRuleEvaluator findById(@NonNull UUID id, @NonNull UUID projectI log.info("Finding AutomationRuleEvaluator with id '{}', projectId '{}'", id, projectId); return template.inTransaction(READ_ONLY, handle -> { var dao = handle.attach(AutomationRuleEvaluatorDAO.class); - var AutomationRuleEvaluator = dao.findById(id, projectId, workspaceId) + var AutomationRuleEvaluator = dao.findById(id, projectId, workspaceId, AutomationRule.AutomationRuleAction.EVALUATOR) .orElseThrow(this::newNotFoundException); log.info("Found AutomationRuleEvaluator with id '{}', projectId '{}'", id, projectId); return AutomationRuleEvaluator; @@ -191,9 +191,10 @@ public AutomationRuleEvaluator.AutomationRuleEvaluatorPage find(int pageNum, int return template.inTransaction(READ_ONLY, handle -> { var dao = handle.attach(AutomationRuleEvaluatorDAO.class); - var total = dao.findCount(projectId, workspaceId); + var total = dao.findCountByActionType(projectId, workspaceId, AutomationRule.AutomationRuleAction.EVALUATOR); + var offset = (pageNum - 1) * size; - var automationRuleEvaluators = dao.find(size, offset, projectId, workspaceId); + var automationRuleEvaluators = dao.find(size, offset, projectId, workspaceId, AutomationRule.AutomationRuleAction.EVALUATOR); log.info("Found {} AutomationRuleEvaluators for projectId '{}'", automationRuleEvaluators.size(), projectId); return new AutomationRuleEvaluator.AutomationRuleEvaluatorPage(pageNum, automationRuleEvaluators.size(), total,