from pathlib import Path import yaml from django.conf import settings REQUIRED_FIELDS = [ ("id",), ("name",), ("description",), ("agent", "role"), ("agent", "goal"), ("agent", "instructions"), ("rag", "enabled"), ("tools",), ("output", "type"), ("audit", "enabled"), ] class ScenarioNotFound(KeyError): pass class ScenarioValidationError(ValueError): pass def _get_nested(config: dict, path: tuple[str, ...]): value = config for key in path: if not isinstance(value, dict) or key not in value: raise ScenarioValidationError("缺失必填字段: " + ".".join(path)) value = value[key] return value def validate_scenario(config: dict) -> dict: # 仅校验真正影响运行闭环的必填字段; # 页面展示字段允许缺失,并在归一化阶段补默认值。 for field_path in REQUIRED_FIELDS: _get_nested(config, field_path) return normalize_scenario(config) def normalize_scenario(config: dict) -> dict: """补齐页面和其它模块常用的派生字段,避免模板层重复判断。""" normalized = dict(config) normalized["applicable_questions"] = list(config.get("applicable_questions") or []) normalized["rag"] = dict(config.get("rag", {})) normalized["rag"]["enabled"] = bool(normalized["rag"].get("enabled")) normalized["tools"] = list(config.get("tools") or []) normalized["tool_count"] = len(normalized["tools"]) normalized["is_enabled"] = True return normalized def _scenario_files() -> list[Path]: config_dir = Path(settings.SCENARIO_CONFIG_DIR) if not config_dir.exists(): return [] return sorted([*config_dir.glob("*.yaml"), *config_dir.glob("*.yml")]) def _read_yaml_file(path: Path) -> dict: with path.open("r", encoding="utf-8") as file: return yaml.safe_load(file) or {} def _collect_scenario_load_result() -> tuple[list[dict], list[dict]]: """ 统一读取配置目录中的所有场景文件。 返回值: - scenarios: 校验通过的场景列表 - issues: 非法 YAML / 缺字段等错误摘要,供首页展示 """ scenarios = [] issues = [] for path in _scenario_files(): try: config = _read_yaml_file(path) scenarios.append(validate_scenario(config)) except (yaml.YAMLError, ScenarioValidationError) as exc: issues.append( { "file_name": path.name, "message": str(exc), } ) return scenarios, issues def list_scenarios() -> list[dict]: # 首页每次读取最新 YAML,便于复试现场快速改题。 scenarios, _issues = _collect_scenario_load_result() return scenarios def list_scenario_issues() -> list[dict]: """返回配置异常摘要,便于页面明确提示而不是直接 500。""" _scenarios, issues = _collect_scenario_load_result() return issues def get_scenario(scenario_id: str) -> dict: for scenario in list_scenarios(): if scenario["id"] == scenario_id: return scenario raise ScenarioNotFound(f"场景不存在: {scenario_id}")