Files
dify/web/features/agent-v2/agent-composer/knowledge-validation.ts

255 lines
8.8 KiB
TypeScript

import type { AgentKnowledgeRetrievalItem } from './form-state'
import { useTranslation } from 'react-i18next'
import { comparisonOperatorNotRequireValue } from '@/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/utils'
import { MetadataFilteringModeEnum } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
import { checkoutRerankModelConfiguredInRetrievalSettings } from '@/app/components/workflow/nodes/knowledge-retrieval/utils'
import { RETRIEVE_TYPE } from '@/types/app'
export type KnowledgeValidationIssueCode =
| 'name_required'
| 'name_duplicate'
| 'datasets_required'
| 'custom_query_required'
| 'single_model_required'
| 'rerank_model_required'
| 'metadata_model_required'
| 'metadata_conditions_required'
| 'metadata_value_required'
| 'space_unavailable'
| 'binding_invalid'
type KnowledgeValidationField = 'name' | 'datasets' | 'query' | 'retrieval' | 'metadata'
type KnowledgeValidationIssue = {
itemId: string
code: KnowledgeValidationIssueCode
field: KnowledgeValidationField
}
export type KnowledgeValidationResult = {
byId: Record<string, Partial<Record<KnowledgeValidationField, KnowledgeValidationIssueCode>>>
firstIssue?: KnowledgeValidationIssue
isValid: boolean
}
const getKnowledgeRetrievalConcreteName = (item: AgentKnowledgeRetrievalItem) =>
item.name?.trim() ?? ''
export const getKnowledgeRetrievalSetName = (item: AgentKnowledgeRetrievalItem) =>
getKnowledgeRetrievalConcreteName(item) || item.id
export const useKnowledgeValidationMessage = () => {
const { t } = useTranslation(['agentV2'])
const { t: tCommon } = useTranslation(['common', 'modelProvider'])
const { t: tAppDebug } = useTranslation(['appDebug'])
const { t: tWorkflow } = useTranslation(['workflowModels'])
return (issueCode?: KnowledgeValidationIssueCode, metadataFieldNames?: string) => {
switch (issueCode) {
case 'space_unavailable':
return t(($) => $['agentDetail.configure.knowledgeFs.unavailable'])
case 'binding_invalid':
return t(($) => $['agentDetail.configure.knowledgeFs.invalid'])
case 'name_required':
return tCommon(($) => $['errorMsg.fieldRequired'], {
field: t(($) => $['agentDetail.configure.knowledgeRetrieval.dialog.nameLabel']),
})
case 'name_duplicate':
return tAppDebug(($) => $['varKeyError.keyAlreadyExists'], {
key: t(($) => $['agentDetail.configure.knowledgeRetrieval.dialog.nameLabel']),
})
case 'datasets_required':
return tCommon(($) => $['errorMsg.fieldRequired'], {
field: t(($) => $['agentDetail.configure.knowledgeRetrieval.dialog.knowledge.label']),
})
case 'custom_query_required':
return tCommon(($) => $['errorMsg.fieldRequired'], {
field: t(
($) => $['agentDetail.configure.knowledgeRetrieval.dialog.query.customInputLabel'],
),
})
case 'single_model_required':
return tCommon(($) => $['errorMsg.fieldRequired'], {
field: tCommon(($) => $['modelProvider.systemReasoningModel.key'], {
ns: 'modelProvider',
}),
})
case 'metadata_model_required':
return t(
($) => $['agentDetail.configure.knowledgeRetrieval.validation.metadataModelRequired'],
)
case 'rerank_model_required':
return tCommon(($) => $['errorMsg.fieldRequired'], {
field: tCommon(($) => $['modelProvider.rerankModel.key'], { ns: 'modelProvider' }),
})
case 'metadata_conditions_required':
return tCommon(($) => $['errorMsg.fieldRequired'], {
field: tWorkflow(($) => $['nodes.knowledgeRetrieval.metadata.panel.conditions'], {
ns: 'workflowModels',
}),
})
case 'metadata_value_required':
return tAppDebug(($) => $['errorMessage.valueOfVarRequired'], {
key:
metadataFieldNames ??
tWorkflow(($) => $['nodes.knowledgeRetrieval.metadata.panel.conditions']),
})
default:
return undefined
}
}
}
const getKnowledgeDatasetCount = (item: AgentKnowledgeRetrievalItem) =>
item.selectedDatasets?.length ?? item.datasetRefs?.length ?? 0
export const getMetadataConditionsMissingValues = (item: AgentKnowledgeRetrievalItem) => {
if (item.metadataFilterMode !== MetadataFilteringModeEnum.manual) return []
return (item.metadataFilteringConditions?.conditions ?? []).filter(
({ comparison_operator, value }) =>
!comparisonOperatorNotRequireValue(comparison_operator) &&
(value === undefined ||
value === null ||
(typeof value === 'string' && !value.trim()) ||
(Array.isArray(value) && value.length === 0)),
)
}
const getNormalizedKnowledgeName = (item: AgentKnowledgeRetrievalItem) =>
getKnowledgeRetrievalConcreteName(item).toLowerCase()
export const validateKnowledgeRetrievals = (
retrievals: AgentKnowledgeRetrievalItem[],
nameScope: AgentKnowledgeRetrievalItem[] = retrievals,
): KnowledgeValidationResult => {
const byId: KnowledgeValidationResult['byId'] = {}
const issues: KnowledgeValidationIssue[] = []
const nameCounts = new Map<string, number>()
const spaceCount = retrievals.filter((item) => item.controlSpaceId).length
// Hidden bindings still reserve their names, without requiring field validation.
nameScope.forEach((item) => {
const normalizedName = getNormalizedKnowledgeName(item)
if (!normalizedName) return
nameCounts.set(normalizedName, (nameCounts.get(normalizedName) ?? 0) + 1)
})
const pushIssue = (issue: KnowledgeValidationIssue) => {
byId[issue.itemId] ??= {}
const itemIssues = byId[issue.itemId]
if (itemIssues) itemIssues[issue.field] ??= issue.code
issues.push(issue)
}
retrievals.forEach((item) => {
const setName = getKnowledgeRetrievalConcreteName(item)
const normalizedName = setName.toLowerCase()
if (!setName) {
pushIssue({
itemId: item.id,
code: 'name_required',
field: 'name',
})
} else if ((nameCounts.get(normalizedName) ?? 0) > 1) {
pushIssue({
itemId: item.id,
code: 'name_duplicate',
field: 'name',
})
}
if (item.controlSpaceId) {
if (item.isMissing)
pushIssue({ itemId: item.id, code: 'space_unavailable', field: 'datasets' })
if (
spaceCount > 10 ||
setName.length > 120 ||
/[\u0000-\u001F\u007F]/u.test(setName) ||
(item.description?.length ?? 0) > 2000 ||
!/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i.test(item.controlSpaceId) ||
retrievals.some(
(other) =>
other.id !== item.id &&
(other.controlSpaceId === item.controlSpaceId ||
other.id.toLowerCase() === normalizedName),
)
)
pushIssue({ itemId: item.id, code: 'binding_invalid', field: 'datasets' })
return
}
if (!getKnowledgeDatasetCount(item)) {
pushIssue({
itemId: item.id,
code: 'datasets_required',
field: 'datasets',
})
}
if (item.queryMode === 'custom' && !item.customQuery?.trim()) {
pushIssue({
itemId: item.id,
code: 'custom_query_required',
field: 'query',
})
}
if (
item.retrievalMode === RETRIEVE_TYPE.oneWay &&
(!item.singleRetrievalConfig?.model?.provider || !item.singleRetrievalConfig.model.name)
) {
pushIssue({
itemId: item.id,
code: 'single_model_required',
field: 'retrieval',
})
}
if (
item.metadataFilterMode === MetadataFilteringModeEnum.automatic &&
(!item.metadataModelConfig?.provider || !item.metadataModelConfig.name)
) {
pushIssue({
itemId: item.id,
code: 'metadata_model_required',
field: 'metadata',
})
}
if (
item.retrievalMode !== RETRIEVE_TYPE.oneWay &&
// Saved bindings may have only dataset refs. Without catalog details, an
// explicitly enabled reranker still requires a model; disabled ones do not.
(item.selectedDatasets || item.multipleRetrievalConfig?.reranking_enable) &&
!checkoutRerankModelConfiguredInRetrievalSettings(
item.selectedDatasets ?? [],
item.multipleRetrievalConfig,
)
) {
pushIssue({ itemId: item.id, code: 'rerank_model_required', field: 'retrieval' })
}
if (
item.metadataFilterMode === MetadataFilteringModeEnum.manual &&
!item.metadataFilteringConditions?.conditions.length
) {
pushIssue({
itemId: item.id,
code: 'metadata_conditions_required',
field: 'metadata',
})
}
if (getMetadataConditionsMissingValues(item).length) {
pushIssue({ itemId: item.id, code: 'metadata_value_required', field: 'metadata' })
}
})
return {
byId,
firstIssue: issues[0],
isValid: issues.length === 0,
}
}