From 460975239ca4a0adc45da8db2a382e1364cfdda2 Mon Sep 17 00:00:00 2001 From: Anthony Powell Date: Mon, 13 Jan 2025 19:27:36 -0500 Subject: [PATCH] feat(prompts): Add "Clone Prompt" flow to prompts UI (#5993) * feat(prompts): Implement ClonePromptMutation * feat(prompts): Add Clone prompt flow to prompts UI * chore(prompts): Fix storybook build * Add prompt clone tests * Revalidate clone dialog on change * Fix invocation parameter list display * Reduce number of db writes during prompt clone * Improve prompt clone test readability * Fix prompt clone error handling * Tweak error utils to ignore single quotes * Properly display error messages for prompt save and clone mutations * Fix prompt clone error message display * Fix test * Fix test --- app/.storybook/main.ts | 15 ++ app/schema.graphql | 7 + .../UpsertPromptFromTemplateDialog.tsx | 9 +- app/src/pages/prompt/ClonePromptDialog.tsx | 188 ++++++++++++++++++ .../prompt/PromptInvocationParameters.tsx | 7 +- app/src/pages/prompt/PromptLayout.tsx | 48 ++++- .../ClonePromptDialogMutation.graphql.ts | 94 +++++++++ .../PromptLayout__main.graphql.ts | 41 +++- .../promptLoaderQuery.graphql.ts | 6 +- app/src/utils/__tests__/errorUtils.test.ts | 4 +- app/src/utils/errorUtils.ts | 4 +- .../server/api/mutations/prompt_mutations.py | 62 +++++- .../api/mutations/test_prompt_mutations.py | 183 +++++++++++++++++ 13 files changed, 634 insertions(+), 34 deletions(-) create mode 100644 app/src/pages/prompt/ClonePromptDialog.tsx create mode 100644 app/src/pages/prompt/__generated__/ClonePromptDialogMutation.graphql.ts diff --git a/app/.storybook/main.ts b/app/.storybook/main.ts index c84ff85b54..939023b206 100644 --- a/app/.storybook/main.ts +++ b/app/.storybook/main.ts @@ -1,4 +1,5 @@ import type { StorybookConfig } from "@storybook/react-vite"; +import { mergeConfig } from "vite"; import { resolve } from "path"; const config: StorybookConfig = { @@ -16,5 +17,19 @@ const config: StorybookConfig = { typescript: { reactDocgen: "react-docgen-typescript", }, + async viteFinal(config, { configType }) { + // return the customized config + return mergeConfig(config, { + // customize the Vite config here + optimizeDeps: { + include: ["@storybook/addon-interactions"], + }, + resolve: { + alias: { + "@phoenix": resolve(__dirname, "../src"), + }, + }, + }); + }, }; export default config; diff --git a/app/schema.graphql b/app/schema.graphql index d10b656c49..5e7f09782c 100644 --- a/app/schema.graphql +++ b/app/schema.graphql @@ -215,6 +215,12 @@ input ClearProjectInput { endTime: DateTime } +input ClonePromptInput { + name: String! + description: String = null + promptId: GlobalID! +} + type Cluster { """The ID of the cluster""" id: ID! @@ -1207,6 +1213,7 @@ type Mutation { createChatPrompt(input: CreateChatPromptInput!): Prompt! createChatPromptVersion(input: CreateChatPromptVersionInput!): Prompt! deletePrompt(input: DeletePromptInput!): Query! + clonePrompt(input: ClonePromptInput!): Prompt! deletePromptVersionTag(input: DeletePromptVersionTagInput!): PromptVersionTagMutationPayload! setPromptVersionTag(input: SetPromptVersionTagInput!): PromptVersionTagMutationPayload! createSpanAnnotations(input: [CreateSpanAnnotationInput!]!): SpanAnnotationMutationPayload! diff --git a/app/src/pages/playground/UpsertPromptFromTemplateDialog.tsx b/app/src/pages/playground/UpsertPromptFromTemplateDialog.tsx index 2394dbba88..1bd8860fb5 100644 --- a/app/src/pages/playground/UpsertPromptFromTemplateDialog.tsx +++ b/app/src/pages/playground/UpsertPromptFromTemplateDialog.tsx @@ -14,6 +14,7 @@ import { SavePromptForm, SavePromptSubmitHandler, } from "@phoenix/pages/playground/SavePromptForm"; +import { getErrorMessagesFromRelayMutationError } from "@phoenix/utils/errorUtils"; type UpsertPromptFromTemplateProps = { instanceId: number; @@ -109,10 +110,10 @@ export const UpsertPromptFromTemplateDialog = ({ setDialog(null); }, onError: (error) => { - // eslint-disable-next-line no-console - console.error(error); + const message = getErrorMessagesFromRelayMutationError(error); notifyError({ title: "Error creating prompt", + message: message?.[0], }); setDialog(null); }, @@ -161,10 +162,10 @@ export const UpsertPromptFromTemplateDialog = ({ setDialog(null); }, onError: (error) => { - // eslint-disable-next-line no-console - console.error(error); + const message = getErrorMessagesFromRelayMutationError(error); notifyError({ title: "Error updating prompt", + message: message?.[0], }); setDialog(null); }, diff --git a/app/src/pages/prompt/ClonePromptDialog.tsx b/app/src/pages/prompt/ClonePromptDialog.tsx new file mode 100644 index 0000000000..0188305ac5 --- /dev/null +++ b/app/src/pages/prompt/ClonePromptDialog.tsx @@ -0,0 +1,188 @@ +import React, { Suspense } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { graphql, useMutation } from "react-relay"; +import { useNavigate } from "react-router"; + +import { Dialog, TextArea } from "@arizeai/components"; + +import { + Button, + FieldError, + Flex, + Input, + Label, + Loading, + Text, + TextField, + View, +} from "@phoenix/components"; +import { useNotifySuccess } from "@phoenix/contexts/NotificationContext"; +import { ClonePromptDialogMutation } from "@phoenix/pages/prompt/__generated__/ClonePromptDialogMutation.graphql"; +import { getErrorMessagesFromRelayMutationError } from "@phoenix/utils/errorUtils"; + +export const ClonePromptDialog = ({ + promptId, + promptName, + promptDescription, + setDialog, +}: { + promptId: string; + promptName: string; + promptDescription?: string; + setDialog: (dialog: React.ReactNode | null) => void; +}) => { + const notifySuccess = useNotifySuccess(); + const navigate = useNavigate(); + const [clonePrompt, isClonePending] = useMutation( + graphql` + mutation ClonePromptDialogMutation($input: ClonePromptInput!) { + clonePrompt(input: $input) { + id + } + } + ` + ); + const form = useForm({ + disabled: isClonePending, + reValidateMode: "onBlur", + mode: "onChange", + defaultValues: { + name: `${promptName} (Clone)`, + description: promptDescription, + }, + }); + const { + control, + handleSubmit, + formState: { isValid, errors }, + setError, + } = form; + const onSubmit = handleSubmit((data) => { + clonePrompt({ + variables: { + input: { + promptId, + name: data.name, + description: data.description, + }, + }, + onCompleted: (data) => { + setDialog(null); + notifySuccess({ + title: "Prompt cloned successfully", + action: { + text: "View Prompt", + onClick: () => { + navigate(`/prompts/${data.clonePrompt.id}`); + }, + }, + }); + }, + onError: (error) => { + const message = getErrorMessagesFromRelayMutationError(error); + if (message?.[0]?.includes("already exists")) { + setError("name", { + message: message?.[0], + }); + } else { + setError("root", { + message: message?.[0], + }); + } + }, + }); + }); + return ( + + }> + +
+ { + if (value.trim() === promptName.trim()) { + return "Name must be different from the original prompt name"; + } + return true; + }, + }, + }} + render={({ + field: { onChange, onBlur, value, disabled }, + fieldState: { error }, + }) => ( + + + + {!error && ( + + A name for the cloned prompt. + + )} + {error?.message} + + )} + /> + ( + + +