mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 18:27:19 +08:00
feat(web): judgement condition
This commit is contained in:
parent
4d1499ef75
commit
5316372772
@ -1,6 +1,5 @@
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import Evaluation from '..'
|
||||
import { getEvaluationMockConfig } from '../mock'
|
||||
import { useEvaluationStore } from '../store'
|
||||
|
||||
const mockUseAvailableEvaluationMetrics = vi.hoisted(() => vi.fn())
|
||||
@ -121,22 +120,19 @@ describe('Evaluation', () => {
|
||||
const resourceType = 'apps'
|
||||
const resourceId = 'app-2'
|
||||
const store = useEvaluationStore.getState()
|
||||
const config = getEvaluationMockConfig(resourceType)
|
||||
|
||||
const stringField = config.fieldOptions.find(field => field.type === 'string')!
|
||||
let groupId = ''
|
||||
let itemId = ''
|
||||
let conditionId = ''
|
||||
|
||||
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.addCondition(resourceType, resourceId)
|
||||
|
||||
const group = useEvaluationStore.getState().resources['apps:app-2'].conditions[0]
|
||||
groupId = group.id
|
||||
itemId = group.items[0].id
|
||||
|
||||
store.updateConditionField(resourceType, resourceId, groupId, itemId, stringField.id)
|
||||
store.updateConditionOperator(resourceType, resourceId, groupId, itemId, 'contains')
|
||||
const condition = useEvaluationStore.getState().resources['apps:app-2'].conditions.conditions[0]
|
||||
conditionId = condition.id
|
||||
store.updateConditionOperator(resourceType, resourceId, conditionId, '=')
|
||||
})
|
||||
|
||||
let rerender: ReturnType<typeof render>['rerender']
|
||||
@ -147,7 +143,7 @@ describe('Evaluation', () => {
|
||||
expect(screen.getByPlaceholderText('evaluation.conditions.valuePlaceholder')).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
store.updateConditionOperator(resourceType, resourceId, groupId, itemId, 'is_empty')
|
||||
store.updateConditionOperator(resourceType, resourceId, conditionId, 'is null')
|
||||
rerender(<Evaluation resourceType={resourceType} resourceId={resourceId} />)
|
||||
})
|
||||
|
||||
@ -249,7 +245,7 @@ describe('Evaluation', () => {
|
||||
metric: 'context-precision',
|
||||
}],
|
||||
customized_metrics: null,
|
||||
judgement_conditions: null,
|
||||
judgment_config: null,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -31,6 +31,11 @@ describe('evaluation store', () => {
|
||||
workflowName: config.workflowOptions[0].label,
|
||||
})
|
||||
store.syncCustomMetricMappings(resourceType, resourceId, initialMetric!.id, ['query'])
|
||||
store.syncCustomMetricOutputs(resourceType, resourceId, initialMetric!.id, [{
|
||||
id: 'score',
|
||||
valueType: 'number',
|
||||
}])
|
||||
|
||||
const syncedMetric = useEvaluationStore.getState().resources['apps:app-1'].metrics.find(metric => metric.id === initialMetric!.id)
|
||||
store.updateCustomMetricMapping(resourceType, resourceId, initialMetric!.id, syncedMetric!.customConfig!.mappings[0].id, {
|
||||
outputVariableId: 'answer',
|
||||
@ -40,6 +45,7 @@ describe('evaluation store', () => {
|
||||
expect(isCustomMetricConfigured(configuredMetric!)).toBe(true)
|
||||
expect(configuredMetric!.customConfig!.workflowAppId).toBe('custom-workflow-app-id')
|
||||
expect(configuredMetric!.customConfig!.workflowName).toBe(config.workflowOptions[0].label)
|
||||
expect(configuredMetric!.customConfig!.outputs).toEqual([{ id: 'score', valueType: 'number' }])
|
||||
})
|
||||
|
||||
it('should only add one custom metric', () => {
|
||||
@ -77,7 +83,7 @@ describe('evaluation store', () => {
|
||||
expect(useEvaluationStore.getState().resources['apps:app-2'].metrics.some(metric => metric.id === addedMetric!.id)).toBe(false)
|
||||
})
|
||||
|
||||
it('should upsert builtin metric node selections', () => {
|
||||
it('should upsert builtin metric node selections and prune stale conditions', () => {
|
||||
const resourceType = 'apps'
|
||||
const resourceId = 'app-4'
|
||||
const store = useEvaluationStore.getState()
|
||||
@ -88,63 +94,78 @@ describe('evaluation store', () => {
|
||||
store.addBuiltinMetric(resourceType, resourceId, metricId, [
|
||||
{ node_id: 'node-1', title: 'Answer Node', type: 'answer' },
|
||||
])
|
||||
store.addCondition(resourceType, resourceId)
|
||||
|
||||
store.addBuiltinMetric(resourceType, resourceId, metricId, [
|
||||
{ node_id: 'node-2', title: 'Retriever Node', type: 'retriever' },
|
||||
])
|
||||
|
||||
const metric = useEvaluationStore.getState().resources['apps:app-4'].metrics.find(item => item.optionId === metricId)
|
||||
const state = useEvaluationStore.getState().resources['apps:app-4']
|
||||
const metric = state.metrics.find(item => item.optionId === metricId)
|
||||
|
||||
expect(metric?.nodeInfoList).toEqual([
|
||||
{ node_id: 'node-2', title: 'Retriever Node', type: 'retriever' },
|
||||
])
|
||||
expect(useEvaluationStore.getState().resources['apps:app-4'].metrics.filter(item => item.optionId === metricId)).toHaveLength(1)
|
||||
expect(state.metrics.filter(item => item.optionId === metricId)).toHaveLength(1)
|
||||
expect(state.conditions.conditions).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should update condition groups and adapt operators to field types', () => {
|
||||
const resourceType = 'datasets'
|
||||
const resourceId = 'dataset-1'
|
||||
it('should build numeric conditions from selected metrics', () => {
|
||||
const resourceType = 'apps'
|
||||
const resourceId = 'app-conditions'
|
||||
const store = useEvaluationStore.getState()
|
||||
const config = getEvaluationMockConfig(resourceType)
|
||||
|
||||
store.ensureResource(resourceType, resourceId)
|
||||
store.addBuiltinMetric(resourceType, resourceId, config.builtinMetrics[0].id, [
|
||||
{ node_id: 'node-answer', title: 'Answer Node', type: 'llm' },
|
||||
])
|
||||
store.setConditionLogicalOperator(resourceType, resourceId, 'or')
|
||||
store.addCondition(resourceType, resourceId)
|
||||
|
||||
const initialGroup = useEvaluationStore.getState().resources['datasets:dataset-1'].conditions[0]
|
||||
store.setConditionGroupOperator(resourceType, resourceId, initialGroup.id, 'or')
|
||||
store.addConditionGroup(resourceType, resourceId)
|
||||
const state = useEvaluationStore.getState().resources['apps:app-conditions']
|
||||
const condition = state.conditions.conditions[0]
|
||||
|
||||
const booleanField = config.fieldOptions.find(field => field.type === 'boolean')!
|
||||
const currentItem = useEvaluationStore.getState().resources['datasets:dataset-1'].conditions[0].items[0]
|
||||
store.updateConditionField(resourceType, resourceId, initialGroup.id, currentItem.id, booleanField.id)
|
||||
|
||||
const updatedGroup = useEvaluationStore.getState().resources['datasets:dataset-1'].conditions[0]
|
||||
expect(updatedGroup.logicalOperator).toBe('or')
|
||||
expect(updatedGroup.items[0].operator).toBe('is')
|
||||
expect(getAllowedOperators(resourceType, booleanField.id)).toEqual(['is', 'is_not'])
|
||||
expect(state.conditions.logicalOperator).toBe('or')
|
||||
expect(condition.variableSelector).toEqual(['node-answer', 'answer-correctness'])
|
||||
expect(condition.comparisonOperator).toBe('=')
|
||||
expect(getAllowedOperators(state.metrics, condition.variableSelector)).toEqual(['=', '≠', '>', '<', '≥', '≤', 'is null', 'is not null'])
|
||||
})
|
||||
|
||||
it('should clear values for empty operators', () => {
|
||||
it('should clear values for operators without values', () => {
|
||||
const resourceType = 'apps'
|
||||
const resourceId = 'app-3'
|
||||
const store = useEvaluationStore.getState()
|
||||
const config = getEvaluationMockConfig(resourceType)
|
||||
|
||||
store.ensureResource(resourceType, resourceId)
|
||||
store.addCustomMetric(resourceType, resourceId)
|
||||
|
||||
const stringField = config.fieldOptions.find(field => field.type === 'string')!
|
||||
const item = useEvaluationStore.getState().resources['apps:app-3'].conditions[0].items[0]
|
||||
const customMetric = useEvaluationStore.getState().resources['apps:app-3'].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)
|
||||
|
||||
store.updateConditionField(resourceType, resourceId, useEvaluationStore.getState().resources['apps:app-3'].conditions[0].id, item.id, stringField.id)
|
||||
store.updateConditionOperator(resourceType, resourceId, useEvaluationStore.getState().resources['apps:app-3'].conditions[0].id, item.id, 'is_empty')
|
||||
const condition = useEvaluationStore.getState().resources['apps:app-3'].conditions.conditions[0]
|
||||
|
||||
const updatedItem = useEvaluationStore.getState().resources['apps:app-3'].conditions[0].items[0]
|
||||
store.updateConditionMetric(resourceType, resourceId, condition.id, [config.workflowOptions[0].id, 'reason'])
|
||||
store.updateConditionValue(resourceType, resourceId, condition.id, 'needs follow-up')
|
||||
store.updateConditionOperator(resourceType, resourceId, condition.id, 'empty')
|
||||
|
||||
expect(getAllowedOperators(resourceType, stringField.id)).toEqual(['contains', 'not_contains', 'is', 'is_not', 'is_empty', 'is_not_empty'])
|
||||
expect(requiresConditionValue('is_empty')).toBe(false)
|
||||
expect(updatedItem.value).toBeNull()
|
||||
const updatedCondition = useEvaluationStore.getState().resources['apps:app-3'].conditions.conditions[0]
|
||||
|
||||
expect(requiresConditionValue('empty')).toBe(false)
|
||||
expect(updatedCondition.value).toBeNull()
|
||||
})
|
||||
|
||||
it('should hydrate resource state from evaluation config', () => {
|
||||
it('should hydrate resource state from judgment_config', () => {
|
||||
const resourceType = 'apps'
|
||||
const resourceId = 'app-5'
|
||||
const store = useEvaluationStore.getState()
|
||||
@ -162,15 +183,19 @@ describe('evaluation store', () => {
|
||||
input_fields: {
|
||||
query: 'answer',
|
||||
},
|
||||
},
|
||||
judgement_conditions: [{
|
||||
logical_operator: 'or',
|
||||
items: [{
|
||||
field_id: 'system.has_context',
|
||||
operator: 'is',
|
||||
value: true,
|
||||
output_fields: [{
|
||||
variable: 'reason',
|
||||
value_type: 'string',
|
||||
}],
|
||||
}],
|
||||
},
|
||||
judgment_config: {
|
||||
logical_operator: 'or',
|
||||
conditions: [{
|
||||
variable_selector: ['node-1', 'faithfulness'],
|
||||
comparison_operator: '≥',
|
||||
value: '0.9',
|
||||
}],
|
||||
},
|
||||
}
|
||||
|
||||
store.ensureResource(resourceType, resourceId)
|
||||
@ -206,11 +231,12 @@ describe('evaluation store', () => {
|
||||
expect(hydratedState.metrics[1].customConfig?.workflowId).toBe('workflow-precision-review')
|
||||
expect(hydratedState.metrics[1].customConfig?.mappings[0].inputVariableId).toBe('query')
|
||||
expect(hydratedState.metrics[1].customConfig?.mappings[0].outputVariableId).toBe('answer')
|
||||
expect(hydratedState.conditions[0].logicalOperator).toBe('or')
|
||||
expect(hydratedState.conditions[0].items[0]).toMatchObject({
|
||||
fieldId: 'system.has_context',
|
||||
operator: 'is',
|
||||
value: true,
|
||||
expect(hydratedState.metrics[1].customConfig?.outputs).toEqual([{ id: 'reason', valueType: 'string' }])
|
||||
expect(hydratedState.conditions.logicalOperator).toBe('or')
|
||||
expect(hydratedState.conditions.conditions[0]).toMatchObject({
|
||||
variableSelector: ['node-1', 'faithfulness'],
|
||||
comparisonOperator: '≥',
|
||||
value: '0.9',
|
||||
})
|
||||
expect(hydratedState.activeBatchTab).toBe('history')
|
||||
expect(hydratedState.uploadedFileName).toBe('batch.csv')
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
|
||||
import type {
|
||||
ComparisonOperator,
|
||||
EvaluationFieldOption,
|
||||
ConditionMetricOption,
|
||||
EvaluationResourceProps,
|
||||
JudgmentConditionGroup,
|
||||
JudgmentConditionItem,
|
||||
} from '../../types'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import {
|
||||
@ -20,79 +20,103 @@ import {
|
||||
SelectValue,
|
||||
} from '@/app/components/base/ui/select'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { getEvaluationMockConfig } from '../../mock'
|
||||
import { getAllowedOperators, requiresConditionValue, useEvaluationStore } from '../../store'
|
||||
import { getFieldTypeIconClassName, getOperatorLabel, groupFieldOptions } from '../../utils'
|
||||
import { getAllowedOperators, requiresConditionValue, useEvaluationResource, useEvaluationStore } from '../../store'
|
||||
import {
|
||||
buildConditionMetricOptions,
|
||||
getComparisonOperatorLabel,
|
||||
isSelectorEqual,
|
||||
serializeVariableSelector,
|
||||
} from '../../utils'
|
||||
|
||||
type ConditionFieldLabelProps = {
|
||||
field?: EvaluationFieldOption
|
||||
type ConditionMetricLabelProps = {
|
||||
metric?: ConditionMetricOption
|
||||
placeholder: string
|
||||
}
|
||||
|
||||
type ConditionFieldSelectProps = {
|
||||
field?: EvaluationFieldOption
|
||||
fieldOptions: EvaluationFieldOption[]
|
||||
type ConditionMetricSelectProps = {
|
||||
metric?: ConditionMetricOption
|
||||
metricOptions: ConditionMetricOption[]
|
||||
placeholder: string
|
||||
onChange: (fieldId: string) => void
|
||||
onChange: (variableSelector: [string, string]) => void
|
||||
}
|
||||
|
||||
type ConditionOperatorSelectProps = {
|
||||
field?: EvaluationFieldOption
|
||||
operator: ComparisonOperator
|
||||
operators: ComparisonOperator[]
|
||||
onChange: (operator: ComparisonOperator) => void
|
||||
}
|
||||
|
||||
type FieldValueInputProps = {
|
||||
field?: EvaluationFieldOption
|
||||
operator: ComparisonOperator
|
||||
value: string | number | boolean | null
|
||||
onChange: (value: string | number | boolean | null) => void
|
||||
type ConditionValueInputProps = {
|
||||
metric?: ConditionMetricOption
|
||||
condition: JudgmentConditionItem
|
||||
onChange: (value: string | string[] | boolean | null) => void
|
||||
}
|
||||
|
||||
type ConditionGroupProps = EvaluationResourceProps & {
|
||||
group: JudgmentConditionGroup
|
||||
index: number
|
||||
type ConditionGroupProps = EvaluationResourceProps
|
||||
|
||||
const getMetricValueTypeIconClassName = (valueType: ConditionMetricOption['valueType']) => {
|
||||
if (valueType === 'number')
|
||||
return 'i-ri-hashtag'
|
||||
|
||||
if (valueType === 'boolean')
|
||||
return 'i-ri-checkbox-circle-line'
|
||||
|
||||
return 'i-ri-bar-chart-box-line'
|
||||
}
|
||||
|
||||
const ConditionFieldLabel = ({
|
||||
field,
|
||||
const ConditionMetricLabel = ({
|
||||
metric,
|
||||
placeholder,
|
||||
}: ConditionFieldLabelProps) => {
|
||||
if (!field)
|
||||
}: ConditionMetricLabelProps) => {
|
||||
if (!metric)
|
||||
return <span className="px-1 system-sm-regular text-components-input-text-placeholder">{placeholder}</span>
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 items-center gap-2 px-1">
|
||||
<div className="inline-flex h-6 min-w-0 items-center gap-1 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark pr-1.5 pl-[5px] shadow-xs">
|
||||
<span className={cn(getFieldTypeIconClassName(field.type), 'h-3 w-3 shrink-0 text-text-secondary')} />
|
||||
<span className="truncate system-xs-medium text-text-secondary">{field.label}</span>
|
||||
<span className={cn(getMetricValueTypeIconClassName(metric.valueType), 'h-3 w-3 shrink-0 text-text-secondary')} />
|
||||
<span className="truncate system-xs-medium text-text-secondary">{metric.label}</span>
|
||||
</div>
|
||||
<span className="shrink-0 system-xs-regular text-text-tertiary">{field.type}</span>
|
||||
<span className="shrink-0 system-xs-regular text-text-tertiary">{metric.group}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ConditionFieldSelect = ({
|
||||
field,
|
||||
fieldOptions,
|
||||
const ConditionMetricSelect = ({
|
||||
metric,
|
||||
metricOptions,
|
||||
placeholder,
|
||||
onChange,
|
||||
}: ConditionFieldSelectProps) => {
|
||||
}: ConditionMetricSelectProps) => {
|
||||
const groupedMetricOptions = useMemo(() => {
|
||||
return Object.entries(metricOptions.reduce<Record<string, ConditionMetricOption[]>>((acc, option) => {
|
||||
acc[option.group] = [...(acc[option.group] ?? []), option]
|
||||
return acc
|
||||
}, {}))
|
||||
}, [metricOptions])
|
||||
|
||||
return (
|
||||
<Select value={field?.id ?? ''} onValueChange={value => value && onChange(value)}>
|
||||
<Select
|
||||
value={serializeVariableSelector(metric?.variableSelector)}
|
||||
onValueChange={(value) => {
|
||||
const nextMetric = metricOptions.find(option => serializeVariableSelector(option.variableSelector) === value)
|
||||
if (nextMetric)
|
||||
onChange(nextMetric.variableSelector)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-auto bg-transparent px-1 py-1 hover:bg-transparent focus-visible:bg-transparent">
|
||||
<ConditionFieldLabel field={field} placeholder={placeholder} />
|
||||
<ConditionMetricLabel metric={metric} placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-[320px]">
|
||||
{groupFieldOptions(fieldOptions).map(([groupName, fields]) => (
|
||||
<SelectContent popupClassName="w-[360px]">
|
||||
{groupedMetricOptions.map(([groupName, options]) => (
|
||||
<SelectGroup key={groupName}>
|
||||
<SelectGroupLabel className="px-3 pt-2 pb-1 system-xs-medium-uppercase text-text-tertiary">{groupName}</SelectGroupLabel>
|
||||
{fields.map(option => (
|
||||
<SelectItem key={option.id} value={option.id}>
|
||||
{options.map(option => (
|
||||
<SelectItem key={option.id} value={serializeVariableSelector(option.variableSelector)}>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className={cn(getFieldTypeIconClassName(option.type), 'h-3.5 w-3.5 shrink-0 text-text-tertiary')} />
|
||||
<span className={cn(getMetricValueTypeIconClassName(option.valueType), 'h-3.5 w-3.5 shrink-0 text-text-tertiary')} />
|
||||
<span className="truncate">{option.label}</span>
|
||||
<span className="shrink-0 text-text-quaternary">{option.description}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
@ -104,22 +128,21 @@ const ConditionFieldSelect = ({
|
||||
}
|
||||
|
||||
const ConditionOperatorSelect = ({
|
||||
field,
|
||||
operator,
|
||||
operators,
|
||||
onChange,
|
||||
}: ConditionOperatorSelectProps) => {
|
||||
const { t } = useTranslation('evaluation')
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Select value={operator} onValueChange={value => value && onChange(value as ComparisonOperator)}>
|
||||
<SelectTrigger className="h-8 w-auto min-w-[88px] gap-1 rounded-md bg-transparent px-1.5 py-0 hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt">
|
||||
<span className="truncate system-xs-medium text-text-secondary">{getOperatorLabel(operator, field?.type, t)}</span>
|
||||
<span className="truncate system-xs-medium text-text-secondary">{getComparisonOperatorLabel(operator, t)}</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[1002]" popupClassName="w-[240px] bg-components-panel-bg-blur backdrop-blur-[10px]">
|
||||
{operators.map(nextOperator => (
|
||||
<SelectItem key={nextOperator} value={nextOperator}>
|
||||
{getOperatorLabel(nextOperator, field?.type, t)}
|
||||
{getComparisonOperatorLabel(nextOperator, t)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@ -127,21 +150,20 @@ const ConditionOperatorSelect = ({
|
||||
)
|
||||
}
|
||||
|
||||
const FieldValueInput = ({
|
||||
field,
|
||||
operator,
|
||||
value,
|
||||
const ConditionValueInput = ({
|
||||
metric,
|
||||
condition,
|
||||
onChange,
|
||||
}: FieldValueInputProps) => {
|
||||
}: ConditionValueInputProps) => {
|
||||
const { t } = useTranslation('evaluation')
|
||||
|
||||
if (!field || !requiresConditionValue(operator))
|
||||
if (!metric || !requiresConditionValue(condition.comparisonOperator))
|
||||
return null
|
||||
|
||||
if (field.type === 'boolean') {
|
||||
if (metric.valueType === 'boolean') {
|
||||
return (
|
||||
<div className="px-2 py-1.5">
|
||||
<Select value={value === null ? '' : String(value)} onValueChange={nextValue => onChange(nextValue === 'true')}>
|
||||
<Select value={condition.value === null ? '' : String(condition.value)} onValueChange={nextValue => onChange(nextValue === 'true')}>
|
||||
<SelectTrigger className="bg-transparent hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt">
|
||||
<SelectValue placeholder={t('conditions.selectValue')} />
|
||||
</SelectTrigger>
|
||||
@ -154,38 +176,27 @@ const FieldValueInput = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'enum') {
|
||||
return (
|
||||
<div className="px-2 py-1.5">
|
||||
<Select value={typeof value === 'string' ? value : ''} onValueChange={nextValue => onChange(nextValue)}>
|
||||
<SelectTrigger className="bg-transparent hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt">
|
||||
<SelectValue placeholder={t('conditions.selectValue')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(field.options ?? []).map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const isMultiValue = condition.comparisonOperator === 'in' || condition.comparisonOperator === 'not in'
|
||||
const inputValue = Array.isArray(condition.value)
|
||||
? condition.value.join(', ')
|
||||
: typeof condition.value === 'boolean'
|
||||
? ''
|
||||
: condition.value ?? ''
|
||||
|
||||
return (
|
||||
<div className="px-2 py-1.5">
|
||||
<Input
|
||||
type={field.type === 'number' ? 'number' : 'text'}
|
||||
value={value === null || typeof value === 'boolean' ? '' : value}
|
||||
type={metric.valueType === 'number' && !isMultiValue ? 'number' : 'text'}
|
||||
value={inputValue}
|
||||
className="border-none bg-transparent shadow-none hover:border-none hover:bg-state-base-hover-alt focus:border-none focus:bg-state-base-hover-alt focus:shadow-none"
|
||||
placeholder={t('conditions.valuePlaceholder')}
|
||||
onChange={(e) => {
|
||||
if (field.type === 'number') {
|
||||
const nextValue = e.target.value
|
||||
onChange(nextValue === '' ? null : Number(nextValue))
|
||||
if (isMultiValue) {
|
||||
onChange(e.target.value.split(',').map(item => item.trim()).filter(Boolean))
|
||||
return
|
||||
}
|
||||
|
||||
onChange(e.target.value)
|
||||
onChange(e.target.value === '' ? null : e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -195,20 +206,17 @@ const FieldValueInput = ({
|
||||
const ConditionGroup = ({
|
||||
resourceType,
|
||||
resourceId,
|
||||
group,
|
||||
index,
|
||||
}: ConditionGroupProps) => {
|
||||
const { t } = useTranslation('evaluation')
|
||||
const config = getEvaluationMockConfig(resourceType)
|
||||
const resource = useEvaluationResource(resourceType, resourceId)
|
||||
const metricOptions = useMemo(() => buildConditionMetricOptions(resource.metrics), [resource.metrics])
|
||||
const logicalLabels = {
|
||||
and: t('conditions.logical.and'),
|
||||
or: t('conditions.logical.or'),
|
||||
}
|
||||
const removeConditionGroup = useEvaluationStore(state => state.removeConditionGroup)
|
||||
const setConditionGroupOperator = useEvaluationStore(state => state.setConditionGroupOperator)
|
||||
const addConditionItem = useEvaluationStore(state => state.addConditionItem)
|
||||
const removeConditionItem = useEvaluationStore(state => state.removeConditionItem)
|
||||
const updateConditionField = useEvaluationStore(state => state.updateConditionField)
|
||||
const setConditionLogicalOperator = useEvaluationStore(state => state.setConditionLogicalOperator)
|
||||
const removeCondition = useEvaluationStore(state => state.removeCondition)
|
||||
const updateConditionMetric = useEvaluationStore(state => state.updateConditionMetric)
|
||||
const updateConditionOperator = useEvaluationStore(state => state.updateConditionOperator)
|
||||
const updateConditionValue = useEvaluationStore(state => state.updateConditionValue)
|
||||
|
||||
@ -216,7 +224,6 @@ const ConditionGroup = ({
|
||||
<div className="rounded-2xl border border-divider-subtle bg-components-card-bg p-4">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge>{t('conditions.groupLabel', { index: index + 1 })}</Badge>
|
||||
<div className="flex rounded-lg border border-divider-subtle bg-background-default-subtle p-1">
|
||||
{(['and', 'or'] as const).map(operator => (
|
||||
<button
|
||||
@ -224,65 +231,50 @@ const ConditionGroup = ({
|
||||
type="button"
|
||||
className={cn(
|
||||
'rounded-md px-3 py-1.5 system-xs-medium-uppercase',
|
||||
group.logicalOperator === operator
|
||||
resource.conditions.logicalOperator === operator
|
||||
? 'bg-components-card-bg text-text-primary shadow-xs'
|
||||
: 'text-text-tertiary',
|
||||
)}
|
||||
onClick={() => setConditionGroupOperator(resourceType, resourceId, group.id, operator)}
|
||||
onClick={() => setConditionLogicalOperator(resourceType, resourceId, operator)}
|
||||
>
|
||||
{logicalLabels[operator]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="small" variant="ghost" onClick={() => addConditionItem(resourceType, resourceId, group.id)}>
|
||||
<span aria-hidden="true" className="mr-1 i-ri-add-line h-4 w-4" />
|
||||
{t('conditions.addCondition')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
aria-label={t('conditions.removeGroup')}
|
||||
onClick={() => removeConditionGroup(resourceType, resourceId, group.id)}
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-delete-bin-line h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{group.items.map((item) => {
|
||||
const field = config.fieldOptions.find(option => option.id === item.fieldId)
|
||||
const allowedOperators = getAllowedOperators(resourceType, item.fieldId)
|
||||
const showValue = !!field && requiresConditionValue(item.operator)
|
||||
{resource.conditions.conditions.map((condition) => {
|
||||
const metric = metricOptions.find(option => isSelectorEqual(option.variableSelector, condition.variableSelector))
|
||||
const allowedOperators = getAllowedOperators(resource.metrics, condition.variableSelector)
|
||||
const showValue = !!metric && requiresConditionValue(condition.comparisonOperator)
|
||||
|
||||
return (
|
||||
<div key={item.id} className="flex items-start overflow-hidden rounded-lg">
|
||||
<div key={condition.id} className="flex items-start overflow-hidden rounded-lg">
|
||||
<div className="min-w-0 flex-1 rounded-lg bg-components-input-bg-normal">
|
||||
<div className="flex items-center gap-0 pr-1">
|
||||
<div className="min-w-0 flex-1 py-1">
|
||||
<ConditionFieldSelect
|
||||
field={field}
|
||||
fieldOptions={config.fieldOptions}
|
||||
<ConditionMetricSelect
|
||||
metric={metric}
|
||||
metricOptions={metricOptions}
|
||||
placeholder={t('conditions.fieldPlaceholder')}
|
||||
onChange={value => updateConditionField(resourceType, resourceId, group.id, item.id, value)}
|
||||
onChange={value => updateConditionMetric(resourceType, resourceId, condition.id, value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-3 w-px bg-divider-regular" />
|
||||
<ConditionOperatorSelect
|
||||
field={field}
|
||||
operator={item.operator}
|
||||
operator={condition.comparisonOperator}
|
||||
operators={allowedOperators}
|
||||
onChange={value => updateConditionOperator(resourceType, resourceId, group.id, item.id, value)}
|
||||
onChange={value => updateConditionOperator(resourceType, resourceId, condition.id, value)}
|
||||
/>
|
||||
</div>
|
||||
{showValue && (
|
||||
<div className="border-t border-divider-subtle">
|
||||
<FieldValueInput
|
||||
field={field}
|
||||
operator={item.operator}
|
||||
value={item.value}
|
||||
onChange={value => updateConditionValue(resourceType, resourceId, group.id, item.id, value)}
|
||||
<ConditionValueInput
|
||||
metric={metric}
|
||||
condition={condition}
|
||||
onChange={value => updateConditionValue(resourceType, resourceId, condition.id, value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -292,7 +284,7 @@ const ConditionGroup = ({
|
||||
size="small"
|
||||
variant="ghost"
|
||||
aria-label={t('conditions.removeCondition')}
|
||||
onClick={() => removeConditionItem(resourceType, resourceId, group.id, item.id)}
|
||||
onClick={() => removeCondition(resourceType, resourceId, condition.id)}
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-close-line h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
'use client'
|
||||
|
||||
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 { InlineSectionHeader } from '../section-header'
|
||||
import ConditionGroup from './condition-group'
|
||||
|
||||
@ -13,8 +15,9 @@ const ConditionsSection = ({
|
||||
}: EvaluationResourceProps) => {
|
||||
const { t } = useTranslation('evaluation')
|
||||
const resource = useEvaluationResource(resourceType, resourceId)
|
||||
const addConditionGroup = useEvaluationStore(state => state.addConditionGroup)
|
||||
const canAddCondition = resource.metrics.length > 0
|
||||
const addCondition = useEvaluationStore(state => state.addCondition)
|
||||
const conditionMetricOptions = useMemo(() => buildConditionMetricOptions(resource.metrics), [resource.metrics])
|
||||
const canAddCondition = conditionMetricOptions.length > 0
|
||||
|
||||
return (
|
||||
<section className="max-w-[700px] py-4">
|
||||
@ -23,20 +26,17 @@ const ConditionsSection = ({
|
||||
tooltip={t('conditions.description')}
|
||||
/>
|
||||
<div className="mt-2 space-y-4">
|
||||
{resource.conditions.length === 0 && (
|
||||
{resource.conditions.conditions.length === 0 && (
|
||||
<div className="rounded-xl bg-background-section px-3 py-3 system-xs-regular text-text-tertiary">
|
||||
{t('conditions.emptyDescription')}
|
||||
</div>
|
||||
)}
|
||||
{resource.conditions.map((group, index) => (
|
||||
{resource.conditions.conditions.length > 0 && (
|
||||
<ConditionGroup
|
||||
key={group.id}
|
||||
resourceType={resourceType}
|
||||
resourceId={resourceId}
|
||||
group={group}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
@ -44,7 +44,7 @@ const ConditionsSection = ({
|
||||
!canAddCondition && 'cursor-not-allowed text-components-button-secondary-accent-text-disabled',
|
||||
)}
|
||||
disabled={!canAddCondition}
|
||||
onClick={() => addConditionGroup(resourceType, resourceId)}
|
||||
onClick={() => addCondition(resourceType, resourceId)}
|
||||
>
|
||||
<span aria-hidden="true" className="mr-1 i-ri-add-line h-4 w-4" />
|
||||
{t('conditions.addCondition')}
|
||||
|
||||
@ -156,6 +156,7 @@ const createMetric = (): EvaluationMetric => ({
|
||||
kind: 'custom-workflow',
|
||||
label: 'Custom Evaluator',
|
||||
description: 'Map workflow variables to your evaluation inputs.',
|
||||
valueType: 'number',
|
||||
customConfig: {
|
||||
workflowId: 'workflow-1',
|
||||
workflowAppId: 'workflow-app-1',
|
||||
@ -169,6 +170,7 @@ const createMetric = (): EvaluationMetric => ({
|
||||
inputVariableId: 'retrieved_context',
|
||||
outputVariableId: 'current-node.score',
|
||||
}],
|
||||
outputs: [],
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -73,6 +73,7 @@ const CustomMetricEditorCard = ({
|
||||
const { t } = useTranslation('evaluation')
|
||||
const setCustomMetricWorkflow = useEvaluationStore(state => state.setCustomMetricWorkflow)
|
||||
const syncCustomMetricMappings = useEvaluationStore(state => state.syncCustomMetricMappings)
|
||||
const syncCustomMetricOutputs = useEvaluationStore(state => state.syncCustomMetricOutputs)
|
||||
const updateCustomMetricMapping = useEvaluationStore(state => state.updateCustomMetricMapping)
|
||||
const { data: selectedWorkflow } = useAppWorkflow(metric.customConfig?.workflowAppId ?? '')
|
||||
const { data: currentAppWorkflow } = useAppWorkflow(resourceType === 'apps' ? resourceId : '')
|
||||
@ -126,6 +127,23 @@ const CustomMetricEditorCard = ({
|
||||
syncCustomMetricMappings(resourceType, resourceId, metric.id, inputVariableIds)
|
||||
}, [inputVariableIds, metric.customConfig?.mappings, metric.customConfig?.workflowId, metric.id, resourceId, resourceType, syncCustomMetricMappings])
|
||||
|
||||
useEffect(() => {
|
||||
if (!metric.customConfig?.workflowId)
|
||||
return
|
||||
|
||||
const currentOutputs = metric.customConfig.outputs
|
||||
if (
|
||||
currentOutputs.length === workflowOutputs.length
|
||||
&& currentOutputs.every((output, index) =>
|
||||
output.id === workflowOutputs[index]?.id && output.valueType === workflowOutputs[index]?.valueType,
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
syncCustomMetricOutputs(resourceType, resourceId, metric.id, workflowOutputs)
|
||||
}, [metric.customConfig?.outputs, metric.customConfig?.workflowId, metric.id, resourceId, resourceType, syncCustomMetricOutputs, workflowOutputs])
|
||||
|
||||
if (!metric.customConfig)
|
||||
return null
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ export const buildMetricOption = (metricId: string): MetricOption => ({
|
||||
id: metricId,
|
||||
label: humanizeMetricId(metricId),
|
||||
description: '',
|
||||
valueType: 'number',
|
||||
})
|
||||
|
||||
export const getMetricVisual = (metricId: string): { icon: string, tone: MetricVisualTone } => {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type {
|
||||
ComparisonOperator,
|
||||
EvaluationFieldOption,
|
||||
EvaluationMockConfig,
|
||||
EvaluationResourceType,
|
||||
@ -29,31 +28,37 @@ const builtinMetrics: MetricOption[] = [
|
||||
id: 'answer-correctness',
|
||||
label: 'Answer Correctness',
|
||||
description: 'Compares the response with the expected answer and scores factual alignment.',
|
||||
valueType: 'number',
|
||||
},
|
||||
{
|
||||
id: 'faithfulness',
|
||||
label: 'Faithfulness',
|
||||
description: 'Checks whether the answer stays grounded in the retrieved evidence.',
|
||||
valueType: 'number',
|
||||
},
|
||||
{
|
||||
id: 'relevance',
|
||||
label: 'Relevance',
|
||||
description: 'Evaluates how directly the answer addresses the original request.',
|
||||
valueType: 'number',
|
||||
},
|
||||
{
|
||||
id: 'latency',
|
||||
label: 'Latency',
|
||||
description: 'Captures runtime responsiveness for the full execution path.',
|
||||
valueType: 'number',
|
||||
},
|
||||
{
|
||||
id: 'token-usage',
|
||||
label: 'Token Usage',
|
||||
description: 'Tracks prompt and completion token consumption for the run.',
|
||||
valueType: 'number',
|
||||
},
|
||||
{
|
||||
id: 'tool-success-rate',
|
||||
label: 'Tool Success Rate',
|
||||
description: 'Measures whether each required tool invocation finishes without failure.',
|
||||
valueType: 'number',
|
||||
},
|
||||
]
|
||||
|
||||
@ -62,16 +67,19 @@ const pipelineBuiltinMetrics: MetricOption[] = [
|
||||
id: 'context-precision',
|
||||
label: 'Context Precision',
|
||||
description: 'Measures whether retrieved chunks stay tightly aligned to the request.',
|
||||
valueType: 'number',
|
||||
},
|
||||
{
|
||||
id: 'context-recall',
|
||||
label: 'Context Recall',
|
||||
description: 'Checks whether the retrieval result includes the evidence needed to answer.',
|
||||
valueType: 'number',
|
||||
},
|
||||
{
|
||||
id: 'context-relevance',
|
||||
label: 'Context Relevance',
|
||||
description: 'Scores how useful the retrieved context is for downstream generation.',
|
||||
valueType: 'number',
|
||||
},
|
||||
]
|
||||
|
||||
@ -121,20 +129,6 @@ const snippetFields: EvaluationFieldOption[] = [
|
||||
{ id: 'system.requires_review', label: 'Requires Review', group: 'System', type: 'boolean' },
|
||||
]
|
||||
|
||||
export const getComparisonOperators = (fieldType: EvaluationFieldOption['type']): ComparisonOperator[] => {
|
||||
if (fieldType === 'number')
|
||||
return ['is', 'is_not', 'greater_than', 'less_than', 'greater_or_equal', 'less_or_equal', 'is_empty', 'is_not_empty']
|
||||
|
||||
if (fieldType === 'boolean' || fieldType === 'enum')
|
||||
return ['is', 'is_not']
|
||||
|
||||
return ['contains', 'not_contains', 'is', 'is_not', 'is_empty', 'is_not_empty']
|
||||
}
|
||||
|
||||
export const getDefaultOperator = (fieldType: EvaluationFieldOption['type']): ComparisonOperator => {
|
||||
return getComparisonOperators(fieldType)[0]
|
||||
}
|
||||
|
||||
export const getEvaluationMockConfig = (resourceType: EvaluationResourceType): EvaluationMockConfig => {
|
||||
if (resourceType === 'datasets') {
|
||||
return {
|
||||
|
||||
@ -2,24 +2,30 @@ import type {
|
||||
BatchTestRecord,
|
||||
ComparisonOperator,
|
||||
CustomMetricMapping,
|
||||
EvaluationFieldOption,
|
||||
EvaluationMetric,
|
||||
EvaluationResourceState,
|
||||
EvaluationResourceType,
|
||||
JudgmentConditionGroup,
|
||||
JudgmentConditionItem,
|
||||
JudgmentConfig,
|
||||
MetricOption,
|
||||
} from './types'
|
||||
import type {
|
||||
EvaluationConditionValue,
|
||||
EvaluationConfig,
|
||||
EvaluationCustomizedMetric,
|
||||
EvaluationDefaultMetric,
|
||||
EvaluationJudgementConditionGroup,
|
||||
EvaluationJudgementConditionItem,
|
||||
EvaluationJudgmentCondition,
|
||||
EvaluationJudgmentConditionValue,
|
||||
EvaluationJudgmentConfig,
|
||||
NodeInfo,
|
||||
} from '@/types/evaluation'
|
||||
import { getComparisonOperators, getDefaultOperator, getEvaluationMockConfig } from './mock'
|
||||
import { encodeModelSelection } from './utils'
|
||||
import { getEvaluationMockConfig } from './mock'
|
||||
import {
|
||||
buildConditionMetricOptions,
|
||||
encodeModelSelection,
|
||||
getComparisonOperators,
|
||||
getDefaultComparisonOperator,
|
||||
requiresComparisonValue,
|
||||
} from './utils'
|
||||
|
||||
type EvaluationStoreResources = Record<string, EvaluationResourceState>
|
||||
|
||||
@ -41,6 +47,7 @@ const resolveMetricOption = (resourceType: EvaluationResourceType, metricId: str
|
||||
id: metricId,
|
||||
label: humanizeMetricId(metricId),
|
||||
description: '',
|
||||
valueType: 'number',
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,14 +98,32 @@ const normalizeCustomMetricMappings = (
|
||||
if (!value)
|
||||
return []
|
||||
|
||||
const mappings = Object.entries(value)
|
||||
return Object.entries(value)
|
||||
.filter((entry): entry is [string, string] => {
|
||||
const [, outputVariableId] = entry
|
||||
return typeof outputVariableId === 'string' && !!outputVariableId
|
||||
})
|
||||
.map(([inputVariableId, outputVariableId]) => createCustomMetricMapping(inputVariableId, outputVariableId))
|
||||
}
|
||||
|
||||
return mappings
|
||||
const normalizeCustomMetricOutputs = (
|
||||
value: EvaluationCustomizedMetric['output_fields'],
|
||||
) => {
|
||||
if (!value)
|
||||
return []
|
||||
|
||||
return value
|
||||
.map((output) => {
|
||||
const id = typeof output.variable === 'string' ? output.variable : ''
|
||||
if (!id)
|
||||
return null
|
||||
|
||||
return {
|
||||
id,
|
||||
valueType: typeof output.value_type === 'string' ? output.value_type : null,
|
||||
}
|
||||
})
|
||||
.filter((output): output is { id: string, valueType: string | null } => !!output)
|
||||
}
|
||||
|
||||
const normalizeCustomMetric = (
|
||||
@ -120,92 +145,137 @@ const normalizeCustomMetric = (
|
||||
...customMetric.customConfig,
|
||||
workflowId,
|
||||
mappings: normalizeCustomMetricMappings(value.input_fields),
|
||||
outputs: normalizeCustomMetricOutputs(value.output_fields),
|
||||
}
|
||||
: customMetric.customConfig,
|
||||
}]
|
||||
}
|
||||
|
||||
const normalizeVariableSelector = (value: string[] | undefined): [string, string] | null => {
|
||||
if (!Array.isArray(value) || value.length < 2)
|
||||
return null
|
||||
|
||||
const [scope, metricName] = value
|
||||
return typeof scope === 'string' && !!scope && typeof metricName === 'string' && !!metricName
|
||||
? [scope, metricName]
|
||||
: null
|
||||
}
|
||||
|
||||
const getNormalizedConditionValue = (
|
||||
operator: ComparisonOperator,
|
||||
previousValue: EvaluationJudgmentConditionValue | string | number | boolean | null | undefined,
|
||||
) => {
|
||||
if (!requiresComparisonValue(operator))
|
||||
return null
|
||||
|
||||
if (Array.isArray(previousValue))
|
||||
return previousValue.filter((item): item is string => typeof item === 'string' && !!item)
|
||||
|
||||
if (typeof previousValue === 'boolean')
|
||||
return previousValue
|
||||
|
||||
if (typeof previousValue === 'number')
|
||||
return String(previousValue)
|
||||
|
||||
return typeof previousValue === 'string' ? previousValue : null
|
||||
}
|
||||
|
||||
const getRawJudgmentConfig = (config: EvaluationConfig): EvaluationJudgmentConfig | null | undefined => {
|
||||
if (config.judgment_config)
|
||||
return config.judgment_config
|
||||
|
||||
if (
|
||||
config.judgement_conditions
|
||||
&& !Array.isArray(config.judgement_conditions)
|
||||
&& 'conditions' in config.judgement_conditions
|
||||
) {
|
||||
return config.judgement_conditions as EvaluationJudgmentConfig
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const normalizeConditionItem = (
|
||||
resourceType: EvaluationResourceType,
|
||||
value: EvaluationJudgementConditionItem,
|
||||
): JudgmentConditionGroup['items'][number] => {
|
||||
const fieldId = typeof value.fieldId === 'string'
|
||||
? value.fieldId
|
||||
: typeof value.field_id === 'string'
|
||||
? value.field_id
|
||||
: null
|
||||
const operatorValue = typeof value.operator === 'string' ? value.operator : null
|
||||
const field = getEvaluationMockConfig(resourceType).fieldOptions.find(option => option.id === fieldId)
|
||||
const allowedOperators = field ? getComparisonOperators(field.type) : ['contains']
|
||||
const operator = operatorValue && allowedOperators.includes(operatorValue as ComparisonOperator)
|
||||
? operatorValue as ComparisonOperator
|
||||
: field
|
||||
? getDefaultOperator(field.type)
|
||||
: 'contains'
|
||||
const rawValue: EvaluationConditionValue = value.value ?? null
|
||||
value: EvaluationJudgmentCondition,
|
||||
metrics: EvaluationMetric[],
|
||||
): JudgmentConditionItem | null => {
|
||||
const variableSelector = normalizeVariableSelector(value.variable_selector)
|
||||
if (!variableSelector)
|
||||
return null
|
||||
|
||||
const metricOption = buildConditionMetricOptions(metrics).find(option =>
|
||||
option.variableSelector[0] === variableSelector[0] && option.variableSelector[1] === variableSelector[1],
|
||||
)
|
||||
if (!metricOption)
|
||||
return null
|
||||
|
||||
const allowedOperators = getComparisonOperators(metricOption.valueType)
|
||||
const rawOperator = typeof value.comparison_operator === 'string' ? value.comparison_operator : ''
|
||||
const comparisonOperator = allowedOperators.includes(rawOperator as ComparisonOperator)
|
||||
? rawOperator as ComparisonOperator
|
||||
: getDefaultComparisonOperator(metricOption.valueType)
|
||||
|
||||
return {
|
||||
id: typeof value.id === 'string' ? value.id : createId('condition'),
|
||||
fieldId,
|
||||
operator,
|
||||
value: getConditionValue(field, operator, rawValue),
|
||||
id: createId('condition'),
|
||||
variableSelector,
|
||||
comparisonOperator,
|
||||
value: getConditionValue(metricOption.valueType, comparisonOperator, value.value),
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeConditionGroups = (
|
||||
resourceType: EvaluationResourceType,
|
||||
value: EvaluationConfig['judgement_conditions'],
|
||||
): JudgmentConditionGroup[] => {
|
||||
const groupsValue: EvaluationJudgementConditionGroup[] = Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(value?.groups)
|
||||
? value.groups
|
||||
: []
|
||||
const createEmptyJudgmentConfig = (): JudgmentConfig => {
|
||||
return {
|
||||
logicalOperator: 'and',
|
||||
conditions: [],
|
||||
}
|
||||
}
|
||||
|
||||
const groups = groupsValue
|
||||
.map((group) => {
|
||||
const itemsValue = Array.isArray(group.items) ? group.items : []
|
||||
const items = itemsValue
|
||||
.map(item => normalizeConditionItem(resourceType, item))
|
||||
const normalizeJudgmentConfig = (
|
||||
config: EvaluationConfig,
|
||||
metrics: EvaluationMetric[],
|
||||
): JudgmentConfig => {
|
||||
const rawJudgmentConfig = getRawJudgmentConfig(config)
|
||||
|
||||
if (items.length === 0)
|
||||
return null
|
||||
if (!rawJudgmentConfig)
|
||||
return createEmptyJudgmentConfig()
|
||||
|
||||
return {
|
||||
id: typeof group.id === 'string' ? group.id : createId('group'),
|
||||
logicalOperator: group.logicalOperator === 'or' || group.logical_operator === 'or' ? 'or' : 'and',
|
||||
items,
|
||||
} satisfies JudgmentConditionGroup
|
||||
})
|
||||
.filter((group): group is JudgmentConditionGroup => !!group)
|
||||
const conditions = (rawJudgmentConfig.conditions ?? [])
|
||||
.map(condition => normalizeConditionItem(condition, metrics))
|
||||
.filter((condition): condition is JudgmentConditionItem => !!condition)
|
||||
|
||||
return groups.length > 0 ? groups : [createConditionGroup(resourceType)]
|
||||
return {
|
||||
logicalOperator: rawJudgmentConfig.logical_operator === 'or' ? 'or' : 'and',
|
||||
conditions,
|
||||
}
|
||||
}
|
||||
|
||||
export const buildResourceKey = (resourceType: EvaluationResourceType, resourceId: string) => `${resourceType}:${resourceId}`
|
||||
|
||||
const conditionOperatorsWithoutValue: ComparisonOperator[] = ['is_empty', 'is_not_empty']
|
||||
|
||||
export const requiresConditionValue = (operator: ComparisonOperator) => !conditionOperatorsWithoutValue.includes(operator)
|
||||
export const requiresConditionValue = (operator: ComparisonOperator) => {
|
||||
return requiresComparisonValue(operator)
|
||||
}
|
||||
|
||||
export function getConditionValue(
|
||||
field: EvaluationFieldOption | undefined,
|
||||
valueType: EvaluationMetric['valueType'] | undefined,
|
||||
operator: ComparisonOperator,
|
||||
previousValue: string | number | boolean | null = null,
|
||||
previousValue?: EvaluationJudgmentConditionValue | string | number | boolean | null,
|
||||
) {
|
||||
if (!field || !requiresConditionValue(operator))
|
||||
if (!valueType || !requiresConditionValue(operator))
|
||||
return null
|
||||
|
||||
if (field.type === 'boolean')
|
||||
if (valueType === 'boolean')
|
||||
return typeof previousValue === 'boolean' ? previousValue : null
|
||||
|
||||
if (field.type === 'enum')
|
||||
return typeof previousValue === 'string' ? previousValue : null
|
||||
if (operator === 'in' || operator === 'not in') {
|
||||
if (Array.isArray(previousValue))
|
||||
return previousValue.filter((item): item is string => typeof item === 'string' && !!item)
|
||||
|
||||
if (field.type === 'number')
|
||||
return typeof previousValue === 'number' ? previousValue : null
|
||||
return typeof previousValue === 'string' && previousValue
|
||||
? previousValue.split(',').map(item => item.trim()).filter(Boolean)
|
||||
: []
|
||||
}
|
||||
|
||||
return typeof previousValue === 'string' ? previousValue : null
|
||||
return getNormalizedConditionValue(operator, previousValue)
|
||||
}
|
||||
|
||||
export function createBuiltinMetric(
|
||||
@ -219,6 +289,7 @@ export function createBuiltinMetric(
|
||||
kind: 'builtin',
|
||||
label: metric.label,
|
||||
description: metric.description,
|
||||
valueType: metric.valueType,
|
||||
threshold,
|
||||
nodeInfoList,
|
||||
}
|
||||
@ -263,40 +334,66 @@ export function createCustomMetric(): EvaluationMetric {
|
||||
kind: 'custom-workflow',
|
||||
label: 'Custom Evaluator',
|
||||
description: 'Map workflow variables to your evaluation inputs.',
|
||||
valueType: 'number',
|
||||
customConfig: {
|
||||
workflowId: null,
|
||||
workflowAppId: null,
|
||||
workflowName: null,
|
||||
mappings: [],
|
||||
outputs: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const buildConditionItem = (resourceType: EvaluationResourceType) => {
|
||||
const field = getEvaluationMockConfig(resourceType).fieldOptions[0]
|
||||
const operator = field ? getDefaultOperator(field.type) : 'contains'
|
||||
export const buildConditionItem = (metrics: EvaluationMetric[]): JudgmentConditionItem => {
|
||||
const metricOption = buildConditionMetricOptions(metrics)[0]
|
||||
const comparisonOperator = metricOption ? getDefaultComparisonOperator(metricOption.valueType) : 'is'
|
||||
|
||||
return {
|
||||
id: createId('condition'),
|
||||
fieldId: field?.id ?? null,
|
||||
operator,
|
||||
value: getConditionValue(field, operator),
|
||||
variableSelector: metricOption?.variableSelector ?? null,
|
||||
comparisonOperator,
|
||||
value: getConditionValue(metricOption?.valueType, comparisonOperator),
|
||||
}
|
||||
}
|
||||
|
||||
export function createConditionGroup(resourceType: EvaluationResourceType): JudgmentConditionGroup {
|
||||
export const syncJudgmentConfigWithMetrics = (
|
||||
judgmentConfig: JudgmentConfig,
|
||||
metrics: EvaluationMetric[],
|
||||
): JudgmentConfig => {
|
||||
const metricOptions = buildConditionMetricOptions(metrics)
|
||||
|
||||
return {
|
||||
id: createId('group'),
|
||||
logicalOperator: 'and',
|
||||
items: [buildConditionItem(resourceType)],
|
||||
logicalOperator: judgmentConfig.logicalOperator,
|
||||
conditions: judgmentConfig.conditions
|
||||
.map((condition) => {
|
||||
const metricOption = metricOptions.find(option =>
|
||||
option.variableSelector[0] === condition.variableSelector?.[0]
|
||||
&& option.variableSelector[1] === condition.variableSelector?.[1],
|
||||
)
|
||||
if (!metricOption)
|
||||
return null
|
||||
|
||||
const allowedOperators = getComparisonOperators(metricOption.valueType)
|
||||
const comparisonOperator = allowedOperators.includes(condition.comparisonOperator)
|
||||
? condition.comparisonOperator
|
||||
: getDefaultComparisonOperator(metricOption.valueType)
|
||||
|
||||
return {
|
||||
...condition,
|
||||
comparisonOperator,
|
||||
value: getConditionValue(metricOption.valueType, comparisonOperator, condition.value),
|
||||
}
|
||||
})
|
||||
.filter((condition): condition is JudgmentConditionItem => !!condition),
|
||||
}
|
||||
}
|
||||
|
||||
export const buildInitialState = (resourceType: EvaluationResourceType): EvaluationResourceState => {
|
||||
export const buildInitialState = (_resourceType: EvaluationResourceType): EvaluationResourceState => {
|
||||
return {
|
||||
judgeModelId: null,
|
||||
metrics: [],
|
||||
conditions: [createConditionGroup(resourceType)],
|
||||
conditions: createEmptyJudgmentConfig(),
|
||||
activeBatchTab: 'input-fields',
|
||||
uploadedFileName: null,
|
||||
batchRecords: [],
|
||||
@ -309,14 +406,15 @@ export const buildStateFromEvaluationConfig = (
|
||||
): EvaluationResourceState => {
|
||||
const defaultMetrics = normalizeDefaultMetrics(resourceType, config.default_metrics)
|
||||
const customMetrics = normalizeCustomMetric(config.customized_metrics)
|
||||
const metrics = [...defaultMetrics, ...customMetrics]
|
||||
|
||||
return {
|
||||
...buildInitialState(resourceType),
|
||||
judgeModelId: config.evaluation_model && config.evaluation_model_provider
|
||||
? encodeModelSelection(config.evaluation_model_provider, config.evaluation_model)
|
||||
: null,
|
||||
metrics: [...defaultMetrics, ...customMetrics],
|
||||
conditions: normalizeConditionGroups(resourceType, config.judgement_conditions),
|
||||
metrics,
|
||||
conditions: normalizeJudgmentConfig(config, metrics),
|
||||
}
|
||||
}
|
||||
|
||||
@ -353,12 +451,6 @@ export const updateMetric = (
|
||||
updater: (metric: EvaluationMetric) => EvaluationMetric,
|
||||
) => metrics.map(metric => metric.id === metricId ? updater(metric) : metric)
|
||||
|
||||
export const updateConditionGroup = (
|
||||
groups: JudgmentConditionGroup[],
|
||||
groupId: string,
|
||||
updater: (group: JudgmentConditionGroup) => JudgmentConditionGroup,
|
||||
) => groups.map(group => group.id === groupId ? updater(group) : group)
|
||||
|
||||
export const createBatchTestRecord = (
|
||||
resourceType: EvaluationResourceType,
|
||||
uploadedFileName: string | null | undefined,
|
||||
@ -389,14 +481,19 @@ export const isEvaluationRunnable = (state: EvaluationResourceState) => {
|
||||
return !!state.judgeModelId
|
||||
&& state.metrics.length > 0
|
||||
&& state.metrics.every(isCustomMetricConfigured)
|
||||
&& state.conditions.some(group => group.items.length > 0)
|
||||
}
|
||||
|
||||
export const getAllowedOperators = (resourceType: EvaluationResourceType, fieldId: string | null) => {
|
||||
const field = getEvaluationMockConfig(resourceType).fieldOptions.find(option => option.id === fieldId)
|
||||
export const getAllowedOperators = (
|
||||
metrics: EvaluationMetric[],
|
||||
variableSelector: [string, string] | null,
|
||||
) => {
|
||||
const metricOption = buildConditionMetricOptions(metrics).find(option =>
|
||||
option.variableSelector[0] === variableSelector?.[0]
|
||||
&& option.variableSelector[1] === variableSelector?.[1],
|
||||
)
|
||||
|
||||
if (!field)
|
||||
return ['contains'] as ComparisonOperator[]
|
||||
if (!metricOption)
|
||||
return ['is'] as ComparisonOperator[]
|
||||
|
||||
return getComparisonOperators(field.type)
|
||||
return getComparisonOperators(metricOption.valueType)
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import type {
|
||||
} from './types'
|
||||
import type { EvaluationConfig, NodeInfo } from '@/types/evaluation'
|
||||
import { create } from 'zustand'
|
||||
import { getDefaultOperator, getEvaluationMockConfig } from './mock'
|
||||
import { getEvaluationMockConfig } from './mock'
|
||||
import {
|
||||
buildConditionItem,
|
||||
buildInitialState,
|
||||
@ -13,7 +13,6 @@ import {
|
||||
buildStateFromEvaluationConfig,
|
||||
createBatchTestRecord,
|
||||
createBuiltinMetric,
|
||||
createConditionGroup,
|
||||
createCustomMetric,
|
||||
getAllowedOperators as getAllowedOperatorsFromUtils,
|
||||
getConditionValue,
|
||||
@ -21,10 +20,11 @@ import {
|
||||
isEvaluationRunnable as isEvaluationRunnableFromUtils,
|
||||
requiresConditionValue as requiresConditionValueFromUtils,
|
||||
syncCustomMetricMappings as syncCustomMetricMappingsFromUtils,
|
||||
updateConditionGroup,
|
||||
syncJudgmentConfigWithMetrics,
|
||||
updateMetric,
|
||||
updateResourceState,
|
||||
} from './store-utils'
|
||||
import { buildConditionMetricOptions } from './utils'
|
||||
|
||||
type EvaluationStore = {
|
||||
resources: Record<string, EvaluationResourceState>
|
||||
@ -47,6 +47,12 @@ type EvaluationStore = {
|
||||
metricId: string,
|
||||
inputVariableIds: string[],
|
||||
) => void
|
||||
syncCustomMetricOutputs: (
|
||||
resourceType: EvaluationResourceType,
|
||||
resourceId: string,
|
||||
metricId: string,
|
||||
outputs: Array<{ id: string, valueType: string | null }>,
|
||||
) => void
|
||||
updateCustomMetricMapping: (
|
||||
resourceType: EvaluationResourceType,
|
||||
resourceId: string,
|
||||
@ -54,19 +60,16 @@ type EvaluationStore = {
|
||||
mappingId: string,
|
||||
patch: { inputVariableId?: string | null, outputVariableId?: string | null },
|
||||
) => void
|
||||
addConditionGroup: (resourceType: EvaluationResourceType, resourceId: string) => void
|
||||
removeConditionGroup: (resourceType: EvaluationResourceType, resourceId: string, groupId: string) => void
|
||||
setConditionGroupOperator: (resourceType: EvaluationResourceType, resourceId: string, groupId: string, logicalOperator: 'and' | 'or') => void
|
||||
addConditionItem: (resourceType: EvaluationResourceType, resourceId: string, groupId: string) => void
|
||||
removeConditionItem: (resourceType: EvaluationResourceType, resourceId: string, groupId: string, itemId: string) => void
|
||||
updateConditionField: (resourceType: EvaluationResourceType, resourceId: string, groupId: string, itemId: string, fieldId: string) => void
|
||||
updateConditionOperator: (resourceType: EvaluationResourceType, resourceId: string, groupId: string, itemId: string, operator: ComparisonOperator) => void
|
||||
setConditionLogicalOperator: (resourceType: EvaluationResourceType, resourceId: string, logicalOperator: 'and' | 'or') => void
|
||||
addCondition: (resourceType: EvaluationResourceType, resourceId: string) => 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
|
||||
updateConditionValue: (
|
||||
resourceType: EvaluationResourceType,
|
||||
resourceId: string,
|
||||
groupId: string,
|
||||
itemId: string,
|
||||
value: string | number | boolean | null,
|
||||
conditionId: string,
|
||||
value: string | string[] | boolean | null,
|
||||
) => void
|
||||
setBatchTab: (resourceType: EvaluationResourceType, resourceId: string, tab: EvaluationResourceState['activeBatchTab']) => void
|
||||
setUploadedFileName: (resourceType: EvaluationResourceType, resourceId: string, uploadedFileName: string | null) => void
|
||||
@ -117,17 +120,22 @@ export const useEvaluationStore = create<EvaluationStore>((set, get) => ({
|
||||
|
||||
set((state) => {
|
||||
return {
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, currentResource => ({
|
||||
...currentResource,
|
||||
metrics: currentResource.metrics.some(metric => metric.optionId === optionId && metric.kind === 'builtin')
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, (currentResource) => {
|
||||
const metrics = currentResource.metrics.some(metric => metric.optionId === optionId && metric.kind === 'builtin')
|
||||
? currentResource.metrics.map(metric => metric.optionId === optionId && metric.kind === 'builtin'
|
||||
? {
|
||||
...metric,
|
||||
nodeInfoList,
|
||||
}
|
||||
: metric)
|
||||
: [...currentResource.metrics, createBuiltinMetric(option, nodeInfoList)],
|
||||
})),
|
||||
: [...currentResource.metrics, createBuiltinMetric(option, nodeInfoList)]
|
||||
|
||||
return {
|
||||
...currentResource,
|
||||
metrics,
|
||||
conditions: syncJudgmentConfigWithMetrics(currentResource.conditions, metrics),
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
},
|
||||
@ -144,27 +152,36 @@ export const useEvaluationStore = create<EvaluationStore>((set, get) => ({
|
||||
},
|
||||
addCustomMetric: (resourceType, resourceId) => {
|
||||
set(state => ({
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
||||
...resource,
|
||||
metrics: resource.metrics.some(metric => metric.kind === 'custom-workflow')
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, (resource) => {
|
||||
const metrics = resource.metrics.some(metric => metric.kind === 'custom-workflow')
|
||||
? resource.metrics
|
||||
: [...resource.metrics, createCustomMetric()],
|
||||
})),
|
||||
: [...resource.metrics, createCustomMetric()]
|
||||
|
||||
return {
|
||||
...resource,
|
||||
metrics,
|
||||
conditions: syncJudgmentConfigWithMetrics(resource.conditions, metrics),
|
||||
}
|
||||
}),
|
||||
}))
|
||||
},
|
||||
removeMetric: (resourceType, resourceId, metricId) => {
|
||||
set(state => ({
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
||||
...resource,
|
||||
metrics: resource.metrics.filter(metric => metric.id !== metricId),
|
||||
})),
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, (resource) => {
|
||||
const metrics = resource.metrics.filter(metric => metric.id !== metricId)
|
||||
|
||||
return {
|
||||
...resource,
|
||||
metrics,
|
||||
conditions: syncJudgmentConfigWithMetrics(resource.conditions, metrics),
|
||||
}
|
||||
}),
|
||||
}))
|
||||
},
|
||||
setCustomMetricWorkflow: (resourceType, resourceId, metricId, workflow) => {
|
||||
set(state => ({
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
||||
...resource,
|
||||
metrics: updateMetric(resource.metrics, metricId, metric => ({
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, (resource) => {
|
||||
const metrics = updateMetric(resource.metrics, metricId, metric => ({
|
||||
...metric,
|
||||
customConfig: metric.customConfig
|
||||
? {
|
||||
@ -176,10 +193,17 @@ export const useEvaluationStore = create<EvaluationStore>((set, get) => ({
|
||||
...mapping,
|
||||
outputVariableId: null,
|
||||
})),
|
||||
outputs: [],
|
||||
}
|
||||
: metric.customConfig,
|
||||
})),
|
||||
})),
|
||||
}))
|
||||
|
||||
return {
|
||||
...resource,
|
||||
metrics,
|
||||
conditions: syncJudgmentConfigWithMetrics(resource.conditions, metrics),
|
||||
}
|
||||
}),
|
||||
}))
|
||||
},
|
||||
syncCustomMetricMappings: (resourceType, resourceId, metricId, inputVariableIds) => {
|
||||
@ -198,6 +222,27 @@ export const useEvaluationStore = create<EvaluationStore>((set, get) => ({
|
||||
})),
|
||||
}))
|
||||
},
|
||||
syncCustomMetricOutputs: (resourceType, resourceId, metricId, outputs) => {
|
||||
set(state => ({
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, (resource) => {
|
||||
const metrics = updateMetric(resource.metrics, metricId, metric => ({
|
||||
...metric,
|
||||
customConfig: metric.customConfig
|
||||
? {
|
||||
...metric.customConfig,
|
||||
outputs,
|
||||
}
|
||||
: metric.customConfig,
|
||||
}))
|
||||
|
||||
return {
|
||||
...resource,
|
||||
metrics,
|
||||
conditions: syncJudgmentConfigWithMetrics(resource.conditions, metrics),
|
||||
}
|
||||
}),
|
||||
}))
|
||||
},
|
||||
updateCustomMetricMapping: (resourceType, resourceId, metricId, mappingId, patch) => {
|
||||
set(state => ({
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
||||
@ -214,114 +259,101 @@ export const useEvaluationStore = create<EvaluationStore>((set, get) => ({
|
||||
})),
|
||||
}))
|
||||
},
|
||||
addConditionGroup: (resourceType, resourceId) => {
|
||||
setConditionLogicalOperator: (resourceType, resourceId, logicalOperator) => {
|
||||
set(state => ({
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
||||
...resource,
|
||||
conditions: [...resource.conditions, createConditionGroup(resourceType)],
|
||||
})),
|
||||
}))
|
||||
},
|
||||
removeConditionGroup: (resourceType, resourceId, groupId) => {
|
||||
set(state => ({
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
||||
...resource,
|
||||
conditions: resource.conditions.filter(group => group.id !== groupId),
|
||||
})),
|
||||
}))
|
||||
},
|
||||
setConditionGroupOperator: (resourceType, resourceId, groupId, logicalOperator) => {
|
||||
set(state => ({
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
||||
...resource,
|
||||
conditions: updateConditionGroup(resource.conditions, groupId, group => ({
|
||||
...group,
|
||||
conditions: {
|
||||
...resource.conditions,
|
||||
logicalOperator,
|
||||
})),
|
||||
},
|
||||
})),
|
||||
}))
|
||||
},
|
||||
addConditionItem: (resourceType, resourceId, groupId) => {
|
||||
addCondition: (resourceType, resourceId) => {
|
||||
set(state => ({
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
||||
...resource,
|
||||
conditions: updateConditionGroup(resource.conditions, groupId, group => ({
|
||||
...group,
|
||||
items: [...group.items, buildConditionItem(resourceType)],
|
||||
})),
|
||||
conditions: {
|
||||
...resource.conditions,
|
||||
conditions: [...resource.conditions.conditions, buildConditionItem(resource.metrics)],
|
||||
},
|
||||
})),
|
||||
}))
|
||||
},
|
||||
removeConditionItem: (resourceType, resourceId, groupId, itemId) => {
|
||||
removeCondition: (resourceType, resourceId, conditionId) => {
|
||||
set(state => ({
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
||||
...resource,
|
||||
conditions: updateConditionGroup(resource.conditions, groupId, group => ({
|
||||
...group,
|
||||
items: group.items.filter(item => item.id !== itemId),
|
||||
})),
|
||||
conditions: {
|
||||
...resource.conditions,
|
||||
conditions: resource.conditions.conditions.filter(condition => condition.id !== conditionId),
|
||||
},
|
||||
})),
|
||||
}))
|
||||
},
|
||||
updateConditionField: (resourceType, resourceId, groupId, itemId, fieldId) => {
|
||||
const field = getEvaluationMockConfig(resourceType).fieldOptions.find(option => option.id === fieldId)
|
||||
|
||||
updateConditionMetric: (resourceType, resourceId, conditionId, variableSelector) => {
|
||||
set(state => ({
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
||||
...resource,
|
||||
conditions: updateConditionGroup(resource.conditions, groupId, group => ({
|
||||
...group,
|
||||
items: group.items.map((item) => {
|
||||
if (item.id !== itemId)
|
||||
return item
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, (resource) => {
|
||||
const allowedOperators = getAllowedOperatorsFromUtils(resource.metrics, variableSelector)
|
||||
const comparisonOperator = allowedOperators[0]
|
||||
const metricOption = buildConditionMetricOptions(resource.metrics).find(option =>
|
||||
option.variableSelector[0] === variableSelector[0] && option.variableSelector[1] === variableSelector[1],
|
||||
)
|
||||
|
||||
const nextOperator = field ? getDefaultOperator(field.type) : item.operator
|
||||
|
||||
return {
|
||||
...item,
|
||||
fieldId,
|
||||
operator: nextOperator,
|
||||
value: getConditionValue(field, nextOperator),
|
||||
}
|
||||
}),
|
||||
})),
|
||||
})),
|
||||
return {
|
||||
...resource,
|
||||
conditions: {
|
||||
...resource.conditions,
|
||||
conditions: resource.conditions.conditions.map(condition => condition.id === conditionId
|
||||
? {
|
||||
...condition,
|
||||
variableSelector,
|
||||
comparisonOperator,
|
||||
value: getConditionValue(metricOption?.valueType, comparisonOperator),
|
||||
}
|
||||
: condition),
|
||||
},
|
||||
}
|
||||
}),
|
||||
}))
|
||||
},
|
||||
updateConditionOperator: (resourceType, resourceId, groupId, itemId, operator) => {
|
||||
updateConditionOperator: (resourceType, resourceId, conditionId, operator) => {
|
||||
set((state) => {
|
||||
const fieldOptions = getEvaluationMockConfig(resourceType).fieldOptions
|
||||
|
||||
return {
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, currentResource => ({
|
||||
...currentResource,
|
||||
conditions: updateConditionGroup(currentResource.conditions, groupId, group => ({
|
||||
...group,
|
||||
items: group.items.map((item) => {
|
||||
if (item.id !== itemId)
|
||||
return item
|
||||
conditions: {
|
||||
...currentResource.conditions,
|
||||
conditions: currentResource.conditions.conditions.map((condition) => {
|
||||
if (condition.id !== conditionId)
|
||||
return condition
|
||||
|
||||
const field = fieldOptions.find(option => option.id === item.fieldId)
|
||||
const metricOption = buildConditionMetricOptions(currentResource.metrics)
|
||||
.find(option =>
|
||||
option.variableSelector[0] === condition.variableSelector?.[0]
|
||||
&& option.variableSelector[1] === condition.variableSelector?.[1],
|
||||
)
|
||||
|
||||
return {
|
||||
...item,
|
||||
operator,
|
||||
value: getConditionValue(field, operator, item.value),
|
||||
...condition,
|
||||
comparisonOperator: operator,
|
||||
value: getConditionValue(metricOption?.valueType, operator, condition.value),
|
||||
}
|
||||
}),
|
||||
})),
|
||||
},
|
||||
})),
|
||||
}
|
||||
})
|
||||
},
|
||||
updateConditionValue: (resourceType, resourceId, groupId, itemId, value) => {
|
||||
updateConditionValue: (resourceType, resourceId, conditionId, value) => {
|
||||
set(state => ({
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
||||
...resource,
|
||||
conditions: updateConditionGroup(resource.conditions, groupId, group => ({
|
||||
...group,
|
||||
items: group.items.map(item => item.id === itemId ? { ...item, value } : item),
|
||||
})),
|
||||
conditions: {
|
||||
...resource.conditions,
|
||||
conditions: resource.conditions.conditions.map(condition => condition.id === conditionId ? { ...condition, value } : condition),
|
||||
},
|
||||
})),
|
||||
}))
|
||||
},
|
||||
@ -374,8 +406,11 @@ export const useEvaluationResource = (resourceType: EvaluationResourceType, reso
|
||||
return useEvaluationStore(state => state.resources[resourceKey] ?? (initialResourceCache[resourceKey] ??= buildInitialState(resourceType)))
|
||||
}
|
||||
|
||||
export const getAllowedOperators = (resourceType: EvaluationResourceType, fieldId: string | null) => {
|
||||
return getAllowedOperatorsFromUtils(resourceType, fieldId)
|
||||
export const getAllowedOperators = (
|
||||
metrics: EvaluationResourceState['metrics'],
|
||||
variableSelector: [string, string] | null,
|
||||
) => {
|
||||
return getAllowedOperatorsFromUtils(metrics, variableSelector)
|
||||
}
|
||||
|
||||
export const isCustomMetricConfigured = (metric: EvaluationResourceState['metrics'][number]) => {
|
||||
|
||||
@ -13,17 +13,27 @@ export type BatchTestTab = 'input-fields' | 'history'
|
||||
|
||||
export type FieldType = 'string' | 'number' | 'boolean' | 'enum'
|
||||
|
||||
export type ConditionMetricValueType = 'string' | 'number' | 'boolean'
|
||||
|
||||
export type ComparisonOperator
|
||||
= | 'contains'
|
||||
| 'not_contains'
|
||||
| 'not contains'
|
||||
| 'start with'
|
||||
| 'end with'
|
||||
| 'is'
|
||||
| 'is_not'
|
||||
| 'is_empty'
|
||||
| 'is_not_empty'
|
||||
| 'greater_than'
|
||||
| 'less_than'
|
||||
| 'greater_or_equal'
|
||||
| 'less_or_equal'
|
||||
| 'is not'
|
||||
| 'empty'
|
||||
| 'not empty'
|
||||
| 'in'
|
||||
| 'not in'
|
||||
| '='
|
||||
| '≠'
|
||||
| '>'
|
||||
| '<'
|
||||
| '≥'
|
||||
| '≤'
|
||||
| 'is null'
|
||||
| 'is not null'
|
||||
|
||||
export type JudgeModelOption = {
|
||||
id: string
|
||||
@ -35,6 +45,7 @@ export type MetricOption = {
|
||||
id: string
|
||||
label: string
|
||||
description: string
|
||||
valueType: ConditionMetricValueType
|
||||
}
|
||||
|
||||
export type EvaluationWorkflowOption = {
|
||||
@ -69,6 +80,10 @@ export type CustomMetricConfig = {
|
||||
workflowAppId: string | null
|
||||
workflowName: string | null
|
||||
mappings: CustomMetricMapping[]
|
||||
outputs: Array<{
|
||||
id: string
|
||||
valueType: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
export type EvaluationMetric = {
|
||||
@ -77,6 +92,7 @@ export type EvaluationMetric = {
|
||||
kind: MetricKind
|
||||
label: string
|
||||
description: string
|
||||
valueType: ConditionMetricValueType
|
||||
threshold?: number
|
||||
nodeInfoList?: NodeInfo[]
|
||||
customConfig?: CustomMetricConfig
|
||||
@ -84,15 +100,23 @@ export type EvaluationMetric = {
|
||||
|
||||
export type JudgmentConditionItem = {
|
||||
id: string
|
||||
fieldId: string | null
|
||||
operator: ComparisonOperator
|
||||
value: string | number | boolean | null
|
||||
variableSelector: [string, string] | null
|
||||
comparisonOperator: ComparisonOperator
|
||||
value: string | string[] | boolean | null
|
||||
}
|
||||
|
||||
export type JudgmentConditionGroup = {
|
||||
id: string
|
||||
export type JudgmentConfig = {
|
||||
logicalOperator: 'and' | 'or'
|
||||
items: JudgmentConditionItem[]
|
||||
conditions: JudgmentConditionItem[]
|
||||
}
|
||||
|
||||
export type ConditionMetricOption = {
|
||||
id: string
|
||||
group: string
|
||||
label: string
|
||||
description: string
|
||||
valueType: ConditionMetricValueType
|
||||
variableSelector: [string, string]
|
||||
}
|
||||
|
||||
export type BatchTestRecord = {
|
||||
@ -106,7 +130,7 @@ export type BatchTestRecord = {
|
||||
export type EvaluationResourceState = {
|
||||
judgeModelId: string | null
|
||||
metrics: EvaluationMetric[]
|
||||
conditions: JudgmentConditionGroup[]
|
||||
conditions: JudgmentConfig
|
||||
activeBatchTab: BatchTestTab
|
||||
uploadedFileName: string | null
|
||||
batchRecords: BatchTestRecord[]
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import type { TFunction } from 'i18next'
|
||||
import type { ComparisonOperator, EvaluationFieldOption } from './types'
|
||||
import type {
|
||||
ComparisonOperator,
|
||||
ConditionMetricOption,
|
||||
ConditionMetricValueType,
|
||||
EvaluationMetric,
|
||||
} from './types'
|
||||
|
||||
export const TAB_CLASS_NAME = 'flex-1 rounded-lg px-3 py-2 text-left system-sm-medium'
|
||||
|
||||
const compactOperatorLabels: Partial<Record<ComparisonOperator, string>> = {
|
||||
is: '=',
|
||||
is_not: '!=',
|
||||
greater_than: '>',
|
||||
less_than: '<',
|
||||
greater_or_equal: '>=',
|
||||
less_or_equal: '<=',
|
||||
}
|
||||
const rawOperatorLabels = new Set<ComparisonOperator>(['=', '≠', '>', '<', '≥', '≤'])
|
||||
|
||||
const noValueOperators = new Set<ComparisonOperator>(['empty', 'not empty', 'is null', 'is not null'])
|
||||
|
||||
export const encodeModelSelection = (provider: string, model: string) => `${provider}::${model}`
|
||||
|
||||
@ -25,33 +25,84 @@ export const decodeModelSelection = (judgeModelId: string | null) => {
|
||||
return { provider, model }
|
||||
}
|
||||
|
||||
export const groupFieldOptions = (fieldOptions: EvaluationFieldOption[]) => {
|
||||
return Object.entries(fieldOptions.reduce<Record<string, EvaluationFieldOption[]>>((acc, field) => {
|
||||
acc[field.group] = [...(acc[field.group] ?? []), field]
|
||||
return acc
|
||||
}, {}))
|
||||
}
|
||||
|
||||
export const getOperatorLabel = (
|
||||
export const getComparisonOperatorLabel = (
|
||||
operator: ComparisonOperator,
|
||||
fieldType: EvaluationFieldOption['type'] | undefined,
|
||||
t: TFunction<'evaluation'>,
|
||||
t: TFunction,
|
||||
) => {
|
||||
if (fieldType === 'number' && compactOperatorLabels[operator])
|
||||
return compactOperatorLabels[operator] as string
|
||||
if (rawOperatorLabels.has(operator))
|
||||
return operator
|
||||
|
||||
return t(`conditions.operators.${operator}` as const)
|
||||
return t(`nodes.ifElse.comparisonOperator.${operator}` as never, { ns: 'workflow' } as never) as unknown as string
|
||||
}
|
||||
|
||||
export const getFieldTypeIconClassName = (fieldType: EvaluationFieldOption['type']) => {
|
||||
if (fieldType === 'number')
|
||||
return 'i-ri-hashtag'
|
||||
|
||||
if (fieldType === 'boolean')
|
||||
return 'i-ri-checkbox-circle-line'
|
||||
|
||||
if (fieldType === 'enum')
|
||||
return 'i-ri-list-check-2'
|
||||
|
||||
return 'i-ri-text'
|
||||
export const requiresComparisonValue = (operator: ComparisonOperator) => {
|
||||
return !noValueOperators.has(operator)
|
||||
}
|
||||
|
||||
const getMetricValueType = (valueType: string | null | undefined): ConditionMetricValueType => {
|
||||
if (valueType === 'number' || valueType === 'integer')
|
||||
return 'number'
|
||||
|
||||
if (valueType === 'boolean')
|
||||
return 'boolean'
|
||||
|
||||
return 'string'
|
||||
}
|
||||
|
||||
export const getComparisonOperators = (valueType: ConditionMetricValueType): ComparisonOperator[] => {
|
||||
if (valueType === 'number')
|
||||
return ['=', '≠', '>', '<', '≥', '≤', 'is null', 'is not null']
|
||||
|
||||
if (valueType === 'boolean')
|
||||
return ['is', 'is not', 'is null', 'is not null']
|
||||
|
||||
return ['contains', 'not contains', 'start with', 'end with', 'is', 'is not', 'empty', 'not empty', 'in', 'not in', 'is null', 'is not null']
|
||||
}
|
||||
|
||||
export const getDefaultComparisonOperator = (valueType: ConditionMetricValueType): ComparisonOperator => {
|
||||
return getComparisonOperators(valueType)[0]
|
||||
}
|
||||
|
||||
export const buildConditionMetricOptions = (metrics: EvaluationMetric[]): ConditionMetricOption[] => {
|
||||
return metrics.flatMap((metric) => {
|
||||
if (metric.kind === 'builtin') {
|
||||
return (metric.nodeInfoList ?? []).map((nodeInfo) => {
|
||||
return {
|
||||
id: `${nodeInfo.node_id}:${metric.optionId}`,
|
||||
group: nodeInfo.title,
|
||||
label: metric.label,
|
||||
description: nodeInfo.type,
|
||||
valueType: metric.valueType,
|
||||
variableSelector: [nodeInfo.node_id, metric.optionId] as [string, string],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const customConfig = metric.customConfig
|
||||
|
||||
if (!customConfig?.workflowId)
|
||||
return []
|
||||
|
||||
return customConfig.outputs.map((output) => {
|
||||
return {
|
||||
id: `${customConfig.workflowId}:${output.id}`,
|
||||
group: customConfig.workflowName ?? metric.label,
|
||||
label: output.id,
|
||||
description: customConfig.workflowName ?? metric.label,
|
||||
valueType: getMetricValueType(output.valueType),
|
||||
variableSelector: [customConfig.workflowId, output.id] as [string, string],
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const serializeVariableSelector = (value: [string, string] | null | undefined) => {
|
||||
return value ? JSON.stringify(value) : ''
|
||||
}
|
||||
|
||||
export const isSelectorEqual = (
|
||||
left: [string, string] | null | undefined,
|
||||
right: [string, string] | null | undefined,
|
||||
) => {
|
||||
return left?.[0] === right?.[0] && left?.[1] === right?.[1]
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
"conditions.description": "Define additional rules for when results should pass or fail.",
|
||||
"conditions.emptyDescription": "Add metrics above to configure pass/fail thresholds.",
|
||||
"conditions.emptyTitle": "No conditions yet",
|
||||
"conditions.fieldPlaceholder": "Select field",
|
||||
"conditions.fieldPlaceholder": "Select metric",
|
||||
"conditions.groupLabel": "Group {{index}}",
|
||||
"conditions.logical.and": "AND",
|
||||
"conditions.logical.or": "OR",
|
||||
@ -39,7 +39,7 @@
|
||||
"conditions.operators.not_contains": "Does not contain",
|
||||
"conditions.removeCondition": "Remove condition",
|
||||
"conditions.removeGroup": "Remove condition group",
|
||||
"conditions.selectFieldFirst": "Select a field first",
|
||||
"conditions.selectFieldFirst": "Select a metric first",
|
||||
"conditions.selectValue": "Choose a value",
|
||||
"conditions.title": "Judgment Conditions",
|
||||
"conditions.valuePlaceholder": "Enter a value",
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
"conditions.description": "定义额外规则,决定结果何时通过或失败。",
|
||||
"conditions.emptyDescription": "请先添加上方指标,再配置通过 / 失败阈值。",
|
||||
"conditions.emptyTitle": "还没有条件",
|
||||
"conditions.fieldPlaceholder": "选择字段",
|
||||
"conditions.fieldPlaceholder": "选择指标",
|
||||
"conditions.groupLabel": "条件组 {{index}}",
|
||||
"conditions.logical.and": "且",
|
||||
"conditions.logical.or": "或",
|
||||
@ -39,7 +39,7 @@
|
||||
"conditions.operators.not_contains": "不包含",
|
||||
"conditions.removeCondition": "删除条件",
|
||||
"conditions.removeGroup": "删除条件组",
|
||||
"conditions.selectFieldFirst": "请先选择字段",
|
||||
"conditions.selectFieldFirst": "请先选择指标",
|
||||
"conditions.selectValue": "选择值",
|
||||
"conditions.title": "判定条件",
|
||||
"conditions.valuePlaceholder": "输入值",
|
||||
|
||||
@ -1,5 +1,18 @@
|
||||
export type EvaluationTargetType = 'apps' | 'snippets' | 'datasets'
|
||||
|
||||
export type EvaluationJudgmentConditionValue = string | string[] | boolean
|
||||
|
||||
export type EvaluationJudgmentCondition = {
|
||||
variable_selector?: string[]
|
||||
comparison_operator?: string
|
||||
value?: EvaluationJudgmentConditionValue
|
||||
}
|
||||
|
||||
export type EvaluationJudgmentConfig = {
|
||||
logical_operator?: 'and' | 'or'
|
||||
conditions?: EvaluationJudgmentCondition[]
|
||||
}
|
||||
|
||||
export type EvaluationConditionValue = string | number | boolean | null
|
||||
|
||||
export type EvaluationJudgementConditionItem = {
|
||||
@ -28,7 +41,8 @@ export type EvaluationConfig = {
|
||||
evaluation_model_provider: string | null
|
||||
default_metrics?: EvaluationDefaultMetric[] | null
|
||||
customized_metrics?: EvaluationCustomizedMetric | null
|
||||
judgement_conditions: EvaluationJudgementConditions | null
|
||||
judgment_config?: EvaluationJudgmentConfig | null
|
||||
judgement_conditions?: EvaluationJudgementConditions | null
|
||||
}
|
||||
|
||||
export type NodeInfo = {
|
||||
@ -57,7 +71,7 @@ export type EvaluationConfigData = {
|
||||
evaluation_model_provider?: string
|
||||
default_metrics?: EvaluationDefaultMetric[] | null
|
||||
customized_metrics?: EvaluationCustomizedMetric | null
|
||||
judgment_config?: EvaluationJudgementConditions | null
|
||||
judgment_config?: EvaluationJudgmentConfig | null
|
||||
}
|
||||
|
||||
export type EvaluationRunRequest = EvaluationConfigData & {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user