diff --git a/agenta-web/src/components/pages/observability/ObservabilityDashboard.tsx b/agenta-web/src/components/pages/observability/ObservabilityDashboard.tsx index 859129d88a..11afa3d5e9 100644 --- a/agenta-web/src/components/pages/observability/ObservabilityDashboard.tsx +++ b/agenta-web/src/components/pages/observability/ObservabilityDashboard.tsx @@ -82,6 +82,9 @@ const ObservabilityDashboard = () => { const [isFilterColsDropdownOpen, setIsFilterColsDropdownOpen] = useState(false) const [isTestsetDrawerOpen, setIsTestsetDrawerOpen] = useState(false) const [selectedRowKeys, setSelectedRowKeys] = useState([]) + const [testsetDrawerData, setTestsetDrawerData] = useState< + {key: string; data: Record}[] + >([]) const [columns, setColumns] = useState>([ { title: "ID", @@ -496,6 +499,60 @@ const ObservabilityDashboard = () => { setSort({type, sorted, customRange}) }, []) + const getMatchingTracesByDataKeys = () => { + if (!traces?.length) return [] + + // step 1: extract data from the trace - skiped children for now + // TODO: get the traces children nodes as well + const extractData = traces + .filter((trace) => selectedRowKeys.includes(trace.key)) + .flatMap((trace) => { + const {data, key, ...rest} = trace + return {data, key} + }) + + // step 2: compare each array keys with each other to check similarities + const similarObjects = findSimilarObjects(extractData) + if (similarObjects.length > 0) { + setTestsetDrawerData(similarObjects as any) + setIsTestsetDrawerOpen(true) + } + } + + const findSimilarObjects = (array: any[]): any[][] => { + const getKeySignature = (obj: any): string => { + // Function to extract nested keys + const extractKeys = (obj: any, prefix = ""): string[] => + Object.keys(obj).flatMap((key) => { + const fullPath = prefix ? `${prefix}.${key}` : key + + return typeof obj[key] === "object" && + obj[key] !== null && + !Array.isArray(obj[key]) + ? extractKeys(obj[key], fullPath) + : fullPath + }) + + // Return the normalized keys as a string + return extractKeys(obj).sort().join(",") + } + + // Group objects by their key structure and return groups with more than one item + const groups = array.reduce>((acc, item) => { + const keySignature = getKeySignature(item) + acc[keySignature] = acc[keySignature] || [] + acc[keySignature].push(item) + return acc + }, {}) + + const mostSimilarGroup = Object.values(groups).reduce( + (maxGroup, currentGroup) => + currentGroup.length > maxGroup.length ? currentGroup : maxGroup, + [], + ) + return mostSimilarGroup + } + return (
Observability @@ -546,7 +603,7 @@ const ObservabilityDashboard = () => { Export as CSV
- + {isTestsetDrawerOpen && ( + { + setIsTestsetDrawerOpen(false) + setSelectedRowKeys([]) + setTestsetDrawerData([]) + }} + /> + )} {activeTrace && !!traces?.length && ( ({ editor: { @@ -38,81 +52,191 @@ const useStyles = createUseStyles((theme: JSSTheme) => ({ }, })) -const TestsetDrawer = ({open, setOpen}: any) => { +type Mapping = {data: string; column: string; newColumn?: string} +type TraceData = {key: string; data: Record} +type Props = { + onClose: () => void + data: TraceData[] +} & React.ComponentProps + +const TestsetDrawer = ({onClose, data, ...props}: Props) => { const {appTheme} = useAppTheme() const classes = useStyles() - // testset - const {testsets, isTestsetsLoading} = useLoadTestsetsList() - const [isNewTestset, setIsNewTestset] = useState(false) + const {testsets: listOfTestsets, isTestsetsLoading} = useLoadTestsetsList() + + const [isLoading, setIsLoading] = useState(false) + const [traceData, setTraceData] = useState(data.length > 0 ? data : []) const [testset, setTestset] = useState({name: "", id: ""}) const [testsetName, setTestsetName] = useState("") - // table - const [tableColumns, setTableColumns] = useState([]) + const [editorFormat, setEditorFormat] = useState("JSON") + const [tableColumns, setTableColumns] = useState([]) const [tableRows, setTableRows] = useState([]) - const [isShowlastFiveRows, setIsShowlastFiveRows] = useState(false) - // others - const [formatType, setFormatType] = useState("json") - const [isLoading, setIsLoading] = useState(false) + const [showLastFiveRows, setShowLastFiveRows] = useState(false) + const [dataPreview, setDataPreview] = useState(traceData[0]?.key || "") + const [mappingData, setMappingData] = useState([]) + const [preview, setPreview] = useState<{key: string; data: KeyValuePair[]}>({ + key: traceData[0]?.key || "", + data: [], + }) - // predifind options - const customSelectOptions = [ - {value: "create", label: "Create New Test set"}, - { - value: "divider", - label: , - className: "!p-0 !m-0 !min-h-0.5 !cursor-default", - disabled: true, - }, - ] + const isMapColumnExist = mappingData.some((mapping) => Boolean(mapping.column)) - const handleChange = (value: string) => {} + // predefind options + const customSelectOptions = useMemo( + () => [ + {value: "create", label: "Create New"}, + { + value: "divider", + label: , + className: "!p-0 !m-0 !min-h-0.5 !cursor-default", + disabled: true, + }, + ], + [], + ) - const onTestsetOptionChange = async (option: any) => { - if (option.value === "create") { - setIsNewTestset(true) + const onTestsetOptionChange = async (option: {label: string; value: string}) => { + const {value, label} = option - // reset previous test set data if bing selected - if (tableColumns) { + try { + if (value === "create" && tableColumns.length > 0) { setTableColumns([]) setTableRows([]) - setIsShowlastFiveRows(false) + setShowLastFiveRows(false) + setMappingData((prev) => prev.map((item) => ({...item, column: ""}))) } + + setTestset({name: label, id: value}) + + if (value && value !== "create") { + const data = await fetchTestset(value) + if (data?.csvdata?.length) { + setTableColumns(Object.keys(data.csvdata[0])) + setTableRows(data.csvdata) + } + } + } catch (error) { + message.error("Failed to laod Test sets!") + } + } + + const onRemoveTraceData = () => { + const removeTrace = traceData.filter((trace) => trace.key !== dataPreview) + setTraceData(removeTrace) + + if (removeTrace.length > 0) { + const currentIndex = traceData.findIndex((trace) => trace.key === dataPreview) + // [currentIndex]: Next option in list | [currentIndex - 1]: Previous option if next doesn't exist | [0]: Default to first option + const nextPreview = + removeTrace[currentIndex] || removeTrace[currentIndex - 1] || removeTrace[0] + + setDataPreview(nextPreview.key) } else { - setIsNewTestset(false) + setDataPreview("") } + } - setTestset({name: option.label, id: option.value}) + const formatDataPreview = useMemo(() => { + if (!traceData?.length) return "" - if (option.value && option.value !== "create") { - // fetch testset detailes and assign the columns and rows - const data = await fetchTestset(option.value) + const jsonObject = { + data: traceData.find((trace) => trace?.key === dataPreview)?.data || traceData[0]?.data, + } + if (!jsonObject) return "" - if (data) { - setTableColumns(Object.keys(data.csvdata[0]) as any) - setTableRows(data.csvdata) - } + try { + return editorFormat === "YAML" ? yaml.dump(jsonObject) : getStringOrJson(jsonObject) + } catch (error) { + message.error("Failed to convert JSON to YAML. Please ensure the data is valid.") + return getStringOrJson(jsonObject) + } + }, [editorFormat, traceData, dataPreview]) + + const mappingOptions = useMemo(() => { + const traceKeys = collectKeyPathsFromObject({data: traceData[0]?.data}) + + return traceKeys.map((item) => ({value: item, label: item})) + }, [traceData]) + + useEffect(() => { + // auto render mapping component with data + if (mappingOptions.length > 0) { + setMappingData(mappingOptions.map((item) => ({data: item.value, column: ""}))) } + }, [mappingOptions]) + + const onMappingOptionChange = ({ + pathName, + value, + idx, + }: { + pathName: keyof Mapping + value: string + idx: number + }) => { + setMappingData((prev) => { + const newData = [...prev] + newData[idx] = {...newData[idx], [pathName]: value} + return newData + }) + } + + const onPreviewOptionChange = (value: string) => { + const selectedTraceData = traceData.filter((trace) => trace.key === value) + const newTestsetData = mapAndConvertDataInCsvFormat(selectedTraceData) + + setPreview({key: value, data: newTestsetData}) + } + + useUpdateEffect(() => { + if (isMapColumnExist) { + onPreviewOptionChange(preview.key) + } + }, [mappingData]) + + const mapAndConvertDataInCsvFormat = (traceData: TraceData[]) => { + return traceData.map((item) => { + const formattedItem: Record = {} + + for (const mapping of mappingData) { + const keys = mapping.data.split(".") + let value = keys.reduce((acc: any, key) => acc?.[key], item) + + const targetKey = mapping.column === "create" ? mapping.newColumn : mapping.column + + if (targetKey) { + formattedItem[targetKey] = + typeof value === "string" ? value : JSON.stringify(value) + } + } + + return formattedItem + }) } const onSaveTestset = async () => { try { setIsLoading(true) + const newTestsetData = mapAndConvertDataInCsvFormat(traceData) + if (testset.id === "create") { if (!testsetName) { message.error("Please add a Test set name before saving it") return } - await createNewTestset(testsetName, [{input: null, correct_answer: null}]) + await createNewTestset(testsetName, newTestsetData) message.success("Test set created successfully") } else { - await updateTestset(testset.id as string, testset.name, tableRows) - + await updateTestset(testset.id as string, testset.name, [ + ...newTestsetData, + ...tableRows, + ]) message.success("Test set updated successfully") } - setOpen(false) + onClose() } catch (error) { console.log(error) message.error("Something went wrong. Please try again later") @@ -125,9 +249,10 @@ const TestsetDrawer = ({open, setOpen}: any) => { return (
- Spans selected 65 + Spans selected {traceData.length} + {/******* testset completed ✅ *******/}
Test set
@@ -136,19 +261,27 @@ const TestsetDrawer = ({open, setOpen}: any) => { labelInValue style={{width: 200}} placeholder="Select Test set" - value={{lable: testset.name, value: testset.id}} + value={ + testset.id ? {label: testset.name, value: testset.id} : undefined + } onChange={onTestsetOptionChange} options={[ ...customSelectOptions, - ...testsets.map((item: testset) => ({ + ...listOfTestsets.map((item: testset) => ({ value: item._id, label: item.name, })), ]} + filterOption={(input, option) => + (option?.label ?? "") + .toString() + .toLowerCase() + .includes(input.toLowerCase()) + } loading={isTestsetsLoading} /> - {isNewTestset && ( + {testset.id === "create" && (
{
+ {/******* data-preview completed ✅ *******/}
Data preview
+ onMappingOptionChange({pathName: "data", value, idx}) + } + options={mappingOptions} /> - + onMappingOptionChange({ + pathName: "column", + value, + idx, + }) + } + options={[ + ...(testset.id === "create" ? customSelectOptions : []), + ...tableColumns?.map((column) => ({ + value: column, + lable: column, + })), + ]} + /> + {data.column === "create" && ( +
+ + onMappingOptionChange({ + pathName: "newColumn", + value: e.target.value, + idx, + }) + } + placeholder="Test set name" + /> + +
+ )} +
+ +
))}
-
Preview - {tableColumns.length > 0 ? ( + {isMapColumnExist ? ( <>