优化后端和前端
This commit is contained in:
@@ -332,18 +332,18 @@ operations:
|
||||
def test_static_console_and_runtime_endpoint(self) -> None:
|
||||
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
|
||||
html = response.read().decode("utf-8")
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-101", timeout=3) as response:
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-103", timeout=3) as response:
|
||||
app_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
|
||||
) as response:
|
||||
gesture_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/console-v2.js?v=sync-layout-41", timeout=3
|
||||
f"{self.base_url}/console-v2.js?v=sync-layout-43", timeout=3
|
||||
) as response:
|
||||
console_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/console-v2.css?v=sync-layout-40", timeout=3
|
||||
f"{self.base_url}/console-v2.css?v=sync-layout-41", timeout=3
|
||||
) as response:
|
||||
console_styles = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
@@ -378,12 +378,12 @@ operations:
|
||||
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
|
||||
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
|
||||
self.assertNotIn("透明车体 · 实体设备", html)
|
||||
self.assertIn('src="/app.js?v=dual-seat-101"', html)
|
||||
self.assertIn('src="/app.js?v=dual-seat-103"', html)
|
||||
self.assertIn('href="/styles.css?v=dual-seat-16"', html)
|
||||
self.assertIn('src="/gesture-control.js?v=android-pad-9"', html)
|
||||
self.assertNotIn('class="vehicle-system-hud"', html)
|
||||
self.assertIn('src="/console-v2.js?v=sync-layout-41"', html)
|
||||
self.assertIn('href="/console-v2.css?v=sync-layout-40"', html)
|
||||
self.assertIn('src="/console-v2.js?v=sync-layout-43"', html)
|
||||
self.assertIn('href="/console-v2.css?v=sync-layout-41"', html)
|
||||
self.assertIn('<body class="console-v2">', html)
|
||||
self.assertIn('id="v2BootScreen"', html)
|
||||
self.assertIn('正在加载车机控制与实时状态', html)
|
||||
@@ -612,7 +612,7 @@ operations:
|
||||
self.assertIn('client_type: "web_console"', app_script)
|
||||
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
|
||||
self.assertIn('"cognitive-agent:module-change"', app_script)
|
||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v202"', service_worker)
|
||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v204"', service_worker)
|
||||
self.assertIn('"/assets/pets/self-extension-cat.png?v=3"', service_worker)
|
||||
self.assertIn(".then(() => self.skipWaiting())", service_worker)
|
||||
self.assertNotIn("const cached = await caches.match(request)", service_worker)
|
||||
@@ -852,7 +852,7 @@ operations:
|
||||
) as response:
|
||||
extension_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/app.js?v=dual-seat-101", timeout=3
|
||||
f"{self.base_url}/app.js?v=dual-seat-103", timeout=3
|
||||
) as response:
|
||||
app_script = response.read().decode("utf-8")
|
||||
|
||||
@@ -1314,6 +1314,134 @@ operations:
|
||||
self.assertEqual(result["provider_used"], "local_rules_fallback")
|
||||
self.assertEqual(result["execution_report"]["spec"]["mode"], "dry_run")
|
||||
|
||||
def test_two_stage_http_api_returns_local_commands_before_cloud_supplement(
|
||||
self,
|
||||
) -> None:
|
||||
original_api_key = self.service.openai.api_key
|
||||
self.service.openai.api_key = "two-stage-http-test-key"
|
||||
preview_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests/preview",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"utterance": "空调温度设置为24度,播放音乐",
|
||||
"provider": "openai",
|
||||
"conversation_id": "conv-e2e-two-stage-http",
|
||||
"execution_mode": "dry_run",
|
||||
"reasoning_effort": "medium",
|
||||
"client_type": "web_console",
|
||||
}
|
||||
).encode("utf-8"),
|
||||
)
|
||||
model_output = {
|
||||
"detected_domains": ["ambient_light"],
|
||||
"requests": [
|
||||
{
|
||||
"domain": "ambient_light",
|
||||
"capability": "ambient_light.set_power",
|
||||
"target_scope": "vehicle",
|
||||
"operation": "set",
|
||||
"duration_ms": None,
|
||||
"parameters": [{"name": "state", "value": "on"}],
|
||||
"reason": "补充播放场景氛围灯。",
|
||||
}
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["仅补充本地未解析片段。"],
|
||||
"confidence": 0.9,
|
||||
}
|
||||
try:
|
||||
with patch.object(
|
||||
self.service.openai,
|
||||
"interpret",
|
||||
side_effect=AssertionError("preview must not invoke GPT"),
|
||||
):
|
||||
with self.opener.open(preview_request, timeout=3) as response:
|
||||
preview = json.loads(response.read().decode("utf-8"))
|
||||
self.assertEqual(response.status, 201)
|
||||
local_operation = next(
|
||||
operation
|
||||
for config in preview["function_configs"].values()
|
||||
for operation in config["spec"]["operations"]
|
||||
)
|
||||
self.assertEqual(local_operation["parse_source"], "local_rule")
|
||||
self.assertTrue(preview["supplement_required"])
|
||||
|
||||
supplement_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests/{preview['request_id']}/supplement",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"provider": "openai",
|
||||
"reasoning_effort": "medium",
|
||||
"client_type": "web_console",
|
||||
}
|
||||
).encode("utf-8"),
|
||||
)
|
||||
with patch.object(
|
||||
self.service.openai,
|
||||
"interpret",
|
||||
return_value=model_output,
|
||||
) as interpret:
|
||||
with self.opener.open(supplement_request, timeout=3) as response:
|
||||
result = json.loads(response.read().decode("utf-8"))
|
||||
self.assertEqual(response.status, 201)
|
||||
self.assertEqual(interpret.call_args.args[0], "播放音乐")
|
||||
finally:
|
||||
self.service.openai.api_key = original_api_key
|
||||
|
||||
self.assertEqual(result["phase"], "two_stage_complete")
|
||||
sources = {
|
||||
operation["capability"]: operation["parse_source"]
|
||||
for config in result["function_configs"].values()
|
||||
for operation in config["spec"]["operations"]
|
||||
}
|
||||
self.assertEqual(sources["climate.set_temperature"], "local_rule")
|
||||
self.assertEqual(sources["ambient_light.set_power"], "cloud_supplement")
|
||||
|
||||
def test_preview_streams_validated_commands_before_terminal_result(self) -> None:
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests/preview",
|
||||
method="POST",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/x-ndjson",
|
||||
},
|
||||
data=json.dumps(
|
||||
{
|
||||
"utterance": "空调24度、风量2档",
|
||||
"provider": "local",
|
||||
"conversation_id": "conv-e2e-command-stream",
|
||||
"execution_mode": "dry_run",
|
||||
"client_type": "web_console",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
|
||||
with self.opener.open(request, timeout=3) as response:
|
||||
events = [
|
||||
json.loads(line)
|
||||
for line in response.read().decode("utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
response.headers.get_content_type(),
|
||||
"application/x-ndjson",
|
||||
)
|
||||
self.assertEqual(
|
||||
[event["type"] for event in events],
|
||||
["validated_commands", "result"],
|
||||
)
|
||||
validated = events[0]["payload"]
|
||||
self.assertEqual(validated["status"], "processing")
|
||||
self.assertIsNone(validated["execution_report"])
|
||||
self.assertTrue(validated["command_plans"])
|
||||
self.assertEqual(events[1]["payload"]["status"], "succeeded")
|
||||
|
||||
def test_frontend_suite_covers_every_physical_seat_capability(self) -> None:
|
||||
suite_path = (
|
||||
REPOSITORY_ROOT
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
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()
|
||||
@@ -154,6 +154,44 @@ class Ch1DefaultTargetTests(unittest.TestCase):
|
||||
"front_right",
|
||||
)
|
||||
|
||||
def test_driver_and_passenger_window_aliases_map_to_the_correct_side(self) -> None:
|
||||
cases = (
|
||||
("打开主驾车窗", "front_left", "window.set_motion", {"motion": "open"}),
|
||||
("打开主驾驶的车窗", "front_left", "window.set_motion", {"motion": "open"}),
|
||||
("打开副驾车窗", "front_right", "window.set_motion", {"motion": "open"}),
|
||||
("打开副驾驶的车窗", "front_right", "window.set_motion", {"motion": "open"}),
|
||||
("把副驾车窗开到35%", "front_right", "window.set_position", {"position_pct": 35}),
|
||||
)
|
||||
for index, (utterance, target, capability, parameters) in enumerate(cases):
|
||||
with self.subTest(utterance=utterance):
|
||||
result = self.service.process(
|
||||
utterance,
|
||||
provider="local",
|
||||
conversation_id=f"conv-window-occupant-alias-{index}",
|
||||
execution_mode="dry_run",
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertEqual(set(result["function_configs"]), {"native_window"})
|
||||
operation = result["function_configs"]["native_window"]["spec"][
|
||||
"operations"
|
||||
][0]
|
||||
self.assertEqual(operation["target"]["zone"], target)
|
||||
self.assertEqual(operation["capability"], capability)
|
||||
self.assertEqual(operation["parameters"], parameters)
|
||||
|
||||
def test_window_target_does_not_leak_from_a_later_seat_subject(self) -> None:
|
||||
parsed = merge_native_can_intent({}, "打开副驾车窗和主驾座椅通风")
|
||||
windows = [
|
||||
action
|
||||
for action in parsed["native_can_actions"]
|
||||
if action["capability"] == "window.set_motion"
|
||||
]
|
||||
|
||||
self.assertEqual(len(windows), 1)
|
||||
self.assertEqual(windows[0]["target"], "front_right")
|
||||
self.assertEqual(windows[0]["parameters"], {"motion": "open"})
|
||||
|
||||
def test_group_or_non_driver_seat_scope_is_not_treated_as_unscoped(self) -> None:
|
||||
for index, utterance in enumerate(
|
||||
("打开前排座椅加热", "打开后排座椅加热", "打开所有座椅加热")
|
||||
|
||||
@@ -211,9 +211,17 @@ class DriverSeatNativeCanTests(unittest.TestCase):
|
||||
{"axis": "height", "direction": "up", "duration_ms": 500},
|
||||
),
|
||||
"主驾座椅调整到记忆位置3": (
|
||||
"seat.recall_driver_memory_native",
|
||||
"seat.set_driver_position_native",
|
||||
{"slot": 3},
|
||||
),
|
||||
"调用主驾座椅记忆位置2": (
|
||||
"seat.set_driver_position_native",
|
||||
{"slot": 2},
|
||||
),
|
||||
"恢复主驾座椅到二号记忆位置": (
|
||||
"seat.set_driver_position_native",
|
||||
{"slot": 2},
|
||||
),
|
||||
}
|
||||
for utterance, expected in cases.items():
|
||||
with self.subTest(utterance=utterance):
|
||||
|
||||
@@ -27,9 +27,13 @@ REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
class _PublishResult:
|
||||
rc = 0
|
||||
mid = 17
|
||||
|
||||
def is_published(self) -> bool:
|
||||
return True
|
||||
|
||||
def wait_for_publish(self, timeout: float) -> bool:
|
||||
return timeout > 0
|
||||
raise AssertionError("control publish must never block for acknowledgement")
|
||||
|
||||
|
||||
class _FakeMqttClient:
|
||||
@@ -571,11 +575,14 @@ class MqttCanTransportTests(unittest.TestCase):
|
||||
self.assertEqual(payload["data"], "10 00 00 00 00 00 00 00")
|
||||
self.assertEqual(payload["repeat"], 1)
|
||||
self.assertEqual(payload["intervalMs"], 0)
|
||||
self.assertFalse(payload["expectResponse"])
|
||||
self.assertEqual(payload["responseTimeoutMs"], 100)
|
||||
publication = bus.published_messages[0]
|
||||
self.assertEqual(publication["topic"], topic)
|
||||
self.assertEqual(publication["qos"], qos)
|
||||
self.assertEqual(publication["retain"], retain)
|
||||
self.assertEqual(publication["delivery_status"], "published")
|
||||
self.assertEqual(publication["message_id"], 17)
|
||||
self.assertEqual(publication["payload"], payload)
|
||||
self.assertTrue(
|
||||
{
|
||||
@@ -606,6 +613,40 @@ class MqttCanTransportTests(unittest.TestCase):
|
||||
self.assertEqual(len(self.client.published), 2)
|
||||
self.assertEqual(len(bus.published_messages), 2)
|
||||
|
||||
def test_publish_returns_queued_without_waiting_for_broker_ack(self) -> None:
|
||||
class QueuedPublishResult:
|
||||
rc = 0
|
||||
mid = 29
|
||||
|
||||
@staticmethod
|
||||
def is_published() -> bool:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def wait_for_publish(timeout: float) -> bool:
|
||||
raise AssertionError(
|
||||
f"wait_for_publish({timeout}) must not be called"
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
self.client,
|
||||
"publish",
|
||||
return_value=QueuedPublishResult(),
|
||||
):
|
||||
publication = self.transport.publish(
|
||||
CanFrame(
|
||||
arbitration_id=0x431,
|
||||
data=bytes.fromhex("10 00 00 00 00 00 00 00"),
|
||||
channel=1,
|
||||
is_fd=True,
|
||||
bitrate_switch=True,
|
||||
),
|
||||
command="shade position 20",
|
||||
)
|
||||
|
||||
self.assertEqual(publication["delivery_status"], "queued")
|
||||
self.assertEqual(publication["message_id"], 29)
|
||||
|
||||
def test_can1_whitelist_publishes_rear_lock_candidate_with_debug_command(self) -> None:
|
||||
bus = self.transport.bus(channel=1)
|
||||
bus.set_debug_command("door rear lock")
|
||||
@@ -717,6 +758,96 @@ class MqttCanTransportTests(unittest.TestCase):
|
||||
)
|
||||
)
|
||||
|
||||
def test_wireless_fixed_controls_and_shade_preset_never_wait_for_state(
|
||||
self,
|
||||
) -> None:
|
||||
service = SeatDemoService(REPOSITORY_ROOT)
|
||||
service._vehicle_mqtt_transport = self.transport
|
||||
service.vehicle_mqtt_settings = SimpleNamespace(feedback_timeout_ms=1_500)
|
||||
runtime = NativeCanRuntime(REPOSITORY_ROOT)
|
||||
actions_by_domain = {
|
||||
"climate": [
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_temperature",
|
||||
"target": "driver",
|
||||
"operation": "set",
|
||||
"parameters": {"temperature_c": 24},
|
||||
},
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_fan_level",
|
||||
"target": "vehicle",
|
||||
"operation": "set",
|
||||
"parameters": {"level": 2},
|
||||
},
|
||||
],
|
||||
"roof": [
|
||||
{
|
||||
"domain": "roof",
|
||||
"capability": "roof.set_shade_motion",
|
||||
"target": "vehicle",
|
||||
"operation": "set",
|
||||
"parameters": {"motion": "open"},
|
||||
}
|
||||
],
|
||||
"ambient_light": [
|
||||
{
|
||||
"domain": "ambient_light",
|
||||
"capability": "ambient_light.set_power",
|
||||
"target": "vehicle",
|
||||
"operation": "set",
|
||||
"parameters": {"state": "on"},
|
||||
}
|
||||
],
|
||||
}
|
||||
configs = {
|
||||
f"native_{domain}": runtime.build_function_config(
|
||||
domain,
|
||||
actions,
|
||||
f"req-wireless-no-wait-{domain}",
|
||||
"local_rules",
|
||||
)
|
||||
for domain, actions in actions_by_domain.items()
|
||||
}
|
||||
plans = {key: runtime.compile(config) for key, config in configs.items()}
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
self.transport,
|
||||
"_receive_after",
|
||||
side_effect=AssertionError(
|
||||
"wireless fixed commands must not wait on can/state"
|
||||
),
|
||||
) as receive_after,
|
||||
patch("cognitive_agent.application.native_can_runtime.time.sleep"),
|
||||
):
|
||||
reports = service._execute_orchestrated_reports(
|
||||
configs,
|
||||
plans,
|
||||
set(configs),
|
||||
{},
|
||||
hardware_request=True,
|
||||
simulated_hardware_request=False,
|
||||
simulation_fault="normal",
|
||||
control_transport="wireless_mqtt",
|
||||
)
|
||||
|
||||
receive_after.assert_not_called()
|
||||
self.assertEqual(len(self.client.published), 10)
|
||||
self.assertTrue(
|
||||
all(
|
||||
report["spec"]["status"] == "succeeded"
|
||||
for report in reports.values()
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
all(
|
||||
json.loads(payload)["expectResponse"] is False
|
||||
for _topic, payload, _qos, _retain in self.client.published
|
||||
)
|
||||
)
|
||||
|
||||
def test_model_added_nonwhitelisted_action_does_not_block_whitelisted_publish(
|
||||
self,
|
||||
) -> None:
|
||||
|
||||
@@ -261,6 +261,7 @@ def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands()
|
||||
"capability": "seat.set_backrest_recline",
|
||||
"target": {"zone": "front_right"},
|
||||
"parameters": {"position_pct": 36, "angle_deg": 120},
|
||||
"parse_source": "local_rule",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -272,6 +273,7 @@ def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands()
|
||||
"capability": "climate.set_temperature",
|
||||
"target": {"zone": "driver"},
|
||||
"parameters": {"temperature_c": 24},
|
||||
"parse_source": "cloud_supplement",
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -300,11 +302,13 @@ def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands()
|
||||
"category": "副驾座椅靠背调节",
|
||||
"command": "seat passenger backrest position 36",
|
||||
"frame": "0x6A0 · C7 <SEQ> 64 02 68 01 00 00",
|
||||
"parseSource": "local_rule",
|
||||
},
|
||||
{
|
||||
"channel": "CAN1",
|
||||
"category": "空调温度",
|
||||
"command": "ac temp driver 24",
|
||||
"parseSource": "cloud_supplement",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from cognitive_agent.application.seat_service import SeatDemoService
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _service(tmp_path: Path) -> SeatDemoService:
|
||||
service = SeatDemoService(ROOT)
|
||||
service.artifact_root = tmp_path / "artifacts"
|
||||
service.openai.api_key = "offline-two-stage-test-key"
|
||||
return service
|
||||
|
||||
|
||||
def _operations(result: dict) -> list[dict]:
|
||||
return [
|
||||
operation
|
||||
for config in result.get("function_configs", {}).values()
|
||||
for operation in config.get("spec", {}).get("operations", [])
|
||||
]
|
||||
|
||||
|
||||
def test_preview_validates_local_controls_without_calling_cloud(tmp_path: Path) -> None:
|
||||
service = _service(tmp_path)
|
||||
validated_events: list[dict] = []
|
||||
|
||||
with patch.object(
|
||||
service.openai,
|
||||
"interpret",
|
||||
side_effect=AssertionError("preview must not call hosted parsing"),
|
||||
) as interpret:
|
||||
result = service.preview_native_can(
|
||||
"空调温度设置为24度,风量设置为2档,播放音乐",
|
||||
provider="openai",
|
||||
conversation_id="conv-two-stage-local-preview",
|
||||
execution_mode="dry_run",
|
||||
on_commands_validated=validated_events.append,
|
||||
)
|
||||
|
||||
interpret.assert_not_called()
|
||||
assert result["phase"] == "deterministic_preview"
|
||||
assert result["status"] == "succeeded"
|
||||
assert result["supplement_required"] is True
|
||||
assert result["unresolved_clauses"] == ["播放音乐"]
|
||||
assert [item["capability"] for item in _operations(result)] == [
|
||||
"climate.set_temperature",
|
||||
"climate.set_fan_level",
|
||||
]
|
||||
assert {item["parse_source"] for item in _operations(result)} == {
|
||||
"local_rule"
|
||||
}
|
||||
assert len(validated_events) == 1
|
||||
assert validated_events[0]["phase"] == "deterministic_commands_validated"
|
||||
assert validated_events[0]["status"] == "processing"
|
||||
assert validated_events[0]["execution_report"] is None
|
||||
assert _operations(validated_events[0]) == _operations(result)
|
||||
assert validated_events[0]["command_plans"]
|
||||
|
||||
|
||||
def test_fully_local_cabin_request_streams_all_commands_without_supplement(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service = _service(tmp_path)
|
||||
validated_events: list[dict] = []
|
||||
|
||||
with patch.object(
|
||||
service.openai,
|
||||
"interpret",
|
||||
side_effect=AssertionError("fully local request must not call GPT"),
|
||||
):
|
||||
result = service.preview_native_can(
|
||||
"空调24度、风量2档,遮阳帘打开30%,打开氛围灯",
|
||||
provider="openai",
|
||||
conversation_id="conv-two-stage-all-local",
|
||||
execution_mode="dry_run",
|
||||
on_commands_validated=validated_events.append,
|
||||
)
|
||||
|
||||
operations = _operations(result)
|
||||
assert result["supplement_required"] is False
|
||||
assert [item["capability"] for item in operations] == [
|
||||
"climate.set_temperature",
|
||||
"climate.set_fan_level",
|
||||
"roof.set_shade_position",
|
||||
"ambient_light.set_power",
|
||||
]
|
||||
assert operations[2]["parameters"]["position_pct"] == 30
|
||||
assert {item["parse_source"] for item in operations} == {"local_rule"}
|
||||
assert len(validated_events) == 1
|
||||
assert _operations(validated_events[0]) == operations
|
||||
|
||||
|
||||
def test_supplement_receives_only_unresolved_text_and_deduplicates_local_control(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service = _service(tmp_path)
|
||||
preview = service.preview_native_can(
|
||||
"观影时空调温度设置为24度并播放音乐",
|
||||
provider="openai",
|
||||
conversation_id="conv-two-stage-deduplicate",
|
||||
execution_mode="dry_run",
|
||||
)
|
||||
assert [item["capability"] for item in _operations(preview)] == [
|
||||
"climate.set_temperature"
|
||||
]
|
||||
|
||||
model_output = {
|
||||
"detected_domains": ["climate", "ambient_light"],
|
||||
"requests": [
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_temperature",
|
||||
"target_scope": "driver",
|
||||
"operation": "set",
|
||||
"duration_ms": None,
|
||||
"parameters": [{"name": "temperature_c", "value": 24}],
|
||||
"reason": "模型重复给出了本地温度动作。",
|
||||
},
|
||||
{
|
||||
"domain": "ambient_light",
|
||||
"capability": "ambient_light.set_power",
|
||||
"target_scope": "vehicle",
|
||||
"operation": "set",
|
||||
"duration_ms": None,
|
||||
"parameters": [{"name": "state", "value": "on"}],
|
||||
"reason": "模型补充观影氛围灯。",
|
||||
},
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["仅补充本地规则未覆盖的观影含义。"],
|
||||
"confidence": 0.9,
|
||||
}
|
||||
with patch.object(
|
||||
service.openai,
|
||||
"interpret",
|
||||
return_value=model_output,
|
||||
) as interpret:
|
||||
result = service.supplement_native_can(
|
||||
preview["request_id"],
|
||||
provider="openai",
|
||||
)
|
||||
|
||||
assert interpret.call_args.args[0] == "观影时空调温度设置为24度并播放音乐"
|
||||
operations = _operations(result)
|
||||
assert [item["capability"] for item in operations].count(
|
||||
"climate.set_temperature"
|
||||
) == 1
|
||||
assert [item["capability"] for item in operations].count(
|
||||
"ambient_light.set_power"
|
||||
) == 1
|
||||
assert {
|
||||
item["capability"]: item["parse_source"] for item in operations
|
||||
} == {
|
||||
"climate.set_temperature": "local_rule",
|
||||
"ambient_light.set_power": "cloud_supplement",
|
||||
}
|
||||
cloud_results = (
|
||||
result["phase_execution_reports"]["cloud_supplement"] or {}
|
||||
).get("spec", {}).get("results", [])
|
||||
assert len(cloud_results) == 1
|
||||
|
||||
|
||||
def test_frontend_two_stage_contract_and_source_badges() -> None:
|
||||
app = (ROOT / "apps" / "web-console" / "app.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
console = (ROOT / "apps" / "web-console" / "console-v2.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
styles = (ROOT / "apps" / "web-console" / "console-v2.css").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
server = (ROOT / "src" / "cognitive_agent" / "server.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert '"/api/requests/preview"' in app
|
||||
assert "`/api/requests/${preview.request_id}/supplement`" in app
|
||||
assert "renderResult(preview);" in app
|
||||
assert 'Accept: "application/x-ndjson"' in app
|
||||
assert '"cognitive-agent:validated-can-commands"' in app
|
||||
assert "本地规则命令已执行;云端补充失败" in app
|
||||
assert 'class="v2-can-command-source"' in console
|
||||
assert 'item.parseSource === "local_rule" ? "本地规则" : "云端模型"' in console
|
||||
assert '"cognitive-agent:validated-can-commands"' in console
|
||||
assert '.v2-can-command-source[data-parse-source="local_rule"]' in styles
|
||||
assert 'path not in {"/api/requests", "/api/requests/preview"}' in server
|
||||
assert '"type": "validated_commands"' in server
|
||||
Reference in New Issue
Block a user