Skip to main content
Glama

@arizeai/phoenix-mcp

Official
by Arize-ai
InvocationParametersFormFields.tsx11.8 kB
import { useCallback, useEffect, useMemo } from "react"; import { Control, Controller, FieldErrors, useForm } from "react-hook-form"; import { debounce } from "lodash"; import { FieldError, Input, Label, NumberField, Slider, SliderNumberField, Switch, Text, TextField, } from "@phoenix/components"; import { usePlaygroundContext } from "@phoenix/contexts/PlaygroundContext"; import { AnthropicReasoningConfigField } from "@phoenix/pages/playground/AnthropicReasoningConfigField"; import { Mutable } from "@phoenix/typeUtils"; import { ModelSupportedParamsFetcherQuery$data } from "./__generated__/ModelSupportedParamsFetcherQuery.graphql"; import { InvocationParameterInput } from "./__generated__/PlaygroundOutputSubscription.graphql"; import { paramsToIgnoreInInvocationParametersForm } from "./constants"; import { InvocationParameterJsonEditor } from "./InvocationParameterJsonEditor"; import { areInvocationParamsEqual, toCamelCase } from "./playgroundUtils"; export type InvocationParameter = Mutable< ModelSupportedParamsFetcherQuery$data["modelInvocationParameters"] >[number]; export type HandleInvocationParameterChange = ( parameter: InvocationParameter, value: string | number | string[] | boolean | undefined ) => void; /** * Form field for a single invocation parameter. */ const InvocationParameterFormField = ({ field, value, onChange, errors, control, }: { field: InvocationParameter; value: unknown; onChange: (value: unknown) => void; errors: FieldErrors<Record<string, unknown>>; control: Control<Record<string, unknown>>; }) => { const invocationName = field.invocationName; if (!invocationName) { throw new Error("Invocation name is required"); } const errorMessage = errors[invocationName]?.message; const requiredRuleMessage = field.required ? `${field.label || invocationName} is required` : undefined; const minRuleMessage = field.minValue != null ? `${field.label || invocationName} must be at least ${field.minValue}` : undefined; const maxRuleMessage = field.maxValue != null ? `${field.label || invocationName} must be at most ${field.maxValue}` : undefined; // special case for anthropic reasoning config if (field.canonicalName === "ANTHROPIC_EXTENDED_THINKING") { return <AnthropicReasoningConfigField onChange={onChange} value={value} />; } const { __typename } = field; switch (__typename) { case "InvocationParameterBase": return null; case "BoundedFloatInvocationParameter": { const isNumber = typeof value === "number"; const defaultValue = isNumber ? value : undefined; return ( <Slider label={field.label} defaultValue={defaultValue} step={0.1} minValue={field.minValue} maxValue={field.maxValue} onChange={(value) => { // NB: the type inference here is wrong. In the case // that the defaultValue is undefined, an array is returned here if (Array.isArray(value) && value.length > 0) { return onChange(value[0]); } onChange(value); }} > <SliderNumberField defaultValue={defaultValue} /> </Slider> ); } case "FloatInvocationParameter": case "IntInvocationParameter": return ( <Controller control={control} name={invocationName} rules={{ required: requiredRuleMessage, min: minRuleMessage, max: maxRuleMessage, }} render={({ field: { onBlur } }) => ( <NumberField isRequired={field.required} value={Number(value)} onBlur={onBlur} onChange={(value) => { onChange(value); }} > <Label>{field.label}</Label> <Input /> {errorMessage ? <FieldError>{errorMessage}</FieldError> : null} </NumberField> )} /> ); case "StringListInvocationParameter": if (!Array.isArray(value) && value !== undefined) return null; return ( <Controller control={control} name={invocationName} rules={{ required: requiredRuleMessage, }} render={({ field: { onBlur } }) => ( <TextField isRequired={field.required} defaultValue={value?.join(", ") ?? ""} onBlur={onBlur} onChange={(value) => { if (value === "") { onChange(undefined); return; } onChange(value.split(/, */g)); }} > <Label>{field.label}</Label> <Input /> {errorMessage ? ( <FieldError>{errorMessage}</FieldError> ) : ( <Text slot="description"> A comma separated list of strings </Text> )} </TextField> )} /> ); case "StringInvocationParameter": return ( <Controller control={control} name={invocationName} rules={{ required: requiredRuleMessage, }} render={({ field: { onBlur } }) => ( <TextField isRequired={field.required} defaultValue={value?.toString() || ""} type="text" onBlur={onBlur} onChange={(value) => { if (value === "") { onChange(undefined); return; } onChange(value); }} > <Label>{field.label}</Label> <Input /> {errorMessage ? <FieldError>{errorMessage}</FieldError> : null} </TextField> )} /> ); case "BooleanInvocationParameter": return ( <Switch onChange={onChange} defaultSelected={Boolean(value)}> {field.label} </Switch> ); case "JSONInvocationParameter": { return ( <Controller control={control} name={invocationName} rules={{ required: requiredRuleMessage, }} render={() => ( <InvocationParameterJsonEditor defaultValue={value} onChange={onChange} label={field.label ?? field.invocationName ?? ""} errorMessage={errorMessage} /> )} /> ); } default: return null; } }; const getInvocationParameterValue = ( field: InvocationParameter, parameterInput: InvocationParameterInput ): | string | number | readonly string[] | boolean | null | Record<string, unknown> | unknown[] | undefined => { if (field.invocationInputField === undefined) { throw new Error("Invocation input field is required"); } const maybeValue = parameterInput[ toCamelCase(field.invocationInputField) as keyof InvocationParameterInput ]; return maybeValue; }; const makeInvocationParameterInput = ( field: InvocationParameter, value: unknown ): InvocationParameterInput => { if (field.invocationName === undefined) { throw new Error("Invocation name is required"); } if (field.invocationInputField === undefined) { throw new Error("Invocation input field is required"); } return { invocationName: field.invocationName, canonicalName: field.canonicalName, [toCamelCase(field.invocationInputField)]: value, }; }; type InvocationParametersFormProps = { instanceId: number; }; export const InvocationParametersFormFields = ({ instanceId, }: InvocationParametersFormProps) => { const instance = usePlaygroundContext((state) => state.instances.find((i) => i.id === instanceId) ); if (!instance) { throw new Error("Instance not found"); } const { model } = instance; const upsertInvocationParameterInput = usePlaygroundContext( (state) => state.upsertInvocationParameterInput ); const deleteInvocationParameterInput = usePlaygroundContext( (state) => state.deleteInvocationParameterInput ); const supportedInvocationParameterDefinitions = instance.model.supportedInvocationParameters; const instanceInvocationParameters = instance.model.invocationParameters; // Handle changes to the form state, either deleting or upserting an invocation parameter const onChange = useCallback( (field: InvocationParameter, value: unknown) => { if (!field.invocationName) { throw new Error("Invocation name is required"); } if (value === undefined) { deleteInvocationParameterInput({ instanceId, invocationParameterInputInvocationName: field.invocationName, }); } else { upsertInvocationParameterInput({ instanceId, invocationParameterInput: makeInvocationParameterInput(field, value), }); } }, [instanceId, upsertInvocationParameterInput, deleteInvocationParameterInput] ); // Reduce our invocation parameters array into a form state object const values = useMemo(() => { return supportedInvocationParameterDefinitions .filter( (field) => // remove parameters that we want to ignore in the form !( field.canonicalName != null && paramsToIgnoreInInvocationParametersForm.includes( field.canonicalName ) ) ) .reduce( (acc, field) => { const existingParameter = instanceInvocationParameters.find((p) => areInvocationParamsEqual(p, field) ); const value = existingParameter ? getInvocationParameterValue(field, existingParameter) : undefined; return { ...acc, [field.invocationName!]: value ?? null, }; }, {} as Record<string, unknown> ); }, [instanceInvocationParameters, supportedInvocationParameterDefinitions]); // Mirror the form state in react-hook-form so that we can use the validation and error state const form = useForm({ values, mode: "onBlur", delayError: 0, shouldFocusError: false, resetOptions: { keepErrors: true, }, }); const trigger = form.trigger; const debouncedTrigger = useMemo(() => debounce(trigger, 250), [trigger]); useEffect(() => { // revalidate the form when the values change // debounce to trigger validation only after the user has stopped typing debouncedTrigger(); }, [values, debouncedTrigger]); // Don't bother rendering the form if the model name is not set // Except for Azure OpenAI, where the model name does not influence the invocation parameters if (model.provider !== "AZURE_OPENAI" && model.modelName === null) { return null; } const fieldsForSchema = Object.entries(values).map( ([invocationName, value]) => { const field = supportedInvocationParameterDefinitions.find( (p) => p.invocationName === invocationName ); if (!field) { return null; } // Remount the field when the provider changes so that we don't hang on to stale values const key = `${model.provider ?? "model"}-${field.invocationName}`; return ( <InvocationParameterFormField key={key} field={field} value={value} onChange={(value) => onChange(field, value)} control={form.control} errors={form.formState.errors} /> ); } ); return fieldsForSchema; };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Arize-ai/phoenix'

If you have feedback or need assistance with the MCP directory API, please join our Discord server