mirror of
https://github.com/langgenius/dify.git
synced 2026-04-19 18:27:27 +08:00
feat(web): start run
This commit is contained in:
@ -1,10 +1,15 @@
|
||||
import type { ChangeEvent } from 'react'
|
||||
import type { EvaluationResourceProps } from '../../types'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { upload } from '@/service/base'
|
||||
import { useStartEvaluationRunMutation } from '@/service/use-evaluation'
|
||||
import { getEvaluationMockConfig } from '../../mock'
|
||||
import { useEvaluationResource, useEvaluationStore } from '../../store'
|
||||
import { buildEvaluationRunRequest } from '../../store-utils'
|
||||
|
||||
type InputFieldsTabProps = EvaluationResourceProps & {
|
||||
isPanelReady: boolean
|
||||
@ -23,10 +28,38 @@ const InputFieldsTab = ({
|
||||
.filter(field => field.id.includes('.input.') || field.group.toLowerCase().includes('input'))
|
||||
.slice(0, 4)
|
||||
const displayedRequirementFields = requirementFields.length > 0 ? requirementFields : config.fieldOptions.slice(0, 4)
|
||||
const uploadedFileName = useEvaluationResource(resourceType, resourceId).uploadedFileName
|
||||
const resource = useEvaluationResource(resourceType, resourceId)
|
||||
const uploadedFileId = resource.uploadedFileId
|
||||
const uploadedFileName = resource.uploadedFileName
|
||||
const setBatchTab = useEvaluationStore(state => state.setBatchTab)
|
||||
const setUploadedFile = useEvaluationStore(state => state.setUploadedFile)
|
||||
const setUploadedFileName = useEvaluationStore(state => state.setUploadedFileName)
|
||||
const runBatchTest = useEvaluationStore(state => state.runBatchTest)
|
||||
const startRunMutation = useStartEvaluationRunMutation()
|
||||
const uploadMutation = useMutation({
|
||||
mutationFn: (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
return upload({
|
||||
xhr: new XMLHttpRequest(),
|
||||
data: formData,
|
||||
})
|
||||
},
|
||||
onSuccess: (uploadedFile) => {
|
||||
setUploadedFile(resourceType, resourceId, {
|
||||
id: uploadedFile.id,
|
||||
name: typeof uploadedFile.name === 'string' ? uploadedFile.name : uploadedFileName ?? uploadedFile.id,
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
setUploadedFile(resourceType, resourceId, null)
|
||||
toast.error(t('batch.uploadError'))
|
||||
},
|
||||
})
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const isFileUploading = uploadMutation.isPending
|
||||
const isRunning = startRunMutation.isPending
|
||||
const isRunDisabled = !isRunnable || !uploadedFileId || isFileUploading || isRunning
|
||||
|
||||
const handleDownloadTemplate = () => {
|
||||
const content = ['case_id,input,expected', '1,Example input,Example output'].join('\n')
|
||||
@ -42,7 +75,51 @@ const InputFieldsTab = ({
|
||||
return
|
||||
}
|
||||
|
||||
runBatchTest(resourceType, resourceId)
|
||||
if (isFileUploading) {
|
||||
toast.warning(t('batch.uploading'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!uploadedFileId) {
|
||||
toast.warning(t('batch.fileRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
const body = buildEvaluationRunRequest(resource, uploadedFileId)
|
||||
|
||||
if (!body) {
|
||||
toast.warning(t('batch.validation'))
|
||||
return
|
||||
}
|
||||
|
||||
startRunMutation.mutate({
|
||||
params: {
|
||||
targetType: resourceType,
|
||||
targetId: resourceId,
|
||||
},
|
||||
body,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('batch.runStarted'))
|
||||
setBatchTab(resourceType, resourceId, 'history')
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t('batch.runFailed'))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
event.target.value = ''
|
||||
|
||||
if (!file) {
|
||||
setUploadedFile(resourceType, resourceId, null)
|
||||
return
|
||||
}
|
||||
|
||||
setUploadedFileName(resourceType, resourceId, file.name)
|
||||
uploadMutation.mutate(file)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -73,10 +150,7 @@ const InputFieldsTab = ({
|
||||
hidden
|
||||
type="file"
|
||||
accept=".csv,.xlsx"
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0]
|
||||
setUploadedFileName(resourceType, resourceId, file?.name ?? null)
|
||||
}}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
{isPanelReady && (
|
||||
<button
|
||||
@ -86,7 +160,9 @@ const InputFieldsTab = ({
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-file-upload-line h-5 w-5 text-text-tertiary" />
|
||||
<div className="mt-2 system-sm-semibold text-text-primary">{t('batch.uploadTitle')}</div>
|
||||
<div className="mt-1 system-xs-regular text-text-tertiary">{uploadedFileName ?? t('batch.uploadHint')}</div>
|
||||
<div className="mt-1 system-xs-regular text-text-tertiary">
|
||||
{isFileUploading ? t('batch.uploading') : uploadedFileName ?? t('batch.uploadHint')}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@ -95,7 +171,7 @@ const InputFieldsTab = ({
|
||||
{t('batch.validation')}
|
||||
</div>
|
||||
)}
|
||||
<Button className="w-full justify-center" variant="primary" disabled={!isRunnable} onClick={handleRun}>
|
||||
<Button className="w-full justify-center" variant="primary" disabled={isRunDisabled} loading={isRunning} onClick={handleRun}>
|
||||
{t('batch.run')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -16,11 +16,13 @@ import type {
|
||||
EvaluationJudgmentCondition,
|
||||
EvaluationJudgmentConditionValue,
|
||||
EvaluationJudgmentConfig,
|
||||
EvaluationRunRequest,
|
||||
NodeInfo,
|
||||
} from '@/types/evaluation'
|
||||
import { getEvaluationMockConfig } from './mock'
|
||||
import {
|
||||
buildConditionMetricOptions,
|
||||
decodeModelSelection,
|
||||
encodeModelSelection,
|
||||
getComparisonOperators,
|
||||
getDefaultComparisonOperator,
|
||||
@ -389,6 +391,7 @@ export const buildInitialState = (_resourceType: EvaluationResourceType): Evalua
|
||||
metrics: [],
|
||||
judgmentConfig: createEmptyJudgmentConfig(),
|
||||
activeBatchTab: 'input-fields',
|
||||
uploadedFileId: null,
|
||||
uploadedFileName: null,
|
||||
batchRecords: [],
|
||||
}
|
||||
@ -412,6 +415,100 @@ export const buildStateFromEvaluationConfig = (
|
||||
}
|
||||
}
|
||||
|
||||
const getApiComparisonOperator = (operator: ComparisonOperator) => {
|
||||
if (operator === 'is null')
|
||||
return 'null'
|
||||
|
||||
if (operator === 'is not null')
|
||||
return 'not null'
|
||||
|
||||
return operator
|
||||
}
|
||||
|
||||
const getCustomMetricScopeId = (metric: EvaluationMetric) => {
|
||||
if (metric.kind !== 'custom-workflow')
|
||||
return null
|
||||
|
||||
return metric.customConfig?.workflowAppId ?? metric.customConfig?.workflowId ?? null
|
||||
}
|
||||
|
||||
const buildCustomizedMetricsPayload = (metrics: EvaluationMetric[]): EvaluationRunRequest['customized_metrics'] => {
|
||||
const customMetric = metrics.find(metric => metric.kind === 'custom-workflow')
|
||||
const customConfig = customMetric?.customConfig
|
||||
const evaluationWorkflowId = customMetric ? getCustomMetricScopeId(customMetric) : null
|
||||
|
||||
if (!customConfig || !evaluationWorkflowId)
|
||||
return null
|
||||
|
||||
return {
|
||||
evaluation_workflow_id: evaluationWorkflowId,
|
||||
input_fields: Object.fromEntries(
|
||||
customConfig.mappings
|
||||
.filter((mapping): mapping is CustomMetricMapping & { inputVariableId: string, outputVariableId: string } =>
|
||||
!!mapping.inputVariableId && !!mapping.outputVariableId,
|
||||
)
|
||||
.map(mapping => [mapping.inputVariableId, mapping.outputVariableId]),
|
||||
),
|
||||
output_fields: customConfig.outputs.map(output => ({
|
||||
variable: output.id,
|
||||
value_type: output.valueType ?? undefined,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
const buildJudgmentConfigPayload = (resource: EvaluationResourceState): EvaluationRunRequest['judgment_config'] => {
|
||||
const conditions = resource.judgmentConfig.conditions
|
||||
.filter(condition => !!condition.variableSelector)
|
||||
.map((condition) => {
|
||||
const [scope, metricName] = condition.variableSelector!
|
||||
const customMetric = resource.metrics.find(metric =>
|
||||
metric.kind === 'custom-workflow'
|
||||
&& metric.customConfig?.workflowId === scope,
|
||||
)
|
||||
|
||||
const customScopeId = customMetric ? getCustomMetricScopeId(customMetric) : null
|
||||
|
||||
return {
|
||||
variable_selector: [customScopeId ?? scope, metricName],
|
||||
comparison_operator: getApiComparisonOperator(condition.comparisonOperator),
|
||||
...(requiresComparisonValue(condition.comparisonOperator) ? { value: condition.value ?? undefined } : {}),
|
||||
}
|
||||
})
|
||||
|
||||
if (!conditions.length)
|
||||
return null
|
||||
|
||||
return {
|
||||
logical_operator: resource.judgmentConfig.logicalOperator,
|
||||
conditions,
|
||||
}
|
||||
}
|
||||
|
||||
export const buildEvaluationRunRequest = (
|
||||
resource: EvaluationResourceState,
|
||||
fileId: string,
|
||||
): EvaluationRunRequest | null => {
|
||||
const selectedModel = decodeModelSelection(resource.judgeModelId)
|
||||
|
||||
if (!selectedModel)
|
||||
return null
|
||||
|
||||
return {
|
||||
file_id: fileId,
|
||||
evaluation_model: selectedModel.model,
|
||||
evaluation_model_provider: selectedModel.provider,
|
||||
default_metrics: resource.metrics
|
||||
.filter(metric => metric.kind === 'builtin')
|
||||
.map(metric => ({
|
||||
metric: metric.optionId,
|
||||
value_type: metric.valueType,
|
||||
node_info_list: metric.nodeInfoList ?? [],
|
||||
})),
|
||||
customized_metrics: buildCustomizedMetricsPayload(resource.metrics),
|
||||
judgment_config: buildJudgmentConfigPayload(resource),
|
||||
}
|
||||
}
|
||||
|
||||
const getResourceState = (
|
||||
resources: EvaluationStoreResources,
|
||||
resourceType: EvaluationResourceType,
|
||||
|
||||
@ -76,6 +76,11 @@ type EvaluationStore = {
|
||||
value: string | string[] | boolean | null,
|
||||
) => void
|
||||
setBatchTab: (resourceType: EvaluationResourceType, resourceId: string, tab: EvaluationResourceState['activeBatchTab']) => void
|
||||
setUploadedFile: (
|
||||
resourceType: EvaluationResourceType,
|
||||
resourceId: string,
|
||||
uploadedFile: { id: string, name: string } | null,
|
||||
) => void
|
||||
setUploadedFileName: (resourceType: EvaluationResourceType, resourceId: string, uploadedFileName: string | null) => void
|
||||
runBatchTest: (resourceType: EvaluationResourceType, resourceId: string) => void
|
||||
}
|
||||
@ -103,6 +108,7 @@ export const useEvaluationStore = create<EvaluationStore>((set, get) => ({
|
||||
[buildResourceKey(resourceType, resourceId)]: {
|
||||
...buildStateFromEvaluationConfig(resourceType, config),
|
||||
activeBatchTab: state.resources[buildResourceKey(resourceType, resourceId)]?.activeBatchTab ?? 'input-fields',
|
||||
uploadedFileId: state.resources[buildResourceKey(resourceType, resourceId)]?.uploadedFileId ?? null,
|
||||
uploadedFileName: state.resources[buildResourceKey(resourceType, resourceId)]?.uploadedFileName ?? null,
|
||||
batchRecords: state.resources[buildResourceKey(resourceType, resourceId)]?.batchRecords ?? [],
|
||||
},
|
||||
@ -369,10 +375,20 @@ export const useEvaluationStore = create<EvaluationStore>((set, get) => ({
|
||||
})),
|
||||
}))
|
||||
},
|
||||
setUploadedFile: (resourceType, resourceId, uploadedFile) => {
|
||||
set(state => ({
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
||||
...resource,
|
||||
uploadedFileId: uploadedFile?.id ?? null,
|
||||
uploadedFileName: uploadedFile?.name ?? null,
|
||||
})),
|
||||
}))
|
||||
},
|
||||
setUploadedFileName: (resourceType, resourceId, uploadedFileName) => {
|
||||
set(state => ({
|
||||
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
|
||||
...resource,
|
||||
uploadedFileId: null,
|
||||
uploadedFileName,
|
||||
})),
|
||||
}))
|
||||
|
||||
@ -136,6 +136,7 @@ export type EvaluationResourceState = {
|
||||
metrics: EvaluationMetric[]
|
||||
judgmentConfig: JudgmentConfig
|
||||
activeBatchTab: BatchTestTab
|
||||
uploadedFileId: string | null
|
||||
uploadedFileName: string | null
|
||||
batchRecords: BatchTestRecord[]
|
||||
}
|
||||
|
||||
@ -2,19 +2,24 @@
|
||||
"batch.description": "Execute batch evaluations and track performance history.",
|
||||
"batch.downloadTemplate": "Download Excel Template",
|
||||
"batch.emptyHistory": "No test history yet.",
|
||||
"batch.fileRequired": "Upload an evaluation dataset file before running the test.",
|
||||
"batch.noticeDescription": "Configuration incomplete. Select the Judge Model and Metrics on the left to generate your batch test template.",
|
||||
"batch.noticeTitle": "Quick start",
|
||||
"batch.requirementsDescription": "The input variables required to run this batch test. Ensure your uploaded dataset matches these fields.",
|
||||
"batch.requirementsTitle": "Data requirements",
|
||||
"batch.run": "Run Test",
|
||||
"batch.runFailed": "Failed to start batch test.",
|
||||
"batch.runStarted": "Batch test started.",
|
||||
"batch.status.failed": "Failed",
|
||||
"batch.status.running": "Running",
|
||||
"batch.status.success": "Success",
|
||||
"batch.tabs.history": "Test History",
|
||||
"batch.tabs.input-fields": "Input Fields",
|
||||
"batch.title": "Batch Test",
|
||||
"batch.uploadError": "Failed to upload file.",
|
||||
"batch.uploadHint": "Select a .csv or .xlsx file",
|
||||
"batch.uploadTitle": "Upload test file",
|
||||
"batch.uploading": "Uploading file...",
|
||||
"batch.validation": "Complete the judge model, metrics, and custom mappings before running a batch test.",
|
||||
"conditions.addCondition": "Add Condition",
|
||||
"conditions.addGroup": "Add Condition Group",
|
||||
|
||||
@ -2,19 +2,24 @@
|
||||
"batch.description": "执行批量评测并追踪性能历史。",
|
||||
"batch.downloadTemplate": "下载 Excel 模板",
|
||||
"batch.emptyHistory": "还没有测试历史。",
|
||||
"batch.fileRequired": "请先上传评估数据集文件,再运行测试。",
|
||||
"batch.noticeDescription": "配置尚未完成。请先在左侧选择判定模型和指标,以生成批量测试模板。",
|
||||
"batch.noticeTitle": "快速开始",
|
||||
"batch.requirementsDescription": "运行此批量测试所需的输入变量。请确保上传的数据集包含这些字段。",
|
||||
"batch.requirementsTitle": "数据要求",
|
||||
"batch.run": "运行测试",
|
||||
"batch.runFailed": "启动批量测试失败。",
|
||||
"batch.runStarted": "批量测试已启动。",
|
||||
"batch.status.failed": "失败",
|
||||
"batch.status.running": "运行中",
|
||||
"batch.status.success": "成功",
|
||||
"batch.tabs.history": "测试历史",
|
||||
"batch.tabs.input-fields": "输入字段",
|
||||
"batch.title": "批量测试",
|
||||
"batch.uploadError": "文件上传失败。",
|
||||
"batch.uploadHint": "选择 .csv 或 .xlsx 文件",
|
||||
"batch.uploadTitle": "上传测试文件",
|
||||
"batch.uploading": "文件上传中...",
|
||||
"batch.validation": "运行批量测试前,请先完成判定模型、指标和自定义映射配置。",
|
||||
"conditions.addCondition": "添加条件",
|
||||
"conditions.addGroup": "添加条件组",
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import { consoleClient, consoleQuery } from '@/service/client'
|
||||
|
||||
@ -62,6 +63,18 @@ export const useEvaluationNodeInfoMutation = () => {
|
||||
return useMutation(consoleQuery.evaluation.nodeInfo.mutationOptions())
|
||||
}
|
||||
|
||||
export const useStartEvaluationRunMutation = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation(consoleQuery.evaluation.startRun.mutationOptions({
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.evaluation.logs.key(),
|
||||
})
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export const useAvailableEvaluationWorkflows = (
|
||||
params: AvailableEvaluationWorkflowsParams = {},
|
||||
options?: { enabled?: boolean },
|
||||
|
||||
Reference in New Issue
Block a user