diff --git a/app/package.json b/app/package.json index 0f360f9993..e83b519daf 100644 --- a/app/package.json +++ b/app/package.json @@ -57,7 +57,7 @@ "three": "~0.139.2", "three-stdlib": "^2.30.4", "use-deep-compare-effect": "^1.8.1", - "use-zustand": "^0.0.4", + "use-zustand": "^0.2.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.23.3", "zustand": "^4.5.4" diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 73c661e141..b35da0a4fc 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -165,8 +165,8 @@ importers: specifier: ^1.8.1 version: 1.8.1(react@18.3.1) use-zustand: - specifier: ^0.0.4 - version: 0.0.4(react@18.3.1) + specifier: ^0.2.0 + version: 0.2.0(react@18.3.1) zod: specifier: ^3.23.8 version: 3.23.8 @@ -4492,10 +4492,10 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 - use-zustand@0.0.4: - resolution: {integrity: sha512-UkuONsZoniFoDsm5Xp/gEA1579UOn/ZgKbaE48gHhK+pmBx//PX4UlKdrbaWPpUnORnVTd4/0CvI/rv+1tmn4g==} + use-zustand@0.2.0: + resolution: {integrity: sha512-6BPw170RLVGe57ifpuKOS1j7VbufYX3xHByNaqmLzQYPnhWjzF+lEfmESnaVLCWyOzH6kVDIeVF3kEPsFd9pvw==} peerDependencies: - react: '*' + react: '>=18.0.0' util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} @@ -10232,7 +10232,7 @@ snapshots: dependencies: react: 18.3.1 - use-zustand@0.0.4(react@18.3.1): + use-zustand@0.2.0(react@18.3.1): dependencies: react: 18.3.1 diff --git a/app/src/components/templateEditor/TemplateEditor.tsx b/app/src/components/templateEditor/TemplateEditor.tsx index 4bb1111ad4..f506e49334 100644 --- a/app/src/components/templateEditor/TemplateEditor.tsx +++ b/app/src/components/templateEditor/TemplateEditor.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { githubLight } from "@uiw/codemirror-theme-github"; import { nord } from "@uiw/codemirror-theme-nord"; import CodeMirror, { @@ -16,8 +16,9 @@ import { MustacheLikeTemplating } from "./language/mustacheLike"; import { TemplateLanguages } from "./constants"; import { TemplateLanguage } from "./types"; -type TemplateEditorProps = ReactCodeMirrorProps & { +type TemplateEditorProps = Omit & { templateLanguage: TemplateLanguage; + defaultValue: string; }; const basicSetupOptions: BasicSetupOptions = { @@ -28,10 +29,22 @@ const basicSetupOptions: BasicSetupOptions = { bracketMatching: false, }; +/** + * A template editor that is used to edit the template of a tool. + * + * This is an uncontrolled editor. + * You can only reset the value of the editor by triggering a re-mount, like with the `key` prop, + * or, when the readOnly prop is true, the editor will reset on all value changes. + * This is necessary because controlled react-codemirror editors incessantly reset + * cursor position when value is updated. + */ export const TemplateEditor = ({ templateLanguage, + defaultValue, + readOnly, ...props }: TemplateEditorProps) => { + const [value, setValue] = useState(() => defaultValue); const { theme } = useTheme(); const codeMirrorTheme = theme === "light" ? githubLight : nord; const extensions = useMemo(() => { @@ -51,12 +64,20 @@ export const TemplateEditor = ({ return ext; }, [templateLanguage]); + useEffect(() => { + if (readOnly) { + setValue(defaultValue); + } + }, [readOnly, defaultValue]); + return ( ); }; diff --git a/app/src/pages/playground/ChatMessageToolCallsEditor.tsx b/app/src/pages/playground/ChatMessageToolCallsEditor.tsx index 237c6995ef..bb524b54ca 100644 --- a/app/src/pages/playground/ChatMessageToolCallsEditor.tsx +++ b/app/src/pages/playground/ChatMessageToolCallsEditor.tsx @@ -8,32 +8,40 @@ import { llmProviderToolCallsSchema, openAIToolCallsJSONSchema, } from "@phoenix/schemas/toolCallSchemas"; -import { ChatMessage } from "@phoenix/store"; +import { + selectPlaygroundInstance, + selectPlaygroundInstanceMessage, +} from "@phoenix/store/playground/selectors"; import { safelyParseJSON } from "@phoenix/utils/jsonUtils"; -import { PlaygroundInstanceProps } from "./types"; - /** * Editor for message tool calls */ export function ChatMessageToolCallsEditor({ playgroundInstanceId, - toolCalls, - templateMessages, messageId, -}: PlaygroundInstanceProps & { - toolCalls: ChatMessage["toolCalls"]; - templateMessages: ChatMessage[]; +}: { + playgroundInstanceId: number; messageId: number; }) { - const updateInstance = usePlaygroundContext((state) => state.updateInstance); - const instance = usePlaygroundContext((state) => - state.instances.find((instance) => instance.id === playgroundInstanceId) + const instanceSelector = useMemo( + () => selectPlaygroundInstance(playgroundInstanceId), + [playgroundInstanceId] ); - + const instance = usePlaygroundContext(instanceSelector); if (instance == null) { - throw new Error(`Playground instance ${playgroundInstanceId} not found`); + throw new Error(`Instance ${playgroundInstanceId} not found`); + } + const messageSelector = useMemo( + () => selectPlaygroundInstanceMessage(messageId), + [messageId] + ); + const message = usePlaygroundContext(messageSelector); + if (message == null) { + throw new Error(`Message ${messageId} not found`); } + const toolCalls = message.toolCalls; + const updateMessage = usePlaygroundContext((state) => state.updateMessage); const [editorValue, setEditorValue] = useState(() => JSON.stringify(toolCalls, null, 2) ); @@ -64,25 +72,15 @@ export function ChatMessageToolCallsEditor({ return; } setLastValidToolCalls(toolCalls); - updateInstance({ + updateMessage({ instanceId: playgroundInstanceId, + messageId, patch: { - template: { - __type: "chat", - messages: templateMessages.map((m) => - messageId === m.id - ? { - ...m, - toolCalls, - } - : m - ), - }, + toolCalls, }, - dirty: true, }); }, - [messageId, playgroundInstanceId, templateMessages, updateInstance] + [playgroundInstanceId, messageId, updateMessage] ); const toolCallsJSONSchema = useMemo((): JSONSchema7 | null => { diff --git a/app/src/pages/playground/ModelConfigButton.tsx b/app/src/pages/playground/ModelConfigButton.tsx index 9d0a6ca01b..465bc2e85a 100644 --- a/app/src/pages/playground/ModelConfigButton.tsx +++ b/app/src/pages/playground/ModelConfigButton.tsx @@ -31,7 +31,10 @@ import { import { useNotifySuccess } from "@phoenix/contexts"; import { usePlaygroundContext } from "@phoenix/contexts/PlaygroundContext"; import { usePreferencesContext } from "@phoenix/contexts/PreferencesContext"; -import { PlaygroundInstance } from "@phoenix/store"; +import { + PlaygroundInstance, + PlaygroundNormalizedInstance, +} from "@phoenix/store"; import { ModelConfigButtonDialogQuery } from "./__generated__/ModelConfigButtonDialogQuery.graphql"; import { InvocationParametersFormFields } from "./InvocationParametersFormFields"; @@ -67,7 +70,7 @@ const modelConfigFormCSS = css` function AzureOpenAiModelConfigFormField({ instance, }: { - instance: PlaygroundInstance; + instance: PlaygroundNormalizedInstance; }) { const updateModel = usePlaygroundContext((state) => state.updateModel); const updateModelConfig = useCallback( diff --git a/app/src/pages/playground/Playground.tsx b/app/src/pages/playground/Playground.tsx index 8491de15b9..a9488e22f9 100644 --- a/app/src/pages/playground/Playground.tsx +++ b/app/src/pages/playground/Playground.tsx @@ -1,4 +1,4 @@ -import React, { Suspense, useCallback, useEffect } from "react"; +import React, { Fragment, Suspense, useCallback, useEffect } from "react"; import { graphql, useLazyLoadQuery } from "react-relay"; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; import { BlockerFunction, useBlocker, useSearchParams } from "react-router-dom"; @@ -205,6 +205,9 @@ const playgroundInputOutputPanelContentCSS = css` */ const PLAYGROUND_PROMPT_PANEL_MIN_WIDTH = 632; +const DEFAULT_EXPANDED_PROMPTS = ["prompts"]; +const DEFAULT_EXPANDED_PARAMS = ["input", "output"]; + function PlaygroundContent() { const instances = usePlaygroundContext((state) => state.instances); const templateLanguage = usePlaygroundContext( @@ -246,7 +249,7 @@ function PlaygroundContent() { }, [isRunning, anyDirtyInstances]); return ( - <> +
- + ( @@ -301,7 +303,7 @@ function PlaygroundContent() { ) : (
- + {templateLanguage !== TemplateLanguages.NONE ? ( @@ -321,10 +323,9 @@ function PlaygroundContent() { - {instances.map((instance, i) => ( - + {instances.map((instance) => ( + @@ -348,6 +349,6 @@ function PlaygroundContent() { } /> )} - + ); } diff --git a/app/src/pages/playground/PlaygroundChatTemplate.tsx b/app/src/pages/playground/PlaygroundChatTemplate.tsx index c16b168372..b71e32a575 100644 --- a/app/src/pages/playground/PlaygroundChatTemplate.tsx +++ b/app/src/pages/playground/PlaygroundChatTemplate.tsx @@ -1,4 +1,9 @@ -import React, { PropsWithChildren, useCallback, useState } from "react"; +import React, { + PropsWithChildren, + useCallback, + useMemo, + useState, +} from "react"; import { DndContext, KeyboardSensor, @@ -35,12 +40,12 @@ import { import { TemplateLanguage } from "@phoenix/components/templateEditor/types"; import { usePlaygroundContext } from "@phoenix/contexts/PlaygroundContext"; import { useChatMessageStyles } from "@phoenix/hooks/useChatMessageStyles"; -import { - ChatMessage, - PlaygroundChatTemplate as PlaygroundChatTemplateType, - PlaygroundInstance, -} from "@phoenix/store"; +import { ChatMessage, PlaygroundState } from "@phoenix/store"; import { convertMessageToolCallsToProvider } from "@phoenix/store/playground/playgroundStoreUtils"; +import { + selectPlaygroundInstance, + selectPlaygroundInstanceMessage, +} from "@phoenix/store/playground/selectors"; import { assertUnreachable } from "@phoenix/typeUtils"; import { safelyParseJSON } from "@phoenix/utils/jsonUtils"; @@ -81,10 +86,9 @@ export function PlaygroundChatTemplate(props: PlaygroundChatTemplateProps) { const templateLanguage = usePlaygroundContext( (state) => state.templateLanguage ); - const instances = usePlaygroundContext((state) => state.instances); const updateInstance = usePlaygroundContext((state) => state.updateInstance); - - const playgroundInstance = instances.find((instance) => instance.id === id); + const instanceSelector = useMemo(() => selectPlaygroundInstance(id), [id]); + const playgroundInstance = usePlaygroundContext(instanceSelector); if (!playgroundInstance) { throw new Error(`Playground instance ${id} not found`); } @@ -102,6 +106,8 @@ export function PlaygroundChatTemplate(props: PlaygroundChatTemplateProps) { throw new Error(`Invalid template type ${template.__type}`); } + const messageIds = template.messageIds; + const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { @@ -116,30 +122,26 @@ export function PlaygroundChatTemplate(props: PlaygroundChatTemplateProps) { if (!over || active.id === over.id) { return; } - const activeIndex = template.messages.findIndex( - (message) => message.id === active.id - ); - const overIndex = template.messages.findIndex( - (message) => message.id === over.id + const activeIndex = messageIds.findIndex( + (messageId) => messageId === active.id ); - const newMessages = arrayMove( - template.messages, - activeIndex, - overIndex + const overIndex = messageIds.findIndex( + (messageId) => messageId === over.id ); + const newMessageIds = arrayMove(messageIds, activeIndex, overIndex); updateInstance({ instanceId: id, patch: { template: { __type: "chat", - messages: newMessages, + messageIds: newMessageIds, }, }, dirty: true, }); }} > - +
    - {template.messages.map((message, index) => { + {messageIds.map((messageId) => { return ( ); })} @@ -192,16 +191,20 @@ function MessageEditor({ updateMessage, templateLanguage, playgroundInstanceId, - template, messageMode, }: { playgroundInstanceId: number; message: ChatMessage; - template: PlaygroundChatTemplateType; templateLanguage: TemplateLanguage; updateMessage: (patch: Partial) => void; messageMode: MessageMode; }) { + const onChange = useCallback( + (val: string) => { + updateMessage({ content: val }); + }, + [updateMessage] + ); if (messageMode === "toolCalls") { return ( @@ -267,18 +268,15 @@ function MessageEditor({ ); } + return ( updateMessage({ content: val })} + onChange={onChange} placeholder={ message.role === "system" ? "You are a helpful assistant" @@ -294,19 +292,14 @@ function MessageEditor({ function SortableMessageItem({ playgroundInstanceId, templateLanguage, - template, - message, - instance, -}: PropsWithChildren< - PlaygroundInstanceProps & { - template: PlaygroundChatTemplateType; - message: ChatMessage; - templateLanguage: TemplateLanguage; - index: number; - instance: PlaygroundInstance; - } ->) { - const updateInstance = usePlaygroundContext((state) => state.updateInstance); + messageId, +}: PropsWithChildren<{ + playgroundInstanceId: number; + messageId: number; + templateLanguage: TemplateLanguage; +}>) { + const updateMessage = usePlaygroundContext((state) => state.updateMessage); + const deleteMessage = usePlaygroundContext((state) => state.deleteMessage); const { attributes, listeners, @@ -316,9 +309,25 @@ function SortableMessageItem({ setActivatorNodeRef, isDragging, } = useSortable({ - id: message.id, + id: messageId, }); - + const instanceModelSelector = useMemo( + () => (state: PlaygroundState) => + state.instances.find((instance) => instance.id === playgroundInstanceId) + ?.model, + [playgroundInstanceId] + ); + const instanceModel = usePlaygroundContext(instanceModelSelector); + if (!instanceModel) { + throw new Error( + `Instance model not found for instance ${playgroundInstanceId}` + ); + } + const messageSelector = useMemo( + () => selectPlaygroundInstanceMessage(messageId), + [messageId] + ); + const message = usePlaygroundContext(messageSelector); const messageCardStyles = useChatMessageStyles(message.role); const dragAndDropLiStyles = { transform: CSS.Translate.toString(transform), @@ -332,24 +341,6 @@ function SortableMessageItem({ hasTools ? "toolCalls" : "text" ); - const updateMessage = useCallback( - (patch: Partial) => { - updateInstance({ - instanceId: playgroundInstanceId, - patch: { - template: { - __type: "chat", - messages: template.messages.map((msg) => - msg.id === message.id ? { ...msg, ...patch } : msg - ), - }, - }, - dirty: true, - }); - }, - [message.id, playgroundInstanceId, template.messages, updateInstance] - ); - // Preserves the content of the message before switching message modes // Enables the user to switch back to text mode and restore the previous content const [previousMessageContent, setPreviousMessageContent] = useState< @@ -361,6 +352,17 @@ function SortableMessageItem({ ChatMessage["toolCalls"] >(message.toolCalls); + const onMessageUpdate = useCallback( + (patch: Partial) => { + updateMessage({ + instanceId: playgroundInstanceId, + messageId, + patch, + }); + }, + [playgroundInstanceId, messageId, updateMessage] + ); + return (
  • - msg.id === message.id - ? { ...msg, role, toolCalls } - : msg - ), - }, + role, + toolCalls, }, - dirty: true, }); }} /> @@ -417,25 +413,33 @@ function SortableMessageItem({ case "text": setPreviousMessageToolCalls(message.toolCalls); updateMessage({ - content: previousMessageContent, - toolCalls: undefined, + instanceId: playgroundInstanceId, + messageId, + patch: { + content: previousMessageContent, + toolCalls: undefined, + }, }); break; case "toolCalls": setPreviousMessageContent(message.content); updateMessage({ - content: "", - toolCalls: - previousMessageToolCalls != null - ? convertMessageToolCallsToProvider({ - toolCalls: previousMessageToolCalls, - provider: instance.model.provider, - }) - : [ - createToolCallForProvider( - instance.model.provider - ), - ], + instanceId: playgroundInstanceId, + messageId, + patch: { + content: "", + toolCalls: + previousMessageToolCalls != null + ? convertMessageToolCallsToProvider({ + toolCalls: previousMessageToolCalls, + provider: instanceModel.provider, + }) + : [ + createToolCallForProvider( + instanceModel.provider + ), + ], + }, }); break; default: @@ -457,17 +461,9 @@ function SortableMessageItem({ icon={} />} size="S" onPress={() => { - updateInstance({ + deleteMessage({ instanceId: playgroundInstanceId, - patch: { - template: { - __type: "chat", - messages: template.messages.filter( - (msg) => msg.id !== message.id - ), - }, - }, - dirty: true, + messageId, }); }} /> @@ -484,9 +480,8 @@ function SortableMessageItem({ message={message} messageMode={aiMessageMode} playgroundInstanceId={playgroundInstanceId} - template={template} templateLanguage={templateLanguage} - updateMessage={updateMessage} + updateMessage={onMessageUpdate} />
diff --git a/app/src/pages/playground/PlaygroundChatTemplateFooter.tsx b/app/src/pages/playground/PlaygroundChatTemplateFooter.tsx index 589b5eacf3..140554094a 100644 --- a/app/src/pages/playground/PlaygroundChatTemplateFooter.tsx +++ b/app/src/pages/playground/PlaygroundChatTemplateFooter.tsx @@ -32,6 +32,7 @@ export function PlaygroundChatTemplateFooter({ }: PlaygroundChatTemplateFooterProps) { const instances = usePlaygroundContext((state) => state.instances); const updateInstance = usePlaygroundContext((state) => state.updateInstance); + const addMessage = usePlaygroundContext((state) => state.addMessage); const upsertInvocationParameterInput = usePlaygroundContext( (state) => state.upsertInvocationParameterInput ); @@ -129,22 +130,15 @@ export function PlaygroundChatTemplateFooter({ size="S" icon={} />} onPress={() => { - updateInstance({ - instanceId, - patch: { - template: { - __type: "chat", - messages: [ - ...template.messages, - { - id: generateMessageId(), - role: "user", - content: "", - }, - ], + addMessage({ + playgroundInstanceId: instanceId, + messages: [ + { + id: generateMessageId(), + role: "user", + content: "", }, - }, - dirty: true, + ], }); }} > diff --git a/app/src/pages/playground/PlaygroundDatasetExamplesTable.tsx b/app/src/pages/playground/PlaygroundDatasetExamplesTable.tsx index 5657b42896..107fc3bd83 100644 --- a/app/src/pages/playground/PlaygroundDatasetExamplesTable.tsx +++ b/app/src/pages/playground/PlaygroundDatasetExamplesTable.tsx @@ -79,6 +79,7 @@ import { PlaygroundToolCall, } from "./PlaygroundToolCall"; import { + denormalizePlaygroundInstance, extractVariablesFromInstance, getChatCompletionOverDatasetInput, } from "./playgroundUtils"; @@ -417,6 +418,9 @@ export function PlaygroundDatasetExamplesTable({ }) { const environment = useRelayEnvironment(); const instances = usePlaygroundContext((state) => state.instances); + const allInstanceMessages = usePlaygroundContext( + (state) => state.allInstanceMessages + ); const templateLanguage = usePlaygroundContext( (state) => state.templateLanguage ); @@ -756,8 +760,12 @@ export function PlaygroundDatasetExamplesTable({ const playgroundInstanceOutputColumns = useMemo((): ColumnDef[] => { return instances.map((instance, index) => { - const instanceVariables = extractVariablesFromInstance({ + const enrichedInstance = denormalizePlaygroundInstance( instance, + allInstanceMessages + ); + const instanceVariables = extractVariablesFromInstance({ + instance: enrichedInstance, templateLanguage, }); return { @@ -784,7 +792,7 @@ export function PlaygroundDatasetExamplesTable({ size: 500, }; }); - }, [hasSomeRunIds, instances, templateLanguage]); + }, [hasSomeRunIds, instances, templateLanguage, allInstanceMessages]); const columns: ColumnDef[] = [ { diff --git a/app/src/pages/playground/PlaygroundInput.tsx b/app/src/pages/playground/PlaygroundInput.tsx index 167a42abbb..cbb68db9a6 100644 --- a/app/src/pages/playground/PlaygroundInput.tsx +++ b/app/src/pages/playground/PlaygroundInput.tsx @@ -56,7 +56,7 @@ export function PlaygroundInput() { // change rapidly for a given variable key={i} label={variableKey} - value={variablesMap[variableKey]} + defaultValue={variablesMap[variableKey] ?? ""} onChange={(value) => setVariableValue(variableKey, value)} /> ); diff --git a/app/src/pages/playground/PlaygroundOutputMoveButton.tsx b/app/src/pages/playground/PlaygroundOutputMoveButton.tsx index 160b0fcb64..1bee24947f 100644 --- a/app/src/pages/playground/PlaygroundOutputMoveButton.tsx +++ b/app/src/pages/playground/PlaygroundOutputMoveButton.tsx @@ -7,7 +7,7 @@ import { usePlaygroundContext } from "@phoenix/contexts/PlaygroundContext"; import { ChatMessage, generateMessageId, - PlaygroundInstance, + PlaygroundNormalizedInstance, } from "@phoenix/store"; import { convertMessageToolCallsToProvider } from "@phoenix/store/playground/playgroundStoreUtils"; import { safelyParseJSON } from "@phoenix/utils/jsonUtils"; @@ -21,13 +21,13 @@ export const PlaygroundOutputMoveButton = ({ toolCalls, cleanupOutput, }: { - instance: PlaygroundInstance; + instance: PlaygroundNormalizedInstance; outputContent?: string | ChatMessage[]; toolCalls: PartialOutputToolCall[]; cleanupOutput: () => void; }) => { const instanceId = instance.id; - const updateInstance = usePlaygroundContext((state) => state.updateInstance); + const addMessage = usePlaygroundContext((state) => state.addMessage); return (