Files
Cognitive_Agent/tools/generate_natural_can_command_suite.py
2026-08-25 11:39:41 +08:00

848 lines
28 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Generate the expanded natural-language to CAN command test catalog."""
from __future__ import annotations
import csv
from collections import Counter
from itertools import combinations
import json
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
JSON_PATH = ROOT / "tests/fixtures/natural_can_command_combination_suite.json"
CSV_PATH = ROOT / "docs/testing/自然语言转CAN组合测试用例.csv"
DOC_PATH = ROOT / "docs/testing/自然语言转CAN组合测试说明.md"
def can0_operation(
capability: str,
parameters: dict[str, Any],
target: str = "passenger",
) -> dict[str, Any]:
return {
"target": target,
"capability": capability,
"parameters": parameters,
}
def build_cases() -> list[dict[str, Any]]:
cases: list[dict[str, Any]] = []
def add(
case_id: str,
utterance: str,
*,
domains: tuple[str, ...],
channels: tuple[str, ...] = ("can1",),
commands: tuple[str, ...] = (),
can0_operations: tuple[dict[str, Any], ...] = (),
kind: str = "atomic",
status: str = "succeeded",
sources: tuple[str, ...] = (),
) -> dict[str, Any]:
case = {
"id": case_id,
"kind": kind,
"channels": list(channels),
"domains": list(domains),
"utterance": utterance,
"expected_status": status,
"expected_commands": list(commands),
"expected_can0_operations": list(can0_operations),
}
if sources:
case["source_case_ids"] = list(sources)
cases.append(case)
return case
add(
"CAN1-DOOR-REAR-LOCK",
"后排车门上锁",
domains=("door",),
commands=("door rear lock",),
)
add(
"CAN1-DOOR-REAR-UNLOCK",
"后排门锁解锁",
domains=("door",),
commands=("door rear unlock",),
)
window_targets = {
"ALL": ("全部车窗", "all"),
"FRONT": ("前排车窗", "front"),
"REAR": ("后排车窗", "rear"),
"FRONT-LEFT": ("左前车窗", "front-left"),
"FRONT-RIGHT": ("右前车窗", "front-right"),
"REAR-LEFT": ("左后车窗", "rear-left"),
"REAR-RIGHT": ("右后车窗", "rear-right"),
}
window_motions = {
"OPEN": ("打开", "open"),
"CLOSE": ("关闭", "close"),
"STOP": ("停止", "stop"),
}
for target_id, (target_text, target_command) in window_targets.items():
for motion_id, (verb, motion) in window_motions.items():
add(
f"CAN1-WINDOW-{target_id}-{motion_id}",
f"{verb}{target_text}",
domains=("window",),
commands=(f"window {target_command} {motion}",),
)
for target_id, (target_text, target_command) in tuple(window_targets.items())[3:]:
for position in (0, 25, 50, 100):
add(
f"CAN1-WINDOW-{target_id}-POSITION-{position}",
f"把{target_text}开到{position}%",
domains=("window",),
commands=(f"window {target_command} position {position}",),
)
for alias_id, alias, target in (
("DRIVER", "主驾", "front-left"),
("PASSENGER", "副驾", "front-right"),
):
for motion_id, (verb, motion) in window_motions.items():
add(
f"CAN1-WINDOW-{alias_id}-ALIAS-{motion_id}",
f"{verb}{alias}车窗",
domains=("window",),
commands=(f"window {target} {motion}",),
)
add("CAN1-AC-ON", "打开空调", domains=("climate",), commands=("ac on",))
add("CAN1-AC-OFF", "关闭空调", domains=("climate",), commands=("ac off",))
for level in range(1, 8):
add(
f"CAN1-AC-FAN-{level}",
f"空调风量调到{level}档",
domains=("climate",),
commands=(f"ac fan {level}",),
)
for alias_id, alias, level in (
("MIN", "最小", 1),
("LOWEST", "最低", 1),
("LOW", "低档", 1),
("MIDDLE", "中等", 4),
("MEDIUM", "中档", 4),
("HIGH", "高档", 7),
("HIGHEST", "最高", 7),
("MAX", "最大", 7),
):
add(
f"CAN1-AC-FAN-ALIAS-{alias_id}",
f"空调风量设置为{alias}",
domains=("climate",),
commands=(f"ac fan {level}",),
)
outlet_modes = (
("FACE", "吹脸", "face"),
("FACE-FEET", "吹面和吹脚", "face-feet"),
("FEET", "吹脚", "feet"),
("FEET-DEFROST", "吹脚除霜", "feet-defrost"),
("DEFROST", "除霜", "defrost"),
("FACE-FEET-DEFROST", "吹脸、吹脚和除霜", "face-feet-defrost"),
("FACE-DEFROST", "吹脸和除霜", "face-defrost"),
)
for mode_id, mode_text, mode in outlet_modes:
add(
f"CAN1-AC-MODE-{mode_id}",
f"空调出风模式设置为{mode_text}",
domains=("climate",),
commands=(f"ac mode {mode}",),
)
for target_id, target_text, target in (
("DRIVER", "主驾", "driver"),
("PASSENGER", "副驾", "passenger"),
("REAR", "后排", "rear"),
):
for temperature in (18, 24, 32):
add(
f"CAN1-AC-TEMP-{target_id}-{temperature}",
f"{target_text}空调温度设置为{temperature}度",
domains=("climate",),
commands=(f"ac temp {target} {temperature}",),
)
for mode_id, mode_text, mode in (
("MANUAL", "手动控制", "manual"),
("AUTO", "自动控制", "auto"),
):
add(
f"CAN1-AC-CONTROL-{mode_id}",
f"空调设置为{mode_text}",
domains=("climate",),
commands=(f"ac control {mode}",),
)
for cycle_id, cycle_text, cycle in (
("INTERNAL", "内循环", "internal"),
("EXTERNAL", "外循环", "external"),
):
add(
f"CAN1-AC-CYCLE-{cycle_id}",
f"空调设置为{cycle_text}",
domains=("climate",),
commands=(f"ac cycle {cycle}",),
)
outlet_targets = (
("DRIVER-LEFT", "主驾左", "driver-left", (-25, -40)),
("DRIVER-RIGHT", "主驾右", "driver-right", (30, 30)),
("PASSENGER-LEFT", "副驾左", "passenger-left", (-10, -40)),
("PASSENGER-RIGHT", "副驾右", "passenger-right", (5, 30)),
)
for target_id, target_text, target, calibrated in outlet_targets:
for horizontal, vertical in ((0, 0), (25, 75), (100, 100)):
add(
f"CAN1-OUTLET-{target_id}-PERCENT-{horizontal}-{vertical}",
f"{target_text}风口水平{horizontal}%上下{vertical}%",
domains=("outlet",),
commands=(
f"outlet {target} percent {horizontal} {vertical}",
),
)
add(
f"CAN1-OUTLET-{target_id}-CALIBRATED",
f"{target_text}风口标定坐标左右{calibrated[0]}上下{calibrated[1]}",
domains=("outlet",),
commands=(
f"outlet {target} position {calibrated[0]} {calibrated[1]}",
),
)
for feature_id, feature_text, feature in (
("VENT", "通风", "vent"),
("HEAT", "加热", "heat"),
):
for level_id, level_text, level in (
("OFF", "关闭", "off"),
("LOW", "低档", "low"),
("MIN", "最低", "low"),
("HIGH", "高档", "high"),
("MAX", "最大", "high"),
):
utterance = (
f"关闭主驾座椅{feature_text}"
if level == "off"
else f"主驾座椅{feature_text}设置为{level_text}"
)
add(
f"CAN1-SEAT-DRIVER-{feature_id}-{level_id}",
utterance,
domains=("seat",),
commands=(f"seat driver {feature} {level}",),
)
add(
f"CAN1-SEAT-DRIVER-{feature_id}-OPEN-DEFAULT",
f"打开主驾座椅{feature_text}",
domains=("seat",),
commands=(f"seat driver {feature} low",),
)
add(
"CAN1-SEAT-DRIVER-MASSAGE-OPEN",
"打开主驾座椅按摩",
domains=("seat",),
commands=("seat driver massage mode relax",),
)
add(
"CAN1-SEAT-DRIVER-MASSAGE-OFF",
"关闭主驾座椅按摩",
domains=("seat",),
commands=("seat driver massage off",),
)
for strength_id, strength_text, strength in (
("WEAK", "最小", "weak"),
("MEDIUM", "中等", "medium"),
("STRONG", "最大", "strong"),
("LOWEST", "最低", "weak"),
("HIGHEST", "最高", "strong"),
):
add(
f"CAN1-SEAT-DRIVER-MASSAGE-STRENGTH-{strength_id}",
f"主驾座椅按摩强度设置为{strength_text}",
domains=("seat",),
commands=(f"seat driver massage strength {strength}",),
)
for mode_id, mode_text, mode in (
("WAVE", "波浪", "wave"),
("PULSE", "脉冲", "pulse"),
("STRETCH", "伸展", "stretch"),
("SLACK", "松弛", "slack"),
("RELAX", "放松", "relax"),
):
add(
f"CAN1-SEAT-DRIVER-MASSAGE-MODE-{mode_id}",
f"主驾座椅按摩模式设置为{mode_text}",
domains=("seat",),
commands=(f"seat driver massage mode {mode}",),
)
driver_motion_cases = (
("HORIZONTAL-FORWARD", "主驾座椅向前移动500毫秒", "horizontal forward 500"),
("HORIZONTAL-BACKWARD", "主驾座椅后退两秒", "horizontal backward 2000"),
("HORIZONTAL-STOP", "停止主驾座椅前后移动", "horizontal stop"),
("HEIGHT-UP", "主驾座椅往上移动300毫秒", "height up 300"),
("HEIGHT-DOWN", "主驾座椅向下移动一秒", "height down 1000"),
("HEIGHT-STOP", "停止主驾座椅高度调节", "height stop"),
("BACKREST-FORWARD", "主驾座椅靠背调直500毫秒", "backrest forward 500"),
("BACKREST-BACKWARD", "主驾座椅椅背放倒800毫秒", "backrest backward 800"),
("BACKREST-STOP", "停止主驾座椅靠背动作", "backrest stop"),
("BASIN-UP", "主驾座椅坐盆抬高300毫秒", "basin up 300"),
("BASIN-DOWN", "主驾座椅座垫放低一秒", "basin down 1000"),
("BASIN-STOP", "停止主驾座椅坐盆动作", "basin stop"),
)
for case_id, utterance, suffix in driver_motion_cases:
add(
f"CAN1-SEAT-DRIVER-{case_id}",
utterance,
domains=("seat",),
commands=(f"seat driver {suffix}",),
)
add(
"CAN1-SEAT-DRIVER-STOP-ALL",
"停止主驾座椅移动",
domains=("seat",),
commands=(
"seat driver horizontal stop",
"seat driver height stop",
"seat driver backrest stop",
"seat driver basin stop",
),
)
for slot in (1, 2, 3):
add(
f"CAN1-SEAT-DRIVER-MEMORY-{slot}",
f"调用主驾座椅记忆位置{slot}",
domains=("seat",),
commands=(f"seat driver position {slot}",),
)
for state_id, state_text, state in (
("ON", "打开", "on"),
("OFF", "关闭", "off"),
):
add(
f"CAN1-LIGHT-INDOOR-{state_id}",
f"{state_text}室内灯",
domains=("interior_light",),
commands=(f"light indoor {state}",),
)
add(
f"CAN1-LIGHT-READING-ALL-{state_id}",
f"{state_text}全部阅读灯",
domains=("interior_light",),
commands=(f"light reading all {state}",),
)
add(
f"CAN1-LIGHT-AMBIENT-{state_id}",
f"{state_text}氛围灯",
domains=("ambient_light",),
commands=(f"light ambient {state}",),
)
for color in (1, 32, 70, 127):
add(
f"CAN1-LIGHT-AMBIENT-COLOR-{color}",
f"氛围灯颜色编号设置为{color}",
domains=("ambient_light",),
commands=(f"light ambient color {color}",),
)
for brightness in range(1, 6):
add(
f"CAN1-LIGHT-AMBIENT-BRIGHTNESS-{brightness}",
f"氛围灯亮度设置为{brightness}档",
domains=("ambient_light",),
commands=(f"light ambient brightness {brightness}",),
)
for alias_id, alias, brightness in (
("MIN", "最低", 1),
("MIDDLE", "中等", 3),
("MAX", "最高", 5),
):
add(
f"CAN1-LIGHT-AMBIENT-BRIGHTNESS-ALIAS-{alias_id}",
f"氛围灯亮度设置为{alias}",
domains=("ambient_light",),
commands=(f"light ambient brightness {brightness}",),
)
for motion_id, motion_text, motion in (
("OPEN", "打开", "open"),
("CLOSE", "关闭", "close"),
("HALF", "半开", "half"),
("STOP", "停止", "stop"),
):
utterance = f"遮阳帘{motion_text}" if motion == "half" else f"{motion_text}遮阳帘"
add(
f"CAN1-SHADE-{motion_id}",
utterance,
domains=("roof",),
commands=(f"shade {motion}",),
)
for position in (0, 20, 30, 50, 100):
add(
f"CAN1-SHADE-POSITION-{position}",
f"遮阳帘打开到{position}%",
domains=("roof",),
commands=(f"shade position {position}",),
)
can0_atoms = (
(
"CAN0-SEAT-PASSENGER-HEAT-1",
"副驾驶座椅加热开到1档",
can0_operation("seat.set_heating", {"state": "on", "level": 1}),
),
(
"CAN0-SEAT-PASSENGER-HEAT-2",
"副驾驶座椅加热开到2档",
can0_operation("seat.set_heating", {"state": "on", "level": 2}),
),
(
"CAN0-SEAT-PASSENGER-HEAT-3",
"副驾驶座椅加热开到3档",
can0_operation("seat.set_heating", {"state": "on", "level": 3}),
),
(
"CAN0-SEAT-PASSENGER-HEAT-OFF",
"关闭副驾驶座椅加热",
can0_operation("seat.set_heating", {"state": "off"}),
),
(
"CAN0-SEAT-PASSENGER-VENT-1",
"副驾驶座椅通风开到1档",
can0_operation("seat.set_ventilation", {"state": "on", "level": 1}),
),
(
"CAN0-SEAT-PASSENGER-VENT-2",
"副驾驶座椅通风开到2档",
can0_operation("seat.set_ventilation", {"state": "on", "level": 2}),
),
(
"CAN0-SEAT-PASSENGER-VENT-3",
"副驾驶座椅通风开到3档",
can0_operation("seat.set_ventilation", {"state": "on", "level": 3}),
),
(
"CAN0-SEAT-PASSENGER-VENT-OFF",
"关闭副驾驶座椅通风",
can0_operation("seat.set_ventilation", {"state": "off"}),
),
(
"CAN0-SEAT-PASSENGER-HORIZONTAL-30",
"把副驾驶座椅水平位置调到30%",
can0_operation("seat.set_longitudinal_position", {"position_pct": 30}),
),
(
"CAN0-SEAT-PASSENGER-BASIN-40",
"把副驾驶坐盆倾角调到40%",
can0_operation("seat.set_cushion_tilt", {"position_pct": 40}),
),
(
"CAN0-SEAT-PASSENGER-BACKREST-120",
"把副驾驶靠背调到120度",
can0_operation(
"seat.set_backrest_recline",
{"position_pct": 37, "angle_deg": 120},
),
),
(
"CAN0-SEAT-PASSENGER-LEGREST-125",
"把副驾驶腿托抬到125度",
can0_operation(
"seat.set_legrest_height",
{"position_pct": 42, "angle_deg": 125},
),
),
)
for case_id, utterance, operation in can0_atoms:
add(
case_id,
utterance,
domains=("seat",),
channels=("can0",),
can0_operations=(operation,),
)
atom_by_id = {case["id"]: case for case in cases}
def combine(
case_id: str,
source_ids: tuple[str, ...],
*,
separator: str = ",",
kind: str,
) -> None:
source_cases = [atom_by_id[source_id] for source_id in source_ids]
channels = tuple(
channel
for channel in ("can0", "can1")
if any(channel in item["channels"] for item in source_cases)
)
domains = tuple(
dict.fromkeys(
domain for item in source_cases for domain in item["domains"]
)
)
add(
case_id,
separator.join(item["utterance"] for item in source_cases),
domains=domains,
channels=channels,
commands=tuple(
command
for item in source_cases
for command in item["expected_commands"]
),
can0_operations=tuple(
operation
for item in source_cases
for operation in item["expected_can0_operations"]
),
kind=kind,
sources=source_ids,
)
pair_pool = (
"CAN1-DOOR-REAR-LOCK",
"CAN1-WINDOW-FRONT-LEFT-OPEN",
"CAN1-WINDOW-REAR-RIGHT-POSITION-25",
"CAN1-AC-ON",
"CAN1-AC-FAN-2",
"CAN1-AC-MODE-FACE-FEET",
"CAN1-AC-TEMP-DRIVER-24",
"CAN1-AC-TEMP-PASSENGER-24",
"CAN1-AC-TEMP-REAR-24",
"CAN1-AC-CONTROL-AUTO",
"CAN1-AC-CYCLE-INTERNAL",
"CAN1-OUTLET-DRIVER-LEFT-PERCENT-25-75",
"CAN1-OUTLET-PASSENGER-RIGHT-PERCENT-25-75",
"CAN1-SEAT-DRIVER-VENT-HIGH",
"CAN1-SEAT-DRIVER-HEAT-LOW",
"CAN1-SEAT-DRIVER-MASSAGE-MODE-RELAX",
"CAN1-SEAT-DRIVER-MASSAGE-STRENGTH-MEDIUM",
"CAN1-SEAT-DRIVER-HORIZONTAL-FORWARD",
"CAN1-SEAT-DRIVER-HEIGHT-UP",
"CAN1-LIGHT-INDOOR-ON",
"CAN1-LIGHT-READING-ALL-ON",
"CAN1-LIGHT-AMBIENT-ON",
"CAN1-LIGHT-AMBIENT-COLOR-70",
"CAN1-LIGHT-AMBIENT-BRIGHTNESS-3",
"CAN1-SHADE-POSITION-30",
)
for index, source_pair in enumerate(combinations(pair_pool, 2), start=1):
combine(
f"COMBO-PAIR-COMMA-{index:03d}",
source_pair,
kind="pairwise",
)
combine(
f"COMBO-PAIR-SEMICOLON-{index:03d}",
source_pair,
separator=";",
kind="pairwise_separator_variant",
)
multi_anchors = (
"CAN1-DOOR-REAR-LOCK",
"CAN1-WINDOW-FRONT-LEFT-OPEN",
"CAN1-AC-FAN-2",
"CAN1-OUTLET-DRIVER-LEFT-PERCENT-25-75",
"CAN1-SEAT-DRIVER-VENT-HIGH",
"CAN1-LIGHT-INDOOR-ON",
"CAN1-LIGHT-AMBIENT-COLOR-70",
"CAN1-SHADE-POSITION-30",
"CAN0-SEAT-PASSENGER-HEAT-2",
)
for index, source_ids in enumerate(combinations(multi_anchors, 3), start=1):
combine(
f"COMBO-TRIPLE-{index:03d}",
source_ids,
kind="three_command",
)
for index, source_ids in enumerate(combinations(multi_anchors, 4), start=1):
combine(
f"COMBO-QUADRUPLE-{index:03d}",
source_ids,
kind="four_command",
)
cross_can1 = pair_pool[:15]
cross_can0 = tuple(item[0] for item in can0_atoms[0:8])
for index, (can1_id, can0_id) in enumerate(
((left, right) for left in cross_can1 for right in cross_can0),
start=1,
):
combine(
f"COMBO-CROSS-CHANNEL-{index:03d}",
(can1_id, can0_id),
kind="cross_can0_can1",
)
scenes = (
(
"MOVIE",
(
"CAN1-AC-ON",
"CAN1-AC-TEMP-DRIVER-24",
"CAN1-AC-FAN-2",
"CAN1-SHADE-POSITION-100",
"CAN1-LIGHT-AMBIENT-COLOR-70",
"CAN1-LIGHT-AMBIENT-BRIGHTNESS-3",
),
),
(
"DRIVER-COMFORT",
(
"CAN1-SEAT-DRIVER-VENT-LOW",
"CAN1-SEAT-DRIVER-MASSAGE-MODE-RELAX",
"CAN1-SEAT-DRIVER-MASSAGE-STRENGTH-MEDIUM",
"CAN1-AC-TEMP-DRIVER-24",
"CAN1-AC-FAN-2",
),
),
(
"PASSENGER-COMFORT",
(
"CAN0-SEAT-PASSENGER-HEAT-2",
"CAN0-SEAT-PASSENGER-HORIZONTAL-30",
"CAN0-SEAT-PASSENGER-BASIN-40",
"CAN0-SEAT-PASSENGER-BACKREST-120",
"CAN0-SEAT-PASSENGER-LEGREST-125",
),
),
(
"CABIN-BRIGHT",
(
"CAN1-LIGHT-INDOOR-ON",
"CAN1-LIGHT-READING-ALL-ON",
"CAN1-LIGHT-AMBIENT-ON",
"CAN1-LIGHT-AMBIENT-COLOR-70",
"CAN1-LIGHT-AMBIENT-BRIGHTNESS-5",
),
),
(
"VENTILATION",
(
"CAN1-AC-ON",
"CAN1-AC-FAN-7",
"CAN1-AC-MODE-FACE-FEET",
"CAN1-AC-CYCLE-EXTERNAL",
"CAN1-OUTLET-DRIVER-LEFT-PERCENT-25-75",
"CAN1-OUTLET-PASSENGER-RIGHT-PERCENT-25-75",
),
),
(
"PARK-CLOSE",
(
"CAN1-WINDOW-ALL-CLOSE",
"CAN1-SHADE-CLOSE",
"CAN1-DOOR-REAR-LOCK",
"CAN1-LIGHT-INDOOR-OFF",
"CAN1-LIGHT-AMBIENT-OFF",
"CAN1-AC-OFF",
),
),
(
"DUAL-SEAT",
(
"CAN1-SEAT-DRIVER-VENT-HIGH",
"CAN1-SEAT-DRIVER-MASSAGE-MODE-WAVE",
"CAN0-SEAT-PASSENGER-VENT-2",
"CAN0-SEAT-PASSENGER-BACKREST-120",
"CAN1-AC-TEMP-DRIVER-24",
"CAN1-AC-TEMP-PASSENGER-24",
),
),
(
"ALL-ZONES-CLIMATE",
(
"CAN1-AC-ON",
"CAN1-AC-TEMP-DRIVER-24",
"CAN1-AC-TEMP-PASSENGER-24",
"CAN1-AC-TEMP-REAR-24",
"CAN1-AC-FAN-4",
"CAN1-AC-CONTROL-AUTO",
"CAN1-AC-CYCLE-INTERNAL",
),
),
(
"WINDOWS-SHADE",
(
"CAN1-WINDOW-FRONT-LEFT-POSITION-25",
"CAN1-WINDOW-FRONT-RIGHT-POSITION-50",
"CAN1-WINDOW-REAR-LEFT-POSITION-100",
"CAN1-WINDOW-REAR-RIGHT-POSITION-0",
"CAN1-SHADE-POSITION-30",
),
),
(
"DRIVER-POSTURE",
(
"CAN1-SEAT-DRIVER-HORIZONTAL-FORWARD",
"CAN1-SEAT-DRIVER-HEIGHT-UP",
"CAN1-SEAT-DRIVER-BACKREST-BACKWARD",
"CAN1-SEAT-DRIVER-BASIN-DOWN",
),
),
)
for scene_id, source_ids in scenes:
combine(
f"COMBO-SCENE-{scene_id}",
source_ids,
kind="multi_command_scene",
)
safety_cases = (
("DRIVER-VENT-LEVEL-4", "主驾座椅通风4档"),
("DRIVER-VENT-MIDDLE", "主驾座椅通风中档"),
("DRIVER-ABSOLUTE-BACKREST", "把驾驶位靠背调到50%"),
("DRIVER-UNVERIFIED-MODE", "主驾进入零重力模式"),
("FRONT-ROW-SEAT", "打开前排座椅加热"),
("PASSENGER-HEAT-LEVEL-4", "副驾座椅加热开到4档"),
("PASSENGER-VENT-MISSING-STATE", "副驾座椅通风"),
("PASSENGER-CONTRADICTORY", "打开副驾加热然后关闭副驾加热"),
("PASSENGER-MASSAGE", "把副驾座椅按摩打开"),
("MULTIPLE-SEAT-MODES", "进入办公模式和零重力模式"),
("PASSENGER-LEGREST-EXTEND", "把副驾腿托伸长到50%"),
("WRONG-AXIS-UNIT", "坐垫向前移动10度"),
("MISSING-SEAT-AXIS", "向前移动10度"),
("REAR-SEAT", "打开后排座椅加热"),
("ALL-SEATS", "打开所有座椅加热"),
("OUTLET-DISABLE-WITHOUT-MODE", "关闭吹面出风口"),
)
for case_id, utterance in safety_cases:
add(
f"SAFETY-{case_id}",
utterance,
domains=("safety",),
channels=(),
kind="safety_rejection",
status="needs_confirmation",
)
return cases
def write_outputs(cases: list[dict[str, Any]]) -> None:
kind_counts = Counter(case["kind"] for case in cases)
channel_counts = Counter(
"+".join(case["channels"]) or "none" for case in cases
)
payload = {
"schema": "natural-can-command-combination-suite/v1",
"generated_on": "2026-08-24",
"description": "自然语言控制指令到CAN0/CAN1语义控制指令的原子、组合和安全测试目录。",
"summary": {
"total_cases": len(cases),
"by_kind": dict(sorted(kind_counts.items())),
"by_channel": dict(sorted(channel_counts.items())),
},
"cases": cases,
}
JSON_PATH.write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
with CSV_PATH.open("w", encoding="utf-8-sig", newline="") as handle:
writer = csv.DictWriter(
handle,
fieldnames=(
"id",
"kind",
"channels",
"domains",
"utterance",
"expected_status",
"expected_commands",
"expected_can0_operations",
"source_case_ids",
),
)
writer.writeheader()
for case in cases:
writer.writerow(
{
**case,
"channels": "/".join(case["channels"]),
"domains": "/".join(case["domains"]),
"expected_commands": " | ".join(case["expected_commands"]),
"expected_can0_operations": json.dumps(
case["expected_can0_operations"], ensure_ascii=False
),
"source_case_ids": " | ".join(
case.get("source_case_ids", [])
),
}
)
kind_lines = "\n".join(
f"| `{kind}` | {count} |" for kind, count in sorted(kind_counts.items())
)
channel_lines = "\n".join(
f"| `{channel}` | {count} |"
for channel, count in sorted(channel_counts.items())
)
DOC_PATH.write_text(
f"""# 自然语言转 CAN 控制指令组合测试说明
- 生成日期:2026-08-24
- 总用例数:**{len(cases)}**
- 执行方式:本地规则、`dry_run`,不发送真实 CAN 帧
- 完整逐条清单:`docs/testing/自然语言转CAN组合测试用例.csv`
- 机器可执行数据:`tests/fixtures/natural_can_command_combination_suite.json`
## 覆盖数量
| 用例类型 | 数量 |
| --- | ---: |
{kind_lines}
| 通道 | 数量 |
| --- | ---: |
{channel_lines}
## 组合策略
1. 原子用例覆盖 CAN1 门锁、车窗、空调、风口、主驾座椅、灯光、遮阳帘,以及 CAN0 副驾座椅。
2. 两两组合从 25 个互不冲突的控制槽中取组合,同时覆盖中文逗号和分号分隔形式。
3. 三指令和四指令组合覆盖 CAN0/CAN1、座椅、空调、灯光、车窗、门锁、风口与遮阳帘的交叉。
4. 跨通道组合明确检查副驾座椅留在 CAN0、主驾和车身控制留在 CAN1。
5. 多指令场景覆盖观影、双座椅、全温区空调、泊车关闭、车窗与遮阳帘等常用场景。
6. 安全用例要求白名单外目标、档位、能力和矛盾指令不生成 CAN 控制流。
## 执行
```bash
PYTHONPATH=src:domains/seat/src .venv/bin/python -m pytest -q tests/integration/test_natural_can_command_combinations.py
```
重新生成清单:
```bash
.venv/bin/python tools/generate_natural_can_command_suite.py
```
""",
encoding="utf-8",
)
def main() -> None:
cases = build_cases()
ids = [case["id"] for case in cases]
if len(ids) != len(set(ids)):
duplicates = [item for item, count in Counter(ids).items() if count > 1]
raise ValueError(f"duplicate case ids: {duplicates}")
write_outputs(cases)
print(f"generated {len(cases)} cases")
print(JSON_PATH)
print(CSV_PATH)
print(DOC_PATH)
if __name__ == "__main__":
main()