diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/filter/Operator.java b/apps/opik-backend/src/main/java/com/comet/opik/api/filter/Operator.java index e3a3f1c9dc..5a7f67aef1 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/filter/Operator.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/filter/Operator.java @@ -12,6 +12,7 @@ public enum Operator { STARTS_WITH("starts_with"), ENDS_WITH("ends_with"), EQUAL("="), + NOT_EQUAL("!="), GREATER_THAN(">"), GREATER_THAN_EQUAL(">="), LESS_THAN("<"), diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/filter/FilterQueryBuilder.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/filter/FilterQueryBuilder.java index 55b2ee2b68..2fac76f528 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/filter/FilterQueryBuilder.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/filter/FilterQueryBuilder.java @@ -64,6 +64,14 @@ public class FilterQueryBuilder { "has(groupArray(tuple(lower(name), %1$s)), tuple(lower(:filterKey%2$d), toDecimal64(:filter%2$d, 9))) = 1", FieldType.DICTIONARY, "lower(JSON_VALUE(%1$s, :filterKey%2$d)) = lower(:filter%2$d)")), + Operator.NOT_EQUAL, new EnumMap<>(Map.of( + FieldType.STRING, "lower(%1$s) != lower(:filter%2$d)", + FieldType.DATE_TIME, "%1$s != parseDateTime64BestEffort(:filter%2$d, 9)", + FieldType.NUMBER, "%1$s != :filter%2$d", + FieldType.FEEDBACK_SCORES_NUMBER, + "has(groupArray(tuple(lower(name), %1$s)), tuple(lower(:filterKey%2$d), toDecimal64(:filter%2$d, 9))) = 0", + FieldType.DICTIONARY, + "lower(JSON_VALUE(%1$s, :filterKey%2$d)) != lower(:filter%2$d)")), Operator.GREATER_THAN, new EnumMap<>(Map.of( FieldType.DATE_TIME, "%1$s > parseDateTime64BestEffort(:filter%2$d, 9)", FieldType.NUMBER, "%1$s > :filter%2$d", diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/SpansResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/SpansResourceTest.java index 1383fdea24..bea0185152 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/SpansResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/SpansResourceTest.java @@ -90,6 +90,7 @@ import java.util.Set; import java.util.UUID; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -1109,6 +1110,44 @@ void getByProjectName__whenFilterByCorrespondingField__thenReturnSpansFiltered(S apiKey); } + @ParameterizedTest + @MethodSource("equalAndNotEqualFilters") + void getByProjectName__whenFilterTotalEstimatedCostEqual_NotEqual__thenReturnSpansFiltered(Operator operator, + Function, List> getUnexpectedSpans, + Function, List> getExpectedSpans) { + String workspaceName = UUID.randomUUID().toString(); + String workspaceId = UUID.randomUUID().toString(); + String apiKey = UUID.randomUUID().toString(); + + mockTargetWorkspace(apiKey, workspaceName, workspaceId); + + var projectName = generator.generate().toString(); + var spans = PodamFactoryUtils.manufacturePojoList(podamFactory, Span.class) + .stream() + .map(span -> span.toBuilder() + .projectId(null) + .projectName(projectName) + .feedbackScores(null) + .build()) + .collect(Collectors.toCollection(ArrayList::new)); + spans.set(0, spans.getFirst().toBuilder() + .model("gpt-3.5-turbo-1106") + .usage(Map.of("completion_tokens", Math.abs(podamFactory.manufacturePojo(Integer.class)), + "prompt_tokens", Math.abs(podamFactory.manufacturePojo(Integer.class)))) + .build()); + + spans.forEach(expectedSpan -> SpansResourceTest.this.createAndAssert(expectedSpan, apiKey, workspaceName)); + var expectedSpans = getExpectedSpans.apply(spans); + var unexpectedSpans = getUnexpectedSpans.apply(spans); + + var filters = List.of(SpanFilter.builder() + .field(SpanField.TOTAL_ESTIMATED_COST) + .operator(operator) + .value("0") + .build()); + getAndAssertPage(workspaceName, projectName, filters, spans, expectedSpans.reversed(), unexpectedSpans, apiKey); + } + static Stream getByProjectName__whenFilterByCorrespondingField__thenReturnSpansFiltered() { return Stream.of( Arguments.of(SpanField.TOTAL_ESTIMATED_COST, Operator.GREATER_THAN, "0"), @@ -1116,8 +1155,11 @@ static Stream getByProjectName__whenFilterByCorrespondingField__thenR Arguments.of(SpanField.PROVIDER, Operator.EQUAL, null)); } - @Test - void getByProjectName__whenFilterNameEqual__thenReturnSpansFiltered() { + @ParameterizedTest + @MethodSource("equalAndNotEqualFilters") + void getByProjectName__whenFilterNameEqual_NotEqual__thenReturnSpansFiltered(Operator operator, + Function, List> getExpectedSpans, + Function, List> getUnexpectedSpans) { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1134,19 +1176,25 @@ void getByProjectName__whenFilterNameEqual__thenReturnSpansFiltered() { .build()) .collect(Collectors.toCollection(ArrayList::new)); spans.forEach(expectedSpan -> SpansResourceTest.this.createAndAssert(expectedSpan, apiKey, workspaceName)); - var expectedSpans = List.of(spans.getFirst()); - var unexpectedSpans = List.of(podamFactory.manufacturePojo(Span.class).toBuilder() - .projectId(null) - .build()); - unexpectedSpans.forEach( - expectedSpan -> SpansResourceTest.this.createAndAssert(expectedSpan, apiKey, workspaceName)); + var expectedSpans = getExpectedSpans.apply(spans); + var unexpectedSpans = getUnexpectedSpans.apply(spans); var filters = List.of(SpanFilter.builder() .field(SpanField.NAME) - .operator(Operator.EQUAL) + .operator(operator) .value(spans.getFirst().name().toUpperCase()) .build()); - getAndAssertPage(workspaceName, projectName, filters, spans, expectedSpans, unexpectedSpans, apiKey); + getAndAssertPage(workspaceName, projectName, filters, spans, expectedSpans.reversed(), unexpectedSpans, apiKey); + } + + private Stream equalAndNotEqualFilters() { + return Stream.of( + Arguments.of(Operator.EQUAL, + (Function, List>) spans -> List.of(spans.getFirst()), + (Function, List>) spans -> spans.subList(1, spans.size())), + Arguments.of(Operator.NOT_EQUAL, + (Function, List>) spans -> spans.subList(1, spans.size()), + (Function, List>) spans -> List.of(spans.getFirst()))); } @Test @@ -1286,8 +1334,11 @@ void getByProjectName__whenFilterNameNotContains__thenReturnSpansFiltered() { getAndAssertPage(workspaceName, projectName, filters, spans, expectedSpans, unexpectedSpans, apiKey); } - @Test - void getByProjectName__whenFilterStartTimeEqual__thenReturnSpansFiltered() { + @ParameterizedTest + @MethodSource("equalAndNotEqualFilters") + void getByProjectName__whenFilterStartTimeEqual_NotEqual__thenReturnSpansFiltered(Operator operator, + Function, List> getExpectedSpans, + Function, List> getUnexpectedSpans) { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1304,19 +1355,15 @@ void getByProjectName__whenFilterStartTimeEqual__thenReturnSpansFiltered() { .build()) .collect(Collectors.toCollection(ArrayList::new)); spans.forEach(expectedSpan -> SpansResourceTest.this.createAndAssert(expectedSpan, apiKey, workspaceName)); - var expectedSpans = List.of(spans.getFirst()); - var unexpectedSpans = List.of(podamFactory.manufacturePojo(Span.class).toBuilder() - .projectId(null) - .build()); - unexpectedSpans.forEach( - expectedSpan -> SpansResourceTest.this.createAndAssert(expectedSpan, apiKey, workspaceName)); + var expectedSpans = getExpectedSpans.apply(spans); + var unexpectedSpans = getUnexpectedSpans.apply(spans); var filters = List.of(SpanFilter.builder() .field(SpanField.START_TIME) - .operator(Operator.EQUAL) + .operator(operator) .value(spans.getFirst().startTime().toString()) .build()); - getAndAssertPage(workspaceName, projectName, filters, spans, expectedSpans, unexpectedSpans, apiKey); + getAndAssertPage(workspaceName, projectName, filters, spans, expectedSpans.reversed(), unexpectedSpans, apiKey); } @Test @@ -1566,8 +1613,11 @@ void getByProjectName__whenFilterOutputEqual__thenReturnSpansFiltered() { getAndAssertPage(workspaceName, projectName, filters, spans, expectedSpans, unexpectedSpans, apiKey); } - @Test - void getByProjectName__whenFilterMetadataEqualString__thenReturnSpansFiltered() { + @ParameterizedTest + @MethodSource("equalAndNotEqualFilters") + void getByProjectName__whenFilterMetadataEqualString__thenReturnSpansFiltered(Operator operator, + Function, List> getExpectedSpans, + Function, List> getUnexpectedSpans) { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1590,20 +1640,16 @@ void getByProjectName__whenFilterMetadataEqualString__thenReturnSpansFiltered() "Chat-GPT 4.0\"}]}")) .build()); spans.forEach(expectedSpan -> SpansResourceTest.this.createAndAssert(expectedSpan, apiKey, workspaceName)); - var expectedSpans = List.of(spans.getFirst()); - var unexpectedSpans = List.of(podamFactory.manufacturePojo(Span.class).toBuilder() - .projectId(null) - .build()); - unexpectedSpans.forEach( - expectedSpan -> SpansResourceTest.this.createAndAssert(expectedSpan, apiKey, workspaceName)); + var expectedSpans = getExpectedSpans.apply(spans); + var unexpectedSpans = getUnexpectedSpans.apply(spans); var filters = List.of(SpanFilter.builder() .field(SpanField.METADATA) - .operator(Operator.EQUAL) + .operator(operator) .key("$.model[0].version") .value("OPENAI, CHAT-GPT 4.0") .build()); - getAndAssertPage(workspaceName, projectName, filters, spans, expectedSpans, unexpectedSpans, apiKey); + getAndAssertPage(workspaceName, projectName, filters, spans, expectedSpans.reversed(), unexpectedSpans, apiKey); } @Test @@ -2439,8 +2485,11 @@ void getByProjectName__whenFilterUsageLessThanEqual__thenReturnSpansFiltered(Str getAndAssertPage(workspaceName, projectName, filters, spans, expectedSpans, unexpectedSpans, apiKey); } - @Test - void getByProjectName__whenFilterFeedbackScoresEqual__thenReturnSpansFiltered() { + @ParameterizedTest + @MethodSource + void getByProjectName__whenFilterFeedbackScoresEqual_NotEqual__thenReturnSpansFiltered(Operator operator, + Function, List> getExpectedSpans, + Function, List> getUnexpectedSpans) { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); @@ -2474,31 +2523,33 @@ void getByProjectName__whenFilterFeedbackScoresEqual__thenReturnSpansFiltered() .forEach( feedbackScore -> createAndAssert(span.id(), feedbackScore, workspaceName, apiKey))); - var expectedSpans = List.of(spans.getFirst()); - var unexpectedSpans = List.of(podamFactory.manufacturePojo(Span.class).toBuilder() - .projectId(null) - .build()); - unexpectedSpans.forEach( - expectedSpan -> SpansResourceTest.this.createAndAssert(expectedSpan, apiKey, workspaceName)); - unexpectedSpans.forEach( - span -> span.feedbackScores() - .forEach( - feedbackScore -> createAndAssert(span.id(), feedbackScore, workspaceName, apiKey))); + var expectedSpans = getExpectedSpans.apply(spans); + var unexpectedSpans = getUnexpectedSpans.apply(spans); var filters = List.of( SpanFilter.builder() .field(SpanField.FEEDBACK_SCORES) - .operator(Operator.EQUAL) + .operator(operator) .key(spans.getFirst().feedbackScores().get(1).name().toUpperCase()) .value(spans.getFirst().feedbackScores().get(1).value().toString()) .build(), SpanFilter.builder() .field(SpanField.FEEDBACK_SCORES) - .operator(Operator.EQUAL) + .operator(operator) .key(spans.getFirst().feedbackScores().get(2).name().toUpperCase()) .value(spans.getFirst().feedbackScores().get(2).value().toString()) .build()); - getAndAssertPage(workspaceName, projectName, filters, spans, expectedSpans, unexpectedSpans, apiKey); + getAndAssertPage(workspaceName, projectName, filters, spans, expectedSpans.reversed(), unexpectedSpans, apiKey); + } + + private Stream getByProjectName__whenFilterFeedbackScoresEqual_NotEqual__thenReturnSpansFiltered() { + return Stream.of( + Arguments.of(Operator.EQUAL, + (Function, List>) spans -> List.of(spans.getFirst()), + (Function, List>) spans -> spans.subList(1, spans.size())), + Arguments.of(Operator.NOT_EQUAL, + (Function, List>) spans -> spans.subList(2, spans.size()), + (Function, List>) spans -> spans.subList(0, 2))); } @Test