-
Notifications
You must be signed in to change notification settings - Fork 1.2k
[issue-3494] [FE] [BE] Ability to add tags to multiple threads #3494 #3715
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
base: main
Are you sure you want to change the base?
Changes from all commits
0be9683
793bf3f
882a19b
a33476c
8b7903c
7a1d24e
17f28a3
d9907d2
8803cc2
4cbce3f
84de9c5
921b40e
e40682c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| 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 io.swagger.v3.oas.annotations.media.Schema; | ||
| import jakarta.validation.Valid; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import jakarta.validation.constraints.Size; | ||
| import lombok.Builder; | ||
|
|
||
| import java.util.List; | ||
| import java.util.UUID; | ||
|
|
||
| @Builder(toBuilder = true) | ||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) | ||
| public record TraceThreadBatchUpdate( | ||
| @Schema(description = "List of thread model IDs to update", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "Thread model IDs are required") @Size(min = 1, max = 1000, message = "Thread model IDs must contain between 1 and 1000 items") List<@NotNull UUID> threadModelIds, | ||
|
|
||
| @Schema(description = "Update to apply to all threads", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "Update is required") @Valid TraceThreadUpdate update) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ | |
| import com.comet.opik.api.TraceSearchStreamRequest; | ||
| import com.comet.opik.api.TraceThread; | ||
| import com.comet.opik.api.TraceThread.TraceThreadPage; | ||
| import com.comet.opik.api.TraceThreadBatchUpdate; | ||
| import com.comet.opik.api.TraceThreadIdentifier; | ||
| import com.comet.opik.api.TraceThreadStatus; | ||
| import com.comet.opik.api.TraceThreadUpdate; | ||
|
|
@@ -10923,6 +10924,84 @@ void createAndUpdateThread() { | |
| .build()), List.of(actualThread)); | ||
|
|
||
| } | ||
|
|
||
| @Test | ||
| void batchUpdateThreads() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's create a new separated subclass for these new tests. It's a different endpoint, so we usually follow that convention. |
||
| // Create multiple threads | ||
| var thread1 = createThread(); | ||
| var thread2 = createThread(); | ||
| var thread3 = createThread(); | ||
|
Comment on lines
+10931
to
+10933
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a blocker: I'd find or create a flavour of these createThread methods to do it in batches. Test would be faster and more maintainable. |
||
|
|
||
| // Check that they don't have tags | ||
| assertThat(thread1.tags()).isNull(); | ||
| assertThat(thread2.tags()).isNull(); | ||
| assertThat(thread3.tags()).isNull(); | ||
|
Comment on lines
+10935
to
+10938
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: better keep a list of threads and iterate on all of them to assert this. |
||
|
|
||
| // Batch update threads | ||
| var tags = Set.of("tag1", "tag2", "tag3"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use podam factory to just generate a random TraceThreadUpdate. |
||
| var update = TraceThreadUpdate.builder().tags(tags).build(); | ||
| var batchUpdate = TraceThreadBatchUpdate.builder() | ||
| .threadModelIds(List.of(thread1.threadModelId(), thread2.threadModelId(), thread3.threadModelId())) | ||
| .update(update) | ||
| .build(); | ||
|
|
||
| traceResourceClient.batchUpdateThreads(batchUpdate, API_KEY, TEST_WORKSPACE, 204); | ||
|
|
||
| // Check that batch update was applied to all threads | ||
| var actualThread1 = traceResourceClient.getTraceThread(thread1.id(), thread1.projectId(), API_KEY, | ||
| TEST_WORKSPACE); | ||
| var actualThread2 = traceResourceClient.getTraceThread(thread2.id(), thread2.projectId(), API_KEY, | ||
| TEST_WORKSPACE); | ||
| var actualThread3 = traceResourceClient.getTraceThread(thread3.id(), thread3.projectId(), API_KEY, | ||
| TEST_WORKSPACE); | ||
|
Comment on lines
+10951
to
+10956
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, please search all threads in one call if possible. |
||
|
|
||
| TraceAssertions.assertThreads(List.of(thread1.toBuilder().tags(tags).build()), List.of(actualThread1)); | ||
| TraceAssertions.assertThreads(List.of(thread2.toBuilder().tags(tags).build()), List.of(actualThread2)); | ||
| TraceAssertions.assertThreads(List.of(thread3.toBuilder().tags(tags).build()), List.of(actualThread3)); | ||
|
Comment on lines
+10958
to
+10960
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use one assertion, but passing the list with all threads instead of one by one. |
||
| } | ||
|
|
||
| @Test | ||
| void batchUpdateThreadsWithEmptyListShouldSucceed() { | ||
| var update = TraceThreadUpdate.builder().tags(Set.of("tag1")).build(); | ||
| var batchUpdate = TraceThreadBatchUpdate.builder() | ||
| .threadModelIds(List.of()) | ||
| .update(update) | ||
| .build(); | ||
|
|
||
| traceResourceClient.batchUpdateThreads(batchUpdate, API_KEY, TEST_WORKSPACE, 422); | ||
| } | ||
|
|
||
| @Test | ||
| void batchUpdateThreadsWithExistingTags() { | ||
|
Comment on lines
+10974
to
+10975
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be a parameterised tests of the first one, basically:
|
||
| // Create threads with existing tags | ||
| var thread1 = createThread(); | ||
| var thread2 = createThread(); | ||
|
|
||
| // Add initial tags | ||
| var initialTags = Set.of("existing1", "existing2"); | ||
| var initialUpdate = TraceThreadUpdate.builder().tags(initialTags).build(); | ||
| traceResourceClient.updateThread(initialUpdate, thread1.threadModelId(), API_KEY, TEST_WORKSPACE, 204); | ||
| traceResourceClient.updateThread(initialUpdate, thread2.threadModelId(), API_KEY, TEST_WORKSPACE, 204); | ||
|
|
||
| // Batch update with new tags | ||
| var newTags = Set.of("new1", "new2"); | ||
| var update = TraceThreadUpdate.builder().tags(newTags).build(); | ||
| var batchUpdate = TraceThreadBatchUpdate.builder() | ||
| .threadModelIds(List.of(thread1.threadModelId(), thread2.threadModelId())) | ||
| .update(update) | ||
| .build(); | ||
|
|
||
| traceResourceClient.batchUpdateThreads(batchUpdate, API_KEY, TEST_WORKSPACE, 204); | ||
|
|
||
| // Check that tags were replaced (not appended) | ||
| var actualThread1 = traceResourceClient.getTraceThread(thread1.id(), thread1.projectId(), API_KEY, | ||
| TEST_WORKSPACE); | ||
| var actualThread2 = traceResourceClient.getTraceThread(thread2.id(), thread2.projectId(), API_KEY, | ||
| TEST_WORKSPACE); | ||
|
|
||
| assertThat(actualThread1.tags()).isEqualTo(newTags); | ||
| assertThat(actualThread2.tags()).isEqualTo(newTags); | ||
| } | ||
| } | ||
|
|
||
| private TraceThread createThread() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { useMutation, useQueryClient } from "@tanstack/react-query"; | ||
| import { AxiosError } from "axios"; | ||
| import get from "lodash/get"; | ||
|
|
||
| import api, { TRACES_REST_ENDPOINT } from "@/api/api"; | ||
| import { useToast } from "@/components/ui/use-toast"; | ||
|
|
||
| type UseThreadBatchUpdateMutationParams = { | ||
| threadModelIds: string[]; | ||
| tags: string[]; | ||
| }; | ||
|
|
||
| const useThreadBatchUpdateMutation = () => { | ||
| const queryClient = useQueryClient(); | ||
| const { toast } = useToast(); | ||
|
|
||
| return useMutation({ | ||
| mutationFn: async ({ | ||
| threadModelIds, | ||
| tags, | ||
| }: UseThreadBatchUpdateMutationParams) => { | ||
| const { data } = await api.patch(`${TRACES_REST_ENDPOINT}threads/batch`, { | ||
| thread_model_ids: threadModelIds, | ||
| update: { | ||
| tags, | ||
| }, | ||
| }); | ||
| return data; | ||
| }, | ||
| onError: (error: AxiosError) => { | ||
| const message = get( | ||
| error, | ||
| ["response", "data", "message"], | ||
| error.message, | ||
| ); | ||
|
|
||
| toast({ | ||
| title: "Error", | ||
| description: `Failed to update threads: ${message}`, | ||
| variant: "destructive", | ||
| }); | ||
| }, | ||
| onSuccess: () => { | ||
| toast({ | ||
| title: "Threads updated", | ||
| description: "Tags have been successfully updated", | ||
| }); | ||
|
Comment on lines
+44
to
+47
|
||
|
|
||
| return queryClient.invalidateQueries({ | ||
| queryKey: [TRACES_REST_ENDPOINT, "threads"], | ||
| }); | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| export default useThreadBatchUpdateMutation; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this query and all the related methods in this DAO, the service etc. There's a minimal difference, which is allowing multiple
idsinstead of one.Let's just update the existing query and code to allow that:
id IN :ids.2.1 You can keep the old methods, but overload them to just pass a set with a single UUID.