mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
feat(knowledge): validate required fields based on plugin schema
Add business-agnostic validation for knowledge base creation: - Backend: dynamically validate required fields from plugin's creation_schema and retrieval_schema, with support for show_if conditional fields - Frontend: expose validation function from DynamicFormComponent and validate before KBForm submission - Add i18n translations for validation error messages Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -31,15 +31,126 @@ class KnowledgeService:
|
|||||||
if not knowledge_engine_plugin_id:
|
if not knowledge_engine_plugin_id:
|
||||||
raise ValueError('knowledge_engine_plugin_id is required')
|
raise ValueError('knowledge_engine_plugin_id is required')
|
||||||
|
|
||||||
|
creation_settings = kb_data.get('creation_settings', {})
|
||||||
|
retrieval_settings = kb_data.get('retrieval_settings', {})
|
||||||
|
|
||||||
|
# Validate required fields based on plugin's creation_schema and retrieval_schema
|
||||||
|
await self._validate_schema_required_fields(
|
||||||
|
knowledge_engine_plugin_id,
|
||||||
|
creation_settings,
|
||||||
|
retrieval_settings,
|
||||||
|
)
|
||||||
|
|
||||||
kb = await self.ap.rag_mgr.create_knowledge_base(
|
kb = await self.ap.rag_mgr.create_knowledge_base(
|
||||||
name=kb_data.get('name', 'Untitled'),
|
name=kb_data.get('name', 'Untitled'),
|
||||||
knowledge_engine_plugin_id=knowledge_engine_plugin_id,
|
knowledge_engine_plugin_id=knowledge_engine_plugin_id,
|
||||||
creation_settings=kb_data.get('creation_settings', {}),
|
creation_settings=creation_settings,
|
||||||
retrieval_settings=kb_data.get('retrieval_settings', {}),
|
retrieval_settings=retrieval_settings,
|
||||||
description=kb_data.get('description', ''),
|
description=kb_data.get('description', ''),
|
||||||
)
|
)
|
||||||
return kb.uuid
|
return kb.uuid
|
||||||
|
|
||||||
|
async def _validate_schema_required_fields(
|
||||||
|
self,
|
||||||
|
plugin_id: str,
|
||||||
|
creation_settings: dict,
|
||||||
|
retrieval_settings: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Validate required fields based on plugin's creation_schema and retrieval_schema.
|
||||||
|
|
||||||
|
This is a business-agnostic validation that checks all fields marked as
|
||||||
|
required in the plugin's schema, regardless of field type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugin_id: Knowledge Engine plugin ID.
|
||||||
|
creation_settings: User-provided creation settings.
|
||||||
|
retrieval_settings: User-provided retrieval settings.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If any required field is missing or empty.
|
||||||
|
"""
|
||||||
|
# Validate creation_schema
|
||||||
|
try:
|
||||||
|
creation_schema = await self.ap.plugin_connector.get_rag_creation_schema(plugin_id)
|
||||||
|
self._check_required_fields(creation_schema, creation_settings, 'creation_settings')
|
||||||
|
except ValueError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
self.ap.logger.warning(f'Failed to get creation_schema for validation: {e}')
|
||||||
|
|
||||||
|
# Validate retrieval_schema
|
||||||
|
try:
|
||||||
|
retrieval_schema = await self.ap.plugin_connector.get_rag_retrieval_schema(plugin_id)
|
||||||
|
self._check_required_fields(retrieval_schema, retrieval_settings, 'retrieval_settings')
|
||||||
|
except ValueError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
self.ap.logger.warning(f'Failed to get retrieval_schema for validation: {e}')
|
||||||
|
|
||||||
|
def _check_required_fields(
|
||||||
|
self,
|
||||||
|
schema: dict | list,
|
||||||
|
settings: dict,
|
||||||
|
context: str,
|
||||||
|
) -> None:
|
||||||
|
"""Check required fields in schema against provided settings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schema: Plugin-defined schema (can be list or dict with 'schema' key).
|
||||||
|
settings: User-provided settings values.
|
||||||
|
context: Context name for error messages (e.g., 'creation_settings').
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If a required field is missing or empty.
|
||||||
|
"""
|
||||||
|
if not schema:
|
||||||
|
return
|
||||||
|
|
||||||
|
# schema can be a list directly, or a dict with 'schema' key
|
||||||
|
items = schema if isinstance(schema, list) else schema.get('schema', [])
|
||||||
|
if not items:
|
||||||
|
return
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
field_name = item.get('name')
|
||||||
|
if not field_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
is_required = item.get('required', False)
|
||||||
|
if not is_required:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check show_if condition - if field is conditionally shown, only validate when condition is met
|
||||||
|
show_if = item.get('show_if')
|
||||||
|
if show_if:
|
||||||
|
depend_field = show_if.get('field')
|
||||||
|
operator = show_if.get('operator')
|
||||||
|
expected_value = show_if.get('value')
|
||||||
|
|
||||||
|
if depend_field and operator:
|
||||||
|
depend_value = settings.get(depend_field)
|
||||||
|
# If show_if condition is not met, skip validation for this field
|
||||||
|
if operator == 'eq' and depend_value != expected_value:
|
||||||
|
continue
|
||||||
|
if operator == 'neq' and depend_value == expected_value:
|
||||||
|
continue
|
||||||
|
if operator == 'in' and isinstance(expected_value, list) and depend_value not in expected_value:
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = settings.get(field_name)
|
||||||
|
|
||||||
|
# Validate required field has a non-empty value
|
||||||
|
if value is None or (isinstance(value, str) and value.strip() == ''):
|
||||||
|
# Get field label for friendly error message
|
||||||
|
label = item.get('label', {})
|
||||||
|
field_label = (
|
||||||
|
label.get('en_US', field_name)
|
||||||
|
or label.get('zh_Hans', field_name)
|
||||||
|
or label.get('zh_Hant', field_name)
|
||||||
|
or field_name
|
||||||
|
)
|
||||||
|
raise ValueError(f'{field_label} is required ({context}.{field_name})')
|
||||||
|
|
||||||
async def update_knowledge_base(self, kb_uuid: str, kb_data: dict) -> None:
|
async def update_knowledge_base(self, kb_uuid: str, kb_data: dict) -> None:
|
||||||
"""更新知识库"""
|
"""更新知识库"""
|
||||||
# Filter to only mutable fields
|
# Filter to only mutable fields
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ export default function DynamicFormComponent({
|
|||||||
isEditing,
|
isEditing,
|
||||||
externalDependentValues,
|
externalDependentValues,
|
||||||
systemContext,
|
systemContext,
|
||||||
|
onValidate,
|
||||||
}: {
|
}: {
|
||||||
itemConfigList: IDynamicFormItemSchema[];
|
itemConfigList: IDynamicFormItemSchema[];
|
||||||
onSubmit?: (val: object) => unknown;
|
onSubmit?: (val: object) => unknown;
|
||||||
@@ -205,6 +206,9 @@ export default function DynamicFormComponent({
|
|||||||
/** Extra variables accessible via the `__system.*` namespace in show_if conditions.
|
/** Extra variables accessible via the `__system.*` namespace in show_if conditions.
|
||||||
* e.g. `{ is_wizard: true }` makes `show_if: { field: "__system.is_wizard", ... }` work. */
|
* e.g. `{ is_wizard: true }` makes `show_if: { field: "__system.is_wizard", ... }` work. */
|
||||||
systemContext?: Record<string, unknown>;
|
systemContext?: Record<string, unknown>;
|
||||||
|
/** Callback to expose validation function to parent component.
|
||||||
|
* Parent can call this function to trigger validation and get validity state. */
|
||||||
|
onValidate?: (validateFn: () => Promise<boolean>) => void;
|
||||||
}) {
|
}) {
|
||||||
const isInitialMount = useRef(true);
|
const isInitialMount = useRef(true);
|
||||||
const previousInitialValues = useRef(initialValues);
|
const previousInitialValues = useRef(initialValues);
|
||||||
@@ -352,6 +356,17 @@ export default function DynamicFormComponent({
|
|||||||
}, {} as FormValues),
|
}, {} as FormValues),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Expose validation function to parent component
|
||||||
|
const validate = async (): Promise<boolean> => {
|
||||||
|
// Trigger validation for all fields
|
||||||
|
const result = await form.trigger();
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onValidate?.(validate);
|
||||||
|
}, [onValidate]);
|
||||||
|
|
||||||
// 当 initialValues 变化时更新表单值
|
// 当 initialValues 变化时更新表单值
|
||||||
// 但要避免因为内部表单更新触发的 onSubmit 导致的 initialValues 变化而重新设置表单
|
// 但要避免因为内部表单更新触发的 onSubmit 导致的 initialValues 变化而重新设置表单
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ const getFormSchema = (t: (key: string) => string) =>
|
|||||||
* Parse creation schema from Knowledge Engine to IDynamicFormItemSchema[]
|
* Parse creation schema from Knowledge Engine to IDynamicFormItemSchema[]
|
||||||
*/
|
*/
|
||||||
function parseCreationSchema(
|
function parseCreationSchema(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
schemaItems: any | any[] | undefined,
|
schemaItems: any | any[] | undefined,
|
||||||
): IDynamicFormItemSchema[] {
|
): IDynamicFormItemSchema[] {
|
||||||
if (!schemaItems) return [];
|
if (!schemaItems) return [];
|
||||||
@@ -107,6 +106,10 @@ export default function KBForm({
|
|||||||
const savedSnapshotRef = useRef<string>('');
|
const savedSnapshotRef = useRef<string>('');
|
||||||
const isInitializing = useRef(true);
|
const isInitializing = useRef(true);
|
||||||
|
|
||||||
|
// Refs to store validation functions from dynamic forms
|
||||||
|
const configValidateRef = useRef<(() => Promise<boolean>) | null>(null);
|
||||||
|
const retrievalValidateRef = useRef<(() => Promise<boolean>) | null>(null);
|
||||||
|
|
||||||
const formSchema = getFormSchema(t);
|
const formSchema = getFormSchema(t);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
@@ -235,7 +238,24 @@ export default function KBForm({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = (data: z.infer<typeof formSchema>) => {
|
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
||||||
|
// Validate dynamic forms before submission
|
||||||
|
if (configValidateRef.current) {
|
||||||
|
const configValid = await configValidateRef.current();
|
||||||
|
if (!configValid) {
|
||||||
|
toast.error(t('knowledge.engineSettingsInvalid'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retrievalValidateRef.current) {
|
||||||
|
const retrievalValid = await retrievalValidateRef.current();
|
||||||
|
if (!retrievalValid) {
|
||||||
|
toast.error(t('knowledge.retrievalSettingsInvalid'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const kbData: KnowledgeBase = {
|
const kbData: KnowledgeBase = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description ?? '',
|
description: data.description ?? '',
|
||||||
@@ -490,6 +510,9 @@ export default function KBForm({
|
|||||||
}
|
}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
externalDependentValues={retrievalSettings}
|
externalDependentValues={retrievalSettings}
|
||||||
|
onValidate={(validateFn) =>
|
||||||
|
(configValidateRef.current = validateFn)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -512,6 +535,9 @@ export default function KBForm({
|
|||||||
setRetrievalSettings(val as Record<string, unknown>)
|
setRetrievalSettings(val as Record<string, unknown>)
|
||||||
}
|
}
|
||||||
externalDependentValues={configSettings}
|
externalDependentValues={configSettings}
|
||||||
|
onValidate={(validateFn) =>
|
||||||
|
(retrievalValidateRef.current = validateFn)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -928,6 +928,10 @@ const enUS = {
|
|||||||
engineSettingsDescription:
|
engineSettingsDescription:
|
||||||
'Configuration for the selected knowledge engine',
|
'Configuration for the selected knowledge engine',
|
||||||
engineSettingsReadonly: 'read-only in edit mode',
|
engineSettingsReadonly: 'read-only in edit mode',
|
||||||
|
engineSettingsInvalid:
|
||||||
|
'Engine settings validation failed, please check required fields',
|
||||||
|
retrievalSettingsInvalid:
|
||||||
|
'Retrieval settings validation failed, please check required fields',
|
||||||
retrievalSettings: 'Retrieval Settings',
|
retrievalSettings: 'Retrieval Settings',
|
||||||
retrievalSettingsDescription:
|
retrievalSettingsDescription:
|
||||||
'Configure how documents are retrieved from this knowledge base',
|
'Configure how documents are retrieved from this knowledge base',
|
||||||
|
|||||||
@@ -886,6 +886,8 @@ const zhHans = {
|
|||||||
engineSettings: '引擎设置',
|
engineSettings: '引擎设置',
|
||||||
engineSettingsDescription: '所选知识引擎的配置',
|
engineSettingsDescription: '所选知识引擎的配置',
|
||||||
engineSettingsReadonly: '编辑模式下不可修改',
|
engineSettingsReadonly: '编辑模式下不可修改',
|
||||||
|
engineSettingsInvalid: '引擎设置中存在无效项,请检查必填字段',
|
||||||
|
retrievalSettingsInvalid: '检索设置中存在无效项,请检查必填字段',
|
||||||
retrievalSettings: '检索设置',
|
retrievalSettings: '检索设置',
|
||||||
retrievalSettingsDescription: '配置从此知识库检索文档的方式',
|
retrievalSettingsDescription: '配置从此知识库检索文档的方式',
|
||||||
dangerZone: '危险区域',
|
dangerZone: '危险区域',
|
||||||
|
|||||||
Reference in New Issue
Block a user