"""QualityGate - 产出质量管理 多维度质量检查:必填字段、字数、JSON Schema、自定义验证器。 """ import importlib import logging from dataclasses import dataclass from typing import Callable from agentkit.skills.base import Skill logger = logging.getLogger(__name__) @dataclass class QualityCheck: """单条质量检查结果""" name: str passed: bool message: str | None = None @dataclass class QualityResult: """质量检查汇总结果""" passed: bool checks: list[QualityCheck] can_retry: bool class QualityGate: """产出质量管理 — 多维度质量检查""" async def validate( self, output: dict[str, object], skill: Skill, skill_context: dict[str, object] | None = None, ) -> QualityResult: """对产出执行多维度质量检查 检查维度: 1. 必填字段检查 2. 最低字数检查 3. JSON Schema 验证(如 skill.config.output_schema 存在) 4. 自定义验证器(如 skill.config.quality_gate.custom_validator 存在) 5. 技能匹配验证(如 skill_context 含 intent_keywords) """ checks: list[QualityCheck] = [] qg = skill.config.quality_gate # 1. 必填字段检查 for field in qg.required_fields: present = field in output and output[field] is not None checks.append( QualityCheck( name=f"required_field:{field}", passed=present, message=f"Field '{field}' is missing" if not present else None, ) ) # 2. 最低字数检查 if qg.min_word_count > 0: content = output.get("content", "") if isinstance(content, str): word_count = len(content.split()) else: word_count = len(str(content).split()) passed = word_count >= qg.min_word_count checks.append( QualityCheck( name="min_word_count", passed=passed, message=( f"Word count {word_count} < minimum {qg.min_word_count}" if not passed else None ), ) ) # 3. JSON Schema 验证 if skill.config.output_schema: try: import jsonschema jsonschema.validate(output, skill.config.output_schema) checks.append(QualityCheck(name="schema", passed=True)) except jsonschema.ValidationError as e: checks.append(QualityCheck(name="schema", passed=False, message=str(e))) except ImportError: # jsonschema 未安装,跳过 pass # 4. 自定义验证器 if qg.custom_validator: try: validator = self._import_validator(qg.custom_validator) result = validator(output) # 支持异步验证器 if hasattr(result, "__await__"): result = await result checks.append(QualityCheck(name="custom", passed=bool(result))) except Exception as e: # 验证器导入/执行失败,跳过并记录警告 checks.append( QualityCheck( name="custom", passed=True, message=f"Validator skipped: {e}", ) ) # 5. 技能匹配验证(轻量级路由一致性检查) skill_match_check = self._check_skill_match(output, skill_context) if skill_match_check is not None: checks.append(skill_match_check) # 警告升级逻辑:当 skill_match 警告存在且其他维度有失败时,升级为失败 if ( skill_match_check is not None and skill_match_check.message and "Warning" in skill_match_check.message ): other_failed = any(not c.passed for c in checks if c.name != "skill_match") if other_failed: # 升级:将 skill_match 的 passed 也设为 False checks = [ QualityCheck(name=c.name, passed=False, message=c.message) if c.name == "skill_match" else c for c in checks ] return QualityResult( passed=all(c.passed for c in checks), checks=checks, can_retry=qg.max_retries > 0, ) # 允许的验证器模块前缀白名单 _ALLOWED_VALIDATOR_PREFIXES = ( "agentkit.", "app.agent_framework.", ) @staticmethod def _check_skill_match( output: dict[str, object], skill_context: dict[str, object] | None, ) -> QualityCheck | None: """第五维度:技能匹配验证 当 skill_context 含 intent_keywords 时,检查输出内容是否包含 至少一个关键词。不匹配时标记为警告(passed=True + message), 当其他维度也有失败时升级为 passed=False。 Returns: QualityCheck 或 None(当 skill_context 无效时跳过) """ if not skill_context: return None intent_keywords: list[str] | None = skill_context.get("intent_keywords") if not intent_keywords: return None # 拼接输出中所有字符串值 all_text = " ".join( str(v) for v in output.values() if isinstance(v, (str, int, float, bool)) ).lower() matched = any(kw.lower() in all_text for kw in intent_keywords) if matched: return QualityCheck(name="skill_match", passed=True) return QualityCheck( name="skill_match", passed=True, # 警告级别,不单独拦截 message="Warning: output may not match routed skill", ) def _import_validator(self, dotted_path: str) -> Callable: """从点分路径导入自定义验证器函数 出于安全考虑,只允许导入白名单前缀下的模块。 """ # 安全校验:只允许白名单前缀的模块 if not any(dotted_path.startswith(prefix) for prefix in self._ALLOWED_VALIDATOR_PREFIXES): raise ImportError( f"Validator '{dotted_path}' is not in allowed module prefixes: " f"{self._ALLOWED_VALIDATOR_PREFIXES}" ) try: module_path, func_name = dotted_path.rsplit(".", 1) module = importlib.import_module(module_path) handler = getattr(module, func_name) if not callable(handler): raise ValueError(f"'{dotted_path}' is not callable") return handler except (ImportError, AttributeError, ValueError) as e: raise ImportError(f"Failed to import validator '{dotted_path}': {e}") from e