Skip to content

Commit

Permalink
feat(Span displays): Add Root, LLM, and Retriever span visualizations (
Browse files Browse the repository at this point in the history
…#1107)

* shuffling

* update standard.md to mention python versions tested

* de-circularizing

* finishing up decircularization

* fix imports in tests

* more import fixes

* starting spans work

* more import fixes

* updating docs and docstrings with new locations

* add temp to bedrock and ignored warning

* trying to remove circular import

* more debugging

* more fixes

* more of the same

* last few

* missed on

* nits

* typo

* fix feedbackmode imports

* 2 more fixes

* starting spans work

* work

* working on spans

* opentelemetry work

* working on spans

* span work

* organize imports

* starting span categorization logic

* nits

* typo in module doc

* spans for custom apps

* clear outputs

* fix dictnamespace typing

* span types

* colors

* style updates

* span types

* more style fixes

* add dist build

* revert some changes

* revert ipynb changes

---------

Co-authored-by: Piotr Mardziel <[email protected]>
  • Loading branch information
walnutdust and piotrm0 authored Apr 29, 2024
1 parent b99afb5 commit 2b9c3c2
Show file tree
Hide file tree
Showing 30 changed files with 882 additions and 359 deletions.
27 changes: 19 additions & 8 deletions trulens_eval/trulens_eval/pages/Evaluations.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from trulens_eval.react_components.record_viewer import record_viewer
from trulens_eval.schema.feedback import Select
from trulens_eval.schema.record import Record
from trulens_eval.trace.category import Categorizer
from trulens_eval.utils.json import jsonify_for_ui
from trulens_eval.utils.serial import Lens
from trulens_eval.utils.streamlit import init_from_args
Expand Down Expand Up @@ -340,13 +341,17 @@ def get_icon(feedback_name):
)
)

selected_fcol = pills(
"Feedback functions (click on a pill to learn more)",
feedback_with_valid_results,
index=None,
format_func=lambda fcol: f"{fcol} {row[fcol]:.4f}",
icons=icons
)
selected_fcol = None
if len(feedback_with_valid_results) > 0:
selected_fcol = pills(
"Feedback functions (click on a pill to learn more)",
feedback_with_valid_results,
index=None,
format_func=lambda fcol: f"{fcol} {row[fcol]:.4f}",
icons=icons
)
else:
st.write("No feedback functions found.")

def display_feedback_call(call, feedback_name):

Expand Down Expand Up @@ -413,7 +418,13 @@ def highlight(s):
pass

st.subheader("Trace details")
val = record_viewer(record_json, app_json)

try:
spans = Categorizer.spans_of_record(Record(**record_json))
except Exception:
spans = []

val = record_viewer(record_json, app_json, spans)
st.markdown("")

