diff --git a/web/app/components/evaluation/__tests__/index.spec.tsx b/web/app/components/evaluation/__tests__/index.spec.tsx
index 5146e49535..1346b363ce 100644
--- a/web/app/components/evaluation/__tests__/index.spec.tsx
+++ b/web/app/components/evaluation/__tests__/index.spec.tsx
@@ -1,5 +1,6 @@
import { act, fireEvent, render, screen } from '@testing-library/react'
import Evaluation from '..'
+import ConditionsSection from '../components/conditions-section'
import { useEvaluationStore } from '../store'
const mockUseAvailableEvaluationMetrics = vi.hoisted(() => vi.fn())
@@ -150,6 +151,50 @@ describe('Evaluation', () => {
expect(screen.queryByPlaceholderText('evaluation.conditions.valuePlaceholder')).not.toBeInTheDocument()
})
+ it('should add a condition from grouped metric dropdown items', () => {
+ const resourceType = 'apps'
+ const resourceId = 'app-conditions-dropdown'
+ const store = useEvaluationStore.getState()
+
+ act(() => {
+ store.ensureResource(resourceType, resourceId)
+ store.setJudgeModel(resourceType, resourceId, 'openai::gpt-4o-mini')
+ store.addBuiltinMetric(resourceType, resourceId, 'faithfulness', [
+ { node_id: 'node-faithfulness', title: 'Retriever Node', type: 'retriever' },
+ ])
+ store.addCustomMetric(resourceType, resourceId)
+
+ const customMetric = useEvaluationStore.getState().resources['apps:app-conditions-dropdown'].metrics.find(metric => metric.kind === 'custom-workflow')!
+ store.setCustomMetricWorkflow(resourceType, resourceId, customMetric.id, {
+ workflowId: 'workflow-1',
+ workflowAppId: 'workflow-app-1',
+ workflowName: 'Review Workflow',
+ })
+ store.syncCustomMetricOutputs(resourceType, resourceId, customMetric.id, [{
+ id: 'reason',
+ valueType: 'string',
+ }])
+ })
+
+ render()
+
+ fireEvent.click(screen.getByRole('combobox', { name: 'evaluation.conditions.addCondition' }))
+
+ expect(screen.getByText('Faithfulness')).toBeInTheDocument()
+ expect(screen.getByText('Review Workflow')).toBeInTheDocument()
+ expect(screen.getByText('Retriever Node')).toBeInTheDocument()
+ expect(screen.getByText('reason')).toBeInTheDocument()
+ expect(screen.getByText('evaluation.conditions.valueTypes.number')).toBeInTheDocument()
+ expect(screen.getByText('evaluation.conditions.valueTypes.string')).toBeInTheDocument()
+
+ fireEvent.click(screen.getByRole('option', { name: /reason/i }))
+
+ const condition = useEvaluationStore.getState().resources['apps:app-conditions-dropdown'].judgmentConfig.conditions[0]
+
+ expect(condition.variableSelector).toEqual(['workflow-1', 'reason'])
+ expect(screen.getAllByText('Review Workflow').length).toBeGreaterThan(0)
+ })
+
it('should render the metric no-node empty state', () => {
mockUseAvailableEvaluationMetrics.mockReturnValue({
data: {
diff --git a/web/app/components/evaluation/__tests__/store.spec.ts b/web/app/components/evaluation/__tests__/store.spec.ts
index 8f6e557d5a..eb7cd74b4d 100644
--- a/web/app/components/evaluation/__tests__/store.spec.ts
+++ b/web/app/components/evaluation/__tests__/store.spec.ts
@@ -132,6 +132,35 @@ describe('evaluation store', () => {
expect(getAllowedOperators(state.metrics, condition.variableSelector)).toEqual(['=', '≠', '>', '<', '≥', '≤', 'is null', 'is not null'])
})
+ it('should add a condition from the selected custom metric output', () => {
+ const resourceType = 'apps'
+ const resourceId = 'app-condition-selector'
+ const store = useEvaluationStore.getState()
+ const config = getEvaluationMockConfig(resourceType)
+
+ store.ensureResource(resourceType, resourceId)
+ store.addCustomMetric(resourceType, resourceId)
+
+ const customMetric = useEvaluationStore.getState().resources['apps:app-condition-selector'].metrics.find(metric => metric.kind === 'custom-workflow')!
+ store.setCustomMetricWorkflow(resourceType, resourceId, customMetric.id, {
+ workflowId: config.workflowOptions[0].id,
+ workflowAppId: 'custom-workflow-app-id',
+ workflowName: config.workflowOptions[0].label,
+ })
+ store.syncCustomMetricOutputs(resourceType, resourceId, customMetric.id, [{
+ id: 'reason',
+ valueType: 'string',
+ }])
+
+ store.addCondition(resourceType, resourceId, [config.workflowOptions[0].id, 'reason'])
+
+ const condition = useEvaluationStore.getState().resources['apps:app-condition-selector'].judgmentConfig.conditions[0]
+
+ expect(condition.variableSelector).toEqual([config.workflowOptions[0].id, 'reason'])
+ expect(condition.comparisonOperator).toBe('contains')
+ expect(condition.value).toBeNull()
+ })
+
it('should clear values for operators without values', () => {
const resourceType = 'apps'
const resourceId = 'app-3'
diff --git a/web/app/components/evaluation/components/conditions-section/add-condition-select.tsx b/web/app/components/evaluation/components/conditions-section/add-condition-select.tsx
new file mode 100644
index 0000000000..9e370e565a
--- /dev/null
+++ b/web/app/components/evaluation/components/conditions-section/add-condition-select.tsx
@@ -0,0 +1,75 @@
+'use client'
+
+import type { ConditionMetricOptionGroup, EvaluationResourceProps } from '../../types'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectGroupLabel,
+ SelectItem,
+ SelectTrigger,
+} from '@/app/components/base/ui/select'
+import { cn } from '@/utils/classnames'
+import { useEvaluationStore } from '../../store'
+import { getConditionMetricValueTypeTranslationKey } from '../../utils'
+
+type AddConditionSelectProps = EvaluationResourceProps & {
+ metricOptionGroups: ConditionMetricOptionGroup[]
+ disabled: boolean
+}
+
+const AddConditionSelect = ({
+ resourceType,
+ resourceId,
+ metricOptionGroups,
+ disabled,
+}: AddConditionSelectProps) => {
+ const { t } = useTranslation('evaluation')
+ const addCondition = useEvaluationStore(state => state.addCondition)
+ const [selectKey, setSelectKey] = useState(0)
+
+ return (
+
+ )
+}
+
+export default AddConditionSelect
diff --git a/web/app/components/evaluation/components/conditions-section/condition-group.tsx b/web/app/components/evaluation/components/conditions-section/condition-group.tsx
index 2825488893..7cb73f1ca7 100644
--- a/web/app/components/evaluation/components/conditions-section/condition-group.tsx
+++ b/web/app/components/evaluation/components/conditions-section/condition-group.tsx
@@ -24,6 +24,8 @@ import { getAllowedOperators, requiresConditionValue, useEvaluationResource, use
import {
buildConditionMetricOptions,
getComparisonOperatorLabel,
+ getConditionMetricValueTypeTranslationKey,
+ groupConditionMetricOptions,
isSelectorEqual,
serializeVariableSelector,
} from '../../utils'
@@ -75,9 +77,9 @@ const ConditionMetricLabel = ({
- {metric.label}
+ {metric.itemLabel}
-
{metric.group}
+
{metric.groupLabel}
)
}
@@ -88,11 +90,9 @@ const ConditionMetricSelect = ({
placeholder,
onChange,
}: ConditionMetricSelectProps) => {
+ const { t } = useTranslation('evaluation')
const groupedMetricOptions = useMemo(() => {
- return Object.entries(metricOptions.reduce>((acc, option) => {
- acc[option.group] = [...(acc[option.group] ?? []), option]
- return acc
- }, {}))
+ return groupConditionMetricOptions(metricOptions)
}, [metricOptions])
return (
@@ -108,15 +108,17 @@ const ConditionMetricSelect = ({
- {groupedMetricOptions.map(([groupName, options]) => (
-
- {groupName}
- {options.map(option => (
+ {groupedMetricOptions.map(group => (
+
+ {group.label}
+ {group.options.map(option => (
-
+
- {option.label}
- {option.description}
+ {option.itemLabel}
+
+ {t(getConditionMetricValueTypeTranslationKey(option.valueType))}
+
))}
diff --git a/web/app/components/evaluation/components/conditions-section/index.tsx b/web/app/components/evaluation/components/conditions-section/index.tsx
index d00830c1c8..fb28a56a38 100644
--- a/web/app/components/evaluation/components/conditions-section/index.tsx
+++ b/web/app/components/evaluation/components/conditions-section/index.tsx
@@ -3,10 +3,10 @@
import type { EvaluationResourceProps } from '../../types'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
-import { cn } from '@/utils/classnames'
-import { useEvaluationResource, useEvaluationStore } from '../../store'
-import { buildConditionMetricOptions } from '../../utils'
+import { useEvaluationResource } from '../../store'
+import { buildConditionMetricOptions, groupConditionMetricOptions } from '../../utils'
import { InlineSectionHeader } from '../section-header'
+import AddConditionSelect from './add-condition-select'
import ConditionGroup from './condition-group'
const ConditionsSection = ({
@@ -15,8 +15,8 @@ const ConditionsSection = ({
}: EvaluationResourceProps) => {
const { t } = useTranslation('evaluation')
const resource = useEvaluationResource(resourceType, resourceId)
- const addCondition = useEvaluationStore(state => state.addCondition)
const conditionMetricOptions = useMemo(() => buildConditionMetricOptions(resource.metrics), [resource.metrics])
+ const groupedConditionMetricOptions = useMemo(() => groupConditionMetricOptions(conditionMetricOptions), [conditionMetricOptions])
const canAddCondition = conditionMetricOptions.length > 0
return (
@@ -37,18 +37,12 @@ const ConditionsSection = ({
resourceId={resourceId}
/>
)}
-
+ />
)
diff --git a/web/app/components/evaluation/store-utils.ts b/web/app/components/evaluation/store-utils.ts
index e29d2a6bdb..0f547e7488 100644
--- a/web/app/components/evaluation/store-utils.ts
+++ b/web/app/components/evaluation/store-utils.ts
@@ -330,8 +330,17 @@ export function createCustomMetric(): EvaluationMetric {
}
}
-export const buildConditionItem = (metrics: EvaluationMetric[]): JudgmentConditionItem => {
- const metricOption = buildConditionMetricOptions(metrics)[0]
+export const buildConditionItem = (
+ metrics: EvaluationMetric[],
+ variableSelector?: [string, string] | null,
+): JudgmentConditionItem => {
+ const metricOptions = buildConditionMetricOptions(metrics)
+ const metricOption = variableSelector
+ ? metricOptions.find(option =>
+ option.variableSelector[0] === variableSelector[0]
+ && option.variableSelector[1] === variableSelector[1],
+ ) ?? metricOptions[0]
+ : metricOptions[0]
const comparisonOperator = metricOption ? getDefaultComparisonOperator(metricOption.valueType) : 'is'
return {
diff --git a/web/app/components/evaluation/store.ts b/web/app/components/evaluation/store.ts
index 454c8ee569..cfc006b595 100644
--- a/web/app/components/evaluation/store.ts
+++ b/web/app/components/evaluation/store.ts
@@ -61,7 +61,11 @@ type EvaluationStore = {
patch: { inputVariableId?: string | null, outputVariableId?: string | null },
) => void
setConditionLogicalOperator: (resourceType: EvaluationResourceType, resourceId: string, logicalOperator: 'and' | 'or') => void
- addCondition: (resourceType: EvaluationResourceType, resourceId: string) => void
+ addCondition: (
+ resourceType: EvaluationResourceType,
+ resourceId: string,
+ variableSelector?: [string, string] | null,
+ ) => void
removeCondition: (resourceType: EvaluationResourceType, resourceId: string, conditionId: string) => void
updateConditionMetric: (resourceType: EvaluationResourceType, resourceId: string, conditionId: string, variableSelector: [string, string]) => void
updateConditionOperator: (resourceType: EvaluationResourceType, resourceId: string, conditionId: string, operator: ComparisonOperator) => void
@@ -270,13 +274,13 @@ export const useEvaluationStore = create((set, get) => ({
})),
}))
},
- addCondition: (resourceType, resourceId) => {
+ addCondition: (resourceType, resourceId, variableSelector) => {
set(state => ({
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
...resource,
judgmentConfig: {
...resource.judgmentConfig,
- conditions: [...resource.judgmentConfig.conditions, buildConditionItem(resource.metrics)],
+ conditions: [...resource.judgmentConfig.conditions, buildConditionItem(resource.metrics, variableSelector)],
},
})),
}))
diff --git a/web/app/components/evaluation/types.ts b/web/app/components/evaluation/types.ts
index acfcfdd11b..baa73f6c3f 100644
--- a/web/app/components/evaluation/types.ts
+++ b/web/app/components/evaluation/types.ts
@@ -112,13 +112,17 @@ export type JudgmentConfig = {
export type ConditionMetricOption = {
id: string
- group: string
- label: string
- description: string
+ groupLabel: string
+ itemLabel: string
valueType: ConditionMetricValueType
variableSelector: [string, string]
}
+export type ConditionMetricOptionGroup = {
+ label: string
+ options: ConditionMetricOption[]
+}
+
export type BatchTestRecord = {
id: string
fileName: string
diff --git a/web/app/components/evaluation/utils.ts b/web/app/components/evaluation/utils.ts
index 48b75d5c3e..3fba8158d6 100644
--- a/web/app/components/evaluation/utils.ts
+++ b/web/app/components/evaluation/utils.ts
@@ -2,6 +2,7 @@ import type { TFunction } from 'i18next'
import type {
ComparisonOperator,
ConditionMetricOption,
+ ConditionMetricOptionGroup,
ConditionMetricValueType,
EvaluationMetric,
} from './types'
@@ -69,9 +70,8 @@ export const buildConditionMetricOptions = (metrics: EvaluationMetric[]): Condit
return (metric.nodeInfoList ?? []).map((nodeInfo) => {
return {
id: `${nodeInfo.node_id}:${metric.optionId}`,
- group: nodeInfo.title,
- label: metric.label,
- description: nodeInfo.type,
+ groupLabel: metric.label,
+ itemLabel: nodeInfo.title || nodeInfo.node_id,
valueType: metric.valueType,
variableSelector: [nodeInfo.node_id, metric.optionId] as [string, string],
}
@@ -86,9 +86,8 @@ export const buildConditionMetricOptions = (metrics: EvaluationMetric[]): Condit
return customConfig.outputs.map((output) => {
return {
id: `${customConfig.workflowId}:${output.id}`,
- group: customConfig.workflowName ?? metric.label,
- label: output.id,
- description: customConfig.workflowName ?? metric.label,
+ groupLabel: customConfig.workflowName ?? metric.label,
+ itemLabel: output.id,
valueType: getMetricValueType(output.valueType),
variableSelector: [customConfig.workflowId, output.id] as [string, string],
}
@@ -96,6 +95,30 @@ export const buildConditionMetricOptions = (metrics: EvaluationMetric[]): Condit
})
}
+export const groupConditionMetricOptions = (metricOptions: ConditionMetricOption[]): ConditionMetricOptionGroup[] => {
+ const groups = metricOptions.reduce