352 lines
14 KiB
Python
352 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from cognitive_agent.application.native_can_intent import (
|
|
parse_exact_native_can_command,
|
|
)
|
|
from cognitive_agent.application.native_can_runtime import (
|
|
NativeCanCompileError,
|
|
NativeCanRuntime,
|
|
)
|
|
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
|
|
|
|
|
HARDWARE_DISABLED_ENV = {
|
|
"COGNITIVE_AGENT_CH1_HIL_ENABLED": "false",
|
|
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
|
|
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
|
|
}
|
|
|
|
|
|
class Can1SemanticWhitelistTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.temporary_directory = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temporary_directory.cleanup)
|
|
temporary_root = Path(self.temporary_directory.name)
|
|
memory_path = temporary_root / "driver-seat-memory.json"
|
|
memory_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"version": 1,
|
|
"positions": {
|
|
str(slot): {
|
|
"payload_15": "15 00 00 00 00 00 00 00",
|
|
"payload_16": "16 00 00 00 00 00 00 00",
|
|
}
|
|
for slot in (1, 2, 3)
|
|
},
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
self.environment = patch.dict(
|
|
os.environ,
|
|
{
|
|
**HARDWARE_DISABLED_ENV,
|
|
"COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH": str(memory_path),
|
|
},
|
|
clear=False,
|
|
)
|
|
self.environment.start()
|
|
self.addCleanup(self.environment.stop)
|
|
self.runtime = NativeCanRuntime(REPOSITORY_ROOT)
|
|
self.service = SeatDemoService(REPOSITORY_ROOT)
|
|
self.service.artifact_root = temporary_root / "artifacts"
|
|
|
|
@staticmethod
|
|
def _valid_exact_commands() -> list[str]:
|
|
commands = [f"door rear {state}" for state in ("lock", "unlock")]
|
|
commands.extend(
|
|
f"window {target} {motion}"
|
|
for target in (
|
|
"all",
|
|
"front",
|
|
"rear",
|
|
"front-left",
|
|
"front-right",
|
|
"rear-left",
|
|
"rear-right",
|
|
)
|
|
for motion in ("open", "close", "stop")
|
|
)
|
|
commands.extend(
|
|
f"window {target} position {position}"
|
|
for target in (
|
|
"front-left",
|
|
"front-right",
|
|
"rear-left",
|
|
"rear-right",
|
|
)
|
|
for position in (0, 100)
|
|
)
|
|
commands.extend(("ac on", "ac off"))
|
|
commands.extend(f"ac fan {level}" for level in range(1, 8))
|
|
commands.extend(
|
|
f"ac mode {mode}"
|
|
for mode in (
|
|
"face",
|
|
"face-feet",
|
|
"feet",
|
|
"feet-defrost",
|
|
"defrost",
|
|
"face-feet-defrost",
|
|
"face-defrost",
|
|
)
|
|
)
|
|
commands.extend(
|
|
f"ac temp {target} {temperature}"
|
|
for target in ("driver", "passenger", "rear")
|
|
for temperature in (18, 32)
|
|
)
|
|
commands.extend(("ac control manual", "ac control auto"))
|
|
commands.extend(
|
|
f"outlet {target} percent {horizontal} {vertical}"
|
|
for target in (
|
|
"driver-left",
|
|
"driver-right",
|
|
"passenger-left",
|
|
"passenger-right",
|
|
)
|
|
for horizontal, vertical in ((0, 0), (100, 100))
|
|
)
|
|
commands.extend(
|
|
(
|
|
"outlet driver-left position -25 -40",
|
|
"outlet driver-right position 30 30",
|
|
"outlet passenger-left position -10 -40",
|
|
"outlet passenger-right position 5 30",
|
|
)
|
|
)
|
|
commands.extend(
|
|
f"seat driver {feature} {level}"
|
|
for feature in ("vent", "heat")
|
|
for level in ("off", "low", "high")
|
|
)
|
|
commands.append("seat driver massage off")
|
|
commands.extend(
|
|
f"seat driver massage strength {strength}"
|
|
for strength in ("weak", "medium", "strong")
|
|
)
|
|
commands.extend(
|
|
f"seat driver massage mode {mode}"
|
|
for mode in ("wave", "pulse", "stretch", "slack", "relax")
|
|
)
|
|
commands.extend(
|
|
(
|
|
"seat driver horizontal forward 100",
|
|
"seat driver horizontal backward 2000",
|
|
"seat driver horizontal stop",
|
|
"seat driver height up 100",
|
|
"seat driver height down 2000",
|
|
"seat driver height stop",
|
|
"seat driver backrest forward 100",
|
|
"seat driver backrest backward 2000",
|
|
"seat driver backrest stop",
|
|
"seat driver basin up 100",
|
|
"seat driver basin down 2000",
|
|
"seat driver basin stop",
|
|
)
|
|
)
|
|
commands.extend(f"seat driver position {slot}" for slot in (1, 2, 3))
|
|
commands.extend(
|
|
f"seat driver memory recall {slot}" for slot in (1, 2, 3)
|
|
)
|
|
commands.extend(f"light indoor {state}" for state in ("on", "off"))
|
|
commands.extend(
|
|
f"light reading all {state}" for state in ("on", "off")
|
|
)
|
|
commands.extend(f"light ambient {state}" for state in ("on", "off"))
|
|
commands.extend(f"light ambient color {value}" for value in (1, 127))
|
|
commands.extend(
|
|
f"light ambient brightness {value}" for value in (1, 5)
|
|
)
|
|
commands.extend(
|
|
f"shade {motion}" for motion in ("open", "close", "half", "stop")
|
|
)
|
|
commands.extend(
|
|
f"shade position {position}" for position in (0, 1, 50, 99, 100)
|
|
)
|
|
return commands
|
|
|
|
def _compile_exact(self, command: str) -> dict:
|
|
actions = parse_exact_native_can_command(command)
|
|
self.assertEqual(len(actions), 1, command)
|
|
action = actions[0]
|
|
config = self.runtime.build_function_config(
|
|
action["domain"],
|
|
actions,
|
|
"req-can-one-whitelist",
|
|
"local_rules",
|
|
)
|
|
return self.runtime.compile(config)
|
|
|
|
@staticmethod
|
|
def _debug_commands(result: dict) -> list[str]:
|
|
return [
|
|
command["signals"]["debug_command"]
|
|
for plan in result.get("command_plans", {}).values()
|
|
for command in plan["spec"]["commands"]
|
|
]
|
|
|
|
def test_every_documented_exact_command_parses_and_compiles(self) -> None:
|
|
commands = self._valid_exact_commands()
|
|
self.assertEqual(len(commands), 119)
|
|
|
|
for command in commands:
|
|
with self.subTest(command=command):
|
|
plan = self._compile_exact(command)
|
|
self.assertEqual(
|
|
[
|
|
item["signals"]["debug_command"]
|
|
for item in plan["spec"]["commands"]
|
|
],
|
|
[command],
|
|
)
|
|
|
|
def test_out_of_whitelist_exact_values_are_rejected(self) -> None:
|
|
commands = (
|
|
"door rear open",
|
|
"window vehicle open",
|
|
"window all position 50",
|
|
"window front-left position -1",
|
|
"window front-left position 101",
|
|
"ac fan 0",
|
|
"ac fan 8",
|
|
"ac mode auto",
|
|
"ac temp driver 17",
|
|
"ac temp driver 33",
|
|
"ac temp driver 24.5",
|
|
"ac control off",
|
|
"outlet driver-left percent -1 50",
|
|
"outlet driver-left percent 101 50",
|
|
"seat driver vent medium",
|
|
"seat driver horizontal forward 99",
|
|
"seat driver horizontal forward 2001",
|
|
"seat driver horizontal stop 500",
|
|
"seat driver position 0",
|
|
"seat driver position 4",
|
|
"light ambient color 0",
|
|
"light ambient color 128",
|
|
"light ambient brightness 0",
|
|
"light ambient brightness 6",
|
|
"shade position -1",
|
|
"shade position 101",
|
|
)
|
|
for command in commands:
|
|
with self.subTest(command=command):
|
|
self.assertEqual(parse_exact_native_can_command(command), [])
|
|
|
|
def test_calibrated_outlet_values_must_fit_the_selected_outlet(self) -> None:
|
|
action = parse_exact_native_can_command(
|
|
"outlet driver-left position -26 -40"
|
|
)[0]
|
|
config = self.runtime.build_function_config(
|
|
action["domain"],
|
|
[action],
|
|
"req-can-one-calibration",
|
|
"local_rules",
|
|
)
|
|
|
|
with self.assertRaisesRegex(
|
|
NativeCanCompileError, "horizontal_value exceeds the calibrated range"
|
|
):
|
|
self.runtime.compile(config)
|
|
|
|
def test_representative_chinese_commands_cover_every_whitelist_family(self) -> None:
|
|
cases = (
|
|
("后排车门上锁", ["door rear lock"]),
|
|
("后排门锁解锁", ["door rear unlock"]),
|
|
("打开全部车窗", ["window all open"]),
|
|
("关闭前排车窗", ["window front close"]),
|
|
("停止后排车窗", ["window rear stop"]),
|
|
("把副驾车窗开到35%", ["window front-right position 35"]),
|
|
("打开空调", ["ac on"]),
|
|
("关闭空调", ["ac off"]),
|
|
("风量调到7档", ["ac fan 7"]),
|
|
("空调出风模式设为吹脸", ["ac mode face"]),
|
|
("空调出风模式设为吹面和吹脚", ["ac mode face-feet"]),
|
|
("空调出风模式设为吹脚", ["ac mode feet"]),
|
|
("空调出风模式设为吹脚除霜", ["ac mode feet-defrost"]),
|
|
("空调出风模式设为除霜", ["ac mode defrost"]),
|
|
("空调出风模式设为吹脸和吹脚除霜", ["ac mode face-feet-defrost"]),
|
|
("空调出风模式设为吹脸除霜", ["ac mode face-defrost"]),
|
|
("主驾空调温度设为18度", ["ac temp driver 18"]),
|
|
("副驾空调温度设为32度", ["ac temp passenger 32"]),
|
|
("后排空调温度设为24度", ["ac temp rear 24"]),
|
|
("空调设置为自动控制", ["ac control auto"]),
|
|
("空调设置为手动控制", ["ac control manual"]),
|
|
("主驾左风口水平25%上下75%", ["outlet driver-left percent 25 75"]),
|
|
("副驾右风口左右25%垂直75%", ["outlet passenger-right percent 25 75"]),
|
|
("主驾左风口标定坐标左右-25上下-40", ["outlet driver-left position -25 -40"]),
|
|
("打开主驾座椅通风", ["seat driver vent low"]),
|
|
("关闭主驾座椅加热", ["seat driver heat off"]),
|
|
("主驾座椅加热高档", ["seat driver heat high"]),
|
|
("关闭主驾座椅按摩", ["seat driver massage off"]),
|
|
("主驾座椅按摩强度调为中等", ["seat driver massage strength medium"]),
|
|
("主驾座椅按摩模式设为放松", ["seat driver massage mode relax"]),
|
|
("主驾座椅向前移动500毫秒", ["seat driver horizontal forward 500"]),
|
|
("主驾座椅高度向下调500毫秒", ["seat driver height down 500"]),
|
|
("主驾座椅靠背后仰800毫秒", ["seat driver backrest backward 800"]),
|
|
("把主驾座椅坐盆抬高300毫秒", ["seat driver basin up 300"]),
|
|
("调用主驾座椅记忆位置2", ["seat driver position 2"]),
|
|
("打开室内灯", ["light indoor on"]),
|
|
("关闭全部阅读灯", ["light reading all off"]),
|
|
("打开氛围灯", ["light ambient on"]),
|
|
("关闭氛围灯", ["light ambient off"]),
|
|
("把氛围灯颜色编号调到70", ["light ambient color 70"]),
|
|
("氛围灯亮度调整为3档", ["light ambient brightness 3"]),
|
|
("打开遮阳帘", ["shade open"]),
|
|
("关闭遮阳帘", ["shade close"]),
|
|
("遮阳帘半开", ["shade half"]),
|
|
("停止遮阳帘", ["shade stop"]),
|
|
("遮阳帘开到20%", ["shade position 20"]),
|
|
("遮阳帘打开30%", ["shade position 30"]),
|
|
)
|
|
for index, (utterance, expected) in enumerate(cases):
|
|
with self.subTest(utterance=utterance):
|
|
result = self.service.process(
|
|
utterance,
|
|
provider="local",
|
|
conversation_id=f"conv-can-one-natural-{index}",
|
|
execution_mode="dry_run",
|
|
)
|
|
self.assertEqual(result["status"], "succeeded")
|
|
self.assertEqual(self._debug_commands(result), expected)
|
|
|
|
rear_context = self.service.conversations.snapshot(
|
|
"conv-can-one-natural-18"
|
|
)
|
|
self.assertEqual(
|
|
rear_context["domain_devices"]["climate"]["zones"]["rear_row"][
|
|
"temperature_c"
|
|
],
|
|
24,
|
|
)
|
|
|
|
def test_fractional_values_are_not_silently_truncated(self) -> None:
|
|
utterances = (
|
|
"空调温度设为24.5度",
|
|
"风量设为2.5档",
|
|
"氛围灯颜色编号调到70.5",
|
|
"氛围灯亮度调到3.5档",
|
|
)
|
|
for index, utterance in enumerate(utterances):
|
|
with self.subTest(utterance=utterance):
|
|
result = self.service.process(
|
|
utterance,
|
|
provider="local",
|
|
conversation_id=f"conv-can-one-fraction-{index}",
|
|
execution_mode="dry_run",
|
|
)
|
|
self.assertEqual(self._debug_commands(result), [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|