with tab2:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ module.exports = {
'import/first': 'error',
'import/newline-after-import': 'error',
'import/no-duplicates': 'error',
'max-classes-per-file': 'off'
},
settings: {
'import/resolver': {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os

import streamlit.components.v1 as components
from trulens_eval.utils.json import jsonify

# Create a _RELEASE constant. We'll set this to False while we're developing
# the component, and True when we're ready to package and distribute it.
Expand Down Expand Up @@ -44,7 +45,7 @@
# `declare_component` and call it done. The wrapper allows us to customize
# our component's API: we can pre-process its input args, post-process its
# output value, and add a docstring for users.
def record_viewer(record_json, app_json, key=None):
def record_viewer(record_json, app_json, spans, key=None):
"""Create a new instance of "record_viewer", which produces a timeline
Parameters
Expand All @@ -55,6 +56,9 @@ def record_viewer(record_json, app_json, key=None):
app_json: obj
JSON of the app serialized by `json.loads`.
spans: List[Span]
List of spans in the record.
key: str or None
An optional key that uniquely identifies this component. If this is
None, and the component's arguments are changed, the component will
Expand All @@ -67,14 +71,17 @@ def record_viewer(record_json, app_json, key=None):
this returns a JavaScript null, which is interpreted in python as a 0.
"""

raw_spans=list(map(lambda span: jsonify(span), spans))

# Call through to our private component function. Arguments we pass here
# will be sent to the frontend, where they'll be available in an "args"
# dictionary.
#
# "default" is a special argument that specifies the initial return
# value of the component before the user has interacted with it.
component_value = _record_viewer(
record_json=record_json, app_json=app_json, key=key, default=""
record_json=record_json, app_json=app_json, raw_spans=raw_spans, key=key, default=""
)

return component_value

This file was deleted.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Record viewer</title>
<script type="module" crossorigin src="./assets/index-22978a10.js"></script>
<script type="module" crossorigin src="./assets/index-69ff0e1b.js"></script>
<link rel="stylesheet" href="./assets/index-00b8e16d.css">
</head>
<body style="margin: 0">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,24 @@ export default function RecordInfo({ appJSON, nodeMap, recordJSON, root }: Recor
<Grid
container
sx={{
border: ({ palette }) => `0.5px solid ${palette.grey[300]}`,
borderRadius: 0.5,
[`& .${gridClasses.item}`]: {
border: ({ palette }) => `0.5px solid ${palette.grey[300]}`,
border: ({ palette }) => `1px solid ${palette.grey[300]}`,
borderRadius: ({ spacing }) => spacing(0.5),
[`& > .${gridClasses.item}`]: {
border: ({ palette }) => `1px solid ${palette.grey[300]}`,
},
}}
>
<Grid item xs={12} md={isTimeline ? 12 : 5} lg={isTimeline ? 12 : 4}>
<Grid
item
xs={12}
md={isTimeline ? 12 : 5}
lg={isTimeline ? 12 : 4}
sx={{ display: 'flex', flexDirection: 'column' }}
>
<Tabs
value={selectedSpanView}
onChange={(_event, value) => setSelectedSpanView(value as SPAN_VIEW)}
sx={{ borderBottom: ({ palette }) => `1px solid ${palette.grey[300]}` }}
sx={{ borderBottom: ({ palette }) => `2px solid ${palette.grey[300]}` }}
>
{SPAN_VIEWS.map((tab) => (
<Tab label={tab} value={tab} key={tab} id={tab} />
Expand All @@ -123,7 +129,7 @@ export default function RecordInfo({ appJSON, nodeMap, recordJSON, root }: Recor
<Tabs
value={selectedTab}
onChange={(_event, value) => setSelectedTab(value as RECORD_CONTENT_TABS)}
sx={{ borderBottom: ({ palette }) => `1px solid ${palette.grey[300]}` }}
sx={{ borderBottom: ({ palette }) => `2px solid ${palette.grey[300]}` }}
>
{SPAN_TREE_TABS.map((tab) => (
<Tab label={tab} value={tab} key={tab} id={tab} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ export default function RecordTable({ root, selectedNodeId, setSelectedNodeId }:
}

const recordTableSx: SxProps<Theme> = {
borderRadius: 4,
border: ({ palette }) => `0.5px solid ${palette.grey[300]}`,
borderRadius: ({ spacing }) => spacing(0.5),
minWidth: 650,

'& th': {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Box, IconButton, SxProps, TableCell, TableRow, Theme, Typography } from
import { useEffect, useState } from 'react';
import { Streamlit } from 'streamlit-component-lib';

import SpanIconDisplay from '@/RecordTable/SpanIconDisplay';
import { SpanTooltip } from '@/SpanTooltip';
import { StackTreeNode } from '@/utils/StackTreeNode';

Expand All @@ -27,7 +28,7 @@ export default function RecordTableRowRecursive({

const [expanded, setExpanded] = useState<boolean>(true);

const { nodeId, startTime, timeTaken, selector, label } = node;
const { nodeId, startTime, timeTaken, selector, label, span } = node;

const isNodeSelected = selectedNodeId === nodeId;

Expand All @@ -47,11 +48,11 @@ export default function RecordTableRowRecursive({
{expanded ? <ArrowDropDown /> : <ArrowRight />}
</IconButton>
)}
<Box sx={{ display: 'flex', alignItems: 'center', ml: node.children.length === 0 ? 5 : 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', ml: node.children.length === 0 ? 5 : 0, gap: 1 }}>
<SpanIconDisplay spanType={span?.type} />

<Typography fontWeight="bold">{label}</Typography>
<Typography variant="code" sx={{ ml: 1, px: 1 }}>
{selector}
</Typography>
<Typography variant="code">{selector}</Typography>
</Box>
</Box>
</TableCell>
Expand Down Expand Up @@ -90,7 +91,7 @@ export default function RecordTableRowRecursive({
const recordBarSx: SxProps<Theme> = {
position: 'relative',
height: 20,
borderRadius: 0.5,
borderRadius: ({ spacing }) => spacing(0.5),
};

const recordRowSx: SxProps<Theme> = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Box, SxProps, Theme } from '@mui/material';

import { DEFAULT_SPAN_TYPE_PROPS, SPAN_TYPE_PROPS } from '@/icons/SpanIcon';
import { SpanType } from '@/utils/Span';

interface SpanIconProps {
spanType?: SpanType;
}

export default function SpanIconDisplay({ spanType = SpanType.UNTYPED }: SpanIconProps) {
const { backgroundColor, Icon, color } = SPAN_TYPE_PROPS[spanType] ?? DEFAULT_SPAN_TYPE_PROPS;

return (
<Box sx={{ ...containerSx, backgroundColor, color }}>
<Icon sx={{ ...iconSx, color }} />
</Box>
);
}

const iconSx: SxProps<Theme> = {
p: 0.5,
height: '16px',
width: '16px',
};

const containerSx: SxProps<Theme> = {
display: 'flex',
borderRadius: ({ spacing }) => spacing(0.5),
flexDirection: 'column',
justifyContent: 'center',
};
Original file line number Diff line number Diff line change
@@ -1,56 +1,20 @@
import { Grid, Stack, Typography } from '@mui/material';

import JSONViewer from '@/JSONViewer';
import LabelAndValue from '@/LabelAndValue';
import Panel from '@/Panel';
import Section from '@/RecordTree/Details/Section';
import { summarySx } from '@/RecordTree/Details/styles';
import TracePanel from '@/RecordTree/Details/TracePanel';
import LLMDetails from '@/RecordTree/Details/NodeSpecificDetails/LLMDetails';
import NodeDetailsContainer from '@/RecordTree/Details/NodeSpecificDetails/NodeDetailsContainer';
import { CommonDetailsProps } from '@/RecordTree/Details/NodeSpecificDetails/types';
import { SpanLLM, SpanRetriever } from '@/utils/Span';
import { StackTreeNode } from '@/utils/StackTreeNode';
import { RecordJSONRaw } from '@/utils/types';

type DetailsProps = {
selectedNode: StackTreeNode;
recordJSON: RecordJSONRaw;
};

export default function NodeDetails({ selectedNode, recordJSON }: DetailsProps) {
const { timeTaken: nodeTime, raw, selector } = selectedNode;

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { args, rets } = raw ?? {};

let returnValueDisplay = <Typography>No return values recorded</Typography>;
if (rets) {
if (typeof rets === 'string') returnValueDisplay = <Typography>{rets}</Typography>;
if (typeof rets === 'object') returnValueDisplay = <JSONViewer src={rets as object} />;
}
import RetrieverDetails from './NodeSpecificDetails/RetrieverDetails';

return (
<>
<Stack direction="row" sx={summarySx}>
<LabelAndValue label="Time taken" value={<Typography>{nodeTime} ms</Typography>} />
</Stack>
export default function NodeDetails({ selectedNode, recordJSON }: CommonDetailsProps) {
const { span } = selectedNode;

<Grid container gap={1}>
<Grid item xs={12}>
<Panel header="Span I/O">
<Stack gap={2}>
<Section title="Arguments" subtitle={selector ? `${selector}.args` : undefined}>
{args ? <JSONViewer src={args} /> : 'No arguments recorded.'}
</Section>
if (!span) return <NodeDetailsContainer selectedNode={selectedNode} recordJSON={recordJSON} />;
if (span instanceof SpanLLM)
return <LLMDetails selectedNode={selectedNode as StackTreeNode<SpanLLM>} recordJSON={recordJSON} />;

<Section title="Return values" subtitle={selector ? `${selector}.rets` : undefined}>
{returnValueDisplay}
</Section>
</Stack>
</Panel>
</Grid>
if (span instanceof SpanRetriever)
return <RetrieverDetails selectedNode={selectedNode as StackTreeNode<SpanRetriever>} recordJSON={recordJSON} />;

<Grid item xs={12}>
<TracePanel recordJSON={recordJSON} />
</Grid>
</Grid>
</>
);
return <NodeDetailsContainer selectedNode={selectedNode} recordJSON={recordJSON} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Typography } from '@mui/material';

import LabelAndValue from '@/LabelAndValue';
import NodeDetailsContainer from '@/RecordTree/Details/NodeSpecificDetails/NodeDetailsContainer';
import { CommonDetailsProps } from '@/RecordTree/Details/NodeSpecificDetails/types';
import { SpanLLM } from '@/utils/Span';

type LLMDetailsProps = CommonDetailsProps<SpanLLM>;

export default function LLMDetails({ selectedNode, recordJSON }: LLMDetailsProps) {
return (
<NodeDetailsContainer
selectedNode={selectedNode}
recordJSON={recordJSON}
labels={
<LabelAndValue label="Model name" value={<Typography>{selectedNode.span?.modelName ?? 'N/A'}</Typography>} />
}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Grid, Stack, Typography } from '@mui/material';
import { PropsWithChildren, ReactElement } from 'react';

import JSONViewer from '@/JSONViewer';
import LabelAndValue from '@/LabelAndValue';
import Panel from '@/Panel';
import { CommonDetailsProps } from '@/RecordTree/Details/NodeSpecificDetails/types';
import Section from '@/RecordTree/Details/Section';
import { summarySx } from '@/RecordTree/Details/styles';
import TracePanel from '@/RecordTree/Details/TracePanel';
import { toHumanSpanType } from '@/utils/Span';

type DetailsProps = PropsWithChildren<
CommonDetailsProps & {
labels?: ReactElement;
}
>;

export default function NodeDetailsContainer({ selectedNode, recordJSON, children, labels }: DetailsProps) {
const { timeTaken: nodeTime, raw, selector, span } = selectedNode;

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { args, rets } = raw ?? {};

let returnValueDisplay = <Typography>No return values recorded</Typography>;
if (rets) {
if (typeof rets === 'string') returnValueDisplay = <Typography>{rets}</Typography>;
if (typeof rets === 'object') returnValueDisplay = <JSONViewer src={rets as object} />;
}

return (
<>
<Stack direction="row" sx={summarySx}>
<LabelAndValue label="Span type" value={<Typography>{toHumanSpanType(span?.type)}</Typography>} />
<LabelAndValue label="Time taken" value={<Typography>{nodeTime} ms</Typography>} />
{labels}
</Stack>

<Grid container gap={1}>
<Grid item xs={12}>
<Panel header="Span I/O">
<Stack gap={2}>
<Section title="Arguments" subtitle={selector ? `${selector}.args` : undefined}>
{args ? <JSONViewer src={args} /> : 'No arguments recorded.'}
</Section>

<Section title="Return values" subtitle={selector ? `${selector}.rets` : undefined}>
{returnValueDisplay}
</Section>
</Stack>
</Panel>
</Grid>

{children}

<Grid item xs={12}>
<TracePanel recordJSON={recordJSON} />
</Grid>
</Grid>
</>
);
}
Loading

0 comments on commit 2b9c3c2

Please sign in to comment.