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:
|
def validate_scenario(config: dict) -> dict:
|
||||||
# 只校验真正影响运行闭环的必填字段;
|
# 仅校验真正影响运行闭环的必填字段;
|
||||||
# 页面展示类字段如 applicable_questions 允许缺失,并在归一化阶段补默认值。
|
# 页面展示字段允许缺失,并在归一化阶段补默认值。
|
||||||
for field_path in REQUIRED_FIELDS:
|
for field_path in REQUIRED_FIELDS:
|
||||||
_get_nested(config, field_path)
|
_get_nested(config, field_path)
|
||||||
return normalize_scenario(config)
|
return normalize_scenario(config)
|
||||||
|
|
||||||
|
|
||||||
def normalize_scenario(config: dict) -> dict:
|
def normalize_scenario(config: dict) -> dict:
|
||||||
"""补齐页面和其他模块常用的派生字段,减少模板中的条件判断。"""
|
"""补齐页面和其它模块常用的派生字段,避免模板层重复判断。"""
|
||||||
normalized = dict(config)
|
normalized = dict(config)
|
||||||
normalized["applicable_questions"] = list(config.get("applicable_questions") or [])
|
normalized["applicable_questions"] = list(config.get("applicable_questions") or [])
|
||||||
normalized["rag"] = dict(config.get("rag", {}))
|
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")])
|
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]:
|
def list_scenarios() -> list[dict]:
|
||||||
# 首页每次读取最新 YAML,便于复试现场快速改题。
|
# 首页每次读取最新 YAML,便于复试现场快速改题。
|
||||||
scenarios = []
|
scenarios, _issues = _collect_scenario_load_result()
|
||||||
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))
|
|
||||||
return scenarios
|
return scenarios
|
||||||
|
|
||||||
|
|
||||||
|
def list_scenario_issues() -> list[dict]:
|
||||||
|
"""返回配置异常摘要,便于页面明确提示而不是直接 500。"""
|
||||||
|
_scenarios, issues = _collect_scenario_load_result()
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
def get_scenario(scenario_id: str) -> dict:
|
def get_scenario(scenario_id: str) -> dict:
|
||||||
for scenario in list_scenarios():
|
for scenario in list_scenarios():
|
||||||
if scenario["id"] == scenario_id:
|
if scenario["id"] == scenario_id:
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
from .services import list_scenarios
|
from .services import list_scenario_issues, list_scenarios
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
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>
|
<p class="page-lead">当前首页直接读取 YAML 场景配置。你可以从这里进入对话、上传资料,再用审计日志验证整条执行链路。</p>
|
||||||
</header>
|
</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">
|
<section class="card-grid">
|
||||||
{% for scenario in scenarios %}
|
{% for scenario in scenarios %}
|
||||||
<article class="card">
|
<article class="card">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import pytest
|
|||||||
from apps.scenarios.services import (
|
from apps.scenarios.services import (
|
||||||
ScenarioNotFound,
|
ScenarioNotFound,
|
||||||
get_scenario,
|
get_scenario,
|
||||||
|
list_scenario_issues,
|
||||||
list_scenarios,
|
list_scenarios,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,3 +39,91 @@ def test_home_page_shows_applicable_questions(client):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "适用题型" in content
|
assert "适用题型" in content
|
||||||
assert "SOP 问答" 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