Source code for core.config_validator

"""
配置验证模块

该模块提供配置文件结构验证、参数合法性检查和配置一致性验证功能。
"""

import json
import logging
import os
from typing import Any, Dict

from jsonschema import ValidationError, validate

HAS_JSONSCHEMA = True

logger = logging.getLogger(__name__)


[docs] class ConfigValidator: """配置验证器类""" # 定义配置模式 CONFIG_SCHEMA = { "type": "object", "properties": { "storage": { "type": "object", "properties": {"base_path": {"type": "string"}, "output_path": {"type": "string"}}, "required": ["base_path", "output_path"], }, "processors": { "type": "object", "patternProperties": { "^[a-zA-Z_][a-zA-Z0-9_]*$": { "type": "object", "properties": { "enabled": {"type": "boolean"}, "config": {"type": "object"}, "module": {"type": "string"}, "class": {"type": "string"}, }, "required": ["enabled"], } }, }, "connectors": {"type": "object"}, "rag_settings": { "type": "object", "properties": { "top_k": {"type": "integer", "minimum": 1, "maximum": 100}, "threshold": {"type": "number", "minimum": 0.0, "maximum": 1.0}, "prompt_template": {"type": "string"}, }, }, "llm": { "type": "object", "properties": { "type": {"type": "string"}, "api_url": {"type": "string"}, "api_key": {"type": "string"}, "model": {"type": "string"}, "embedding_model": {"type": "string"}, "settings": {"type": "object"}, }, }, "logging": { "type": "object", "properties": { "level": {"type": "string", "enum": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]}, "format": {"type": "string"}, "file": {"type": "string"}, }, }, }, "required": ["storage", "processors"], } # 标准处理器名称映射(支持向后兼容) PROCESSOR_NAME_MAPPING = { # 旧名称 -> 新名称 "PreProcessor": "pre_processor", "OCRProcessor": "ocr_processor", "StructureProcessor": "structure_processor", "ClassificationProcessor": "classification_processor", "FormatConverter": "format_converter", "EmbeddingProcessor": "embedding_processor", # 新名称保持不变 "pre_processor": "pre_processor", "ocr_processor": "ocr_processor", "structure_processor": "structure_processor", "classification_processor": "classification_processor", "format_converter": "format_converter", "embedding_processor": "embedding_processor", } # 必需的处理器(按执行顺序) REQUIRED_PROCESSORS = ["pre_processor", "ocr_processor", "structure_processor", "embedding_processor"]
[docs] def __init__(self) -> None: """初始化配置验证器""" self.validation_errors: list[str] = [] self.warnings: list[str] = []
[docs] def validate_config(self, config: Dict[str, Any]) -> bool: """ 验证配置的完整性和合法性 Args: config: 配置字典 Returns: bool: 验证是否通过 """ self.validation_errors = [] self.warnings = [] try: # JSON Schema验证(如果可用) if HAS_JSONSCHEMA: validate(instance=config, schema=self.CONFIG_SCHEMA) logger.info("配置结构验证通过") else: self.validation_errors.append("jsonschema is required for configuration validation") return False except ValidationError as e: self.validation_errors.append(f"配置结构验证失败: {e.message}") logger.error(f"配置结构验证失败: {e.message}") return False except Exception as e: self.validation_errors.append(f"Configuration validation could not run: {e}") return False # 自定义验证 self._validate_storage_paths(config.get("storage", {})) self._validate_processors(config.get("processors", {})) self._validate_connectors(config.get("connectors", {})) self._validate_rag_settings(config.get("rag_settings", {})) self._validate_logging_config(config.get("logging", {})) # 输出警告 for warning in self.warnings: logger.warning(warning) # 检查是否有验证错误 if self.validation_errors: for error in self.validation_errors: logger.error(error) return False logger.info("配置验证完成,所有检查通过") return True
def _validate_storage_paths(self, storage_config: Dict[str, Any]) -> None: """验证存储路径配置""" base_path = storage_config.get("base_path") output_path = storage_config.get("output_path") if base_path and not os.path.isabs(base_path): self.warnings.append(f"建议使用绝对路径作为base_path: {base_path}") if output_path and not os.path.isabs(output_path): self.warnings.append(f"建议使用绝对路径作为output_path: {output_path}") def _validate_processors(self, processors_config: Dict[str, Any]) -> None: """验证处理器配置""" if not processors_config: self.validation_errors.append("处理器配置不能为空") return # 标准化处理器名称 normalized_processors = set() for processor_name in processors_config.keys(): if processor_name in self.PROCESSOR_NAME_MAPPING: normalized_name = self.PROCESSOR_NAME_MAPPING[processor_name] normalized_processors.add(normalized_name) else: self.warnings.append(f"未知的处理器类型: {processor_name}") # 检查必需的处理器 missing_processors = set(self.REQUIRED_PROCESSORS) - normalized_processors if missing_processors: self.validation_errors.append(f"缺少必需的处理器: {', '.join(missing_processors)}") # 检查处理器启用状态 enabled_processors = [] for processor_name, processor_config in processors_config.items(): if processor_config.get("enabled", False): normalized_name = self.PROCESSOR_NAME_MAPPING.get(processor_name, processor_name) enabled_processors.append(normalized_name) if not enabled_processors: self.validation_errors.append("至少需要启用一个处理器") # 验证处理器顺序(基本检查) required_enabled = [p for p in self.REQUIRED_PROCESSORS if p in enabled_processors] if len(required_enabled) > 1: logger.info(f"启用的必需处理器: {', '.join(required_enabled)}") def _validate_connectors(self, connectors_config: Dict[str, Any]) -> None: """验证连接器配置""" if not connectors_config: self.warnings.append("未配置任何连接器") return # 检查OCR连接器 ocr_connector = connectors_config.get("OCRAPIConnector", {}) if ocr_connector: for api_name, api_config in ocr_connector.items(): if not api_config.get("api_key"): self.warnings.append(f"OCR连接器 {api_name} 缺少API密钥") # 检查LLM连接器 llm_connector = connectors_config.get("LLMConnector", {}) if llm_connector: if not llm_connector.get("api_key"): self.warnings.append("LLM连接器缺少API密钥") if not llm_connector.get("model"): self.warnings.append("LLM连接器缺少模型配置") def _validate_rag_settings(self, rag_config: Dict[str, Any]) -> None: """验证RAG设置""" if not rag_config: self.warnings.append("未配置RAG设置,将使用默认值") return top_k = rag_config.get("top_k", 5) if not isinstance(top_k, int) or top_k < 1 or top_k > 100: self.validation_errors.append("top_k必须是1-100之间的整数") threshold = rag_config.get("threshold", 0.75) if not isinstance(threshold, (int, float)) or threshold < 0.0 or threshold > 1.0: self.validation_errors.append("threshold必须是0.0-1.0之间的数值") prompt_template = rag_config.get("prompt_template") if prompt_template and not isinstance(prompt_template, str): self.validation_errors.append("prompt_template必须是字符串") elif prompt_template and "{context}" not in prompt_template: self.warnings.append("prompt_template建议包含{context}占位符") def _validate_logging_config(self, logging_config: Dict[str, Any]) -> None: """验证日志配置""" if not logging_config: self.warnings.append("未配置日志设置,将使用默认配置") return log_level = logging_config.get("level", "INFO") valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] if log_level not in valid_levels: self.validation_errors.append(f"日志级别必须是: {', '.join(valid_levels)}") log_file = logging_config.get("file") if log_file: log_dir = os.path.dirname(log_file) if log_dir and not os.path.exists(log_dir): self.warnings.append(f"日志目录不存在,需要创建: {log_dir}")
[docs] def normalize_processor_config(self, config: Dict[str, Any]) -> Dict[str, Any]: """ 标准化处理器配置名称 Args: config: 原始配置 Returns: Dict[str, Any]: 标准化后的配置 """ if "processors" not in config: return config normalized_config = config.copy() old_processors = normalized_config["processors"] new_processors = {} for old_name, processor_config in old_processors.items(): # 获取标准化名称 new_name = self.PROCESSOR_NAME_MAPPING.get(old_name, old_name) # 如果是旧名称,记录转换 if old_name != new_name: logger.info(f"处理器名称标准化: {old_name} -> {new_name}") new_processors[new_name] = processor_config normalized_config["processors"] = new_processors return normalized_config
[docs] def get_validation_report(self) -> Dict[str, Any]: """ 获取验证报告 Returns: Dict[str, Any]: 包含错误和警告的报告 """ return { "errors": self.validation_errors, "warnings": self.warnings, "is_valid": len(self.validation_errors) == 0, }
[docs] def validate_config_file(config_path: str) -> tuple[bool, Dict[str, Any]]: """ 验证配置文件 Args: config_path: 配置文件路径 Returns: tuple: (是否验证通过, 验证报告) """ try: with open(config_path, "r", encoding="utf-8") as f: config = json.load(f) validator = ConfigValidator() is_valid = validator.validate_config(config) report = validator.get_validation_report() return is_valid, report except FileNotFoundError: return False, {"errors": [f"配置文件不存在: {config_path}"], "warnings": []} except json.JSONDecodeError as e: return False, {"errors": [f"配置文件JSON格式错误: {str(e)}"], "warnings": []} except Exception as e: return False, {"errors": [f"验证配置文件时发生错误: {str(e)}"], "warnings": []}
# 默认配置生成器
[docs] def generate_default_config() -> Dict[str, Any]: """ 生成默认配置 Returns: Dict[str, Any]: 默认配置字典 """ return { "storage": {"base_path": "./data", "output_path": "./output"}, "processors": { "pre_processor": {"enabled": True, "config": {"enhance_image": True, "correct_skew": True}}, "ocr_processor": {"enabled": True, "config": {"api": "azure", "language": "zh-Hans"}}, "structure_processor": {"enabled": True, "config": {"detect_sections": True, "detect_headings": True}}, "classification_processor": {"enabled": False, "config": {"api": "openai", "min_confidence": 0.7}}, "format_converter": {"enabled": False, "config": {"output_formats": ["markdown"], "preserve_layout": True}}, "embedding_processor": { "enabled": True, "config": { "model_name_or_path": "sentence-transformers/all-MiniLM-L6-v2", "chunk_size": 500, "chunk_overlap": 50, "batch_size": 32, }, }, }, "connectors": { "OCRAPIConnector": { "azure": {"endpoint": "YOUR_AZURE_ENDPOINT", "api_key": "YOUR_AZURE_API_KEY", "region": "eastus"} }, "LLMConnector": { "type": "openai", "api_url": "https://api.openai.com/v1", "api_key": "YOUR_OPENAI_API_KEY", "model": "gpt-3.5-turbo", "embedding_model": "text-embedding-3-small", "settings": {"temperature": 0.7, "max_tokens": 2000}, }, }, "rag_settings": { "top_k": 5, "threshold": 0.75, "prompt_template": "基于以下文献内容回答问题:\n\n{context}\n\n问题: {question}\n\n回答:", }, "llm": { "type": "openai", "api_url": "https://api.openai.com/v1", "api_key": "YOUR_OPENAI_API_KEY", "model": "gpt-3.5-turbo", "embedding_model": "text-embedding-3-small", "settings": {"temperature": 0.7, "max_tokens": 2000}, }, "logging": { "level": "INFO", "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", "file": "./logs/system.log", }, }