diff --git a/src/langbot/pkg/api/http/service/knowledge.py b/src/langbot/pkg/api/http/service/knowledge.py index 3170a113..48cb7cac 100644 --- a/src/langbot/pkg/api/http/service/knowledge.py +++ b/src/langbot/pkg/api/http/service/knowledge.py @@ -31,15 +31,126 @@ class KnowledgeService: if not knowledge_engine_plugin_id: 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( name=kb_data.get('name', 'Untitled'), knowledge_engine_plugin_id=knowledge_engine_plugin_id, - creation_settings=kb_data.get('creation_settings', {}), - retrieval_settings=kb_data.get('retrieval_settings', {}), + creation_settings=creation_settings, + retrieval_settings=retrieval_settings, description=kb_data.get('description', ''), ) 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: """更新知识库""" # Filter to only mutable fields diff --git a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx index 75c9f47f..f52f7cd3 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx @@ -195,6 +195,7 @@ export default function DynamicFormComponent({ isEditing, externalDependentValues, systemContext, + onValidate, }: { itemConfigList: IDynamicFormItemSchema[]; onSubmit?: (val: object) => unknown; @@ -205,6 +206,9 @@ export default function DynamicFormComponent({ /** Extra variables accessible via the `__system.*` namespace in show_if conditions. * e.g. `{ is_wizard: true }` makes `show_if: { field: "__system.is_wizard", ... }` work. */ systemContext?: Record; + /** Callback to expose validation function to parent component. + * Parent can call this function to trigger validation and get validity state. */ + onValidate?: (validateFn: () => Promise) => void; }) { const isInitialMount = useRef(true); const previousInitialValues = useRef(initialValues); @@ -352,6 +356,17 @@ export default function DynamicFormComponent({ }, {} as FormValues), }); + // Expose validation function to parent component + const validate = async (): Promise => { + // Trigger validation for all fields + const result = await form.trigger(); + return result; + }; + + useEffect(() => { + onValidate?.(validate); + }, [onValidate]); + // 当 initialValues 变化时更新表单值 // 但要避免因为内部表单更新触发的 onSubmit 导致的 initialValues 变化而重新设置表单 useEffect(() => { diff --git a/web/src/app/home/knowledge/components/kb-form/KBForm.tsx b/web/src/app/home/knowledge/components/kb-form/KBForm.tsx index af9250fb..8f4f76a0 100644 --- a/web/src/app/home/knowledge/components/kb-form/KBForm.tsx +++ b/web/src/app/home/knowledge/components/kb-form/KBForm.tsx @@ -57,7 +57,6 @@ const getFormSchema = (t: (key: string) => string) => * Parse creation schema from Knowledge Engine to IDynamicFormItemSchema[] */ function parseCreationSchema( - // eslint-disable-next-line @typescript-eslint/no-explicit-any schemaItems: any | any[] | undefined, ): IDynamicFormItemSchema[] { if (!schemaItems) return []; @@ -107,6 +106,10 @@ export default function KBForm({ const savedSnapshotRef = useRef(''); const isInitializing = useRef(true); + // Refs to store validation functions from dynamic forms + const configValidateRef = useRef<(() => Promise) | null>(null); + const retrievalValidateRef = useRef<(() => Promise) | null>(null); + const formSchema = getFormSchema(t); const form = useForm>({ @@ -235,7 +238,24 @@ export default function KBForm({ } }; - const onSubmit = (data: z.infer) => { + const onSubmit = async (data: z.infer) => { + // 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 = { name: data.name, description: data.description ?? '', @@ -490,6 +510,9 @@ export default function KBForm({ } isEditing={isEditing} externalDependentValues={retrievalSettings} + onValidate={(validateFn) => + (configValidateRef.current = validateFn) + } /> @@ -512,6 +535,9 @@ export default function KBForm({ setRetrievalSettings(val as Record) } externalDependentValues={configSettings} + onValidate={(validateFn) => + (retrievalValidateRef.current = validateFn) + } /> diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 693d09a8..fc95f41a 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -928,6 +928,10 @@ const enUS = { engineSettingsDescription: 'Configuration for the selected knowledge engine', 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', retrievalSettingsDescription: 'Configure how documents are retrieved from this knowledge base', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 5e676688..75910035 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -886,6 +886,8 @@ const zhHans = { engineSettings: '引擎设置', engineSettingsDescription: '所选知识引擎的配置', engineSettingsReadonly: '编辑模式下不可修改', + engineSettingsInvalid: '引擎设置中存在无效项,请检查必填字段', + retrievalSettingsInvalid: '检索设置中存在无效项,请检查必填字段', retrievalSettings: '检索设置', retrievalSettingsDescription: '配置从此知识库检索文档的方式', dangerZone: '危险区域',