Skip to main content
Glama

@arizeai/phoenix-mcp

Official
by Arize-ai
SpanDetails.tsx63.1 kB
import { PropsWithChildren, ReactNode, Suspense, useMemo, useRef } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { graphql, useLazyLoadQuery } from "react-relay"; import { type ImperativePanelHandle, Panel, PanelGroup, PanelResizeHandle, } from "react-resizable-panels"; import { useNavigate } from "react-router"; import { json } from "@codemirror/lang-json"; import { githubDark, githubLight } from "@uiw/codemirror-theme-github"; import CodeMirror, { BasicSetupOptions, EditorView, } from "@uiw/react-codemirror"; import { css } from "@emotion/react"; import { TabbedCard } from "@arizeai/components"; import { DocumentAttributePostfixes, EmbeddingAttributePostfixes, LLMAttributePostfixes, MessageAttributePostfixes, RerankerAttributePostfixes, RetrievalAttributePostfixes, SemanticAttributePrefixes, ToolAttributePostfixes, } from "@arizeai/openinference-semantic-conventions"; import { Alert, Button, Card, CardProps, ContextualHelp, CopyToClipboardButton, Counter, DialogTrigger, Disclosure, DisclosureGroup, DisclosurePanel, DisclosureTrigger, ErrorBoundary, ExternalLink, Flex, Heading, Icon, Icons, Keyboard, LazyTabPanel, LinkButton, List, ListItem, Loading, Modal, ModalOverlay, Tab, TabList, Tabs, Text, ToggleButton, Token, TokenProps, View, ViewProps, } from "@phoenix/components"; import { GenerativeProviderIcon } from "@phoenix/components/generative"; import { ConnectedMarkdownBlock, ConnectedMarkdownModeSelect, MarkdownDisplayProvider, } from "@phoenix/components/markdown"; import { compactResizeHandleCSS } from "@phoenix/components/resize"; import { SpanKindIcon } from "@phoenix/components/trace"; import { useNotifySuccess, usePreferencesContext, useTheme, } from "@phoenix/contexts"; import { useDimensions } from "@phoenix/hooks"; import { useChatMessageStyles } from "@phoenix/hooks/useChatMessageStyles"; import { AttributeDocument, AttributeEmbedding, AttributeEmbeddingEmbedding, AttributeLlm, AttributeLLMToolDefinition, AttributeMessage, AttributeMessageContent, AttributePromptTemplate, AttributeReranker, AttributeRetrieval, AttributeTool, isAttributeMessages, } from "@phoenix/openInference/tracing/types"; import { assertUnreachable, isStringArray } from "@phoenix/typeUtils"; import { isModelProvider } from "@phoenix/utils/generativeUtils"; import { safelyParseJSON } from "@phoenix/utils/jsonUtils"; import { formatFloat, numberFormatter } from "@phoenix/utils/numberFormatUtils"; import { RetrievalEvaluationLabel } from "../project/RetrievalEvaluationLabel"; import { SpanHeader } from "../SpanHeader"; import { MimeType, SpanDetailsQuery, SpanDetailsQuery$data, } from "./__generated__/SpanDetailsQuery.graphql"; import { SpanActionMenu } from "./SpanActionMenu"; import { SpanAside } from "./SpanAside"; import { SpanFeedback } from "./SpanFeedback"; import { SpanImage } from "./SpanImage"; import { SpanToDatasetExampleDialog } from "./SpanToDatasetExampleDialog"; /** * A span attribute object that is a map of string to an unknown value */ type AttributeObject = { [SemanticAttributePrefixes.retrieval]?: AttributeRetrieval; [SemanticAttributePrefixes.embedding]?: AttributeEmbedding; [SemanticAttributePrefixes.tool]?: AttributeTool; [SemanticAttributePrefixes.reranker]?: AttributeReranker; [SemanticAttributePrefixes.llm]?: AttributeLlm; }; type Span = Extract<SpanDetailsQuery$data["span"], { __typename: "Span" }>; type DocumentEvaluation = NonNullable<Span["documentEvaluations"]>[number]; /** * Hook that safely parses a JSON string. */ const useSafelyParsedJSON = ( jsonStr: string ): { json: { [key: string]: unknown } | null; parseError?: unknown } => { return useMemo(() => { return safelyParseJSON(jsonStr); }, [jsonStr]); }; const spanHasException = (span: Span) => { return span.events.some((event) => event.name === "exception"); }; /** * Card props to apply across all cards */ const defaultCardProps: Partial<CardProps> = { backgroundColor: "light", borderColor: "light", collapsible: true, }; const CONDENSED_VIEW_CONTAINER_WIDTH_THRESHOLD = 900; const ASIDE_PANEL_DEFAULT_SIZE = 33; const EDIT_ANNOTATION_HOTKEY = "e"; export function SpanDetails({ spanNodeId, }: { /** * The Global ID of the span */ spanNodeId: string; }) { const isAnnotatingSpans = usePreferencesContext( (state) => state.isAnnotatingSpans ); const setIsAnnotatingSpans = usePreferencesContext( (state) => state.setIsAnnotatingSpans ); const asidePanelRef = useRef<ImperativePanelHandle>(null); const spanDetailsContainerRef = useRef<HTMLDivElement>(null); const spanDetailsContainerDimensions = useDimensions(spanDetailsContainerRef); const isCondensedView = spanDetailsContainerDimensions?.width ? spanDetailsContainerDimensions.width < CONDENSED_VIEW_CONTAINER_WIDTH_THRESHOLD : true; const { span } = useLazyLoadQuery<SpanDetailsQuery>( graphql` query SpanDetailsQuery($id: ID!) { span: node(id: $id) { __typename ... on Span { id spanId trace { id traceId } name spanKind statusCode: propagatedStatusCode statusMessage startTime parentId latencyMs tokenCountTotal startTime endTime id input { value mimeType } output { value mimeType } attributes events @required(action: THROW) { name message timestamp } documentRetrievalMetrics { evaluationName ndcg precision hit } documentEvaluations { documentPosition name label score explanation } spanAnnotations { id name } ...SpanHeader_span ...SpanFeedback_annotations ...SpanAside_span } } } `, { id: spanNodeId, } ); if (span.__typename !== "Span") { throw new Error( "Expected a span, but got a different type" + span.__typename ); } useHotkeys( EDIT_ANNOTATION_HOTKEY, () => { if (!isAnnotatingSpans) { setIsAnnotatingSpans(true); } }, { preventDefault: true } ); const hasExceptions = useMemo<boolean>(() => { return spanHasException(span); }, [span]); return ( <PanelGroup direction="horizontal" autoSaveId="span-details-layout"> <Panel order={1}> <Flex direction="column" flex="1 1 auto" height="100%" ref={spanDetailsContainerRef} > <View paddingTop="size-100" paddingBottom="size-50" paddingStart="size-150" paddingEnd="size-200" flex="none" > <Flex direction="row" alignItems="center" data-testid="span-header-row" > <SpanHeader span={span} /> <Flex flex="none" direction="row" alignItems="center" gap="size-100" > <LinkButton variant={span.spanKind !== "llm" ? "default" : "primary"} leadingVisual={<Icon svg={<Icons.PlayCircleOutline />} />} isDisabled={span.spanKind !== "llm"} to={`/playground/spans/${span.id}`} size="S" aria-label="Prompt Playground" > {isCondensedView ? null : "Playground"} </LinkButton> <AddSpanToDatasetButton span={span} buttonText={isCondensedView ? null : "Add to Dataset"} /> <ToggleButton size="S" isSelected={isAnnotatingSpans} onPress={() => { setIsAnnotatingSpans(!isAnnotatingSpans); const asidePanel = asidePanelRef.current; // expand the panel if it is not the minimum size already if (asidePanel) { const size = asidePanel.getSize(); if (size < ASIDE_PANEL_DEFAULT_SIZE) { asidePanel.resize(ASIDE_PANEL_DEFAULT_SIZE); } } }} leadingVisual={<Icon svg={<Icons.Edit2Outline />} />} trailingVisual={ !isCondensedView && !isAnnotatingSpans && ( <Keyboard>{EDIT_ANNOTATION_HOTKEY}</Keyboard> ) } > {isCondensedView ? null : "Annotate"} </ToggleButton> <SpanActionMenu traceId={span.trace.traceId} spanId={span.spanId} /> </Flex> </Flex> </View> <Tabs> <TabList> <Tab id="info">Info</Tab> <Tab id="annotations"> Annotations <Counter>{span.spanAnnotations.length}</Counter> </Tab> <Tab id="attributes">Attributes</Tab> <Tab id="events"> Events{" "} <Counter variant={hasExceptions ? "danger" : "default"}> {span.events.length} </Counter> </Tab> </TabList> <LazyTabPanel id="info"> <Flex direction="row" height="100%"> <SpanInfoWrap> <ErrorBoundary> <SpanInfo span={span} /> </ErrorBoundary> </SpanInfoWrap> </Flex> </LazyTabPanel> <LazyTabPanel id="annotations"> <SpanFeedback span={span} /> </LazyTabPanel> <LazyTabPanel id="attributes"> <View padding="size-200" height="100%" maxHeight="100%" overflow="auto" > <Card title="All Attributes" {...defaultCardProps} titleExtra={attributesContextualHelp} extra={<CopyToClipboardButton text={span.attributes} />} > <JSONBlock>{span.attributes}</JSONBlock> </Card> </View> </LazyTabPanel> <LazyTabPanel id="events"> <View overflow="auto"> <SpanEventsList events={span.events} /> </View> </LazyTabPanel> </Tabs> </Flex> </Panel> {isAnnotatingSpans && <PanelResizeHandle css={compactResizeHandleCSS} />} {isAnnotatingSpans && ( <Panel order={2} ref={asidePanelRef} defaultSize={ASIDE_PANEL_DEFAULT_SIZE} minSize={10} collapsible onCollapse={() => { setIsAnnotatingSpans(false); }} > <SpanAside span={span} /> </Panel> )} </PanelGroup> ); } const spanInfoWrapCSS = css` flex: 1 1 auto; overflow-y: auto; // Overflow fails to take into account padding & > *:after { content: ""; display: block; height: var(--ac-global-dimension-static-size-400); } `; /** * A wrapper for the span info to style it with the appropriate overflow */ function SpanInfoWrap({ children }: PropsWithChildren) { return ( <div css={spanInfoWrapCSS} data-testid="span-info-wrap"> {children} </div> ); } function AddSpanToDatasetButton({ span, buttonText, }: { span: Span; buttonText: string | null; }) { const notifySuccess = useNotifySuccess(); const navigate = useNavigate(); return ( <DialogTrigger> <Button variant="default" size="S" leadingVisual={<Icon svg={<Icons.DatabaseOutline />} />} > {buttonText} </Button> <ModalOverlay> <Modal variant="slideover" size="L"> <Suspense fallback={<Loading />}> <SpanToDatasetExampleDialog spanId={span.id} onCompleted={(datasetId) => { notifySuccess({ title: "Span Added to Dataset", message: "Successfully added span to dataset", action: { text: "View Dataset", onClick: () => { navigate(`/datasets/${datasetId}/examples`); }, }, }); }} /> </Suspense> </Modal> </ModalOverlay> </DialogTrigger> ); } function SpanInfo({ span }: { span: Span }) { const { spanKind, attributes } = span; // Parse the attributes once const { json: attributesObject, parseError } = useSafelyParsedJSON(attributes); const statusDescription = useMemo(() => { return span.statusMessage ? ( <Alert variant="danger" title="Status Description"> {span.statusMessage} </Alert> ) : null; }, [span]); // Handle the case where the attributes are not a valid JSON object if (parseError || !attributesObject) { return ( <View padding="size-200"> <Flex direction="column" gap="size-200"> {statusDescription} <Alert variant="warning" title="Un-parsable attributes"> {`Failed to parse span attributes. ${parseError instanceof Error ? parseError.message : ""}`} </Alert> <Card {...defaultCardProps} title="Attributes"> <View padding="size-100">{attributes}</View> </Card> </Flex> </View> ); } let content: ReactNode; switch (spanKind) { case "llm": { content = <LLMSpanInfo span={span} spanAttributes={attributesObject} />; break; } case "retriever": { content = ( <RetrieverSpanInfo span={span} spanAttributes={attributesObject} /> ); break; } case "reranker": { content = ( <RerankerSpanInfo span={span} spanAttributes={attributesObject} /> ); break; } case "embedding": { content = ( <EmbeddingSpanInfo span={span} spanAttributes={attributesObject} /> ); break; } case "tool": { content = <ToolSpanInfo span={span} spanAttributes={attributesObject} />; break; } default: content = <SpanIO span={span} />; } return ( <View padding="size-200"> <Flex direction="column" gap="size-200"> {statusDescription} {content} {attributesObject?.metadata ? ( <Card {...defaultCardProps} title="Metadata"> <JSONBlock>{JSON.stringify(attributesObject.metadata)}</JSONBlock> </Card> ) : null} </Flex> </View> ); } function LLMSpanInfo(props: { span: Span; spanAttributes: AttributeObject }) { const { spanAttributes, span } = props; const { input, output } = span; const llmAttributes = useMemo<AttributeLlm | null>( () => spanAttributes[SemanticAttributePrefixes.llm] || null, [spanAttributes] ); const provider = llmAttributes?.[LLMAttributePostfixes.provider]; const modelName = useMemo<string | null>(() => { if (llmAttributes == null) { return null; } const maybeModelName = llmAttributes[LLMAttributePostfixes.model_name]; if (typeof maybeModelName === "string") { return maybeModelName; } return null; }, [llmAttributes]); const inputMessages = useMemo<AttributeMessage[]>(() => { if (llmAttributes == null) { return []; } const inputMessagesValue = llmAttributes[LLMAttributePostfixes.input_messages]; // At this point, we cannot trust the type of outputMessagesValue if (!isAttributeMessages(inputMessagesValue)) { return []; } return (inputMessagesValue ?.map((obj) => obj[SemanticAttributePrefixes.message]) .filter(Boolean) || []) as AttributeMessage[]; }, [llmAttributes]); const llmTools = useMemo<AttributeLLMToolDefinition[]>(() => { if (llmAttributes == null) { return []; } const tools = llmAttributes[LLMAttributePostfixes.tools]; if (!Array.isArray(tools)) { return []; } const toolDefinitions = tools ?.map((obj) => obj[SemanticAttributePrefixes.tool]) .filter(Boolean) as AttributeLLMToolDefinition[]; return toolDefinitions; }, [llmAttributes]); const llmToolSchemas = useMemo<string[]>(() => { return llmTools.reduce((acc, tool) => { if (tool?.json_schema) { acc.push(tool.json_schema); } return acc; }, [] as string[]); }, [llmTools]); const outputMessages = useMemo<AttributeMessage[]>(() => { if (llmAttributes == null) { return []; } const outputMessagesValue = llmAttributes[LLMAttributePostfixes.output_messages]; // At this point, we cannot trust the type of outputMessagesValue if (!isAttributeMessages(outputMessagesValue)) { return []; } return (outputMessagesValue .map((obj) => obj[SemanticAttributePrefixes.message]) .filter(Boolean) || []) as AttributeMessage[]; }, [llmAttributes]); const prompts = useMemo<string[]>(() => { if (llmAttributes == null) { return []; } const maybePrompts = llmAttributes[LLMAttributePostfixes.prompts]; if (!isStringArray(maybePrompts)) { return []; } return maybePrompts; }, [llmAttributes]); const promptTemplateObject = useMemo<AttributePromptTemplate | null>(() => { if (llmAttributes == null) { return null; } const maybePromptTemplate = llmAttributes[LLMAttributePostfixes.prompt_template]; if (maybePromptTemplate == null) { return null; } return maybePromptTemplate; }, [llmAttributes]); const invocation_parameters_str = useMemo<string>(() => { if (llmAttributes == null) { return "{}"; } return (llmAttributes[LLMAttributePostfixes.invocation_parameters] || "{}") as string; }, [llmAttributes]); const modelNameTitleEl = useMemo<ReactNode>(() => { if (modelName == null) { return null; } let icon = <SpanKindIcon spanKind="llm" />; const normalizedProvider = provider?.toUpperCase(); // Show the provider if it exists if ( typeof normalizedProvider === "string" && isModelProvider(normalizedProvider) ) { icon = <GenerativeProviderIcon provider={normalizedProvider} />; } return ( <Flex direction="row" gap="size-100" alignItems="center"> {icon} <Text size="M" weight="heavy"> {modelName} </Text> </Flex> ); }, [modelName, provider]); const hasInput = input != null && input.value != null; const hasInputMessages = inputMessages.length > 0; const hasLLMToolSchemas = llmToolSchemas.length > 0; const hasOutput = output != null && output.value != null; const hasOutputMessages = outputMessages.length > 0; const hasPrompts = prompts.length > 0; const hasInvocationParams = Object.keys(safelyParseJSON(invocation_parameters_str).json || {}).length > 0; const hasPromptTemplateObject = promptTemplateObject != null; return ( <Flex direction="column" gap="size-200"> <Card collapsible backgroundColor="light" borderColor="light" titleSeparator={false} title={modelNameTitleEl} > <Tabs> <TabList> {hasInputMessages && <Tab id="input-messages">Input Messages</Tab>} {hasLLMToolSchemas && <Tab id="tools">Tools</Tab>} {hasInput && <Tab id="input">Input</Tab>} {hasPromptTemplateObject && ( <Tab id="prompt-template">Prompt Template</Tab> )} {hasPrompts && <Tab id="prompts">Prompts</Tab>} {hasInvocationParams && ( <Tab id="invocation-params">Invocation Params</Tab> )} </TabList> {hasInputMessages && ( <LazyTabPanel id="input-messages"> <LLMMessagesList messages={inputMessages} /> </LazyTabPanel> )} {hasLLMToolSchemas && ( <LazyTabPanel id="tools"> <LLMToolSchemasList toolSchemas={llmToolSchemas} /> </LazyTabPanel> )} {hasInput && ( <LazyTabPanel id="input"> <View padding="size-200"> <MarkdownDisplayProvider> <Card {...defaultCardProps} title="LLM Input" extra={ <Flex direction="row" gap="size-100"> <ConnectedMarkdownModeSelect /> <CopyToClipboardButton text={input.value} /> </Flex> } > <CodeBlock {...input} /> </Card> </MarkdownDisplayProvider> </View> </LazyTabPanel> )} {hasPromptTemplateObject && ( <LazyTabPanel id="prompt-template"> <View padding="size-200"> <Flex direction="column" gap="size-100"> <View borderRadius="medium" borderColor="light" backgroundColor="light" borderWidth="thin" padding="size-200" > <CopyToClipboard text={promptTemplateObject.template}> <Text color="text-700" fontStyle="italic"> prompt template </Text> <PreBlock>{promptTemplateObject.template}</PreBlock> </CopyToClipboard> </View> <View borderRadius="medium" borderColor="light" backgroundColor="light" borderWidth="thin" padding="size-200" > <CopyToClipboard text={JSON.stringify(promptTemplateObject.variables)} > <Text color="text-700" fontStyle="italic"> template variables </Text> <JSONBlock> {JSON.stringify(promptTemplateObject.variables)} </JSONBlock> </CopyToClipboard> </View> </Flex> </View> </LazyTabPanel> )} {hasPrompts && ( <LazyTabPanel id="prompts"> <LLMPromptsList prompts={prompts} /> </LazyTabPanel> )} {hasInvocationParams && ( <LazyTabPanel id="invocation-params"> <CopyToClipboard text={invocation_parameters_str} padding="size-100" > <JSONBlock>{invocation_parameters_str}</JSONBlock> </CopyToClipboard> </LazyTabPanel> )} </Tabs> </Card> {hasOutput || hasOutputMessages ? ( <TabbedCard {...defaultCardProps}> <Tabs> <TabList> {hasOutputMessages && ( <Tab id="output-messages">Output Messages</Tab> )} {hasOutput && <Tab id="output">Output</Tab>} </TabList> {hasOutputMessages && ( <LazyTabPanel id="output-messages"> <LLMMessagesList messages={outputMessages} /> </LazyTabPanel> )} {hasOutput && ( <LazyTabPanel id="output"> <View padding="size-200"> <MarkdownDisplayProvider> <Card {...defaultCardProps} title="LLM Output" extra={ <Flex direction="row" gap="size-100"> <ConnectedMarkdownModeSelect /> <CopyToClipboardButton text={output.value} /> </Flex> } > <CodeBlock {...output} /> </Card> </MarkdownDisplayProvider> </View> </LazyTabPanel> )} </Tabs> </TabbedCard> ) : null} </Flex> ); } function RetrieverSpanInfo(props: { span: Span; spanAttributes: AttributeObject; }) { const { spanAttributes, span } = props; const { input } = span; const retrieverAttributes = useMemo<AttributeRetrieval | null>( () => spanAttributes[SemanticAttributePrefixes.retrieval] || null, [spanAttributes] ); const documents = useMemo<AttributeDocument[]>(() => { if (retrieverAttributes == null) { return []; } return (retrieverAttributes[RetrievalAttributePostfixes.documents] ?.map((obj) => obj[SemanticAttributePrefixes.document]) .filter(Boolean) || []) as AttributeDocument[]; }, [retrieverAttributes]); // Construct a map of document position to document evaluations const documentEvaluationsMap = useMemo< Record<number, DocumentEvaluation[]> >(() => { const documentEvaluations = span.documentEvaluations; return documentEvaluations.reduce( (acc, documentEvaluation) => { const documentPosition = documentEvaluation.documentPosition; const evaluations = acc[documentPosition] || []; return { ...acc, [documentPosition]: [...evaluations, documentEvaluation], }; }, {} as Record<number, DocumentEvaluation[]> ); }, [span.documentEvaluations]); const hasInput = input != null && input.value != null; const isText = hasInput && input.mimeType === "text"; const hasDocuments = documents.length > 0; const hasDocumentRetrievalMetrics = span.documentRetrievalMetrics.length > 0; return ( <Flex direction="column" gap="size-200"> {hasInput ? ( <MarkdownDisplayProvider> <Card title="Input" {...defaultCardProps} extra={ <Flex direction="row" gap="size-100" alignItems="center"> {isText ? ( <ConnectedMarkdownModeSelect /> ) : ( <CopyToClipboardButton text={input.value} /> )} </Flex> } > <CodeBlock {...input} /> </Card> </MarkdownDisplayProvider> ) : null} {hasDocuments ? ( <MarkdownDisplayProvider> <Card title="Documents" {...defaultCardProps} titleExtra={ hasDocumentRetrievalMetrics && ( <Flex direction="row" alignItems="center" gap="size-100"> {span.documentRetrievalMetrics.map((retrievalMetric) => { return ( <> <RetrievalEvaluationLabel key="ndcg" name={retrievalMetric.evaluationName} metric="ndcg" score={retrievalMetric.ndcg} /> <RetrievalEvaluationLabel key="precision" name={retrievalMetric.evaluationName} metric="precision" score={retrievalMetric.precision} /> <RetrievalEvaluationLabel key="hit" name={retrievalMetric.evaluationName} metric="hit" score={retrievalMetric.hit} /> </> ); })} </Flex> ) } extra={<ConnectedMarkdownModeSelect />} > <ul css={css` display: flex; flex-direction: column; gap: var(--ac-global-dimension-static-size-200); padding: var(--ac-global-dimension-static-size-200); `} > {documents.map((document, idx) => { return ( <li key={idx}> <DocumentItem document={document} documentEvaluations={documentEvaluationsMap[idx]} borderColor={"seafoam-700"} backgroundColor={"seafoam-100"} tokenColor="var(--ac-global-color-seafoam-1000)" /> </li> ); })} </ul> </Card> </MarkdownDisplayProvider> ) : null} </Flex> ); } function RerankerSpanInfo(props: { span: Span; spanAttributes: AttributeObject; }) { const { spanAttributes } = props; const rerankerAttributes = useMemo<AttributeReranker | null>( () => spanAttributes[SemanticAttributePrefixes.reranker] || null, [spanAttributes] ); const query = useMemo<string | null>(() => { if (rerankerAttributes == null) { return null; } return rerankerAttributes[RerankerAttributePostfixes.query] || null; }, [rerankerAttributes]); const input_documents = useMemo<AttributeDocument[]>(() => { if (rerankerAttributes == null) { return []; } return (rerankerAttributes[RerankerAttributePostfixes.input_documents] ?.map((obj) => obj[SemanticAttributePrefixes.document]) .filter(Boolean) || []) as AttributeDocument[]; }, [rerankerAttributes]); const output_documents = useMemo<AttributeDocument[]>(() => { if (rerankerAttributes == null) { return []; } return (rerankerAttributes[RerankerAttributePostfixes.output_documents] ?.map((obj) => obj[SemanticAttributePrefixes.document]) .filter(Boolean) || []) as AttributeDocument[]; }, [rerankerAttributes]); const numInputDocuments = input_documents.length; const numOutputDocuments = output_documents.length; return ( <Flex direction="column" gap="size-200"> <MarkdownDisplayProvider> {query && ( <Card title="Query" {...defaultCardProps}> <View padding="size-200"> <ConnectedMarkdownBlock>{query}</ConnectedMarkdownBlock> </View> </Card> )} </MarkdownDisplayProvider> <Card title={"Input Documents"} titleExtra={<Counter>{numInputDocuments}</Counter>} {...defaultCardProps} defaultOpen={false} > { <ul css={css` padding: var(--ac-global-dimension-static-size-200); display: flex; flex-direction: column; gap: var(--ac-global-dimension-static-size-200); `} > {input_documents.map((document, idx) => { return ( <li key={idx}> <DocumentItem document={document} borderColor={"seafoam-700"} backgroundColor={"seafoam-100"} tokenColor="var(--ac-global-color-seafoam-1000)" /> </li> ); })} </ul> } </Card> <Card title={"Output Documents"} titleExtra={<Counter>{numOutputDocuments}</Counter>} {...defaultCardProps} > { <ul css={css` padding: var(--ac-global-dimension-static-size-200); display: flex; flex-direction: column; gap: var(--ac-global-dimension-static-size-200); `} > {output_documents.map((document, idx) => { return ( <li key={idx}> <DocumentItem document={document} borderColor={"celery-700"} backgroundColor={"celery-100"} tokenColor="var(--ac-global-color-celery-1000)" /> </li> ); })} </ul> } </Card> </Flex> ); } function EmbeddingSpanInfo(props: { span: Span; spanAttributes: AttributeObject; }) { const { spanAttributes } = props; const embeddingAttributes = useMemo<AttributeEmbedding | null>( () => spanAttributes[SemanticAttributePrefixes.embedding] || null, [spanAttributes] ); const embeddings = useMemo<AttributeEmbeddingEmbedding[]>(() => { if (embeddingAttributes == null) { return []; } return (embeddingAttributes[EmbeddingAttributePostfixes.embeddings] ?.map((obj) => obj[SemanticAttributePrefixes.embedding]) .filter(Boolean) || []) as AttributeEmbeddingEmbedding[]; }, [embeddingAttributes]); const hasEmbeddings = embeddings.length > 0; const modelName = embeddingAttributes?.[EmbeddingAttributePostfixes.model_name]; return ( <Flex direction="column" gap="size-200"> {hasEmbeddings ? ( <Card title={ "Embeddings" + (typeof modelName === "string" ? `: ${modelName}` : "") } {...defaultCardProps} > { <ul css={css` display: flex; flex-direction: column; gap: var(--ac-global-dimension-static-size-200); padding: var(--ac-global-dimension-static-size-200); `} > {embeddings.map((embedding, idx) => { return ( <li key={idx}> <MarkdownDisplayProvider> <Card {...defaultCardProps} backgroundColor="purple-100" borderColor="purple-700" title="Embedded Text" > <ConnectedMarkdownBlock> {embedding[EmbeddingAttributePostfixes.text] || ""} </ConnectedMarkdownBlock> </Card> </MarkdownDisplayProvider> </li> ); })} </ul> } </Card> ) : ( <SpanIO span={props.span} /> )} </Flex> ); } function ToolSpanInfo(props: { span: Span; spanAttributes: AttributeObject }) { const { span, spanAttributes } = props; const { input, output } = span; const hasInput = typeof input?.value === "string"; const hasOutput = typeof output?.value === "string"; const inputIsText = input?.mimeType === "text"; const outputIsText = output?.mimeType === "text"; const toolAttributes = useMemo<AttributeTool>( () => spanAttributes[SemanticAttributePrefixes.tool] || {}, [spanAttributes] ); const hasToolAttributes = Object.keys(toolAttributes).length > 0; const toolName = toolAttributes[ToolAttributePostfixes.name]; const toolDescription = toolAttributes[ToolAttributePostfixes.description]; const toolParameters = toolAttributes[ToolAttributePostfixes.parameters]; if (!hasInput && !hasOutput && !hasToolAttributes) { return null; } return ( <Flex direction="column" gap="size-200"> {hasInput ? ( <MarkdownDisplayProvider> <Card title="Input" {...defaultCardProps} extra={ <Flex direction="row" gap="size-100" alignItems="center"> {inputIsText ? <ConnectedMarkdownModeSelect /> : null} <CopyToClipboardButton text={input.value} /> </Flex> } > <CodeBlock {...input} /> </Card> </MarkdownDisplayProvider> ) : null} {hasOutput ? ( <MarkdownDisplayProvider> <Card title="Output" {...defaultCardProps} backgroundColor="green-100" borderColor="green-700" extra={ <Flex direction="row" gap="size-100" alignItems="center"> {outputIsText ? <ConnectedMarkdownModeSelect /> : null} <CopyToClipboardButton text={output.value} /> </Flex> } > <CodeBlock {...output} /> </Card> </MarkdownDisplayProvider> ) : null} {hasToolAttributes ? ( <Card title={"Tool" + (typeof toolName === "string" ? `: ${toolName}` : "")} {...defaultCardProps} > <Flex direction="column"> {toolDescription != null ? ( <View paddingStart="size-200" paddingEnd="size-200" paddingTop="size-100" paddingBottom="size-100" borderBottomColor="dark" borderBottomWidth="thin" backgroundColor="light" > <Flex direction="column" alignItems="start" gap="size-50"> <Text color="text-700" fontStyle="italic"> Description </Text> <Text>{toolDescription as string}</Text> </Flex> </View> ) : null} {toolParameters != null ? ( <View paddingStart="size-200" paddingEnd="size-200" paddingTop="size-100" paddingBottom="size-100" borderBottomColor="dark" borderBottomWidth="thin" > <Flex direction="column" alignItems="start" width="100%"> <Text color="text-700" fontStyle="italic"> Parameters </Text> <div css={css` .cm-editor { background-color: transparent !important; } `} > <JSONBlock basicSetup={{ lineNumbers: false, foldGutter: false }} > {JSON.stringify(toolParameters) as string} </JSONBlock> </div> </Flex> </View> ) : null} </Flex> </Card> ) : null} </Flex> ); } // Labels that get highlighted as danger in the document evaluations const DANGER_DOCUMENT_EVALUATION_LABELS = ["irrelevant", "unrelated"]; function DocumentItem({ document, documentEvaluations, backgroundColor, borderColor, tokenColor, }: { document: AttributeDocument; documentEvaluations?: DocumentEvaluation[] | null; backgroundColor: ViewProps["backgroundColor"]; borderColor: ViewProps["borderColor"]; tokenColor: TokenProps["color"]; }) { const metadata = document[DocumentAttributePostfixes.metadata]; const hasEvaluations = documentEvaluations && documentEvaluations.length; const documentContent = document[DocumentAttributePostfixes.content]; return ( <Card {...defaultCardProps} backgroundColor={backgroundColor} borderColor={borderColor} title={ <Flex direction="row" gap="size-50" alignItems="center"> <Icon svg={<Icons.FileOutline />} /> <Heading level={4}> document {document[DocumentAttributePostfixes.id]} </Heading> </Flex> } extra={ typeof document[DocumentAttributePostfixes.score] === "number" && ( <Token color={tokenColor}> {`score ${numberFormatter( document[DocumentAttributePostfixes.score] )}`} </Token> ) } > <Flex direction="column"> {documentContent && ( <ConnectedMarkdownBlock>{documentContent}</ConnectedMarkdownBlock> )} {metadata && ( <> <View borderColor={borderColor} borderTopWidth="thin"> <View paddingX="size-200" paddingY="size-100" borderColor={borderColor} borderBottomWidth="thin" > <Heading level={4}>Document Metadata</Heading> </View> <JSONBlock basicSetup={{ lineNumbers: false }}> {JSON.stringify(metadata)} </JSONBlock> </View> </> )} {hasEvaluations && ( <View borderColor={borderColor} borderTopWidth="thin" padding="size-200" > <Flex direction="column" gap="size-100"> <Heading level={3} weight="heavy"> Evaluations </Heading> <ul> {documentEvaluations.map((documentEvaluation, idx) => { // Highlight the label as danger if it is a danger classification const evalTokenColor = documentEvaluation.label && DANGER_DOCUMENT_EVALUATION_LABELS.includes( documentEvaluation.label ) ? "var(--ac-global-color-danger)" : tokenColor; return ( <li key={idx}> <View padding="size-200" borderWidth="thin" borderColor={borderColor} borderRadius="medium" > <Flex direction="column" gap="size-50"> <Flex direction="row" gap="size-100"> <Text weight="heavy" elementType="h5"> {documentEvaluation.name} </Text> {documentEvaluation.label && ( <Token color={evalTokenColor}> {documentEvaluation.label} </Token> )} {typeof documentEvaluation.score === "number" && ( <Token color={evalTokenColor}> <Flex direction="row" gap="size-50"> <Text size="XS" weight="heavy" color="inherit" > score </Text> <Text size="XS"> {formatFloat(documentEvaluation.score)} </Text> </Flex> </Token> )} </Flex> {typeof documentEvaluation.explanation && ( <p css={css` margin-top: var( --ac-global-dimension-static-size-100 ); margin-bottom: 0; `} > {documentEvaluation.explanation} </p> )} </Flex> </View> </li> ); })} </ul> </Flex> </View> )} </Flex> </Card> ); } function LLMMessage({ message }: { message: AttributeMessage }) { const messageContent = message[MessageAttributePostfixes.content]; // as of multi-modal models, a message can also be a list const messagesContents = message[MessageAttributePostfixes.contents]; const toolCalls = message[MessageAttributePostfixes.tool_calls] ?.map((obj) => obj[SemanticAttributePrefixes.tool_call]) .filter(Boolean); const hasFunctionCall = message[MessageAttributePostfixes.function_call_arguments_json] && message[MessageAttributePostfixes.function_call_name]; const role = message[MessageAttributePostfixes.role] || "unknown"; const messageStyles = useChatMessageStyles(role); const toolCallDisclosureIds = useMemo(() => { return toolCalls?.map((_, idx) => `tool-call-${idx}`) || []; }, [toolCalls]); const toolResultId = message[MessageAttributePostfixes.tool_call_id]; return ( <MarkdownDisplayProvider> <Card {...defaultCardProps} {...messageStyles} title={ role + (message[MessageAttributePostfixes.name] ? `: ${message[MessageAttributePostfixes.name]}` : "") } extra={ <Flex direction="row" gap="size-100" alignItems="center"> <ConnectedMarkdownModeSelect /> <CopyToClipboardButton text={messageContent || JSON.stringify(message)} /> </Flex> } > <ErrorBoundary> {messagesContents ? ( <MessageContentsList messageContents={messagesContents} /> ) : null} </ErrorBoundary> <Flex direction="column" alignItems="start"> <DisclosureGroup css={css` width: 100%; // when any .ac-disclosure-trigger is hovered, show the child .copy-to-clipboard-button .ac-disclosure-trigger { width: 100%; .copy-to-clipboard-button { visibility: hidden; } } .ac-disclosure-trigger:hover, .ac-disclosure-trigger:focus-within, .ac-disclosure-trigger:focus-visible { .copy-to-clipboard-button { visibility: visible; } } `} defaultExpandedKeys={[ "tool-content", ...toolCallDisclosureIds, "function-call", ]} > {/* when the message is a tool result, show the tool result in a disclosure */} {messageContent && role.toLowerCase() === "tool" ? ( <Disclosure id="tool-content"> <DisclosureTrigger arrowPosition="start" justifyContent="space-between" > <Text> Tool Result{toolResultId ? `: ${toolResultId}` : ""} </Text> {toolResultId ? ( <CopyToClipboardButton text={toolResultId} /> ) : null} </DisclosureTrigger> <DisclosurePanel> <View width="100%"> <ConnectedMarkdownBlock> {messageContent} </ConnectedMarkdownBlock> </View> </DisclosurePanel> </Disclosure> ) : // when the message is any other kind, just show the content without a disclosure messageContent ? ( <View width="100%"> <ConnectedMarkdownBlock> {messageContent} </ConnectedMarkdownBlock> </View> ) : null} {(toolCalls?.length ?? 0) > 0 ? toolCalls?.map((toolCall, idx) => { if (!toolCall) { return null; } const id = toolCall.id; const parsedArguments = safelyParseJSON( toolCall?.function?.arguments as string ); return ( <Disclosure key={idx} id={toolCallDisclosureIds[idx]} css={ idx === 0 ? css` border-top: 1px solid var(--ac-global-border-color-default); ` : null } > <DisclosureTrigger arrowPosition="start" justifyContent="space-between" > <span>Tool Call{id ? `: ${id}` : ""}</span> {id ? <CopyToClipboardButton text={id} /> : null} </DisclosureTrigger> <DisclosurePanel> <pre key={idx} css={css` text-wrap: wrap; margin: 0; padding: var(--ac-global-dimension-static-size-200); `} > {toolCall?.function?.name as string}( {parsedArguments.json ? JSON.stringify(parsedArguments.json, null, 2) : `${toolCall?.function?.arguments}`} ) </pre> </DisclosurePanel> </Disclosure> ); }) : null} {/*functionCall is deprecated and is superseded by toolCalls, so we don't expect both to be present*/} {hasFunctionCall ? ( <Disclosure id="function-call"> <DisclosureTrigger> <Text>Function Call</Text> </DisclosureTrigger> <DisclosurePanel> <pre css={css` text-wrap: wrap; margin: var(--ac-global-dimension-static-size-100) 0; `} > { message[ MessageAttributePostfixes.function_call_name ] as string } ( {JSON.stringify( JSON.parse( message[ MessageAttributePostfixes.function_call_arguments_json ] as string ), null, 2 )} ) </pre> </DisclosurePanel> </Disclosure> ) : null} </DisclosureGroup> </Flex> </Card> </MarkdownDisplayProvider> ); } function LLMToolSchema({ toolSchema, index, }: { toolSchema: string; index: number; }) { const titleEl = ( <Flex direction="row" gap="size-100" alignItems="center"> <SpanKindIcon spanKind="tool" /> <Text weight="heavy">Tool</Text> </Flex> ); return ( <Card title={titleEl} titleExtra={<Counter>#{index + 1}</Counter>} {...defaultCardProps} backgroundColor="yellow-100" borderColor="yellow-700" extra={<CopyToClipboardButton text={toolSchema} />} > <CodeBlock value={toolSchema} mimeType={"json"} /> </Card> ); } function LLMMessagesList({ messages }: { messages: AttributeMessage[] }) { return ( <ul css={css` display: flex; flex-direction: column; gap: var(--ac-global-dimension-static-size-100); padding: var(--ac-global-dimension-static-size-200); `} > {messages.map((message, idx) => { return ( <li key={idx}> <LLMMessage message={message} /> </li> ); })} </ul> ); } function LLMToolSchemasList({ toolSchemas }: { toolSchemas: string[] }) { return ( <ul css={css` display: flex; flex-direction: column; gap: var(--ac-global-dimension-static-size-100); padding: var(--ac-global-dimension-static-size-200); `} > {toolSchemas.map((toolSchema, idx) => { return ( <li key={idx}> <LLMToolSchema toolSchema={toolSchema} index={idx} /> </li> ); })} </ul> ); } function LLMPromptsList({ prompts }: { prompts: string[] }) { return ( <ul data-testid="llm-prompts-list" css={css` padding: var(--ac-global-dimension-size-200); display: flex; flex-direction: column; gap: var(--ac-global-dimension-size-100); `} > {prompts.map((prompt, idx) => { return ( <li key={idx}> <View backgroundColor="grey-100" borderColor="grey-500" borderWidth="thin" borderRadius="medium" padding="size-100" > <CopyToClipboard text={prompt}> <CodeBlock value={prompt} mimeType="text" /> </CopyToClipboard> </View> </li> ); })} </ul> ); } const messageContentListCSS = css` display: flex; flex-direction: row; gap: var(--ac-global-dimension-size-200); flex-wrap: wrap; `; /** * A list of message contents. Used for multi-modal models. */ function MessageContentsList({ messageContents, }: { messageContents: AttributeMessageContent[]; }) { return ( <ul css={messageContentListCSS} data-testid="message-content-list"> {messageContents.map((messageContent, idx) => { return ( <MessageContentListItem key={idx} messageContentAttribute={messageContent} /> ); })} </ul> ); } /** * Display text content in full width. */ const messageContentTextListItemCSS = css` flex: 1 1 100%; padding: var(--ac-global-dimension-static-size-200); `; /** * Displays multi-modal message content. Typically an image or text. * Examples: * {"message_content":{"text":"What is in this image?","type":"text"}} * {"message_content":{"type":"image","image":{"image":{"url":"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"}}}} */ function MessageContentListItem({ messageContentAttribute, }: { messageContentAttribute: AttributeMessageContent; }) { const { message_content } = messageContentAttribute; const text = message_content?.text; const image = message_content?.image; const imageUrl = image?.image?.url; return ( <li css={text ? messageContentTextListItemCSS : null}> {text ? ( <ConnectedMarkdownBlock margin="none">{text}</ConnectedMarkdownBlock> ) : null} {imageUrl ? <SpanImage url={imageUrl} /> : null} </li> ); } function SpanIO({ span }: { span: Span }) { const { input, output } = span; const isMissingIO = input == null && output == null; const inputIsText = input?.mimeType === "text"; const outputIsText = output?.mimeType === "text"; return ( <Flex direction="column" gap="size-200"> {input && input.value != null ? ( <MarkdownDisplayProvider> <Card title="Input" {...defaultCardProps} extra={ <Flex direction="row" gap="size-100" alignItems="center"> {inputIsText ? <ConnectedMarkdownModeSelect /> : null} <CopyToClipboardButton text={input.value} /> </Flex> } > <CodeBlock {...input} /> </Card> </MarkdownDisplayProvider> ) : null} {output && output.value != null ? ( <MarkdownDisplayProvider> <Card title="Output" {...defaultCardProps} backgroundColor="green-100" borderColor="green-700" extra={ <Flex direction="row" gap="size-100" alignItems="center"> {outputIsText ? <ConnectedMarkdownModeSelect /> : null} <CopyToClipboardButton text={output.value} /> </Flex> } > <CodeBlock {...output} /> </Card> </MarkdownDisplayProvider> ) : null} {isMissingIO ? ( <Card title="All Attributes" titleExtra={attributesContextualHelp} {...defaultCardProps} extra={<CopyToClipboardButton text={span.attributes} />} > <JSONBlock>{span.attributes}</JSONBlock> </Card> ) : null} </Flex> ); } const codeMirrorCSS = css` width: 100%; .cm-editor, .cm-gutters { background-color: transparent; } `; function CopyToClipboard({ text, children, padding, }: PropsWithChildren<{ text: string; padding?: "size-100" }>) { const paddingValue = padding ? `var(--ac-global-dimension-${padding})` : "0"; return ( <div css={css` position: relative; .copy-to-clipboard-button { transition: opacity 0.2s ease-in-out; opacity: 0; position: absolute; right: ${paddingValue}; top: ${paddingValue}; z-index: 1; } &:hover .copy-to-clipboard-button { opacity: 1; } `} > <CopyToClipboardButton text={text} /> {children} </div> ); } /** * A block of JSON content that is not editable. */ function JSONBlock({ children, basicSetup = {}, }: { children: string; basicSetup?: BasicSetupOptions; }) { const { theme } = useTheme(); const codeMirrorTheme = theme === "light" ? githubLight : githubDark; // We need to make sure that the content can actually be displayed // As JSON as we cannot fully trust the backend to always send valid JSON const { value, mimeType } = useMemo(() => { try { // Attempt to pretty print the JSON. This may fail if the JSON is invalid. // E.g. sometimes it contains NANs due to poor JSON.dumps in the backend return { value: JSON.stringify(JSON.parse(children), null, 2), mimeType: "json" as const, }; } catch (e) { // Fall back to string return { value: children, mimeType: "text" as const }; } }, [children]); if (mimeType === "json") { return ( <CodeMirror value={value} basicSetup={{ lineNumbers: true, foldGutter: true, bracketMatching: true, syntaxHighlighting: true, highlightActiveLine: false, highlightActiveLineGutter: false, ...basicSetup, }} extensions={[json(), EditorView.lineWrapping]} editable={false} theme={codeMirrorTheme} css={codeMirrorCSS} /> ); } else { return <PreBlock>{value}</PreBlock>; } } function PreBlock({ children }: { children: string }) { return ( <pre data-testid="pre-block" css={css` white-space: pre-wrap; padding: var(--ac-global-dimension-static-size-200); font-size: var(--ac-global-font-size-s); `} > {children} </pre> ); } function CodeBlock({ value, mimeType }: { value: string; mimeType: MimeType }) { let content; switch (mimeType) { case "json": content = <JSONBlock>{value}</JSONBlock>; break; case "text": content = <ConnectedMarkdownBlock>{value}</ConnectedMarkdownBlock>; break; default: assertUnreachable(mimeType); } return content; } function EmptyIndicator({ text }: { text: string }) { return ( <Flex direction="column" alignItems="center" flex="1 1 auto" height="size-2400" justifyContent="center" gap="size-100" > <Text size="L">{text}</Text> </Flex> ); } function SpanEventsList({ events }: { events: Span["events"] }) { if (events.length === 0) { return <EmptyIndicator text="No events" />; } return ( <List> {events.map((event, idx) => { const isException = event.name === "exception"; return ( <ListItem key={idx}> <Flex direction="row" alignItems="center" gap="size-100"> <View flex="none"> <div data-event-type={isException ? "exception" : "info"} css={css` &[data-event-type="exception"] { --px-event-icon-color: var(--ac-global-color-danger); } &[data-event-type="info"] { --px-event-icon-color: var(--ac-global-color-info); } .ac-icon-wrap { color: var(--px-event-icon-color); } `} > <Icon svg={ isException ? ( <Icons.AlertTriangleOutline /> ) : ( <Icons.InfoOutline /> ) } /> </div> </View> <Flex direction="column" gap="size-25" flex="1 1 auto"> <Text weight="heavy">{event.name}</Text> <Text color="text-700">{event.message}</Text> </Flex> <View> <Text color="text-700"> {new Date(event.timestamp).toLocaleString()} </Text> </View> </Flex> </ListItem> ); })} </List> ); } const attributesContextualHelp = ( <ContextualHelp> <Heading weight="heavy" level={4}> Span Attributes </Heading> <Text> Attributes are key-value pairs that represent metadata associated with a span. For detailed descriptions of specific attributes, consult the semantic conventions section of the OpenInference tracing specification. </Text> <footer> <ExternalLink href="https://github.com/Arize-ai/openinference/blob/main/spec/semantic_conventions.md"> Semantic Conventions </ExternalLink> </footer> </ContextualHelp> );

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