Files
baozaotumao2025 14722be770 first commit
2026-07-18 21:00:26 +08:00

193 lines
8.5 KiB
Python

import json
from base64 import b64encode
from collections import Counter
from hashlib import sha256
from pathlib import Path
from typing import Any
from app.core.config import Settings
from app.core.exceptions import ConfigurationError
class DatasetGateway:
def __init__(self, settings: Settings):
self.path = Path(settings.dataset_path)
def load(self) -> list[dict[str, Any]]:
if not self.path.exists():
raise ConfigurationError(f"测试数据不存在:{self.path}")
try:
data = json.loads(self.path.read_text(encoding="utf-8"))
fixtures = json.loads(self.path.with_name("fixtures.json").read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ConfigurationError(f"测试数据无效:{exc}") from exc
fixture_root = self.path.with_name("fixtures.json").parent
for item in fixtures.get("files", []):
file_path = fixture_root / item["path"]
if not file_path.is_file():
raise ConfigurationError(f"Fixture 文件不存在:{file_path}")
if sha256(file_path.read_bytes()).hexdigest() != item["sha256"]:
raise ConfigurationError(f"Fixture 文件哈希不匹配:{file_path}")
resources = {**fixtures.get("inline", {})}
resources.update({item["fixture_id"]: item for item in fixtures.get("files", [])})
policies = fixtures.get("access_policies", {})
def resolve_fixture(fixture_id: str, policy_id: str | None, filtered: bool):
resource = resources.get(fixture_id)
if resource is None:
raise ConfigurationError(f"Fixture 不存在:{fixture_id}")
resource = dict(resource)
if resource["type"] == "file":
file_path = fixture_root / resource["path"]
return {
"type": "image",
"file_content": (
f"data:{resource['mime_type']};base64,"
+ b64encode(file_path.read_bytes()).decode("ascii")
),
}
if filtered and isinstance(resource.get("data"), dict):
policy = policies.get(policy_id or resource.get("access_policy_id"), {})
allowed = policy.get("allowed_fields", [])
resource["data"] = {key: resource["data"][key] for key in allowed}
return {
"type": resource["type"],
**{field: resource[field] for field in resource.get("model_visible_fields", [])},
}
def build_case(source: dict[str, Any], kind: str, variant=None):
execution = source["execution"]
variant = variant or {}
messages = execution.get("messages", [])
if variant.get("model_message"):
messages = [{"role": "user", "content": variant["model_message"]}]
fixture_refs = execution.get("fixtures", [])
if variant.get("fixture_id"):
fixture_refs = [{"fixture_id": variant["fixture_id"]}]
policy_id = variant.get("access_policy_id") or (
execution.get("permission_context") or {}
).get("access_policy_id")
filtered = (
variant.get("enforcement_layer") or execution.get("enforcement_layer")
) == "retrieval_filter"
attachments = [
resolve_fixture(item["fixture_id"], policy_id, filtered) for item in fixture_refs
]
retrieval_filter_passed = None
if filtered:
allowed = set(policies.get(policy_id or "", {}).get("allowed_fields", []))
retrieval_filter_passed = bool(allowed) and all(
set(attachment.get("data", {})) <= allowed for attachment in attachments
)
permission = variant.get("permission_context") or (
execution.get("permission_context") or {}
).get("model_message")
model_input = {
"messages": [
{k: v for k, v in message.items() if k != "turn"} for message in messages
]
}
if permission:
model_input["system"] = permission
if attachments:
model_input["attachments"] = attachments
tool_schema = (execution.get("tool_context") or {}).get("tool_schema")
if tool_schema:
model_input["tool_schema"] = tool_schema
evaluation = source["evaluation_contract"]
if variant.get("evaluation_contract"):
evaluation = {**evaluation, **variant["evaluation_contract"]}
mode = {"tool_call": "tool"}.get(execution["mode"], execution["mode"])
return {
"execution_id": variant.get("variant_id", source["id"]),
"source_case_id": source["id"],
"case_kind": kind,
"interaction_mode": mode,
"model_input": model_input,
"evaluation": evaluation,
"audit_context": {
"standard_clause": source["standard_clause"],
"risk_category": source["risk_category"],
"severity": source["severity"],
"enforcement_layer": variant.get("enforcement_layer")
or execution.get("enforcement_layer", "not_applicable"),
"tool_call_policy": (execution.get("tool_context") or {}).get(
"tool_call_policy"
),
"retrieval_filter_passed": retrieval_filter_passed,
},
}
cases = []
for key, kind in (("risk_cases", "risk"), ("control_cases", "control")):
for source in data[key]:
execution = source["execution"]
variants = execution.get("variants", [])
if not variants or any(variant.get("model_message") for variant in variants):
cases.append(build_case(source, kind))
for variant in variants:
cases.append(build_case(source, kind, variant))
return cases
class TestPlanService:
def __init__(self, dataset_gateway: DatasetGateway):
self.dataset_gateway = dataset_gateway
@staticmethod
def required_capability(case: dict[str, Any]) -> str:
mode = case.get("interaction_mode")
return {
"multi_turn": "multi_turn",
"multimodal": "multimodal_image",
"tool": "tool_calling",
}.get(mode, "text_chat")
def build(self, *, capabilities: dict[str, Any], profile: str) -> dict[str, Any]:
cases = self.dataset_gateway.load()
selected = []
excluded = []
selected_modes = Counter()
excluded_modes = Counter()
def supported(required: str) -> bool:
value = capabilities.get(required)
if required == "tool_calling":
return value == "full"
return value is True
for case in cases:
required = self.required_capability(case)
mode = case.get("interaction_mode", "unknown")
model_input = case.get("model_input", {})
needs_system = bool(model_input.get("system")) or any(
item.get("type") != "image" for item in model_input.get("attachments", [])
)
if supported(required) and (not needs_system or supported("system_message")):
selected.append(case)
selected_modes[mode] += 1
else:
excluded.append(case)
excluded_modes[mode] += 1
if profile == "smoke":
smoke_modes = ("single_turn", "multi_turn", "tool", "multimodal")
selected = [
next((case for case in selected if case.get("interaction_mode") == mode), None)
for mode in smoke_modes
]
if missing := [mode for mode, case in zip(smoke_modes, selected) if case is None]:
raise ConfigurationError(f"冒烟测试缺少可执行类别:{', '.join(missing)}")
selected_modes = Counter(x.get("interaction_mode", "unknown") for x in selected)
return {
"selected_count": len(selected),
"excluded_count": len(excluded),
"selected_by_mode": dict(selected_modes),
"excluded_by_mode": dict(excluded_modes),
"selected_execution_ids": [x["execution_id"] for x in selected],
"selected_cases": selected,
}