diff --git a/app/src/@types/generative.d.ts b/app/src/@types/generative.d.ts index 08309ffeb6..326b6a4160 100644 --- a/app/src/@types/generative.d.ts +++ b/app/src/@types/generative.d.ts @@ -1 +1,6 @@ declare type ModelProvider = "OPENAI" | "AZURE_OPENAI" | "ANTHROPIC"; + +/** + * The role of a chat message + */ +declare type ChatMessageRole = "user" | "system" | "ai" | "tool"; diff --git a/app/src/App.tsx b/app/src/App.tsx index 6f462abba4..2c98a09883 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -4,6 +4,7 @@ import { ThemeProvider as EmotionThemeProvider } from "@emotion/react"; import { Provider, theme } from "@arizeai/components"; +import { CredentialsProvider } from "./contexts/CredentialsContext"; import { FeatureFlagsProvider } from "./contexts/FeatureFlagsContext"; import { FunctionalityProvider } from "./contexts/FunctionalityContext"; import { PreferencesProvider } from "./contexts/PreferencesContext"; @@ -33,11 +34,13 @@ export function AppContent() { - - - - - + + + + + + + diff --git a/app/src/contexts/CredentialsContext.tsx b/app/src/contexts/CredentialsContext.tsx new file mode 100644 index 0000000000..ad26b4ede1 --- /dev/null +++ b/app/src/contexts/CredentialsContext.tsx @@ -0,0 +1,35 @@ +import React, { createContext, PropsWithChildren, useState } from "react"; +import { useZustand } from "use-zustand"; + +import { + createCredentialsStore, + CredentialsProps, + CredentialsState, + CredentialsStore, +} from "@phoenix/store"; + +export const CredentialsContext = createContext(null); + +export function CredentialsProvider({ + children, + ...props +}: PropsWithChildren>) { + const [store] = useState(() => + createCredentialsStore(props) + ); + return ( + + {children} + + ); +} + +export function useCredentialsContext( + selector: (state: CredentialsState) => T, + equalityFn?: (left: T, right: T) => boolean +): T { + const store = React.useContext(CredentialsContext); + if (!store) + throw new Error("Missing CredentialsContext.Provider in the tree"); + return useZustand(store, selector, equalityFn); +} diff --git a/app/src/contexts/PlaygroundContext.tsx b/app/src/contexts/PlaygroundContext.tsx index e6a353f9c2..e3c115e5d9 100644 --- a/app/src/contexts/PlaygroundContext.tsx +++ b/app/src/contexts/PlaygroundContext.tsx @@ -6,7 +6,7 @@ import { PlaygroundProps, PlaygroundState, PlaygroundStore, -} from "@phoenix/store/playgroundStore"; +} from "@phoenix/store"; export const PlaygroundContext = createContext(null); diff --git a/app/src/pages/playground/MessageRolePicker.tsx b/app/src/pages/playground/MessageRolePicker.tsx index d10e46fbc2..6d0c26131b 100644 --- a/app/src/pages/playground/MessageRolePicker.tsx +++ b/app/src/pages/playground/MessageRolePicker.tsx @@ -3,8 +3,6 @@ import { css } from "@emotion/react"; import { Item, Picker } from "@arizeai/components"; -import { ChatMessageRole } from "@phoenix/store"; - import { isChatMessageRole } from "./playgroundUtils"; const hiddenLabelCSS = css` diff --git a/app/src/pages/playground/Playground.tsx b/app/src/pages/playground/Playground.tsx index 14adc0962e..f25d6548e7 100644 --- a/app/src/pages/playground/Playground.tsx +++ b/app/src/pages/playground/Playground.tsx @@ -11,6 +11,7 @@ import { import { InitialPlaygroundState } from "@phoenix/store"; import { NUM_MAX_PLAYGROUND_INSTANCES } from "./constants"; +import { PlaygroundCredentialsDropdown } from "./PlaygroundCredentialsDropdown"; import { PlaygroundInputTypeTypeRadioGroup } from "./PlaygroundInputModeRadioGroup"; import { PlaygroundInstance } from "./PlaygroundInstance"; import { PlaygroundRunButton } from "./PlaygroundRunButton"; @@ -33,9 +34,7 @@ export function Playground(props: InitialPlaygroundState) { Playground - + diff --git a/app/src/pages/playground/PlaygroundChatTemplate.tsx b/app/src/pages/playground/PlaygroundChatTemplate.tsx index 0c1caf850a..39755e7e94 100644 --- a/app/src/pages/playground/PlaygroundChatTemplate.tsx +++ b/app/src/pages/playground/PlaygroundChatTemplate.tsx @@ -26,7 +26,6 @@ import { usePlaygroundContext } from "@phoenix/contexts/PlaygroundContext"; import { useChatMessageStyles } from "@phoenix/hooks/useChatMessageStyles"; import { ChatMessage, - ChatMessageRole, generateMessageId, PlaygroundChatTemplate as PlaygroundChatTemplateType, } from "@phoenix/store"; @@ -140,7 +139,7 @@ export function PlaygroundChatTemplate(props: PlaygroundChatTemplateProps) { ...template.messages, { id: generateMessageId(), - role: ChatMessageRole.user, + role: "user", content: "", }, ], diff --git a/app/src/pages/playground/PlaygroundCredentialsDropdown.tsx b/app/src/pages/playground/PlaygroundCredentialsDropdown.tsx new file mode 100644 index 0000000000..4023bed5b0 --- /dev/null +++ b/app/src/pages/playground/PlaygroundCredentialsDropdown.tsx @@ -0,0 +1,69 @@ +import React from "react"; + +import { + DropdownButton, + DropdownMenu, + DropdownTrigger, + Flex, + Form, + Heading, + Text, + TextField, + View, +} from "@arizeai/components"; + +import { useCredentialsContext } from "@phoenix/contexts/CredentialsContext"; +import { usePlaygroundContext } from "@phoenix/contexts/PlaygroundContext"; +import { CredentialKey } from "@phoenix/store"; + +export const ProviderToCredentialKeyMap: Record = + { + OPENAI: "OPENAI_API_KEY", + ANTHROPIC: "ANTHROPIC_API_KEY", + AZURE_OPENAI: "AZURE_OPENAI_API_KEY", + }; + +export function PlaygroundCredentialsDropdown() { + const currentProviders = usePlaygroundContext((state) => + Array.from( + new Set(state.instances.map((instance) => instance.model.provider)) + ) + ); + const setCredential = useCredentialsContext((state) => state.setCredential); + const credentials = useCredentialsContext((state) => state); + return ( + + API Keys + + + + + API Keys + + + API keys are stored in your browser and used to communicate with + their respective API's. + +
+ {currentProviders.map((provider) => { + const credentialKey = ProviderToCredentialKeyMap[provider]; + return ( + { + setCredential({ credential: credentialKey, value }); + }} + value={credentials[credentialKey]} + /> + ); + })} + +
+
+
+
+ ); +} diff --git a/app/src/pages/playground/PlaygroundOutput.tsx b/app/src/pages/playground/PlaygroundOutput.tsx index 2ac91203d7..e5c41c42c8 100644 --- a/app/src/pages/playground/PlaygroundOutput.tsx +++ b/app/src/pages/playground/PlaygroundOutput.tsx @@ -6,11 +6,7 @@ import { Card, Flex, Icon, Icons } from "@arizeai/components"; import { usePlaygroundContext } from "@phoenix/contexts/PlaygroundContext"; import { useChatMessageStyles } from "@phoenix/hooks/useChatMessageStyles"; -import { - ChatMessage, - ChatMessageRole, - generateMessageId, -} from "@phoenix/store"; +import { ChatMessage, generateMessageId } from "@phoenix/store"; import { assertUnreachable } from "@phoenix/typeUtils"; import { @@ -70,7 +66,7 @@ export function PlaygroundOutput(props: PlaygroundOutputProps) { message={{ id: generateMessageId(), content: instance.output, - role: ChatMessageRole.ai, + role: "ai", }} /> ); @@ -144,13 +140,13 @@ function toGqlChatCompletionRole( role: ChatMessageRole ): ChatCompletionMessageRole { switch (role) { - case ChatMessageRole.system: + case "system": return "SYSTEM"; - case ChatMessageRole.user: + case "user": return "USER"; - case ChatMessageRole.tool: + case "tool": return "TOOL"; - case ChatMessageRole.ai: + case "ai": return "AI"; default: assertUnreachable(role); @@ -204,7 +200,7 @@ function PlaygroundOutputText(props: PlaygroundInstanceProps) { message={{ id: generateMessageId(), content: output, - role: ChatMessageRole.ai, + role: "ai", }} /> ); diff --git a/app/src/pages/playground/__tests__/playgroundUtils.test.ts b/app/src/pages/playground/__tests__/playgroundUtils.test.ts index 7ec0b816b4..cd012c62f8 100644 --- a/app/src/pages/playground/__tests__/playgroundUtils.test.ts +++ b/app/src/pages/playground/__tests__/playgroundUtils.test.ts @@ -1,7 +1,6 @@ import { _resetInstanceId, _resetMessageId, - ChatMessageRole, PlaygroundInstance, } from "@phoenix/store"; @@ -36,13 +35,11 @@ const expectedPlaygroundInstanceWithIO: PlaygroundInstance = { // These id's are not 0, 1, 2, because we create a playground instance (including messages) at the top of the transformSpanAttributesToPlaygroundInstance function // Doing so increments the message id counter messages: [ - { id: 2, content: "You are a chatbot", role: ChatMessageRole.system }, - { id: 3, content: "hello?", role: ChatMessageRole.user }, + { id: 2, content: "You are a chatbot", role: "system" }, + { id: 3, content: "hello?", role: "user" }, ], }, - output: [ - { id: 4, content: "This is an AI Answer", role: ChatMessageRole.ai }, - ], + output: [{ id: 4, content: "This is an AI Answer", role: "ai" }], }; const defaultTemplate = { @@ -50,12 +47,12 @@ const defaultTemplate = { messages: [ { id: 0, - role: ChatMessageRole.system, + role: "system", content: "You are a chatbot", }, { id: 1, - role: ChatMessageRole.user, + role: "user", content: "{{question}}", }, ], diff --git a/app/src/pages/playground/constants.tsx b/app/src/pages/playground/constants.tsx index 8f34607c82..624e0e1ca5 100644 --- a/app/src/pages/playground/constants.tsx +++ b/app/src/pages/playground/constants.tsx @@ -1,8 +1,6 @@ -import { ChatMessageRole } from "@phoenix/store"; - export const NUM_MAX_PLAYGROUND_INSTANCES = 4; -export const DEFAULT_CHAT_ROLE = ChatMessageRole.user; +export const DEFAULT_CHAT_ROLE = "user"; /** * Map of {@link ChatMessageRole} to potential role values. diff --git a/app/src/pages/playground/playgroundUtils.ts b/app/src/pages/playground/playgroundUtils.ts index ffe15aa51c..66d8290f53 100644 --- a/app/src/pages/playground/playgroundUtils.ts +++ b/app/src/pages/playground/playgroundUtils.ts @@ -1,7 +1,6 @@ import { PlaygroundInstance } from "@phoenix/store"; import { ChatMessage, - ChatMessageRole, createPlaygroundInstance, generateMessageId, } from "@phoenix/store"; diff --git a/app/src/pages/playground/schemas.ts b/app/src/pages/playground/schemas.ts index f572e20bf0..11bdde6c35 100644 --- a/app/src/pages/playground/schemas.ts +++ b/app/src/pages/playground/schemas.ts @@ -6,7 +6,7 @@ import { SemanticAttributePrefixes, } from "@arizeai/openinference-semantic-conventions"; -import { ChatMessage, ChatMessageRole } from "@phoenix/store"; +import { ChatMessage } from "@phoenix/store"; import { schemaForType } from "@phoenix/typeUtils"; /** @@ -76,7 +76,9 @@ export const outputSchema = z.object({ /** * The zod schema for {@link chatMessageRoles} */ -export const chatMessageRolesSchema = z.nativeEnum(ChatMessageRole); +export const chatMessageRolesSchema = schemaForType()( + z.enum(["user", "ai", "system", "tool"]) +); const chatMessageSchema = schemaForType()( z.object({ diff --git a/app/src/store/credentialsStore.tsx b/app/src/store/credentialsStore.tsx new file mode 100644 index 0000000000..ab854a6c5c --- /dev/null +++ b/app/src/store/credentialsStore.tsx @@ -0,0 +1,49 @@ +import { create, StateCreator } from "zustand"; +import { devtools, persist } from "zustand/middleware"; + +export interface CredentialsProps { + /** + * The API key for the OpenAI API. + */ + OPENAI_API_KEY?: string; + /** + * The API key for the Azure OpenAI API. + */ + AZURE_OPENAI_API_KEY?: string; + /** + * The API key for the Anthropic API. + */ + ANTHROPIC_API_KEY?: string; +} + +export type CredentialKey = keyof CredentialsProps; + +export interface CredentialsState extends CredentialsProps { + /** + * Setter for a given credential + * @param credential the name of the credential to set + * @param value the value of the credential + */ + setCredential: (params: { + credential: keyof CredentialsProps; + value: string; + }) => void; +} + +export const createCredentialsStore = ( + initialProps: Partial +) => { + const credentialsStore: StateCreator = (set) => ({ + setCredential: ({ credential, value }) => { + set({ [credential]: value }); + }, + ...initialProps, + }); + return create()( + persist(devtools(credentialsStore), { + name: "arize-phoenix-credentials", + }) + ); +}; + +export type CredentialsStore = ReturnType; diff --git a/app/src/store/index.tsx b/app/src/store/index.tsx index 98fbe30497..13b6d059a0 100644 --- a/app/src/store/index.tsx +++ b/app/src/store/index.tsx @@ -1,3 +1,4 @@ export * from "./pointCloudStore"; export * from "./tracingStore"; -export * from "./playgroundStore"; +export * from "./playground"; +export * from "./credentialsStore"; diff --git a/app/src/store/playground/index.ts b/app/src/store/playground/index.ts new file mode 100644 index 0000000000..a26b3b3411 --- /dev/null +++ b/app/src/store/playground/index.ts @@ -0,0 +1,2 @@ +export * from "./playgroundStore"; +export * from "./types"; diff --git a/app/src/store/playgroundStore.tsx b/app/src/store/playground/playgroundStore.tsx similarity index 53% rename from app/src/store/playgroundStore.tsx rename to app/src/store/playground/playgroundStore.tsx index b6fb0d25fc..fe3a1292c8 100644 --- a/app/src/store/playgroundStore.tsx +++ b/app/src/store/playground/playgroundStore.tsx @@ -1,7 +1,15 @@ import { create, StateCreator } from "zustand"; import { devtools } from "zustand/middleware"; -export type GenAIOperationType = "chat" | "text_completion"; +import { + GenAIOperationType, + InitialPlaygroundState, + PlaygroundChatTemplate, + PlaygroundInputMode, + PlaygroundInstance, + PlaygroundState, + PlaygroundTextCompletionTemplate, +} from "./types"; let playgroundInstanceIdIndex = 0; let playgroundRunIdIndex = 0; @@ -36,184 +44,17 @@ export const _resetMessageId = () => { playgroundMessageIdIndex = 0; }; -/** - * The input mode for the playground - * @example "manual" or "dataset" - */ -export type PlaygroundInputMode = "manual" | "dataset"; - -/** - * A playground template can be a chat completion or text completion (legacy) - */ -export type PlaygroundTemplate = - | PlaygroundChatTemplate - | PlaygroundTextCompletionTemplate; - -/** - * Array of roles for a chat message with a LLM - */ -export enum ChatMessageRole { - system = "system", - user = "user", - tool = "tool", - ai = "ai", -} - -/** - * A chat message with a role and content - * @example { role: "user", content: "What is the meaning of life?" } - */ -export type ChatMessage = { - id: number; - role: ChatMessageRole; - content: string; -}; - -/** - * A template for a chat completion playground - * Takes a list of messages for multi-turn - * @see https://platform.openai.com/docs/guides/chat-completions - */ -export type PlaygroundChatTemplate = { - __type: "chat"; - messages: ChatMessage[]; -}; - -export type PlaygroundTextCompletionTemplate = { - __type: "text_completion"; - prompt: string; -}; - -export interface PlaygroundProps { - /** - * How the LLM API should be invoked. Distinguishes between chat and text_completion. - * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/ - * @default "chat" - */ - operationType: GenAIOperationType; - /** - * The input mode for the playground(s) - * NB: the input mode for all instances is synchronized - * @default "manual" - */ - inputMode: PlaygroundInputMode; - /** - * The current playground instances(s) - * Defaults to a single instance until a second instance is added - */ - instances: Array; -} - -export type InitialPlaygroundState = Partial; - -type DatasetInput = { - datasetId: string; -}; - -type ManualInput = { - variables: Record; -}; - -type PlaygroundInput = DatasetInput | ManualInput; - -type ModelConfig = { - provider: ModelProvider; - modelName: string | null; -}; -/** - * A single instance of the playground that has - * - a template - * - tools - * - input (dataset or manual) - * - output (experiment or spans) - */ -export interface PlaygroundInstance { - /** - * An ID to uniquely identify the instance - */ - id: number; - template: PlaygroundTemplate; - tools: unknown; - input: PlaygroundInput; - model: ModelConfig; - output: ChatMessage[] | undefined | string; - activeRunId: number | null; - /** - * Whether or not the playground instance is actively running or not - **/ - isRunning: boolean; -} - -/** - * All actions for a playground instance must contain the index of the playground - */ -interface PlaygroundInstanceActionParams { - playgroundInstanceId: number; -} -interface AddMessageParams extends PlaygroundInstanceActionParams {} - -export interface PlaygroundState extends PlaygroundProps { - /** - * Setter for the invocation mode - * @param operationType - */ - setOperationType: (operationType: GenAIOperationType) => void; - /** - * Setter for the input mode. - */ - setInputMode: (inputMode: PlaygroundInputMode) => void; - /** - * Add a comparison instance to the playground - */ - addInstance: () => void; - /** - * Delete a specific instance of the playground - * @param instanceId the instance to delete - */ - deleteInstance: (instanceId: number) => void; - /** - * Add a message to a playground instance - */ - addMessage: (params: AddMessageParams) => void; - /** - * Update an instance of the playground - */ - updateInstance: (params: { - instanceId: number; - patch: Partial; - }) => void; - /** - * Update an instance's model configuration - */ - updateModel: (params: { - instanceId: number; - model: Partial; - }) => void; - /** - * Run all the active playground Instances - */ - runPlaygroundInstances: () => void; - /** - * Run a specific playground instance - */ - runPlaygroundInstance: (instanceId: number) => void; - /** - * Mark a given playground instance as completed - */ - markPlaygroundInstanceComplete: (instanceId: number) => void; -} - const generateChatCompletionTemplate = (): PlaygroundChatTemplate => ({ __type: "chat", messages: [ { id: generateMessageId(), - role: ChatMessageRole.system, + role: "system", content: "You are a chatbot", }, { id: generateMessageId(), - role: ChatMessageRole.user, + role: "user", content: "{{question}}", }, ], @@ -247,35 +88,18 @@ export const createPlaygroundStore = ( instances: [createPlaygroundInstance()], setOperationType: (operationType: GenAIOperationType) => { if (operationType === "chat") { - // TODO: this is incorrect, it should only change the template set({ - instances: [ - { - id: generateInstanceId(), - model: { provider: "OPENAI", modelName: "gpt-4o" }, - template: generateChatCompletionTemplate(), - tools: {}, - input: { variables: {} }, - output: undefined, - activeRunId: null, - isRunning: false, - }, - ], + instances: get().instances.map((instance) => ({ + ...instance, + template: generateChatCompletionTemplate(), + })), }); } else { set({ - instances: [ - { - id: generateInstanceId(), - model: { provider: "OPENAI", modelName: "gpt-4o" }, - template: DEFAULT_TEXT_COMPLETION_TEMPLATE, - tools: {}, - input: { variables: {} }, - output: undefined, - activeRunId: null, - isRunning: false, - }, - ], + instances: get().instances.map((instance) => ({ + ...instance, + template: DEFAULT_TEXT_COMPLETION_TEMPLATE, + })), }); } set({ operationType }); diff --git a/app/src/store/playground/types.ts b/app/src/store/playground/types.ts new file mode 100644 index 0000000000..043007f485 --- /dev/null +++ b/app/src/store/playground/types.ts @@ -0,0 +1,163 @@ +export type GenAIOperationType = "chat" | "text_completion"; +/** + * The input mode for the playground + * @example "manual" or "dataset" + */ +export type PlaygroundInputMode = "manual" | "dataset"; + +/** + * A chat message with a role and content + * @example { role: "user", content: "What is the meaning of life?" } + */ +export type ChatMessage = { + id: number; + role: ChatMessageRole; + content: string; +}; + +/** + * A template for a chat completion playground + * Takes a list of messages for multi-turn + * @see https://platform.openai.com/docs/guides/chat-completions + */ +export type PlaygroundChatTemplate = { + __type: "chat"; + messages: ChatMessage[]; +}; + +/** + * A template for a text completion playground + * A single prompt for text completion + */ +export type PlaygroundTextCompletionTemplate = { + __type: "text_completion"; + prompt: string; +}; + +/** + * A playground template can be a chat completion or text completion (legacy) + */ +export type PlaygroundTemplate = + | PlaygroundChatTemplate + | PlaygroundTextCompletionTemplate; + +type DatasetInput = { + datasetId: string; +}; + +type ManualInput = { + variables: Record; +}; + +type PlaygroundInput = DatasetInput | ManualInput; + +type ModelConfig = { + provider: ModelProvider; + modelName: string | null; +}; + +/** + * A single instance of the playground that has + * - a template + * - tools + * - input (dataset or manual) + * - output (experiment or spans) + */ +export interface PlaygroundInstance { + /** + * An ID to uniquely identify the instance + */ + id: number; + template: PlaygroundTemplate; + tools: unknown; + input: PlaygroundInput; + model: ModelConfig; + output: ChatMessage[] | undefined | string; + activeRunId: number | null; + /** + * Whether or not the playground instance is actively running or not + **/ + isRunning: boolean; +} + +/** + * All actions for a playground instance must contain the id of the instance + */ +interface PlaygroundInstanceActionParams { + playgroundInstanceId: number; +} + +export interface AddMessageParams extends PlaygroundInstanceActionParams {} + +export interface PlaygroundProps { + /** + * How the LLM API should be invoked. Distinguishes between chat and text_completion. + * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/ + * @default "chat" + */ + operationType: GenAIOperationType; + /** + * The input mode for the playground(s) + * NB: the input mode for all instances is synchronized + * @default "manual" + */ + inputMode: PlaygroundInputMode; + /** + * The current playground instances(s) + * Defaults to a single instance until a second instance is added + */ + instances: Array; +} + +export type InitialPlaygroundState = Partial; + +export interface PlaygroundState extends PlaygroundProps { + /** + * Setter for the invocation mode + * @param operationType + */ + setOperationType: (operationType: GenAIOperationType) => void; + /** + * Setter for the input mode. + */ + setInputMode: (inputMode: PlaygroundInputMode) => void; + /** + * Add a comparison instance to the playground + */ + addInstance: () => void; + /** + * Delete a specific instance of the playground + * @param instanceId the instance to delete + */ + deleteInstance: (instanceId: number) => void; + /** + * Add a message to a playground instance + */ + addMessage: (params: AddMessageParams) => void; + /** + * Update an instance of the playground + */ + updateInstance: (params: { + instanceId: number; + patch: Partial; + }) => void; + /** + * Update an instance's model configuration + */ + updateModel: (params: { + instanceId: number; + model: Partial; + }) => void; + /** + * Run all the active playground Instances + */ + runPlaygroundInstances: () => void; + /** + * Run a specific playground instance + */ + runPlaygroundInstance: (instanceId: number) => void; + /** + * Mark a given playground instance as completed + */ + markPlaygroundInstanceComplete: (instanceId: number) => void; +}