Files
Cognitive_Agent/domains/seat/src/seat_domain/validator.py
T

273 lines
10 KiB
Python
Executable File

"""Seat-specific semantic validation beyond JSON Schema."""
from __future__ import annotations
from typing import Any
SUPPORTED_TARGETS = {"passenger"}
SUPPORTED_FUNCTIONS = {"ventilation", "heating"}
SUPPORTED_STATES = {"on", "off"}
SUPPORTED_MOTION_AXES = {
"longitudinal",
"cushion_tilt",
"backrest_recline",
"legrest_height",
"legrest_extension",
}
ANGLE_RANGES = {
"backrest_recline": (95, 163),
"legrest_height": (95, 167),
}
def validate_semantics(
parsed: dict[str, Any], function_config: dict[str, Any] | None
) -> list[dict[str, str]]:
issues: list[dict[str, str]] = []
detected_domains = set(parsed.get("detected_domains", ["seat"]))
if parsed.get("actions") and "seat" not in detected_domains:
issues.append(
{
"severity": "error",
"code": "DOMAIN_ACTION_MISMATCH",
"path": "detected_domains",
"message": "GPT 生成了座椅动作,但 detected_domains 中没有 seat。",
}
)
for index, deferred in enumerate(parsed.get("deferred_requests", [])):
domain = str(deferred.get("domain"))
capability = str(deferred.get("capability"))
# An N7-synchronised scene may use a vehicle-owned capability namespace
# that intentionally differs from its routing domain (for example
# ``domain=local_vehicle`` with ``capability=local_scene.seat_preset``).
# It is not executed by this validator's local backend: the original
# action is forwarded unchanged to ``transto_n7/ctl``. Keep the strict
# namespace check for every ordinary deferred request, while allowing
# this explicitly marked transfer backend to retain the source contract.
n7_transfer = (
deferred.get("execution_backend") == "n7_vehicle_transfer"
and deferred.get("rejection_code") == "PC_CAPABILITY_UNAVAILABLE"
)
if domain not in detected_domains or (
not n7_transfer and not capability.startswith(f"{domain}.")
):
issues.append(
{
"severity": "error",
"code": "DEFERRED_REQUEST_MISMATCH",
"path": f"deferred_requests[{index}]",
"message": "暂缓请求的领域、能力名和 detected_domains 不一致。",
}
)
issues.append(
{
"severity": "warning",
"code": "UNAVAILABLE_DOMAIN",
"path": f"deferred_requests[{index}]",
"message": f"已理解 {capability} 目标,但 {domain} 域当前仅有能力目录,未接入执行器。",
}
)
for ambiguity in parsed.get("ambiguities", []):
issues.append(
{
"severity": "error",
"code": "AMBIGUOUS_INTENT",
"path": ambiguity["field"],
"message": ambiguity["question"],
}
)
seen: dict[tuple[str, str], str] = {}
active_functions: dict[str, set[str]] = {}
has_motion = False
for index, action in enumerate(parsed.get("actions", [])):
path = f"actions[{index}]"
if action.get("target") not in SUPPORTED_TARGETS:
issues.append(
{
"severity": "error",
"code": "UNSUPPORTED_TARGET",
"path": f"{path}.target",
"message": "当前所有座椅能力仅支持前排副驾。",
}
)
if action.get("action_type") == "dynamic_mode_stop":
has_motion = True
continue
if action.get("action_type") == "dynamic_mode":
has_motion = True
if action.get("mode_id") not in {1, 2}:
issues.append(
{
"severity": "error",
"code": "UNSUPPORTED_DYNAMIC_MODE",
"path": f"{path}.mode_id",
"message": "动态座椅模式编号必须是 1 或 2。",
}
)
continue
if action.get("action_type") == "position":
has_motion = True
axis = action.get("axis")
position = action.get("position_pct")
if axis not in SUPPORTED_MOTION_AXES:
issues.append(
{
"severity": "error",
"code": "UNSUPPORTED_MOTION_AXIS",
"path": f"{path}.axis",
"message": "请求的座椅位置轴未在当前能力目录中注册。",
}
)
if (
not isinstance(position, int)
or isinstance(position, bool)
or not 0 <= position <= 100
):
issues.append(
{
"severity": "error",
"code": "UNSUPPORTED_POSITION_RANGE",
"path": f"{path}.position_pct",
"message": "座椅电机绝对目标必须是 0-100 的整数百分比。",
}
)
angle = action.get("angle_deg")
if angle is not None:
angle_range = ANGLE_RANGES.get(str(axis))
if (
angle_range is None
or not isinstance(angle, (int, float))
or isinstance(angle, bool)
or not angle_range[0] <= float(angle) <= angle_range[1]
):
issues.append(
{
"severity": "error",
"code": "UNSUPPORTED_ANGLE_RANGE",
"path": f"{path}.angle_deg",
"message": "座椅夹角超出当前虚拟标定范围。",
}
)
key = (str(action.get("target")), str(axis))
previous_position = seen.get(key)
if previous_position is not None and previous_position != str(position):
issues.append(
{
"severity": "error",
"code": "CONFLICTING_OPERATIONS",
"path": path,
"message": "同一个座椅电机轴不能在一份配置中包含不同目标位置。",
}
)
seen[key] = str(position)
continue
if action.get("function") not in SUPPORTED_FUNCTIONS:
issues.append(
{
"severity": "error",
"code": "UNSUPPORTED_CAPABILITY",
"path": f"{path}.function",
"message": "当前只支持座椅通风和加热。",
}
)
if action.get("state") not in SUPPORTED_STATES:
issues.append(
{
"severity": "error",
"code": "UNSUPPORTED_STATE",
"path": f"{path}.state",
"message": "当前控制信号只支持打开或关闭。",
}
)
requested_level = action.get("requested_level")
if requested_level is not None and requested_level not in {1, 2, 3}:
issues.append(
{
"severity": "error",
"code": "UNSUPPORTED_LEVEL_RANGE",
"path": f"{path}.requested_level",
"message": "当前舒适推断和 0x4FA 虚拟回读仅支持 1-3 档。",
}
)
elif requested_level is not None:
issues.append(
{
"severity": "warning",
"code": "LEVEL_COMMAND_SIMULATED",
"path": f"{path}.requested_level",
"message": (
"目标档位已写入 FunctionConfig 和会话状态;"
"0x42A 仅编译开关信号,档位回读仅在 dry-run 中模拟。"
),
}
)
key = (str(action.get("target")), str(action.get("function")))
previous_state = seen.get(key)
if previous_state is not None and previous_state != action.get("state"):
issues.append(
{
"severity": "error",
"code": "CONFLICTING_OPERATIONS",
"path": path,
"message": "同一个座椅功能不能在一份配置中同时打开和关闭。",
}
)
seen[key] = str(action.get("state"))
if action.get("state") == "on":
active_functions.setdefault(str(action.get("target")), set()).add(
str(action.get("function"))
)
if has_motion:
issues.append(
{
"severity": "warning",
"code": "SYNTHETIC_MOTION_CAN",
"path": "platform.reference_vehicle.seat_motion",
"message": "座椅位置控制和反馈 CAN 矩阵完全为演示虚构,仅用于验证模型与配置链路。",
}
)
for target, functions in active_functions.items():
if functions == {"ventilation", "heating"}:
issues.append(
{
"severity": "warning",
"code": "COMBINED_THERMAL_SIMULATED",
"path": f"actions.{target}",
"message": (
"同一座位的加热与通风组合目标仅用于 dry-run;"
"实车前必须确认 ECU 是否允许两功能同时开启。"
),
}
)
if (
function_config is None
and not parsed.get("deferred_requests")
and not parsed.get("ambiguities")
and not parsed.get("preset_actions")
):
issues.append(
{
"severity": "error",
"code": "NO_EXECUTABLE_OPERATION",
"path": "spec.operations",
"message": "没有生成可执行的座椅操作。",
}
)
issues.append(
{
"severity": "warning",
"code": "VIRTUAL_EXECUTION_ONLY",
"path": "platform.reference_vehicle",
"message": "CAN 元数据不完整,当前仅允许虚拟执行,禁止实车下发。",
}
)
return issues