1006 lines
41 KiB
Python
Executable File
1006 lines
41 KiB
Python
Executable File
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
|
from cognitive_agent.infrastructure.llm.openai_responses import OpenAIIntentError
|
|
|
|
|
|
class SeatServiceIntegrationTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.memory_environment = patch.dict(
|
|
os.environ,
|
|
{
|
|
"COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH": str(
|
|
REPOSITORY_ROOT / "tests" / "fixtures" / "driver_seat_memory.json"
|
|
)
|
|
},
|
|
)
|
|
self.memory_environment.start()
|
|
self.addCleanup(self.memory_environment.stop)
|
|
self.service = SeatDemoService(REPOSITORY_ROOT)
|
|
self.temporary_directory = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temporary_directory.cleanup)
|
|
self.service.artifact_root = Path(self.temporary_directory.name)
|
|
|
|
def test_supported_request_completes_full_dry_run(self) -> None:
|
|
result = self.service.process(
|
|
"打开副驾座椅通风,同时关闭副驾座椅加热", provider="local"
|
|
)
|
|
|
|
self.assertEqual(result["status"], "succeeded")
|
|
self.assertEqual(len(result["function_config"]["spec"]["operations"]), 2)
|
|
telemetry = result["execution_report"]["spec"]["results"][0]["telemetry"]
|
|
self.assertEqual(telemetry["command_frame"]["data_hex"], "00 90")
|
|
self.assertTrue(Path(result["artifact_path"]).is_absolute())
|
|
self.assertTrue(
|
|
(self.service.artifact_root / result["request_id"] / "result.json").is_file()
|
|
)
|
|
for event in result["trace_events"]:
|
|
self.assertEqual(self.service.contracts.validate(event), [])
|
|
|
|
def test_explicit_legrest_height_does_not_execute_the_seat_height_axis(self) -> None:
|
|
result = self.service.preview_native_can(
|
|
"副驾腿托高度调到50%",
|
|
provider="local",
|
|
execution_mode="dry_run",
|
|
conversation_id="conv-legrest-height-regression",
|
|
)
|
|
|
|
self.assertEqual(result["status"], "succeeded")
|
|
actions = result["intent_ir"]["spec"]["actions"]
|
|
self.assertEqual(len(actions), 1)
|
|
self.assertEqual(actions[0]["capability"], "seat.set_legrest_height")
|
|
self.assertEqual(actions[0]["parameters"], {"position_pct": 50})
|
|
self.assertEqual(
|
|
result["actuator_state"]["passenger"]["position"]["legrest_height"][
|
|
"position_pct"
|
|
],
|
|
50,
|
|
)
|
|
|
|
def test_dynamic_mode_runs_until_explicit_natural_language_stop(self) -> None:
|
|
conversation_id = "conv-dynamic-stop"
|
|
started = self.service.process(
|
|
"\u5f00\u542f\u52a8\u6001\u6447\u7bee\u6a21\u5f0f",
|
|
provider="local",
|
|
conversation_id=conversation_id,
|
|
vehicle_id="denza_n7",
|
|
)
|
|
stopped = self.service.process(
|
|
"\u5173\u95ed\u52a8\u6001\u6447\u7bee\u6a21\u5f0f",
|
|
provider="local",
|
|
conversation_id=conversation_id,
|
|
vehicle_id="denza_n7",
|
|
)
|
|
|
|
self.assertEqual(started["status"], "succeeded")
|
|
self.assertEqual(
|
|
started["actuator_state"]["passenger"]["dynamic_mode"],
|
|
{
|
|
"mode_id": 2,
|
|
"name": "\u52a8\u6001\u6447\u7bee\u6a21\u5f0f",
|
|
"state": "running",
|
|
"repeat": "until_stopped_or_timeout",
|
|
"max_runtime_ms": 600_000,
|
|
},
|
|
)
|
|
self.assertEqual(stopped["status"], "succeeded")
|
|
operation = stopped["function_config"]["spec"]["operations"][0]
|
|
self.assertEqual(operation["capability"], "seat.stop_dynamic_mode")
|
|
self.assertEqual(operation["parameters"], {})
|
|
self.assertEqual(
|
|
stopped["actuator_state"]["passenger"]["dynamic_mode"]["state"],
|
|
"idle",
|
|
)
|
|
|
|
def test_gear_request_is_preserved_and_simulated(self) -> None:
|
|
result = self.service.process("副驾座椅加热开到三档", provider="local")
|
|
|
|
self.assertEqual(result["status"], "succeeded")
|
|
operation = result["function_config"]["spec"]["operations"][0]
|
|
self.assertEqual(operation["parameters"]["level"], 3)
|
|
telemetry = result["execution_report"]["spec"]["results"][0]["telemetry"]
|
|
self.assertEqual(telemetry["effective_level"], 3)
|
|
self.assertTrue(telemetry["level_command_simulated"])
|
|
self.assertIn(
|
|
"LEVEL_COMMAND_SIMULATED", {issue["code"] for issue in result["validation"]}
|
|
)
|
|
|
|
def test_consecutive_feedback_uses_previous_successful_state(self) -> None:
|
|
conversation_id = "conv-test-context"
|
|
|
|
first = self.service.process(
|
|
"副驾有点热,有点闷",
|
|
provider="local",
|
|
conversation_id=conversation_id,
|
|
)
|
|
second = self.service.process(
|
|
"副驾还有点闷",
|
|
provider="local",
|
|
conversation_id=conversation_id,
|
|
)
|
|
|
|
self.assertEqual(first["status"], "succeeded")
|
|
self.assertEqual(first["actuator_state"]["passenger"]["ventilation"]["level"], 1)
|
|
self.assertEqual(second["status"], "succeeded")
|
|
self.assertEqual(second["conversation_turn"], 2)
|
|
self.assertEqual(second["actuator_state"]["passenger"]["ventilation"]["level"], 2)
|
|
operation = second["function_config"]["spec"]["operations"][0]
|
|
self.assertEqual(operation["parameters"], {"state": "on", "level": 2})
|
|
self.assertIn("提升到2档", "".join(second["decision_summary"]))
|
|
|
|
def test_four_turn_causal_feedback_sequence(self) -> None:
|
|
conversation_id = "conv-causal-sequence"
|
|
utterances = [
|
|
"我有点热",
|
|
"我有点冷",
|
|
"我还有点冷",
|
|
"副驾吹的我有点冷",
|
|
]
|
|
|
|
first, second, third, fourth = [
|
|
self.service.process(text, "local", conversation_id) for text in utterances
|
|
]
|
|
|
|
self.assertEqual(
|
|
first["function_config"]["spec"]["operations"][0]["parameters"],
|
|
{"state": "on", "level": 1},
|
|
)
|
|
second_operation = second["function_config"]["spec"]["operations"][0]
|
|
self.assertEqual(second_operation["capability"], "seat.set_ventilation")
|
|
self.assertEqual(second_operation["parameters"], {"state": "off"})
|
|
third_operation = third["function_config"]["spec"]["operations"][0]
|
|
self.assertEqual(third_operation["capability"], "seat.set_heating")
|
|
self.assertEqual(third_operation["parameters"], {"state": "on", "level": 1})
|
|
fourth_operation = fourth["function_config"]["spec"]["operations"][0]
|
|
self.assertEqual(fourth_operation["target"]["instance"], "passenger")
|
|
self.assertEqual(fourth_operation["capability"], "seat.set_ventilation")
|
|
self.assertEqual(fourth_operation["parameters"], {"state": "off"})
|
|
self.assertEqual(
|
|
fourth["actuator_state"]["passenger"]["heating"],
|
|
{"state": "on", "level": 1},
|
|
)
|
|
self.assertEqual(fourth["conversation_turn"], 4)
|
|
|
|
def test_compound_cold_and_stuffy_feedback_generates_balanced_dry_run(self) -> None:
|
|
result = self.service.process(
|
|
"副驾有点冷、有点闷",
|
|
provider="local",
|
|
conversation_id="conv-compound-comfort",
|
|
)
|
|
|
|
self.assertEqual(result["status"], "succeeded")
|
|
operations = result["function_config"]["spec"]["operations"]
|
|
self.assertEqual(
|
|
{operation["capability"] for operation in operations},
|
|
{"seat.set_heating", "seat.set_ventilation"},
|
|
)
|
|
command = result["command_plan"]["spec"]["commands"][0]
|
|
self.assertEqual(
|
|
command["signals"],
|
|
{
|
|
"Front_Passenger_Seat_Heating_S": 1,
|
|
"Front_Passenger_Seat_Ventilation_S": 1,
|
|
},
|
|
)
|
|
self.assertIn(
|
|
"COMBINED_THERMAL_SIMULATED",
|
|
{issue["code"] for issue in result["validation"]},
|
|
)
|
|
|
|
def test_vehicle_scope_is_routed_away_from_seat_domain(self) -> None:
|
|
result = self.service.process(
|
|
"车上太闷了",
|
|
provider="local",
|
|
conversation_id="conv-vehicle-scope",
|
|
)
|
|
|
|
self.assertEqual(result["status"], "needs_confirmation")
|
|
self.assertEqual(result["intent_ir"]["spec"]["detected_domains"], ["climate"])
|
|
self.assertIsNone(result["function_config"])
|
|
self.assertIsNone(result["command_plan"])
|
|
self.assertEqual(result["actuator_state"]["passenger"]["ventilation"]["state"], "off")
|
|
route = next(event for event in result["trace_events"] if event["stage"] == "domain_route")
|
|
self.assertEqual(route["details"]["domains"], ["climate"])
|
|
|
|
def test_local_rules_precede_openai_for_unscoped_driver_console_command(self) -> None:
|
|
model_output = {
|
|
"detected_domains": ["seat"],
|
|
"requests": [
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_ventilation",
|
|
"target_scope": "passenger",
|
|
"operation": "deactivate",
|
|
"parameters": [{"name": "state", "value": "off"}],
|
|
"reason": "model-owned-plan",
|
|
}
|
|
],
|
|
"ambiguities": [],
|
|
"decision_summary": ["model-owned-plan"],
|
|
"confidence": 0.91,
|
|
}
|
|
context = self.service.conversations.snapshot("conv-model-owned-plan")
|
|
self.service.openai.api_key = "test-key"
|
|
|
|
with patch.object(self.service.openai, "interpret", return_value=model_output) as interpret:
|
|
parsed, provider, notice = self.service._interpret(
|
|
"关闭座椅通风", "openai", context
|
|
)
|
|
|
|
self.assertTrue(
|
|
any("默认选择主驾座椅" in item for item in parsed["decision_summary"])
|
|
)
|
|
self.assertEqual(len(parsed["model_requests"]), 1)
|
|
self.assertEqual(
|
|
parsed["model_requests"][0]["capability"],
|
|
"seat.set_driver_ventilation_native",
|
|
)
|
|
self.assertEqual(parsed["actions"], [])
|
|
self.assertEqual(len(parsed["deferred_requests"]), 1)
|
|
deferred = parsed["deferred_requests"][0]
|
|
self.assertEqual(deferred["capability"], "seat.set_driver_ventilation_native")
|
|
self.assertEqual(deferred["target_scope"], "driver")
|
|
self.assertEqual(deferred["parameters"], {"state": "off"})
|
|
self.assertEqual(deferred["execution_backend"], "native_can")
|
|
self.assertEqual(provider, "local_rules")
|
|
self.assertIsNone(notice)
|
|
interpret.assert_not_called()
|
|
|
|
def test_explicit_gpt_failure_falls_back_to_local_rules(self) -> None:
|
|
self.service.openai.api_key = "test-key"
|
|
|
|
with patch.object(
|
|
self.service.openai,
|
|
"interpret",
|
|
side_effect=OpenAIIntentError("invalid_api_key"),
|
|
):
|
|
result = self.service.process(
|
|
"打开副驾座椅通风,同时播放音乐",
|
|
"openai",
|
|
"conv-explicit-gpt-fallback",
|
|
)
|
|
|
|
self.assertEqual(result["status"], "succeeded")
|
|
self.assertEqual(result["provider_used"], "local_rules_fallback")
|
|
self.assertEqual(
|
|
result["function_config"]["spec"]["operations"][0]["target"]["instance"],
|
|
"passenger",
|
|
)
|
|
self.assertTrue(
|
|
any("已降级到本地规则解析" in item for item in result["decision_summary"])
|
|
)
|
|
|
|
def test_local_first_calls_model_with_only_uncovered_clauses(self) -> None:
|
|
self.service.openai.api_key = "test-key"
|
|
model_output = {
|
|
"detected_domains": ["ambient_light"],
|
|
"requests": [
|
|
{
|
|
"domain": "ambient_light",
|
|
"capability": "ambient_light.set_power",
|
|
"target_scope": "vehicle",
|
|
"operation": "set",
|
|
"parameters": [{"name": "state", "value": "on"}],
|
|
"reason": "播放场景需要氛围灯。",
|
|
}
|
|
],
|
|
"ambiguities": [],
|
|
"decision_summary": ["仅补充本地规则未覆盖的播放场景。"],
|
|
"confidence": 0.9,
|
|
}
|
|
context = self.service.conversations.snapshot("conv-clause-fallback")
|
|
with patch.object(
|
|
self.service.openai, "interpret", return_value=model_output
|
|
) as interpret:
|
|
parsed, provider, notice = self.service._interpret(
|
|
"空调温度设置为25度,播放音乐",
|
|
"openai",
|
|
context,
|
|
)
|
|
|
|
interpret.assert_called_once_with(
|
|
"播放音乐",
|
|
{
|
|
**context,
|
|
"natural_language_routing": {
|
|
"original_utterance": "空调温度设置为25度,播放音乐",
|
|
"parsed_clauses": ["空调温度设置为25度"],
|
|
"resolved_clauses": ["空调温度设置为25度"],
|
|
"unresolved_clauses": ["播放音乐"],
|
|
"cloud_input_utterance": "播放音乐",
|
|
},
|
|
},
|
|
self.service.capability_context,
|
|
)
|
|
self.assertNotIn("natural_language_routing", context)
|
|
self.assertEqual(
|
|
provider, f"local_rules+openai:{self.service.openai.model}"
|
|
)
|
|
self.assertIn("仅未覆盖", str(notice))
|
|
self.assertEqual(
|
|
[item["capability"] for item in parsed["native_can_actions"]],
|
|
["climate.set_temperature"],
|
|
)
|
|
self.assertEqual(
|
|
[item["capability"] for item in parsed["domain_actions"]],
|
|
["ambient_light.set_power"],
|
|
)
|
|
|
|
def test_local_first_compiles_memory_temperature_and_fan_without_model(self) -> None:
|
|
self.service.openai.api_key = "test-key"
|
|
with patch.object(self.service.openai, "interpret") as interpret:
|
|
result = self.service.process(
|
|
"主驾座椅调整到记忆位置3,空调温度设置为25度,风速设置为1档",
|
|
"openai",
|
|
"conv-local-memory-climate",
|
|
)
|
|
|
|
interpret.assert_not_called()
|
|
self.assertEqual(result["status"], "succeeded")
|
|
self.assertEqual(result["provider_used"], "local_rules")
|
|
self.assertEqual(
|
|
set(result["function_configs"]), {"native_seat", "native_climate"}
|
|
)
|
|
commands = [
|
|
command["signals"]["debug_command"]
|
|
for plan in result["command_plans"].values()
|
|
for command in plan["spec"]["commands"]
|
|
]
|
|
self.assertEqual(
|
|
commands,
|
|
["seat driver position 3", "ac temp driver 25", "ac fan 1"],
|
|
)
|
|
|
|
def test_model_climate_request_completes_domain_loop(self) -> None:
|
|
model_output = {
|
|
"detected_domains": ["climate"],
|
|
"requests": [
|
|
{
|
|
"domain": "climate",
|
|
"capability": "climate.set_fan_level",
|
|
"target_scope": "vehicle",
|
|
"operation": "increase",
|
|
"parameters": [{"name": "level", "value": 3}],
|
|
"reason": "Increase cabin airflow.",
|
|
}
|
|
],
|
|
"ambiguities": [],
|
|
"decision_summary": ["整车舱风量增加需求。"],
|
|
"confidence": 0.94,
|
|
}
|
|
self.service.openai.api_key = "test-key"
|
|
|
|
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
|
result = self.service.process(
|
|
"车上太闷了",
|
|
"openai",
|
|
"conv-model-deferred",
|
|
)
|
|
|
|
self.assertEqual(result["status"], "succeeded")
|
|
self.assertEqual(result["model_requests"], model_output["requests"])
|
|
self.assertEqual(result["deferred_requests"], [])
|
|
self.assertIn("climate", result["function_configs"])
|
|
request = result["intent_ir"]["spec"]["requests"][0]
|
|
self.assertEqual(request["domain"], "climate")
|
|
self.assertEqual(request["parameters"]["capability"], "climate.set_fan_level")
|
|
|
|
def test_climate_and_seat_requests_generate_scene(self) -> None:
|
|
model_output = {
|
|
"detected_domains": ["seat", "climate"],
|
|
"requests": [
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_heating",
|
|
"target_scope": "passenger",
|
|
"operation": "activate",
|
|
"parameters": [
|
|
{"name": "state", "value": "on"},
|
|
{"name": "level", "value": 1},
|
|
],
|
|
"reason": "Warm the passenger seat.",
|
|
},
|
|
{
|
|
"domain": "climate",
|
|
"capability": "climate.set_fan_level",
|
|
"target_scope": "vehicle",
|
|
"operation": "set",
|
|
"parameters": [{"name": "level", "value": 2}],
|
|
"reason": "Improve cabin airflow.",
|
|
},
|
|
],
|
|
"ambiguities": [],
|
|
"decision_summary": ["座椅加热与整车空调风量并行执行。"],
|
|
"confidence": 0.9,
|
|
}
|
|
self.service.openai.api_key = "test-key"
|
|
|
|
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
|
result = self.service.process(
|
|
"打开副驾座椅加热,播放音乐并把车内风量调舒适",
|
|
"openai",
|
|
"conv-model-partial",
|
|
)
|
|
|
|
self.assertEqual(result["status"], "succeeded")
|
|
self.assertIsNotNone(result["execution_report"])
|
|
self.assertEqual(
|
|
result["actuator_state"]["passenger"]["heating"],
|
|
{"state": "on", "level": 1},
|
|
)
|
|
self.assertEqual(result["domain_state"]["climate"]["fan_level"], 2)
|
|
self.assertEqual(
|
|
result["provider_used"],
|
|
f"local_rules+openai:{self.service.openai.model}",
|
|
)
|
|
self.assertEqual(set(result["function_configs"]), {"seat", "climate"})
|
|
self.assertEqual(result["scene_config"]["kind"], "SceneConfig")
|
|
|
|
def test_model_motion_requests_complete_control_and_feedback_loop(self) -> None:
|
|
model_output = {
|
|
"detected_domains": ["seat"],
|
|
"requests": [
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_longitudinal_position",
|
|
"target_scope": "passenger",
|
|
"operation": "set",
|
|
"parameters": [{"name": "position_pct", "value": 35}],
|
|
"reason": "Move the passenger seat backward from its current midpoint.",
|
|
},
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_backrest_recline",
|
|
"target_scope": "passenger",
|
|
"operation": "set",
|
|
"parameters": [{"name": "position_pct", "value": 60}],
|
|
"reason": "Recline the passenger backrest.",
|
|
},
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_legrest_height",
|
|
"target_scope": "passenger",
|
|
"operation": "set",
|
|
"parameters": [{"name": "position_pct", "value": 65}],
|
|
"reason": "Raise the passenger leg rest.",
|
|
},
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_legrest_extension",
|
|
"target_scope": "passenger",
|
|
"operation": "set",
|
|
"parameters": [{"name": "position_pct", "value": 70}],
|
|
"reason": "Extend the passenger leg rest.",
|
|
},
|
|
],
|
|
"ambiguities": [],
|
|
"decision_summary": ["四个电机轴使用绝对位置目标。"],
|
|
"confidence": 0.93,
|
|
}
|
|
self.service.openai.api_key = "test-key"
|
|
|
|
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
|
result = self.service.process(
|
|
"副驾往后一点并把靠背躺下,腿托升起并伸长",
|
|
"openai",
|
|
"conv-model-motion",
|
|
)
|
|
|
|
self.assertEqual(result["status"], "succeeded")
|
|
self.assertEqual(len(result["function_config"]["spec"]["operations"]), 4)
|
|
self.assertEqual(len(result["command_plan"]["spec"]["commands"]), 4)
|
|
self.assertEqual(len(result["execution_report"]["spec"]["results"]), 4)
|
|
passenger = result["actuator_state"]["passenger"]["position"]
|
|
self.assertEqual(passenger["longitudinal"]["position_pct"], 35)
|
|
self.assertEqual(passenger["backrest_recline"]["position_pct"], 60)
|
|
self.assertEqual(passenger["legrest_height"]["position_pct"], 65)
|
|
self.assertEqual(passenger["legrest_extension"]["position_pct"], 70)
|
|
telemetry = result["execution_report"]["spec"]["results"][0]["telemetry"]
|
|
self.assertEqual(telemetry["start_position_pct"], 50)
|
|
self.assertEqual(telemetry["effective_position_pct"], 35)
|
|
self.assertEqual(telemetry["feedback_frame"]["frame_id"], "0x559")
|
|
self.assertEqual(telemetry["fault_mask"], 0)
|
|
codes = {issue["code"] for issue in result["validation"]}
|
|
self.assertIn("SYNTHETIC_MOTION_CAN", codes)
|
|
|
|
def test_motion_follow_up_state_is_supplied_to_model(self) -> None:
|
|
def output(position: int) -> dict[str, object]:
|
|
return {
|
|
"detected_domains": ["seat"],
|
|
"requests": [
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_longitudinal_position",
|
|
"target_scope": "passenger",
|
|
"operation": "increase",
|
|
"parameters": [{"name": "position_pct", "value": position}],
|
|
"reason": "Model resolved the relative request.",
|
|
}
|
|
],
|
|
"ambiguities": [],
|
|
"decision_summary": [f"Move to {position}%."],
|
|
"confidence": 0.9,
|
|
}
|
|
|
|
self.service.openai.api_key = "test-key"
|
|
with patch.object(
|
|
self.service.openai,
|
|
"interpret",
|
|
side_effect=[output(60), output(70)],
|
|
) as interpret:
|
|
first = self.service.process(
|
|
"副驾往后一点", "openai", "conv-motion-follow-up"
|
|
)
|
|
second = self.service.process(
|
|
"还是靠前,再往后一点", "openai", "conv-motion-follow-up"
|
|
)
|
|
|
|
self.assertEqual(
|
|
first["actuator_state"]["passenger"]["position"]["longitudinal"][
|
|
"position_pct"
|
|
],
|
|
60,
|
|
)
|
|
self.assertEqual(
|
|
second["actuator_state"]["passenger"]["position"]["longitudinal"][
|
|
"position_pct"
|
|
],
|
|
70,
|
|
)
|
|
second_context = interpret.call_args_list[1].args[1]
|
|
self.assertEqual(
|
|
second_context["devices"]["passenger"]["position"]["longitudinal"][
|
|
"position_pct"
|
|
],
|
|
60,
|
|
)
|
|
|
|
def test_model_relative_physical_motion_uses_vehicle_profile(self) -> None:
|
|
model_output = {
|
|
"detected_domains": ["seat"],
|
|
"requests": [
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_longitudinal_position",
|
|
"target_scope": "passenger",
|
|
"operation": "decrease",
|
|
"parameters": [{"name": "distance_mm", "value": 50}],
|
|
"reason": "Move forward by a physical distance.",
|
|
},
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_backrest_recline",
|
|
"target_scope": "passenger",
|
|
"operation": "increase",
|
|
"parameters": [{"name": "angle_delta_deg", "value": 10}],
|
|
"reason": "Recline by a relative physical angle.",
|
|
},
|
|
],
|
|
"ambiguities": [],
|
|
"decision_summary": ["使用车型量程把相对物理量换算为绝对百分比。"],
|
|
"confidence": 0.94,
|
|
}
|
|
self.service.openai.api_key = "test-key"
|
|
|
|
with patch.object(self.service.openai, "interpret", return_value=model_output) as interpret:
|
|
result = self.service.process(
|
|
"副驾水平向前移动50毫米,靠背往后10度",
|
|
"openai",
|
|
"conv-vehicle-profile-relative",
|
|
vehicle_id="denza_n7",
|
|
)
|
|
|
|
self.assertEqual(result["status"], "succeeded")
|
|
self.assertEqual(result["vehicle"]["id"], "denza_n7")
|
|
operations = result["function_config"]["spec"]["operations"]
|
|
self.assertEqual(operations[0]["parameters"], {"position_pct": 27})
|
|
self.assertEqual(
|
|
operations[1]["parameters"],
|
|
{"position_pct": 35, "angle_deg": 118.6},
|
|
)
|
|
backrest_motion = operations[1]["motion"]
|
|
self.assertEqual(backrest_motion["mode"], "relative")
|
|
self.assertEqual(backrest_motion["direction"], "increase")
|
|
self.assertEqual(backrest_motion["start_position_pct"], 20)
|
|
self.assertEqual(backrest_motion["delta_position_pct"], 15)
|
|
self.assertEqual(backrest_motion["start_angle_deg"], 108.6)
|
|
self.assertEqual(backrest_motion["delta_angle_deg"], 10)
|
|
context_sent_to_model = interpret.call_args.args[1]
|
|
self.assertEqual(context_sent_to_model["vehicle"]["id"], "denza_n7")
|
|
self.assertEqual(
|
|
context_sent_to_model["devices"]["passenger"]["position"][
|
|
"backrest_recline"
|
|
]["angle_deg"],
|
|
108.6,
|
|
)
|
|
|
|
def test_model_timed_motion_survives_endpoint_resolution_and_reaches_compiler(self) -> None:
|
|
self.service.process(
|
|
"副驾座椅水平位置调到100%",
|
|
provider="local",
|
|
conversation_id="conv-model-timed",
|
|
)
|
|
model_output = {
|
|
"detected_domains": ["seat"],
|
|
"requests": [
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_longitudinal_position",
|
|
"target_scope": "passenger",
|
|
"operation": "increase",
|
|
"duration_ms": 3_000,
|
|
"parameters": [{"name": "position_pct", "value": 100}],
|
|
"reason": "Continue moving the rail backward for three seconds.",
|
|
},
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_legrest_height",
|
|
"target_scope": "passenger",
|
|
"operation": "increase",
|
|
"duration_ms": 3_000,
|
|
"parameters": [],
|
|
"reason": "Raise the leg rest for three seconds.",
|
|
},
|
|
],
|
|
"ambiguities": [],
|
|
"decision_summary": ["保留两个动作各自三秒的定时运动语义。"],
|
|
"confidence": 0.99,
|
|
}
|
|
self.service.openai.api_key = "test-key"
|
|
|
|
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
|
result = self.service.process(
|
|
"副驾座椅水平再后移动3秒,副驾腿托向上调3秒",
|
|
"openai",
|
|
"conv-model-timed",
|
|
vehicle_id="denza_n7",
|
|
)
|
|
|
|
self.assertEqual(result["status"], "succeeded")
|
|
operations = result["function_config"]["spec"]["operations"]
|
|
self.assertEqual(len(operations), 2)
|
|
by_capability = {item["capability"]: item for item in operations}
|
|
rail_motion = by_capability["seat.set_longitudinal_position"]["motion"]
|
|
self.assertEqual(rail_motion["mode"], "timed")
|
|
self.assertEqual(rail_motion["direction"], "increase")
|
|
self.assertEqual(rail_motion["start_position_pct"], 100)
|
|
self.assertEqual(rail_motion["delta_position_pct"], 0)
|
|
self.assertEqual(rail_motion["applied_duration_ms"], 3_000)
|
|
legrest_motion = by_capability["seat.set_legrest_height"]["motion"]
|
|
self.assertEqual(legrest_motion["mode"], "timed")
|
|
self.assertEqual(legrest_motion["direction"], "increase")
|
|
self.assertEqual(legrest_motion["applied_duration_ms"], 3_000)
|
|
|
|
def test_vehicle_profile_angle_limits_are_accepted(self) -> None:
|
|
model_output = {
|
|
"detected_domains": ["seat"],
|
|
"requests": [
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_backrest_recline",
|
|
"target_scope": "passenger",
|
|
"operation": "set",
|
|
"parameters": [{"name": "angle_deg", "value": 163}],
|
|
"reason": "Use the vehicle-specific maximum backrest angle.",
|
|
},
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_legrest_height",
|
|
"target_scope": "passenger",
|
|
"operation": "set",
|
|
"parameters": [{"name": "angle_deg", "value": 167}],
|
|
"reason": "Use the vehicle-specific maximum legrest angle.",
|
|
},
|
|
],
|
|
"ambiguities": [],
|
|
"decision_summary": ["车型角度边界按腾势 N7 配置执行。"],
|
|
"confidence": 0.95,
|
|
}
|
|
self.service.openai.api_key = "test-key"
|
|
|
|
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
|
result = self.service.process(
|
|
"副驾靠背到163度,腿托到167度",
|
|
"openai",
|
|
"conv-vehicle-angle-limits",
|
|
vehicle_id="denza_n7",
|
|
)
|
|
|
|
self.assertEqual(result["status"], "succeeded")
|
|
operations = result["function_config"]["spec"]["operations"]
|
|
self.assertEqual(operations[0]["parameters"], {"position_pct": 100, "angle_deg": 163})
|
|
self.assertEqual(operations[1]["parameters"], {"position_pct": 100, "angle_deg": 167})
|
|
|
|
def test_motion_adapter_does_not_resolve_missing_absolute_target(self) -> None:
|
|
model_output = {
|
|
"detected_domains": ["seat"],
|
|
"requests": [
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_longitudinal_position",
|
|
"target_scope": "passenger",
|
|
"operation": "decrease",
|
|
"parameters": [],
|
|
"reason": "No absolute target was produced.",
|
|
}
|
|
],
|
|
"ambiguities": [],
|
|
"decision_summary": ["Unresolved relative request."],
|
|
"confidence": 0.6,
|
|
}
|
|
self.service.openai.api_key = "test-key"
|
|
|
|
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
|
result = self.service.process(
|
|
"副驾往后一点", "openai", "conv-motion-missing-target"
|
|
)
|
|
|
|
self.assertEqual(result["status"], "needs_confirmation")
|
|
self.assertIsNone(result["function_config"])
|
|
self.assertIn(
|
|
"AMBIGUOUS_INTENT", {issue["code"] for issue in result["validation"]}
|
|
)
|
|
self.assertNotIn(
|
|
"NO_EXECUTABLE_OPERATION",
|
|
{issue["code"] for issue in result["validation"]},
|
|
)
|
|
self.assertNotIn(
|
|
"PUBLIC_SCHEMA", {issue["code"] for issue in result["validation"]}
|
|
)
|
|
|
|
def test_exact_angles_and_named_preset_complete_conversation_loop(self) -> None:
|
|
def plan(requests: list[dict[str, object]], summary: str) -> dict[str, object]:
|
|
return {
|
|
"detected_domains": ["seat"],
|
|
"requests": requests,
|
|
"ambiguities": [],
|
|
"decision_summary": [summary],
|
|
"confidence": 0.96,
|
|
}
|
|
|
|
def request(
|
|
capability: str,
|
|
operation: str,
|
|
parameters: list[dict[str, object]],
|
|
) -> dict[str, object]:
|
|
return {
|
|
"domain": "seat",
|
|
"capability": capability,
|
|
"target_scope": "passenger",
|
|
"operation": operation,
|
|
"parameters": parameters,
|
|
"reason": "Model resolved the user's posture goal against runtime context.",
|
|
}
|
|
|
|
first_plan = plan(
|
|
[
|
|
request(
|
|
"seat.set_backrest_recline",
|
|
"set",
|
|
[{"name": "angle_deg", "value": 128}],
|
|
),
|
|
request(
|
|
"seat.set_legrest_height",
|
|
"set",
|
|
[{"name": "angle_deg", "value": 133}],
|
|
),
|
|
],
|
|
"副驾靠背/坐垫夹角设为128度,坐垫/腿托夹角设为133度。",
|
|
)
|
|
recline_plan = plan(
|
|
[
|
|
request(
|
|
"seat.set_backrest_recline",
|
|
"increase",
|
|
[{"name": "angle_deg", "value": 133}],
|
|
)
|
|
],
|
|
"根据当前128度姿态,将副驾靠背继续后仰至133度。",
|
|
)
|
|
save_plan = plan(
|
|
[
|
|
request(
|
|
"seat.save_posture_preset",
|
|
"save",
|
|
[{"name": "preset_name", "value": "零重力模式"}],
|
|
)
|
|
],
|
|
"将副驾当前五轴姿态保存为零重力模式。",
|
|
)
|
|
leave_preset_plan = plan(
|
|
[
|
|
request(
|
|
"seat.set_backrest_recline",
|
|
"set",
|
|
[{"name": "angle_deg", "value": 105}],
|
|
)
|
|
],
|
|
"将副驾靠背恢复到105度用于验证预设回调。",
|
|
)
|
|
apply_plan = plan(
|
|
[
|
|
request(
|
|
"seat.apply_posture_preset",
|
|
"apply",
|
|
[{"name": "preset_name", "value": "零重力模式"}],
|
|
)
|
|
],
|
|
"调用已保存的副驾零重力模式。",
|
|
)
|
|
|
|
self.service.openai.api_key = "test-key"
|
|
conversation_id = "conv-angle-preset-loop"
|
|
with patch.object(
|
|
self.service.openai,
|
|
"interpret",
|
|
side_effect=[
|
|
first_plan,
|
|
recline_plan,
|
|
save_plan,
|
|
leave_preset_plan,
|
|
apply_plan,
|
|
],
|
|
) as interpret:
|
|
first = self.service.process(
|
|
"副驾调整为零重力模式,要求靠背座椅夹角128度,坐垫脚托夹角133度",
|
|
"openai",
|
|
conversation_id,
|
|
)
|
|
second = self.service.process(
|
|
"副驾靠背再向后一点", "openai", conversation_id
|
|
)
|
|
saved = self.service.process(
|
|
"将当前状态保存为零重力模式", "openai", conversation_id
|
|
)
|
|
changed = self.service.process(
|
|
"副驾靠背恢复直立一些", "openai", conversation_id
|
|
)
|
|
applied = self.service.process(
|
|
"副驾恢复到零重力模式", "openai", conversation_id
|
|
)
|
|
|
|
first_operations = first["function_config"]["spec"]["operations"]
|
|
self.assertEqual(
|
|
first_operations[0]["parameters"],
|
|
{"position_pct": 49, "angle_deg": 128},
|
|
)
|
|
self.assertEqual(
|
|
first_operations[1]["parameters"],
|
|
{"position_pct": 53, "angle_deg": 133},
|
|
)
|
|
self.assertEqual(
|
|
first["actuator_state"]["passenger"]["position"]["backrest_recline"][
|
|
"angle_deg"
|
|
],
|
|
128,
|
|
)
|
|
first_telemetry = first["execution_report"]["spec"]["results"][0]["telemetry"]
|
|
self.assertEqual(first_telemetry["requested_angle_deg"], 128)
|
|
self.assertEqual(first_telemetry["effective_position_pct"], 49)
|
|
|
|
self.assertEqual(
|
|
second["actuator_state"]["passenger"]["position"]["backrest_recline"][
|
|
"angle_deg"
|
|
],
|
|
133,
|
|
)
|
|
relative_motion = second["function_config"]["spec"]["operations"][0][
|
|
"motion"
|
|
]
|
|
self.assertEqual(relative_motion["mode"], "relative")
|
|
self.assertEqual(relative_motion["direction"], "increase")
|
|
self.assertEqual(relative_motion["start_position_pct"], 49)
|
|
self.assertEqual(relative_motion["delta_position_pct"], 7)
|
|
self.assertEqual(relative_motion["delta_angle_deg"], 5)
|
|
second_context = interpret.call_args_list[1].args[1]
|
|
self.assertEqual(
|
|
second_context["devices"]["passenger"]["position"]["backrest_recline"][
|
|
"angle_deg"
|
|
],
|
|
128,
|
|
)
|
|
|
|
self.assertEqual(saved["status"], "succeeded")
|
|
self.assertIsNone(saved["function_config"])
|
|
self.assertEqual(saved["preset_config"]["kind"], "FunctionPreset")
|
|
self.assertEqual(saved["preset_config"]["metadata"]["name"], "零重力模式")
|
|
self.assertFalse(
|
|
saved["execution_report"]["spec"]["results"][0]["telemetry"][
|
|
"can_command_emitted"
|
|
]
|
|
)
|
|
self.assertEqual(
|
|
saved["saved_presets"],
|
|
[{"name": "零重力模式", "target": "passenger"}],
|
|
)
|
|
preset_path = (
|
|
self.service.artifact_root
|
|
/ "user-presets"
|
|
/ f"{conversation_id}.json"
|
|
)
|
|
self.assertTrue(preset_path.is_file())
|
|
|
|
self.assertEqual(
|
|
changed["actuator_state"]["passenger"]["position"]["backrest_recline"][
|
|
"angle_deg"
|
|
],
|
|
105,
|
|
)
|
|
self.assertEqual(applied["status"], "succeeded")
|
|
restored = applied["actuator_state"]["passenger"]["position"]
|
|
self.assertEqual(restored["backrest_recline"]["angle_deg"], 133)
|
|
self.assertEqual(restored["legrest_height"]["angle_deg"], 133)
|
|
self.assertEqual(len(applied["function_config"]["spec"]["operations"]), 5)
|
|
apply_context = interpret.call_args_list[4].args[1]
|
|
self.assertEqual(apply_context["presets"][0]["name"], "零重力模式")
|
|
|
|
def test_missing_target_defaults_to_driver_ch1(self) -> None:
|
|
result = self.service.process("打开座椅通风", provider="local")
|
|
|
|
self.assertEqual(result["status"], "succeeded")
|
|
operation = result["function_config"]["spec"]["operations"][0]
|
|
self.assertEqual(operation["target"]["zone"], "driver")
|
|
self.assertEqual(
|
|
operation["capability"], "seat.set_driver_ventilation_native"
|
|
)
|
|
self.assertEqual(
|
|
result["command_plan"]["spec"]["commands"][0]["bus"],
|
|
"body_can",
|
|
)
|
|
|
|
def test_explicit_driver_target_uses_ch1_native_can(self) -> None:
|
|
result = self.service.process("打开主驾座椅通风", provider="local")
|
|
|
|
self.assertEqual(result["status"], "succeeded")
|
|
operation = result["function_config"]["spec"]["operations"][0]
|
|
self.assertEqual(operation["target"]["zone"], "driver")
|
|
self.assertEqual(
|
|
operation["capability"], "seat.set_driver_ventilation_native"
|
|
)
|
|
command = result["command_plan"]["spec"]["commands"][0]
|
|
self.assertEqual(command["bus"], "body_can")
|
|
self.assertEqual(command["signals"]["frame_id"], "0x431")
|
|
self.assertEqual(command["signals"]["feedback_ids"], "0x4FA")
|
|
|
|
def test_model_driver_target_is_replaced_by_ch1_native_can(self) -> None:
|
|
model_output = {
|
|
"detected_domains": ["seat"],
|
|
"requests": [
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_ventilation",
|
|
"target_scope": "driver",
|
|
"operation": "activate",
|
|
"parameters": [{"name": "state", "value": "on"}],
|
|
"reason": "Ventilate the explicitly requested driver seat.",
|
|
}
|
|
],
|
|
"ambiguities": [],
|
|
"decision_summary": ["Explicit driver-seat request."],
|
|
"confidence": 0.95,
|
|
}
|
|
self.service.openai.api_key = "test-key"
|
|
|
|
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
|
result = self.service.process(
|
|
"打开主驾座椅通风", "openai", "conv-driver-rejected"
|
|
)
|
|
|
|
self.assertEqual(result["status"], "succeeded")
|
|
operation = result["function_config"]["spec"]["operations"][0]
|
|
self.assertEqual(operation["target"]["zone"], "driver")
|
|
self.assertEqual(
|
|
operation["capability"], "seat.set_driver_ventilation_native"
|
|
)
|
|
self.assertFalse(result["deferred_requests"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|