feat(scenarios): 兼容非法配置并展示错误摘要
This commit is contained in:
@@ -36,15 +36,15 @@ def _get_nested(config: dict, path: tuple[str, ...]):
|
||||
|
||||
|
||||
def validate_scenario(config: dict) -> dict:
|
||||
# 只校验真正影响运行闭环的必填字段;
|
||||
# 页面展示类字段如 applicable_questions 允许缺失,并在归一化阶段补默认值。
|
||||
# 仅校验真正影响运行闭环的必填字段;
|
||||
# 页面展示字段允许缺失,并在归一化阶段补默认值。
|
||||
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", {}))
|
||||
@@ -62,16 +62,47 @@ def _scenario_files() -> list[Path]:
|
||||
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 = []
|
||||
for path in _scenario_files():
|
||||
with path.open("r", encoding="utf-8") as file:
|
||||
config = yaml.safe_load(file) or {}
|
||||
scenarios.append(validate_scenario(config))
|
||||
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:
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
from .services import list_scenarios
|
||||
from .services import list_scenario_issues, list_scenarios
|
||||
|
||||
|
||||
def index(request):
|
||||
# 首页只消费服务层给出的场景摘要,不自行拼装配置字段。
|
||||
return render(request, "scenarios/index.html", {"scenarios": list_scenarios()})
|
||||
return render(
|
||||
request,
|
||||
"scenarios/index.html",
|
||||
{
|
||||
"scenarios": list_scenarios(),
|
||||
"scenario_issues": list_scenario_issues(),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -9,6 +9,21 @@
|
||||
<p class="page-lead">当前首页直接读取 YAML 场景配置。你可以从这里进入对话、上传资料,再用审计日志验证整条执行链路。</p>
|
||||
</header>
|
||||
|
||||
{% if scenario_issues %}
|
||||
<section class="panel" style="margin-bottom: 20px;">
|
||||
<h2 class="section-title">配置异常</h2>
|
||||
<p class="muted">以下 YAML 场景文件存在问题,系统已自动跳过,不会影响其它合法场景展示。</p>
|
||||
<ul class="detail-list">
|
||||
{% for issue in scenario_issues %}
|
||||
<li class="detail-item">
|
||||
<strong>{{ issue.file_name }}</strong>
|
||||
<div>{{ issue.message }}</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="card-grid">
|
||||
{% for scenario in scenarios %}
|
||||
<article class="card">
|
||||
|
||||
@@ -3,6 +3,7 @@ import pytest
|
||||
from apps.scenarios.services import (
|
||||
ScenarioNotFound,
|
||||
get_scenario,
|
||||
list_scenario_issues,
|
||||
list_scenarios,
|
||||
)
|
||||
|
||||
@@ -38,3 +39,91 @@ def test_home_page_shows_applicable_questions(client):
|
||||
assert response.status_code == 200
|
||||
assert "适用题型" in content
|
||||
assert "SOP 问答" in content
|
||||
|
||||
|
||||
def test_list_scenarios_skips_invalid_config_and_collects_issues(settings, tmp_path):
|
||||
valid_file = tmp_path / "valid.yaml"
|
||||
invalid_file = tmp_path / "invalid.yaml"
|
||||
valid_file.write_text(
|
||||
"""
|
||||
id: demo_valid
|
||||
name: 有效场景
|
||||
description: 用于测试
|
||||
agent:
|
||||
role: 测试助手
|
||||
goal: 正常返回
|
||||
instructions:
|
||||
- 输出结果
|
||||
rag:
|
||||
enabled: false
|
||||
tools: []
|
||||
output:
|
||||
type: general_answer
|
||||
audit:
|
||||
enabled: true
|
||||
""".strip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
invalid_file.write_text(
|
||||
"""
|
||||
id: broken
|
||||
name: 非法场景
|
||||
description: 缺少 agent.goal
|
||||
agent:
|
||||
role: 测试助手
|
||||
instructions:
|
||||
- 输出结果
|
||||
rag:
|
||||
enabled: true
|
||||
tools: []
|
||||
output:
|
||||
type: general_answer
|
||||
audit:
|
||||
enabled: true
|
||||
""".strip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
settings.SCENARIO_CONFIG_DIR = tmp_path
|
||||
|
||||
scenarios = list_scenarios()
|
||||
issues = list_scenario_issues()
|
||||
|
||||
assert [scenario["id"] for scenario in scenarios] == ["demo_valid"]
|
||||
assert len(issues) == 1
|
||||
assert issues[0]["file_name"] == "invalid.yaml"
|
||||
assert "agent.goal" in issues[0]["message"]
|
||||
|
||||
|
||||
def test_home_page_shows_invalid_scenario_issues_instead_of_500(client, settings, tmp_path):
|
||||
valid_file = tmp_path / "valid.yaml"
|
||||
invalid_file = tmp_path / "invalid.yaml"
|
||||
valid_file.write_text(
|
||||
"""
|
||||
id: demo_valid
|
||||
name: 有效场景
|
||||
description: 用于测试
|
||||
agent:
|
||||
role: 测试助手
|
||||
goal: 正常返回
|
||||
instructions:
|
||||
- 输出结果
|
||||
rag:
|
||||
enabled: false
|
||||
tools: []
|
||||
output:
|
||||
type: general_answer
|
||||
audit:
|
||||
enabled: true
|
||||
""".strip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
invalid_file.write_text("id: broken\nname: 缺失结构", encoding="utf-8")
|
||||
settings.SCENARIO_CONFIG_DIR = tmp_path
|
||||
|
||||
response = client.get("/")
|
||||
|
||||
content = response.content.decode("utf-8")
|
||||
assert response.status_code == 200
|
||||
assert "有效场景" in content
|
||||
assert "配置异常" in content
|
||||
assert "invalid.yaml" in content
|
||||
|
||||
Reference in New Issue
Block a user