Skip to content

Commit d08e40c

Browse files
committed
[issue-2232] [FE/BE] Add prompt version restore functionality
Implements the ability to restore previous prompt versions by creating new versions with content from selected versions. Backend changes: - Add restorePromptVersion method to PromptService - Add POST /prompts/{promptId}/versions/{versionId}/restore endpoint - Add comprehensive integration test for restore functionality Frontend changes: - Add useRestorePromptVersionMutation API hook - Add restore button with Undo-2 icon to CommitHistory component - Add confirmation modal for restore action - Add tooltip with 'Restore this version' message Implements issue-2232: Add revert to previous version feature for prompts
1 parent 17c0246 commit d08e40c

File tree

6 files changed

+344
-43
lines changed

6 files changed

+344
-43
lines changed

apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/PromptResource.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,4 +285,28 @@ public Response retrievePromptVersion(
285285
return Response.ok(promptVersion).build();
286286
}
287287

288+
@POST
289+
@Path("/{promptId}/versions/{versionId}/restore")
290+
@Operation(operationId = "restorePromptVersion", summary = "Restore prompt version", description = "Restore a prompt version by creating a new version with the content from the specified version", responses = {
291+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = PromptVersion.class))),
292+
@ApiResponse(responseCode = "404", description = "Not Found", content = @Content(schema = @Schema(implementation = io.dropwizard.jersey.errors.ErrorMessage.class))),
293+
@ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(schema = @Schema(implementation = ErrorMessage.class))),
294+
})
295+
@RateLimited
296+
@JsonView({PromptVersion.View.Detail.class})
297+
public Response restorePromptVersion(@PathParam("promptId") UUID promptId, @PathParam("versionId") UUID versionId) {
298+
299+
String workspaceId = requestContext.get().getWorkspaceId();
300+
301+
log.info("Restoring prompt version with id '{}' for prompt id '{}' on workspace_id '{}'",
302+
versionId, promptId, workspaceId);
303+
304+
PromptVersion restoredVersion = promptService.restorePromptVersion(promptId, versionId);
305+
306+
log.info("Successfully restored prompt version with id '{}' for prompt id '{}' on workspace_id '{}'",
307+
versionId, promptId, workspaceId);
308+
309+
return Response.ok(restoredVersion).build();
310+
}
311+
288312
}

apps/opik-backend/src/main/java/com/comet/opik/domain/PromptService.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ public interface PromptService {
6464

6565
PromptVersion retrievePromptVersion(String name, String commit);
6666

67+
PromptVersion restorePromptVersion(UUID promptId, UUID versionId);
68+
6769
Mono<Map<UUID, String>> getVersionsCommitByVersionsIds(Set<UUID> versionsIds);
6870
}
6971

@@ -519,6 +521,51 @@ public PromptVersion retrievePromptVersion(@NonNull String name, String commit)
519521
});
520522
}
521523

