Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[OPIK-418] Add endpoint to return all feedback score names #646

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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 lombok.Builder;

import java.util.List;

@Builder(toBuilder = true)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record FeedbackScoreNames(List<ScoreName> scores) {

public record ScoreName(String name) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.comet.opik.api.resources.v1.priv;

import com.codahale.metrics.annotation.Timed;
import com.comet.opik.api.FeedbackDefinition;
import com.comet.opik.api.FeedbackScoreNames;
import com.comet.opik.domain.FeedbackScoreService;
import com.comet.opik.infrastructure.auth.RequestContext;
import com.fasterxml.jackson.annotation.JsonView;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
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.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.util.UUID;

import static com.comet.opik.api.FeedbackScoreNames.ScoreName;
import static com.comet.opik.utils.AsyncUtils.setRequestContext;

@Path("/v1/private/feedback-scores")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Timed
@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Inject)
@Tag(name = "Feedback-scores", description = "Feedback scores related resources")
public class FeedbackScoreResource {

private final @NonNull FeedbackScoreService feedbackScoreService;
private final @NonNull Provider<RequestContext> requestContext;

@GET
@Path("/names")
@Operation(operationId = "findFeedbackScoreNames", summary = "Find Feedback Score names", description = "Find Feedback Score names", responses = {
@ApiResponse(responseCode = "200", description = "Feedback Scores resource", content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class))))
})
@JsonView({FeedbackDefinition.View.Public.class})
public Response findFeedbackScoreNames(
@QueryParam("project_id") UUID projectId,
@QueryParam("with_experiments_only") boolean withExperimentsOnly) {

String workspaceId = requestContext.get().getWorkspaceId();

log.info("Find feedback score names by project_id '{}' and with_experiments_only '{}', on workspaceId '{}'",
projectId, withExperimentsOnly, workspaceId);
FeedbackScoreNames feedbackScoreNames = feedbackScoreService
.getFeedbackScoreNames(projectId, withExperimentsOnly)
.map(names -> names.stream().map(ScoreName::new).toList())
.map(FeedbackScoreNames::new)
.contextWrite(ctx -> setRequestContext(ctx, requestContext))
.block();
log.info("Found feedback score names by project_id '{}' and with_experiments_only '{}', on workspaceId '{}'",
projectId, withExperimentsOnly, workspaceId);

return Response.ok(feedbackScoreNames).build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ Mono<Long> scoreEntity(EntityType entityType, UUID entityId, FeedbackScore score
Mono<Void> deleteByEntityIds(EntityType entityType, Set<UUID> entityIds);

Mono<Long> scoreBatchOf(EntityType entityType, List<FeedbackScoreBatchItem> scores);

Mono<List<String>> getFeedbackScoreNames(UUID projectId, boolean withExperimentsOnly);
}

@Singleton
Expand Down Expand Up @@ -153,6 +155,47 @@ AND entity_id IN (
;
""";

private static final String SELECT_FEEDBACK_SCORE_NAMES = """
SELECT
distinct name
FROM (
SELECT
idoberko2 marked this conversation as resolved.
Show resolved Hide resolved
name
FROM feedback_scores
WHERE workspace_id = :workspace_id
<if(project_id)>
AND project_id = :project_id
<endif>
<if(with_experiments_only)>
AND entity_id IN (
SELECT
trace_id
FROM (
SELECT
id
FROM experiments
WHERE workspace_id = :workspace_id
ORDER BY id DESC, last_updated_at DESC
LIMIT 1 BY id
) AS e
INNER JOIN (
SELECT
experiment_id,
trace_id
FROM experiment_items
WHERE workspace_id = :workspace_id
ORDER BY id DESC, last_updated_at DESC
LIMIT 1 BY id
) ei ON e.id = ei.experiment_id
)
<endif>
AND entity_type = 'trace'
ORDER BY entity_id DESC, last_updated_at DESC
LIMIT 1 BY entity_id, name
) AS names
;
""";

private final @NonNull TransactionTemplateAsync asyncTemplate;

@Override
Expand Down Expand Up @@ -318,6 +361,40 @@ public Mono<Void> deleteByEntityIds(
};
}

@Override
@WithSpan
public Mono<List<String>> getFeedbackScoreNames(UUID projectId, boolean withExperimentsOnly) {
return asyncTemplate.nonTransaction(connection -> {

ST template = new ST(SELECT_FEEDBACK_SCORE_NAMES);

bindTemplateParam(projectId, withExperimentsOnly, template);

var statement = connection.createStatement(template.render());

bindStatementParam(projectId, statement);

return makeMonoContextAware(bindWorkspaceIdToMono(statement))
.flatMapMany(result -> result.map((row, rowMetadata) -> row.get("name", String.class)))
.distinct()
.collect(Collectors.toList());
});
}

private void bindStatementParam(UUID projectId, Statement statement) {
if (projectId != null) {
statement.bind("project_id", projectId);
}
}

private void bindTemplateParam(UUID projectId, boolean withExperimentsOnly, ST template) {
if (projectId != null) {
template.add("project_id", projectId);
}

template.add("with_experiments_only", withExperimentsOnly);
}

private Mono<? extends Result> cascadeSpanDelete(Set<UUID> traceIds, Connection connection) {
log.info("Deleting feedback scores by span entityId, traceIds count '{}'", traceIds.size());
var statement = connection.createStatement(DELETE_SPANS_CASCADE_FEEDBACK_SCORE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public interface FeedbackScoreService {

Mono<Void> deleteSpanScore(UUID id, String tag);
Mono<Void> deleteTraceScore(UUID id, String tag);

Mono<List<String>> getFeedbackScoreNames(UUID projectId, boolean withExperimentsOnly);
}

@Slf4j
Expand Down Expand Up @@ -228,6 +230,11 @@ public Mono<Void> deleteTraceScore(UUID id, String name) {
return dao.deleteScoreFrom(EntityType.TRACE, id, name);
}

@Override
public Mono<List<String>> getFeedbackScoreNames(UUID projectId, boolean withExperimentsOnly) {
return dao.getFeedbackScoreNames(projectId, withExperimentsOnly);
}

private Mono<Long> failWithNotFound(String errorMessage) {
log.info(errorMessage);
return Mono.error(new NotFoundException(Response.status(404)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.comet.opik.api.resources.utils.resources;

import com.comet.opik.api.Experiment;
import com.comet.opik.api.ExperimentItem;
import com.comet.opik.api.ExperimentItemsBatch;
import com.comet.opik.api.resources.utils.TestUtils;
import com.comet.opik.infrastructure.auth.RequestContext;
import jakarta.ws.rs.client.Entity;
import lombok.RequiredArgsConstructor;
import org.apache.http.HttpStatus;
import org.testcontainers.shaded.com.google.common.net.HttpHeaders;
import ru.vyarus.dropwizard.guice.test.ClientSupport;
import uk.co.jemos.podam.api.PodamFactory;

import java.util.Set;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;

@RequiredArgsConstructor
public class ExperimentResourceClient {

private static final String RESOURCE_PATH = "%s/v1/private/experiments";

private final ClientSupport client;
private final String baseURI;
private final PodamFactory podamFactory;

public UUID createExperiment(String apiKey, String workspaceName) {
var experiment = podamFactory.manufacturePojo(Experiment.class).toBuilder()
.promptVersion(null)
.build();

try (var response = client.target(RESOURCE_PATH.formatted(baseURI))
.request()
.header(HttpHeaders.AUTHORIZATION, apiKey)
.header(RequestContext.WORKSPACE_HEADER, workspaceName)
.post(Entity.json(experiment))) {

assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_CREATED);
return TestUtils.getIdFromLocation(response.getLocation());
}
}

public void createExperimentItem(Set<ExperimentItem> experimentItems, String apiKey, String workspaceName) {
try (var response = client.target(RESOURCE_PATH.formatted(baseURI))
.path("items")
.request()
.header(HttpHeaders.AUTHORIZATION, apiKey)
.header(RequestContext.WORKSPACE_HEADER, workspaceName)
.post(Entity.json(new ExperimentItemsBatch(experimentItems)))) {

assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_NO_CONTENT);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.comet.opik.api.resources.utils.resources;

import com.comet.opik.api.Project;
import com.comet.opik.api.resources.utils.TestUtils;
import com.comet.opik.infrastructure.auth.RequestContext;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.HttpHeaders;
import lombok.RequiredArgsConstructor;
import org.apache.hc.core5.http.HttpStatus;
import ru.vyarus.dropwizard.guice.test.ClientSupport;
import uk.co.jemos.podam.api.PodamFactory;

import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;

@RequiredArgsConstructor
public class ProjectResourceClient {

private static final String RESOURCE_PATH = "%s/v1/private/projects";

private final ClientSupport client;
private final String baseURI;
private final PodamFactory podamFactory;

public UUID createProject(String projectName, String apiKey, String workspaceName) {

var project = podamFactory.manufacturePojo(Project.class).toBuilder()
.name(projectName)
.build();

try (var response = client.target(RESOURCE_PATH.formatted(baseURI))
.request()
.header(HttpHeaders.AUTHORIZATION, apiKey)
.header(RequestContext.WORKSPACE_HEADER, workspaceName)
.post(Entity.json(project))) {

assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_CREATED);

return TestUtils.getIdFromLocation(response.getLocation());
}
}

public Project getProject(UUID projectId, String apiKey, String workspaceName) {

try (var response = client.target(RESOURCE_PATH.formatted(baseURI) + "/" + projectId)
.request()
.header(HttpHeaders.AUTHORIZATION, apiKey)
.header(RequestContext.WORKSPACE_HEADER, workspaceName)
.get()) {

assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK);

return response.readEntity(Project.class);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.comet.opik.api.resources.utils.resources;

import com.comet.opik.api.FeedbackScoreBatch;
import com.comet.opik.api.FeedbackScoreBatchItem;
import com.comet.opik.api.Trace;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.HttpHeaders;
import lombok.RequiredArgsConstructor;
import org.apache.http.HttpStatus;
import ru.vyarus.dropwizard.guice.test.ClientSupport;

import java.util.List;

import static com.comet.opik.infrastructure.auth.RequestContext.WORKSPACE_HEADER;
import static org.assertj.core.api.Assertions.assertThat;

@RequiredArgsConstructor
public class TraceResourceClient {

private static final String RESOURCE_PATH = "%s/v1/private/traces";

private final ClientSupport client;
private final String baseURI;

public void createTrace(Trace trace, String apiKey, String workspaceName) {
try (var response = client.target(RESOURCE_PATH.formatted(baseURI))
.request()
.header(HttpHeaders.AUTHORIZATION, apiKey)
.header(WORKSPACE_HEADER, workspaceName)
.post(Entity.json(trace))) {

assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_CREATED);
}
}

public void feedbackScore(List<FeedbackScoreBatchItem> score, String apiKey, String workspaceName) {

try (var response = client.target(RESOURCE_PATH.formatted(baseURI))
.path("feedback-scores")
.request()
.header(HttpHeaders.AUTHORIZATION, apiKey)
.header(WORKSPACE_HEADER, workspaceName)
.put(Entity.json(new FeedbackScoreBatch(score)))) {

assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_NO_CONTENT);
}
}

}
Loading