优化了副驾座椅控制
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from cognitive_agent.application.mqtt_ota_seat_executor import MqttOtaSeatExecutor
|
||||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
||||
|
||||
|
||||
CATALOG_PATH = (
|
||||
REPOSITORY_ROOT
|
||||
/ "tests"
|
||||
/ "fixtures"
|
||||
/ "natural_can_command_combination_suite.json"
|
||||
)
|
||||
HARDWARE_DISABLED_ENV = {
|
||||
"COGNITIVE_AGENT_CH1_HIL_ENABLED": "false",
|
||||
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
|
||||
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
|
||||
}
|
||||
|
||||
|
||||
class NaturalCanCommandCombinationTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.temporary_directory = tempfile.TemporaryDirectory()
|
||||
temporary_root = Path(cls.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",
|
||||
)
|
||||
cls.environment = patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
**HARDWARE_DISABLED_ENV,
|
||||
"COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH": str(memory_path),
|
||||
},
|
||||
clear=False,
|
||||
)
|
||||
cls.environment.start()
|
||||
cls.service = SeatDemoService(REPOSITORY_ROOT)
|
||||
cls.service.artifact_root = temporary_root / "artifacts"
|
||||
cls.catalog = json.loads(CATALOG_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
cls.environment.stop()
|
||||
cls.temporary_directory.cleanup()
|
||||
|
||||
@staticmethod
|
||||
def _debug_commands(result: dict) -> list[str]:
|
||||
commands: list[str] = []
|
||||
for plan in result.get("command_plans", {}).values():
|
||||
for command in plan.get("spec", {}).get("commands", []):
|
||||
debug_command = command.get("signals", {}).get("debug_command")
|
||||
if isinstance(debug_command, str):
|
||||
commands.append(debug_command)
|
||||
return commands
|
||||
|
||||
@staticmethod
|
||||
def _can0_operations(result: dict, expected: list[dict]) -> Counter[str]:
|
||||
capabilities = {item["capability"] for item in expected}
|
||||
operations: list[dict] = []
|
||||
for config in result.get("function_configs", {}).values():
|
||||
for operation in config.get("spec", {}).get("operations", []):
|
||||
if operation.get("capability") not in capabilities:
|
||||
continue
|
||||
target = operation.get("target", {})
|
||||
operations.append(
|
||||
{
|
||||
"target": target.get("instance", target.get("zone")),
|
||||
"capability": operation["capability"],
|
||||
"parameters": operation.get("parameters", {}),
|
||||
}
|
||||
)
|
||||
return Counter(
|
||||
json.dumps(item, ensure_ascii=False, sort_keys=True)
|
||||
for item in operations
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _expected_can0_operations(expected: list[dict]) -> Counter[str]:
|
||||
return Counter(
|
||||
json.dumps(item, ensure_ascii=False, sort_keys=True)
|
||||
for item in expected
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _single_passenger_motion_operation(result: dict) -> dict:
|
||||
operations = [
|
||||
operation
|
||||
for config in result.get("function_configs", {}).values()
|
||||
for operation in config.get("spec", {}).get("operations", [])
|
||||
if operation.get("capability")
|
||||
in {
|
||||
"seat.set_longitudinal_position",
|
||||
"seat.set_cushion_tilt",
|
||||
"seat.set_backrest_recline",
|
||||
"seat.set_legrest_height",
|
||||
}
|
||||
]
|
||||
if len(operations) != 1:
|
||||
raise AssertionError(f"expected one passenger motion operation: {operations}")
|
||||
return operations[0]
|
||||
|
||||
def test_catalog_metadata_and_all_expanded_cases(self) -> None:
|
||||
cases = self.catalog["cases"]
|
||||
self.assertEqual(
|
||||
self.catalog["schema"],
|
||||
"natural-can-command-combination-suite/v1",
|
||||
)
|
||||
self.assertEqual(self.catalog["summary"]["total_cases"], len(cases))
|
||||
self.assertGreaterEqual(len(cases), 1000)
|
||||
self.assertEqual(len({item["id"] for item in cases}), len(cases))
|
||||
|
||||
for index, case in enumerate(cases):
|
||||
with self.subTest(case=case["id"], utterance=case["utterance"]):
|
||||
result = self.service.process(
|
||||
case["utterance"],
|
||||
provider="local",
|
||||
conversation_id=f"conv-natural-combination-{index:04d}",
|
||||
execution_mode="dry_run",
|
||||
)
|
||||
self.assertEqual(result["status"], case["expected_status"])
|
||||
self.assertEqual(
|
||||
self._debug_commands(result),
|
||||
case["expected_commands"],
|
||||
)
|
||||
expected_can0 = case["expected_can0_operations"]
|
||||
if expected_can0:
|
||||
self.assertEqual(
|
||||
self._can0_operations(result, expected_can0),
|
||||
self._expected_can0_operations(expected_can0),
|
||||
)
|
||||
|
||||
def test_can0_native_motion_and_dynamic_modes_combine_with_can1(self) -> None:
|
||||
cases = (
|
||||
(
|
||||
"把副驾座椅水平位置调到35%,空调温度设置为24度,打开氛围灯",
|
||||
["ac temp driver 24", "light ambient on"],
|
||||
[{
|
||||
"target": "passenger",
|
||||
"capability": "seat.set_longitudinal_position",
|
||||
"parameters": {"position_pct": 35},
|
||||
}],
|
||||
),
|
||||
(
|
||||
"副驾坐盆向上调整10%,关闭后排车窗,空调风量2档",
|
||||
["window rear close", "ac fan 2"],
|
||||
[{
|
||||
"target": "passenger",
|
||||
"capability": "seat.set_cushion_tilt",
|
||||
"parameters": {"position_pct": 40},
|
||||
}],
|
||||
),
|
||||
(
|
||||
"开启木马模式并打开氛围灯",
|
||||
["light ambient on"],
|
||||
[{
|
||||
"target": "passenger",
|
||||
"capability": "seat.start_dynamic_mode",
|
||||
"parameters": {"mode_id": 1},
|
||||
}],
|
||||
),
|
||||
(
|
||||
"开启动态摇篮模式,空调24度,打开遮阳帘",
|
||||
["ac temp driver 24", "shade open"],
|
||||
[{
|
||||
"target": "passenger",
|
||||
"capability": "seat.start_dynamic_mode",
|
||||
"parameters": {"mode_id": 2},
|
||||
}],
|
||||
),
|
||||
(
|
||||
"副驾座椅前后调到20%、坐盆调到30%、靠背调到40%、腿托调到50%,空调风量3档,关闭全部车窗",
|
||||
["ac fan 3", "window all close"],
|
||||
[
|
||||
{
|
||||
"target": "passenger",
|
||||
"capability": capability,
|
||||
"parameters": {"position_pct": position},
|
||||
}
|
||||
for capability, position in (
|
||||
("seat.set_longitudinal_position", 20),
|
||||
("seat.set_cushion_tilt", 30),
|
||||
("seat.set_backrest_recline", 40),
|
||||
("seat.set_legrest_height", 50),
|
||||
)
|
||||
],
|
||||
),
|
||||
(
|
||||
"副驾靠背向后移动15%,主驾温度设为25度,遮阳帘打开",
|
||||
["ac temp driver 25", "shade open"],
|
||||
[{
|
||||
"target": "passenger",
|
||||
"capability": "seat.set_backrest_recline",
|
||||
"parameters": {"position_pct": 35},
|
||||
}],
|
||||
),
|
||||
(
|
||||
"副驾靠背往后转动10%,空调温度设置为24度",
|
||||
["ac temp driver 24"],
|
||||
[{
|
||||
"target": "passenger",
|
||||
"capability": "seat.set_backrest_recline",
|
||||
"parameters": {"position_pct": 30},
|
||||
}],
|
||||
),
|
||||
(
|
||||
"副驾腿托设置为40%,关闭副驾车窗",
|
||||
["window front-right close"],
|
||||
[{
|
||||
"target": "passenger",
|
||||
"capability": "seat.set_legrest_height",
|
||||
"parameters": {"position_pct": 40},
|
||||
}],
|
||||
),
|
||||
)
|
||||
|
||||
for index, (utterance, can1_commands, can0_operations) in enumerate(cases):
|
||||
with self.subTest(utterance=utterance):
|
||||
result = self.service.process(
|
||||
utterance,
|
||||
provider="local",
|
||||
conversation_id=f"conv-can0-can1-targeted-{index}",
|
||||
execution_mode="dry_run",
|
||||
)
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertEqual(self._debug_commands(result), can1_commands)
|
||||
self.assertEqual(
|
||||
self._can0_operations(result, can0_operations),
|
||||
self._expected_can0_operations(can0_operations),
|
||||
)
|
||||
|
||||
def test_passenger_relative_and_absolute_directional_commands_stay_distinct(self) -> None:
|
||||
cases = (
|
||||
("副驾座椅往上移动10%", "basin", "decrease", "up", 10),
|
||||
("副驾座椅往上移动20%", "basin", "decrease", "up", 20),
|
||||
("副驾往下移动10%", "basin", "increase", "down", 10),
|
||||
("副驾往下移动20%", "basin", "increase", "down", 20),
|
||||
("副驾座椅往前移动10%", "horizontal", "decrease", "forward", 10),
|
||||
("副驾座椅往前移动20%", "horizontal", "decrease", "forward", 20),
|
||||
("副驾往后移动10%", "horizontal", "increase", "backward", 10),
|
||||
("副驾往后移动20%", "horizontal", "increase", "backward", 20),
|
||||
("副驾座椅靠背往前移动10%", "backrest", "decrease", "forward", 10),
|
||||
("副驾座椅靠背往前移动30%", "backrest", "decrease", "forward", 30),
|
||||
("副驾靠背往后移动10%", "backrest", "increase", "backward", 10),
|
||||
("副驾靠背往后移动30%", "backrest", "increase", "backward", 30),
|
||||
("副驾推托往上移动20%", "legrest", "increase", "up", 20),
|
||||
("副驾推托往上移动40%", "legrest", "increase", "up", 40),
|
||||
("副驾推托往下移动20%", "legrest", "decrease", "down", 20),
|
||||
("副驾推托往下移动40%", "legrest", "decrease", "down", 40),
|
||||
)
|
||||
|
||||
for index, (utterance, axis, direction, command_direction, amount) in enumerate(
|
||||
cases
|
||||
):
|
||||
with self.subTest(kind="relative", utterance=utterance):
|
||||
result = self.service.process(
|
||||
utterance,
|
||||
provider="local",
|
||||
conversation_id=f"conv-passenger-relative-{index}",
|
||||
execution_mode="dry_run",
|
||||
)
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
operation = self._single_passenger_motion_operation(result)
|
||||
motion = operation["motion"]
|
||||
self.assertEqual(motion["mode"], "relative")
|
||||
self.assertEqual(motion["direction"], direction)
|
||||
self.assertEqual(
|
||||
motion.get(
|
||||
"requested_delta_position_pct",
|
||||
motion["delta_position_pct"],
|
||||
),
|
||||
amount,
|
||||
)
|
||||
self.assertTrue(
|
||||
MqttOtaSeatExecutor._operation_debug_command(operation).startswith(
|
||||
f"seat passenger {axis} relative {command_direction} "
|
||||
)
|
||||
)
|
||||
|
||||
absolute_utterance = utterance.replace("移动", "移动到")
|
||||
with self.subTest(kind="absolute", utterance=absolute_utterance):
|
||||
result = self.service.process(
|
||||
absolute_utterance,
|
||||
provider="local",
|
||||
conversation_id=f"conv-passenger-absolute-{index}",
|
||||
execution_mode="dry_run",
|
||||
)
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
operation = self._single_passenger_motion_operation(result)
|
||||
self.assertNotIn("motion", operation)
|
||||
self.assertEqual(operation["parameters"]["position_pct"], amount)
|
||||
self.assertEqual(
|
||||
MqttOtaSeatExecutor._operation_debug_command(operation),
|
||||
f"seat passenger {axis} position {amount}",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user