524+
@Override
525+
public PromptVersion restorePromptVersion(@NonNull UUID promptId, @NonNull UUID versionId) {
526+
String workspaceId = requestContext.get().getWorkspaceId();
527+
String userName = requestContext.get().getUserName();
528+
529+
log.info("Restoring prompt version with id '{}' for prompt id '{}' on workspace_id '{}'",
530+
versionId, promptId, workspaceId);
531+
532+
// Get the version to restore
533+
PromptVersion versionToRestore = getVersionById(versionId);
534+
535+
// Verify the version belongs to the specified prompt
536+
if (!versionToRestore.promptId().equals(promptId)) {
537+
throw new NotFoundException("Prompt version not found for the specified prompt");
538+
}
539+
540+
// Get the prompt to get its name
541+
Prompt prompt = getById(promptId);
542+
543+
// Create a new version with the content from the old version
544+
UUID newVersionId = idGenerator.generateId();
545+
String newCommit = CommitUtils.getCommit(newVersionId);
546+
547+
PromptVersion newVersion = versionToRestore.toBuilder()
548+
.id(newVersionId)
549+
.commit(newCommit)
550+
.createdBy(userName)
551+
.changeDescription("Restored from version " + versionToRestore.commit())
552+
.build();
553+
554+
PromptVersion restoredVersion = EntityConstraintHandler
555+
.handle(() -> savePromptVersion(workspaceId, newVersion))
556+
.onErrorDo(() -> retryableCreateVersion(workspaceId,
557+
CreatePromptVersion.builder()
558+
.name(prompt.name())
559+
.version(newVersion)
560+
.build(),
561+
prompt, userName));
562+
563+
log.info("Successfully restored prompt version with id '{}' for prompt id '{}' on workspace_id '{}'",
564+
versionId, promptId, workspaceId);
565+
566+
return restoredVersion;
567+
}
568+
522569
@Override
523570
public Mono<Map<UUID, String>> getVersionsCommitByVersionsIds(@NonNull Set<UUID> versionsIds) {
524571

apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/PromptResourceTest.java

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,93 @@ void retrievePromptVersionsByNameAndCommit__whenApiKeyIsPresent__thenReturnPrope
519519
}
520520
}
521521
}
522+
523+
@ParameterizedTest
524+
@MethodSource("credentials")
525+
@DisplayName("Restore prompt version: when api key is present, then return proper response")
526+
void restorePromptVersion__whenApiKeyIsPresent__thenReturnProperResponse(String apiKey, boolean success,
527+
io.dropwizard.jersey.errors.ErrorMessage errorMessage) {
528+
String workspaceName = UUID.randomUUID().toString();
529+
530+
mockTargetWorkspace(okApikey, workspaceName, WORKSPACE_ID);
531+
532+
var prompt = factory.manufacturePojo(Prompt.class).toBuilder()
533+
.id(null)
534+
.createdAt(null)
535+
.lastUpdatedAt(null)
536+
.createdBy(null)
537+
.lastUpdatedBy(null)
538+
.latestVersion(null)
539+
.build();
540+
541+
UUID promptId = createPrompt(prompt, okApikey, workspaceName);
542+
543+
// Create first version to restore from
544+
var promptVersion1 = factory.manufacturePojo(PromptVersion.class).toBuilder()
545+
.id(null)
546+
.promptId(null)
547+
.commit(null)
548+
.createdAt(null)
549+
.createdBy(null)
550+
.variables(null)
551+
.template("Original template content")
552+
.changeDescription("First version")
553+
.build();
554+
555+
CreatePromptVersion request1 = new CreatePromptVersion(prompt.name(), promptVersion1);
556+
promptVersion1 = createPromptVersion(request1, okApikey, workspaceName);
557+
558+
// Create second version
559+
var promptVersion2 = factory.manufacturePojo(PromptVersion.class).toBuilder()
560+
.id(null)
561+
.promptId(null)
562+
.commit(null)
563+
.createdAt(null)
564+
.createdBy(null)
565+
.variables(null)
566+
.template("Modified template content")
567+
.changeDescription("Second version")
568+
.build();
569+
570+
CreatePromptVersion request2 = new CreatePromptVersion(prompt.name(), promptVersion2);
571+
promptVersion2 = createPromptVersion(request2, okApikey, workspaceName);
572+
573+
// Now restore the first version
574+
try (var actualResponse = client.target(RESOURCE_PATH.formatted(baseURI) + "/%s/versions/%s/restore"
575+
.formatted(promptId, promptVersion1.id()))
576+
.request()
577+
.header(HttpHeaders.AUTHORIZATION, apiKey)
578+
.header(RequestContext.WORKSPACE_HEADER, workspaceName)
579+
.post(Entity.json(""))) {
580+
581+
if (success) {
582+
assertThat(actualResponse.getStatus()).isEqualTo(HttpStatus.SC_OK);
583+
584+
var restoredVersion = actualResponse.readEntity(PromptVersion.class);
585+
586+
// Verify the restored version has the same content as the original
587+
assertThat(restoredVersion).isNotNull();
588+
assertThat(restoredVersion.template()).isEqualTo(promptVersion1.template());
589+
assertThat(restoredVersion.metadata()).isEqualTo(promptVersion1.metadata());
590+
assertThat(restoredVersion.type()).isEqualTo(promptVersion1.type());
591+
assertThat(restoredVersion.promptId()).isEqualTo(promptId);
592+
assertThat(restoredVersion.createdBy()).isEqualTo(USER);
593+
assertThat(restoredVersion.changeDescription()).contains("Restored from version");
594+
595+
// Verify it's a new version (different ID and commit)
596+
assertThat(restoredVersion.id()).isNotEqualTo(promptVersion1.id());
597+
assertThat(restoredVersion.id()).isNotEqualTo(promptVersion2.id());
598+
assertThat(restoredVersion.commit()).isNotEqualTo(promptVersion1.commit());
599+
assertThat(restoredVersion.commit()).isNotEqualTo(promptVersion2.commit());
600+
} else {
601+
assertThat(actualResponse.getStatus()).isEqualTo(errorMessage.getCode());
602+
603+
var actualErrorMessage = actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class);
604+
605+
assertThat(actualErrorMessage).isEqualTo(errorMessage);
606+
}
607+
}
608+
}
522609
}
523610

524611
@Nested
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { useMutation, useQueryClient } from "@tanstack/react-query";
2+
import { AxiosError } from "axios";
3+
import get from "lodash/get";
4+
5+
import api, { PROMPTS_REST_ENDPOINT } from "@/api/api";
6+
import { useToast } from "@/components/ui/use-toast";
7+
import { PromptVersion } from "@/types/prompts";
8+
9+
type UseRestorePromptVersionMutationParams = {
10+
promptId: string;
11+
versionId: string;
12+
onSuccess: (promptVersion: PromptVersion) => void;
13+
};
14+
15+
const useRestorePromptVersionMutation = () => {
16+
const queryClient = useQueryClient();
17+
const { toast } = useToast();
18+
19+
return useMutation({
20+
mutationFn: async ({
21+
promptId,
22+
versionId,
23+
}: UseRestorePromptVersionMutationParams) => {
24+
const { data } = await api.post(
25+
`${PROMPTS_REST_ENDPOINT}${promptId}/versions/${versionId}/restore`,
26+
);
27+
28+
return data;
29+
},
30+
onError: (error: AxiosError) => {
31+
const message = get(
32+
error,
33+
["response", "data", "message"],
34+
error.message,
35+
);
36+
37+
toast({
38+
title: "Error",
39+
description: message,
40+
variant: "destructive",
41+
});
42+
},
43+
onSuccess: async (data: PromptVersion, { onSuccess }) => {
44+
toast({
45+
title: "Version restored",
46+
description: "The prompt version has been successfully restored.",
47+
});
48+
49+
onSuccess(data);
50+
},
51+
onSettled: () => {
52+
queryClient.invalidateQueries({ queryKey: ["prompt-versions"] });
53+
queryClient.invalidateQueries({ queryKey: ["prompt"] });
54+
},
55+
});
56+
};
57+
58+
export default useRestorePromptVersionMutation;

0 commit comments

Comments
 (0)