mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
feat(web): dataset evaluation configure
This commit is contained in:
parent
ccc8a5f278
commit
e52dbd49be
@ -54,7 +54,7 @@ describe('Evaluation', () => {
|
|||||||
|
|
||||||
mockUseAvailableEvaluationMetrics.mockReturnValue({
|
mockUseAvailableEvaluationMetrics.mockReturnValue({
|
||||||
data: {
|
data: {
|
||||||
metrics: ['answer-correctness', 'faithfulness'],
|
metrics: ['answer-correctness', 'faithfulness', 'context-precision', 'context-recall', 'context-relevance'],
|
||||||
},
|
},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
})
|
})
|
||||||
@ -240,12 +240,34 @@ describe('Evaluation', () => {
|
|||||||
expect(screen.getByRole('button', { name: 'evaluation.pipeline.uploadAndRun' })).toBeDisabled()
|
expect(screen.getByRole('button', { name: 'evaluation.pipeline.uploadAndRun' })).toBeDisabled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should render selected pipeline metrics from config with the default threshold input', () => {
|
||||||
|
mockUseEvaluationConfig.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
evaluation_model: null,
|
||||||
|
evaluation_model_provider: null,
|
||||||
|
metrics_config: {
|
||||||
|
default_metrics: [{
|
||||||
|
metric: 'context-precision',
|
||||||
|
}],
|
||||||
|
customized_metrics: null,
|
||||||
|
},
|
||||||
|
judgement_conditions: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<Evaluation resourceType="datasets" resourceId="dataset-2" />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Context Precision')).toBeInTheDocument()
|
||||||
|
expect(screen.getByDisplayValue('0.85')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('should enable pipeline batch actions after selecting a judge model and metric', () => {
|
it('should enable pipeline batch actions after selecting a judge model and metric', () => {
|
||||||
render(<Evaluation resourceType="datasets" resourceId="dataset-2" />)
|
render(<Evaluation resourceType="datasets" resourceId="dataset-2" />)
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'select-model' }))
|
fireEvent.click(screen.getByRole('button', { name: 'select-model' }))
|
||||||
fireEvent.click(screen.getByRole('button', { name: /Context Precision/i }))
|
fireEvent.click(screen.getByRole('button', { name: /Context Precision/i }))
|
||||||
|
|
||||||
|
expect(screen.getByDisplayValue('0.85')).toBeInTheDocument()
|
||||||
expect(screen.getByRole('button', { name: 'evaluation.batch.downloadTemplate' })).toBeEnabled()
|
expect(screen.getByRole('button', { name: 'evaluation.batch.downloadTemplate' })).toBeEnabled()
|
||||||
expect(screen.getByRole('button', { name: 'evaluation.pipeline.uploadAndRun' })).toBeEnabled()
|
expect(screen.getByRole('button', { name: 'evaluation.pipeline.uploadAndRun' })).toBeEnabled()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -181,6 +181,7 @@ describe('evaluation store', () => {
|
|||||||
expect(hydratedState.judgeModelId).toBe('openai::gpt-4o-mini')
|
expect(hydratedState.judgeModelId).toBe('openai::gpt-4o-mini')
|
||||||
expect(hydratedState.metrics).toHaveLength(2)
|
expect(hydratedState.metrics).toHaveLength(2)
|
||||||
expect(hydratedState.metrics[0].optionId).toBe('faithfulness')
|
expect(hydratedState.metrics[0].optionId).toBe('faithfulness')
|
||||||
|
expect(hydratedState.metrics[0].threshold).toBe(0.85)
|
||||||
expect(hydratedState.metrics[0].nodeInfoList).toEqual([
|
expect(hydratedState.metrics[0].nodeInfoList).toEqual([
|
||||||
{ node_id: 'node-1', title: 'Retriever', type: 'retriever' },
|
{ node_id: 'node-1', title: 'Retriever', type: 'retriever' },
|
||||||
])
|
])
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import { toast } from '@/app/components/base/ui/toast'
|
import { toast } from '@/app/components/base/ui/toast'
|
||||||
import { useDocLink } from '@/context/i18n'
|
import { useDocLink } from '@/context/i18n'
|
||||||
|
import { useAvailableEvaluationMetrics } from '@/service/use-evaluation'
|
||||||
import { getEvaluationMockConfig } from '../../mock'
|
import { getEvaluationMockConfig } from '../../mock'
|
||||||
import { isEvaluationRunnable, useEvaluationResource, useEvaluationStore } from '../../store'
|
import { isEvaluationRunnable, useEvaluationResource, useEvaluationStore } from '../../store'
|
||||||
import JudgeModelSelector from '../judge-model-selector'
|
import JudgeModelSelector from '../judge-model-selector'
|
||||||
@ -24,8 +25,10 @@ const PipelineEvaluation = ({
|
|||||||
const ensureResource = useEvaluationStore(state => state.ensureResource)
|
const ensureResource = useEvaluationStore(state => state.ensureResource)
|
||||||
const addBuiltinMetric = useEvaluationStore(state => state.addBuiltinMetric)
|
const addBuiltinMetric = useEvaluationStore(state => state.addBuiltinMetric)
|
||||||
const removeMetric = useEvaluationStore(state => state.removeMetric)
|
const removeMetric = useEvaluationStore(state => state.removeMetric)
|
||||||
|
const updateMetricThreshold = useEvaluationStore(state => state.updateMetricThreshold)
|
||||||
const setUploadedFileName = useEvaluationStore(state => state.setUploadedFileName)
|
const setUploadedFileName = useEvaluationStore(state => state.setUploadedFileName)
|
||||||
const runBatchTest = useEvaluationStore(state => state.runBatchTest)
|
const runBatchTest = useEvaluationStore(state => state.runBatchTest)
|
||||||
|
const { data: availableMetricsData } = useAvailableEvaluationMetrics()
|
||||||
const resource = useEvaluationResource(resourceType, resourceId)
|
const resource = useEvaluationResource(resourceType, resourceId)
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const config = getEvaluationMockConfig(resourceType)
|
const config = getEvaluationMockConfig(resourceType)
|
||||||
@ -34,6 +37,12 @@ const PipelineEvaluation = ({
|
|||||||
.filter(metric => metric.kind === 'builtin')
|
.filter(metric => metric.kind === 'builtin')
|
||||||
.map(metric => [metric.optionId, metric]),
|
.map(metric => [metric.optionId, metric]),
|
||||||
), [resource.metrics])
|
), [resource.metrics])
|
||||||
|
const availableMetricIds = useMemo(() => new Set(availableMetricsData?.metrics ?? []), [availableMetricsData?.metrics])
|
||||||
|
const availableBuiltinMetrics = useMemo(() => {
|
||||||
|
return config.builtinMetrics.filter(metric =>
|
||||||
|
availableMetricIds.has(metric.id) || builtinMetricMap.has(metric.id),
|
||||||
|
)
|
||||||
|
}, [availableMetricIds, builtinMetricMap, config.builtinMetrics])
|
||||||
const isConfigReady = !!resource.judgeModelId && builtinMetricMap.size > 0
|
const isConfigReady = !!resource.judgeModelId && builtinMetricMap.size > 0
|
||||||
const isRunnable = isEvaluationRunnable(resource)
|
const isRunnable = isEvaluationRunnable(resource)
|
||||||
|
|
||||||
@ -107,15 +116,21 @@ const PipelineEvaluation = ({
|
|||||||
<section>
|
<section>
|
||||||
<InlineSectionHeader title={t('metrics.title')} tooltip={t('metrics.description')} />
|
<InlineSectionHeader title={t('metrics.title')} tooltip={t('metrics.description')} />
|
||||||
<div className="mt-1 space-y-0.5">
|
<div className="mt-1 space-y-0.5">
|
||||||
{config.builtinMetrics.map(metric => (
|
{availableBuiltinMetrics.map((metric) => {
|
||||||
<PipelineMetricItem
|
const selectedMetric = builtinMetricMap.get(metric.id)
|
||||||
key={metric.id}
|
|
||||||
metric={metric}
|
return (
|
||||||
selected={builtinMetricMap.has(metric.id)}
|
<PipelineMetricItem
|
||||||
disabledCondition
|
key={metric.id}
|
||||||
onToggle={() => handleToggleMetric(metric.id)}
|
metric={metric}
|
||||||
/>
|
selected={!!selectedMetric}
|
||||||
))}
|
threshold={selectedMetric?.threshold}
|
||||||
|
disabledCondition
|
||||||
|
onToggle={() => handleToggleMetric(metric.id)}
|
||||||
|
onThresholdChange={value => updateMetricThreshold(resourceType, resourceId, selectedMetric?.id ?? '', value)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,20 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { MetricOption } from '../../types'
|
import type { MetricOption } from '../../types'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import Checkbox from '@/app/components/base/checkbox'
|
import Checkbox from '@/app/components/base/checkbox'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { DEFAULT_PIPELINE_METRIC_THRESHOLD } from '../../store-utils'
|
||||||
|
|
||||||
type PipelineMetricItemProps = {
|
type PipelineMetricItemProps = {
|
||||||
metric: MetricOption
|
metric: MetricOption
|
||||||
selected: boolean
|
selected: boolean
|
||||||
onToggle: () => void
|
onToggle: () => void
|
||||||
disabledCondition: boolean
|
disabledCondition: boolean
|
||||||
|
threshold?: number
|
||||||
|
onThresholdChange: (value: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const PipelineMetricItem = ({
|
const PipelineMetricItem = ({
|
||||||
@ -17,7 +22,11 @@ const PipelineMetricItem = ({
|
|||||||
selected,
|
selected,
|
||||||
onToggle,
|
onToggle,
|
||||||
disabledCondition,
|
disabledCondition,
|
||||||
|
threshold = DEFAULT_PIPELINE_METRIC_THRESHOLD,
|
||||||
|
onThresholdChange,
|
||||||
}: PipelineMetricItemProps) => {
|
}: PipelineMetricItemProps) => {
|
||||||
|
const { t } = useTranslation('evaluation')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-3 px-1 py-1">
|
<div className="flex items-center justify-between gap-3 px-1 py-1">
|
||||||
<button
|
<button
|
||||||
@ -41,16 +50,38 @@ const PipelineMetricItem = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
{selected
|
||||||
type="button"
|
? (
|
||||||
disabled={disabledCondition}
|
<div className="flex items-center gap-2">
|
||||||
className={cn(
|
<span className="system-xs-medium text-text-accent">{t('pipeline.passIf')}</span>
|
||||||
'system-xs-medium text-text-tertiary',
|
<div className="w-[52px]">
|
||||||
disabledCondition && 'cursor-not-allowed text-components-button-secondary-accent-text-disabled',
|
<Input
|
||||||
)}
|
value={String(threshold)}
|
||||||
>
|
type="number"
|
||||||
+ Condition
|
min={0}
|
||||||
</button>
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
onChange={(event) => {
|
||||||
|
const parsedValue = Number(event.target.value)
|
||||||
|
if (!Number.isNaN(parsedValue))
|
||||||
|
onThresholdChange(parsedValue)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={disabledCondition}
|
||||||
|
className={cn(
|
||||||
|
'system-xs-medium text-text-tertiary',
|
||||||
|
disabledCondition && 'cursor-not-allowed text-components-button-secondary-accent-text-disabled',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
+ Condition
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import { encodeModelSelection } from './utils'
|
|||||||
|
|
||||||
type EvaluationStoreResources = Record<string, EvaluationResourceState>
|
type EvaluationStoreResources = Record<string, EvaluationResourceState>
|
||||||
|
|
||||||
|
export const DEFAULT_PIPELINE_METRIC_THRESHOLD = 0.85
|
||||||
|
|
||||||
const createId = (prefix: string) => `${prefix}-${Math.random().toString(36).slice(2, 10)}`
|
const createId = (prefix: string) => `${prefix}-${Math.random().toString(36).slice(2, 10)}`
|
||||||
|
|
||||||
const humanizeMetricId = (metricId: string) => {
|
const humanizeMetricId = (metricId: string) => {
|
||||||
@ -213,7 +215,11 @@ export function getConditionValue(
|
|||||||
return typeof previousValue === 'string' ? previousValue : null
|
return typeof previousValue === 'string' ? previousValue : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBuiltinMetric(metric: MetricOption, nodeInfoList: NodeInfo[] = []): EvaluationMetric {
|
export function createBuiltinMetric(
|
||||||
|
metric: MetricOption,
|
||||||
|
nodeInfoList: NodeInfo[] = [],
|
||||||
|
threshold = DEFAULT_PIPELINE_METRIC_THRESHOLD,
|
||||||
|
): EvaluationMetric {
|
||||||
return {
|
return {
|
||||||
id: createId('metric'),
|
id: createId('metric'),
|
||||||
optionId: metric.id,
|
optionId: metric.id,
|
||||||
@ -221,6 +227,7 @@ export function createBuiltinMetric(metric: MetricOption, nodeInfoList: NodeInfo
|
|||||||
label: metric.label,
|
label: metric.label,
|
||||||
description: metric.description,
|
description: metric.description,
|
||||||
badges: metric.badges,
|
badges: metric.badges,
|
||||||
|
threshold,
|
||||||
nodeInfoList,
|
nodeInfoList,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ type EvaluationStore = {
|
|||||||
hydrateResource: (resourceType: EvaluationResourceType, resourceId: string, config: EvaluationConfig) => void
|
hydrateResource: (resourceType: EvaluationResourceType, resourceId: string, config: EvaluationConfig) => void
|
||||||
setJudgeModel: (resourceType: EvaluationResourceType, resourceId: string, judgeModelId: string) => void
|
setJudgeModel: (resourceType: EvaluationResourceType, resourceId: string, judgeModelId: string) => void
|
||||||
addBuiltinMetric: (resourceType: EvaluationResourceType, resourceId: string, optionId: string, nodeInfoList?: NodeInfo[]) => void
|
addBuiltinMetric: (resourceType: EvaluationResourceType, resourceId: string, optionId: string, nodeInfoList?: NodeInfo[]) => void
|
||||||
|
updateMetricThreshold: (resourceType: EvaluationResourceType, resourceId: string, metricId: string, threshold: number) => void
|
||||||
addCustomMetric: (resourceType: EvaluationResourceType, resourceId: string) => void
|
addCustomMetric: (resourceType: EvaluationResourceType, resourceId: string) => void
|
||||||
removeMetric: (resourceType: EvaluationResourceType, resourceId: string, metricId: string) => void
|
removeMetric: (resourceType: EvaluationResourceType, resourceId: string, metricId: string) => void
|
||||||
setCustomMetricWorkflow: (
|
setCustomMetricWorkflow: (
|
||||||
@ -126,6 +127,17 @@ export const useEvaluationStore = create<EvaluationStore>((set, get) => ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
updateMetricThreshold: (resourceType, resourceId, metricId, threshold) => {
|
||||||
|
set(state => ({
|
||||||
|
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
||||||
|
...resource,
|
||||||
|
metrics: updateMetric(resource.metrics, metricId, metric => ({
|
||||||
|
...metric,
|
||||||
|
threshold,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
},
|
||||||
addCustomMetric: (resourceType, resourceId) => {
|
addCustomMetric: (resourceType, resourceId) => {
|
||||||
set(state => ({
|
set(state => ({
|
||||||
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
||||||
|
|||||||
@ -82,6 +82,7 @@ export type EvaluationMetric = {
|
|||||||
label: string
|
label: string
|
||||||
description: string
|
description: string
|
||||||
badges: string[]
|
badges: string[]
|
||||||
|
threshold?: number
|
||||||
nodeInfoList?: NodeInfo[]
|
nodeInfoList?: NodeInfo[]
|
||||||
customConfig?: CustomMetricConfig
|
customConfig?: CustomMetricConfig
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,6 +92,7 @@
|
|||||||
"metrics.showMore": "Show more",
|
"metrics.showMore": "Show more",
|
||||||
"metrics.title": "Metrics",
|
"metrics.title": "Metrics",
|
||||||
"metrics.update": "Update",
|
"metrics.update": "Update",
|
||||||
|
"pipeline.passIf": "Pass if \u2265",
|
||||||
"pipeline.uploadAndRun": "Upload & Run Test",
|
"pipeline.uploadAndRun": "Upload & Run Test",
|
||||||
"results.empty": "No evaluation results yet.",
|
"results.empty": "No evaluation results yet.",
|
||||||
"title": "Evaluation"
|
"title": "Evaluation"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user