c812a1a595
Integrate the remote firmware workbench, OTA delivery, and timed mode schedules while preserving the local N7 scene and runtime changes.
1069 lines
34 KiB
Python
1069 lines
34 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import queue
|
|
from collections.abc import Callable, Iterator, Mapping
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
|
|
from cognitive_agent.application.assistant_plan_guard import guard_assistant_plan
|
|
from cognitive_agent.application.codex_assistant import (
|
|
ActiveCodexTurn,
|
|
CodexAssistantService,
|
|
CodexVehicleToolError,
|
|
)
|
|
from cognitive_agent.application.mode_profile import ModeProfileRegistry
|
|
from cognitive_agent.application.seat_service import SeatDemoService
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
REPOSITORY_MODE_IDS = frozenset(
|
|
path.stem
|
|
for path in (ROOT / "config" / "modes" / "vehicles" / "denza_n7").glob(
|
|
"*.yaml"
|
|
)
|
|
)
|
|
|
|
|
|
def _repository_catalog_modes(modes: object) -> list[Mapping[str, object]]:
|
|
assert REPOSITORY_MODE_IDS
|
|
assert isinstance(modes, list)
|
|
mode_ids = {
|
|
str(mode.get("id") or "")
|
|
for mode in modes
|
|
if isinstance(mode, Mapping)
|
|
}
|
|
assert "" not in mode_ids
|
|
assert len(mode_ids) == len(modes), "catalog mode ids must remain unique"
|
|
assert REPOSITORY_MODE_IDS <= mode_ids
|
|
repository_modes = [
|
|
mode
|
|
for mode in modes
|
|
if isinstance(mode, Mapping) and str(mode.get("id") or "") in REPOSITORY_MODE_IDS
|
|
]
|
|
assert {str(mode["id"]) for mode in repository_modes} == REPOSITORY_MODE_IDS
|
|
return repository_modes
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _CapabilityCase:
|
|
capability: str
|
|
message: str
|
|
target: str
|
|
parameters: Mapping[str, object]
|
|
operation: str = "set"
|
|
expected_effect: str = "action"
|
|
|
|
@property
|
|
def domain(self) -> str:
|
|
return self.capability.split(".", 1)[0]
|
|
|
|
def assistant_plan(self) -> dict[str, object]:
|
|
return {
|
|
"detected_domains": [self.domain],
|
|
"requests": [
|
|
{
|
|
"domain": self.domain,
|
|
"capability": self.capability,
|
|
"target_scope": self.target,
|
|
"operation": self.operation,
|
|
"duration_ms": None,
|
|
"parameters": [
|
|
{"name": name, "value": value} for name, value in self.parameters.items()
|
|
],
|
|
"reason": "将用户明确表达的控制目标规范化为登记能力。",
|
|
}
|
|
],
|
|
"ambiguities": [],
|
|
"decision_summary": ["仅规范化原始请求明确涉及的能力。"],
|
|
"confidence": 0.95,
|
|
}
|
|
|
|
|
|
CAPABILITY_CASES = (
|
|
_CapabilityCase(
|
|
"ambient_light.set_brightness_level",
|
|
"氛围灯亮度调到3档",
|
|
"vehicle",
|
|
{"brightness_level": 3},
|
|
),
|
|
_CapabilityCase(
|
|
"ambient_light.set_color_index",
|
|
"设置氛围灯颜色编号32",
|
|
"vehicle",
|
|
{"color_index": 32},
|
|
),
|
|
_CapabilityCase(
|
|
"ambient_light.set_illumination",
|
|
"氛围灯调成蓝色,亮度50%",
|
|
"vehicle",
|
|
{"color_hex": "#0000FF", "brightness_pct": 50},
|
|
),
|
|
_CapabilityCase(
|
|
"ambient_light.set_mode",
|
|
"氛围灯设置为影院模式",
|
|
"vehicle",
|
|
{"mode": "cinema"},
|
|
),
|
|
_CapabilityCase(
|
|
"ambient_light.set_power",
|
|
"打开氛围灯",
|
|
"vehicle",
|
|
{"state": "on"},
|
|
"activate",
|
|
),
|
|
_CapabilityCase(
|
|
"climate.set_ac",
|
|
"空调压缩机设为开启状态",
|
|
"vehicle",
|
|
{"state": "on"},
|
|
"activate",
|
|
),
|
|
_CapabilityCase(
|
|
"climate.set_control_mode",
|
|
"空调设置为自动控制模式",
|
|
"vehicle",
|
|
{"mode": "auto"},
|
|
),
|
|
_CapabilityCase(
|
|
"climate.set_cycle_mode",
|
|
"空调设置为内循环",
|
|
"vehicle",
|
|
{"mode": "internal"},
|
|
),
|
|
_CapabilityCase(
|
|
"climate.set_fan_level",
|
|
"空调风量调到3档",
|
|
"vehicle",
|
|
{"level": 3},
|
|
),
|
|
_CapabilityCase(
|
|
"climate.set_front_defrost",
|
|
"打开前挡风玻璃除雾",
|
|
"vehicle",
|
|
{"state": "on"},
|
|
"activate",
|
|
),
|
|
_CapabilityCase(
|
|
"climate.set_outlet_mode",
|
|
"空调出风模式设置为吹面吹脚",
|
|
"vehicle",
|
|
{"mode": "face_feet"},
|
|
),
|
|
_CapabilityCase(
|
|
"climate.set_power",
|
|
"打开空调",
|
|
"vehicle",
|
|
{"state": "on"},
|
|
"activate",
|
|
),
|
|
_CapabilityCase(
|
|
"climate.set_temperature",
|
|
"空调温度调到23度",
|
|
"vehicle",
|
|
{"temperature_c": 23},
|
|
),
|
|
_CapabilityCase(
|
|
"door.set_rear_lock",
|
|
"后门锁上锁",
|
|
"rear",
|
|
{"state": "lock"},
|
|
),
|
|
_CapabilityCase(
|
|
"interior_light.set_indoor_power",
|
|
"打开车内灯",
|
|
"vehicle",
|
|
{"state": "on"},
|
|
"activate",
|
|
),
|
|
_CapabilityCase(
|
|
"interior_light.set_reading_power",
|
|
"打开所有阅读灯",
|
|
"all",
|
|
{"state": "on"},
|
|
"activate",
|
|
),
|
|
_CapabilityCase(
|
|
"outlet.set_calibrated_position",
|
|
"主驾左风口标定坐标水平-20垂直30",
|
|
"driver_left",
|
|
{"horizontal_value": -20, "vertical_value": 30},
|
|
),
|
|
_CapabilityCase(
|
|
"outlet.set_position_percent",
|
|
"主驾左风口水平25%垂直75%",
|
|
"driver_left",
|
|
{"horizontal_pct": 25, "vertical_pct": 75},
|
|
),
|
|
_CapabilityCase(
|
|
"outlet.set_switch_state",
|
|
"打开主驾左出风口",
|
|
"driver_left",
|
|
{"state": "open"},
|
|
),
|
|
_CapabilityCase(
|
|
"roof.set_automatic_dimming",
|
|
"开启智能天幕自动调光",
|
|
"vehicle",
|
|
{"state": "on"},
|
|
"activate",
|
|
),
|
|
_CapabilityCase(
|
|
"roof.set_shade_motion",
|
|
"打开遮阳帘",
|
|
"vehicle",
|
|
{"motion": "open"},
|
|
),
|
|
_CapabilityCase(
|
|
"roof.set_shade_position",
|
|
"遮阳帘打开30%",
|
|
"vehicle",
|
|
{"position_pct": 30},
|
|
),
|
|
_CapabilityCase(
|
|
"roof.set_transmittance",
|
|
"智能天幕透光率调到30%",
|
|
"vehicle",
|
|
{"transmittance_pct": 30},
|
|
),
|
|
_CapabilityCase(
|
|
"seat.apply_posture_preset",
|
|
"调用座椅预设矩阵姿态",
|
|
"passenger",
|
|
{"preset_name": "矩阵姿态"},
|
|
"apply",
|
|
"preset_apply",
|
|
),
|
|
_CapabilityCase(
|
|
"seat.recall_driver_memory_native",
|
|
"召回主驾座椅2号记忆位置",
|
|
"driver",
|
|
{"slot": 2},
|
|
"apply",
|
|
),
|
|
_CapabilityCase(
|
|
"seat.save_posture_preset",
|
|
"保存当前座椅姿态为矩阵姿态",
|
|
"passenger",
|
|
{"preset_name": "矩阵姿态"},
|
|
"save",
|
|
"preset_save",
|
|
),
|
|
_CapabilityCase(
|
|
"seat.set_backrest_recline",
|
|
"副驾靠背调到120度",
|
|
"passenger",
|
|
{"angle_deg": 120},
|
|
),
|
|
_CapabilityCase(
|
|
"seat.set_cushion_tilt",
|
|
"副驾座椅坐垫高度调到40%",
|
|
"passenger",
|
|
{"position_pct": 40},
|
|
),
|
|
_CapabilityCase(
|
|
"seat.set_driver_axis_native",
|
|
"主驾座椅水平向前移动500毫秒",
|
|
"driver",
|
|
{"axis": "horizontal", "direction": "forward", "duration_ms": 500},
|
|
),
|
|
_CapabilityCase(
|
|
"seat.set_driver_heating_native",
|
|
"打开主驾座椅加热2档",
|
|
"driver",
|
|
{"state": "on", "level": 2},
|
|
"activate",
|
|
),
|
|
_CapabilityCase(
|
|
"seat.set_driver_massage_native",
|
|
"主驾座椅按摩强度开到3档",
|
|
"driver",
|
|
{"action": "strength", "strength": 3},
|
|
),
|
|
_CapabilityCase(
|
|
"seat.set_driver_position_native",
|
|
"主驾座椅记忆位置调到2号",
|
|
"driver",
|
|
{"slot": 2},
|
|
"apply",
|
|
),
|
|
_CapabilityCase(
|
|
"seat.set_driver_ventilation_native",
|
|
"打开主驾座椅通风2档",
|
|
"driver",
|
|
{"state": "on", "level": 2},
|
|
"activate",
|
|
),
|
|
_CapabilityCase(
|
|
"seat.set_heating",
|
|
"打开副驾座椅加热3档",
|
|
"passenger",
|
|
{"state": "on", "level": 3},
|
|
"activate",
|
|
),
|
|
_CapabilityCase(
|
|
"seat.set_legrest_extension",
|
|
"副驾腿托伸出到40%",
|
|
"passenger",
|
|
{"position_pct": 40},
|
|
),
|
|
_CapabilityCase(
|
|
"seat.set_legrest_height",
|
|
"副驾腿托升高到40%",
|
|
"passenger",
|
|
{"position_pct": 40},
|
|
),
|
|
_CapabilityCase(
|
|
"seat.set_longitudinal_position",
|
|
"副驾座椅纵向调到40%",
|
|
"passenger",
|
|
{"position_pct": 40},
|
|
),
|
|
_CapabilityCase(
|
|
"seat.set_ventilation",
|
|
"打开副驾座椅通风3档",
|
|
"passenger",
|
|
{"state": "on", "level": 3},
|
|
"activate",
|
|
),
|
|
_CapabilityCase(
|
|
"seat.start_dynamic_mode",
|
|
"启动副驾木马模式",
|
|
"passenger",
|
|
{"mode_id": 1},
|
|
"activate",
|
|
),
|
|
_CapabilityCase(
|
|
"seat.stop_dynamic_mode",
|
|
"停止副驾木马模式",
|
|
"passenger",
|
|
{},
|
|
"deactivate",
|
|
),
|
|
_CapabilityCase(
|
|
"window.set_motion",
|
|
"打开左前车窗",
|
|
"front_left",
|
|
{"motion": "open"},
|
|
),
|
|
_CapabilityCase(
|
|
"window.set_position",
|
|
"把左前车窗开到50%",
|
|
"front_left",
|
|
{"position_pct": 50},
|
|
),
|
|
)
|
|
|
|
|
|
def _complete_turn(turn: ActiveCodexTurn, answer: str = "已按要求完成。") -> None:
|
|
item_id = f"answer-{turn.turn_id}"
|
|
turn.events.put(
|
|
{
|
|
"method": "item/started",
|
|
"params": {
|
|
"item": {
|
|
"type": "agentMessage",
|
|
"id": item_id,
|
|
"text": "",
|
|
"phase": "final_answer",
|
|
}
|
|
},
|
|
}
|
|
)
|
|
turn.events.put(
|
|
{
|
|
"method": "item/agentMessage/delta",
|
|
"params": {"itemId": item_id, "delta": answer},
|
|
}
|
|
)
|
|
turn.events.put(
|
|
{
|
|
"method": "item/completed",
|
|
"params": {
|
|
"item": {
|
|
"type": "agentMessage",
|
|
"id": item_id,
|
|
"text": answer,
|
|
"phase": "final_answer",
|
|
}
|
|
},
|
|
}
|
|
)
|
|
turn.events.put(
|
|
{
|
|
"method": "turn/completed",
|
|
"params": {
|
|
"turn": {
|
|
"id": turn.turn_id,
|
|
"status": "completed",
|
|
"items": [],
|
|
"error": None,
|
|
}
|
|
},
|
|
}
|
|
)
|
|
|
|
|
|
class _ToolCallingCodexClient:
|
|
def __init__(self, *, available: bool = True) -> None:
|
|
self.available = available
|
|
self.on_turn: Callable[[str, ActiveCodexTurn], None] | None = None
|
|
self.thread_calls = 0
|
|
self.turn_calls = 0
|
|
self.request_tokens: list[str] = []
|
|
|
|
@property
|
|
def status(self) -> str:
|
|
return "ready" if self.available else "failed"
|
|
|
|
def start(self, _callback_url: str, _callback_token: str) -> None:
|
|
return None
|
|
|
|
def close(self) -> None:
|
|
return None
|
|
|
|
def start_thread(self, *, developer_instructions: str, ephemeral: bool = True) -> str:
|
|
assert "get_n7web_capabilities" in developer_instructions
|
|
assert "execute_cockpit_request" in developer_instructions
|
|
assert ephemeral is True
|
|
self.thread_calls += 1
|
|
return f"thread-matrix-{self.thread_calls}"
|
|
|
|
def start_turn(
|
|
self,
|
|
thread_id: str,
|
|
text: str,
|
|
request_token: str,
|
|
*,
|
|
application_context: dict[str, object] | None = None,
|
|
) -> ActiveCodexTurn:
|
|
del text
|
|
assert thread_id.startswith("thread-matrix-")
|
|
assert isinstance(application_context, dict)
|
|
self.turn_calls += 1
|
|
self.request_tokens.append(request_token)
|
|
turn = ActiveCodexTurn(
|
|
thread_id=thread_id,
|
|
turn_id=f"turn-matrix-{self.turn_calls}",
|
|
events=queue.Queue(),
|
|
)
|
|
if self.on_turn is not None:
|
|
self.on_turn(request_token, turn)
|
|
_complete_turn(turn)
|
|
return turn
|
|
|
|
def finish_turn(self, _turn: ActiveCodexTurn) -> None:
|
|
return None
|
|
|
|
def interrupt(self, _turn: ActiveCodexTurn) -> None:
|
|
return None
|
|
|
|
def unsubscribe_thread(self, _thread_id: str) -> None:
|
|
return None
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def vehicle_service(
|
|
tmp_path_factory: pytest.TempPathFactory,
|
|
) -> Iterator[SeatDemoService]:
|
|
memory_path = ROOT / "tests" / "fixtures" / "driver_seat_memory.json"
|
|
previous_memory_path = os.environ.get("COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH")
|
|
os.environ["COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH"] = str(memory_path)
|
|
service = SeatDemoService(ROOT)
|
|
service.artifact_root = Path(tmp_path_factory.mktemp("assistant-matrix-runs"))
|
|
service.mode_profiles = ModeProfileRegistry(
|
|
ROOT / "config" / "modes",
|
|
user_root=Path(tmp_path_factory.mktemp("assistant-matrix-modes")),
|
|
)
|
|
yield service
|
|
service.close()
|
|
if previous_memory_path is None:
|
|
os.environ.pop("COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH", None)
|
|
else:
|
|
os.environ["COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH"] = previous_memory_path
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def catalog(vehicle_service: SeatDemoService) -> AssistantCapabilityCatalog:
|
|
return AssistantCapabilityCatalog(
|
|
vehicle_service,
|
|
peripheral_provider=list,
|
|
nfc_provider=lambda: {"reader": {"state": "ready"}, "card": None},
|
|
)
|
|
|
|
|
|
def _stream_with_catalog(
|
|
assistant: CodexAssistantService,
|
|
catalog: AssistantCapabilityCatalog,
|
|
message: str,
|
|
*,
|
|
execute_control: Callable[..., dict[str, object]],
|
|
conversation_id: str,
|
|
authoritative_answer: str | None = None,
|
|
) -> tuple[dict[str, object], list[dict[str, object]]]:
|
|
snapshot = catalog.snapshot()
|
|
events: list[dict[str, object]] = []
|
|
result = assistant.stream(
|
|
message,
|
|
conversation_id=conversation_id,
|
|
principal="assistant-matrix-user",
|
|
history=[],
|
|
emit=lambda event: events.append(event),
|
|
execute_control=execute_control,
|
|
capability_snapshot=snapshot,
|
|
capability_context=catalog.compact_context(snapshot),
|
|
control_terms=catalog.control_terms(snapshot),
|
|
query_capabilities=catalog.query,
|
|
authoritative_answer=authoritative_answer,
|
|
)
|
|
return result, events
|
|
|
|
|
|
def test_execution_matrix_exactly_matches_every_assistant_callable_capability(
|
|
catalog: AssistantCapabilityCatalog,
|
|
) -> None:
|
|
callable_capabilities = {
|
|
capability["id"]
|
|
for capability in catalog.snapshot()["capabilities"]
|
|
if capability["assistant_callable"] is True
|
|
}
|
|
matrix_capabilities = {case.capability for case in CAPABILITY_CASES}
|
|
|
|
assert len(CAPABILITY_CASES) == len(matrix_capabilities), (
|
|
"the execution matrix must contain exactly one case per atomic capability"
|
|
)
|
|
assert len(callable_capabilities) == 42
|
|
assert matrix_capabilities == callable_capabilities
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"case",
|
|
CAPABILITY_CASES,
|
|
ids=lambda case: case.capability,
|
|
)
|
|
def test_every_assistant_callable_capability_reaches_the_real_tool_boundary(
|
|
vehicle_service: SeatDemoService,
|
|
catalog: AssistantCapabilityCatalog,
|
|
case: _CapabilityCase,
|
|
) -> None:
|
|
proposed_plan = case.assistant_plan()
|
|
guarded = guard_assistant_plan(
|
|
proposed_plan,
|
|
fallback_text=case.message,
|
|
capability_context=vehicle_service.capability_context,
|
|
executable_capabilities={item.capability for item in CAPABILITY_CASES},
|
|
fallback_intents=(),
|
|
trusted_local={},
|
|
)
|
|
assert guarded.rejected_requests == ()
|
|
assert len(guarded.plan["requests"]) == 1
|
|
guarded_request = guarded.plan["requests"][0]
|
|
assert guarded_request["capability"] == case.capability
|
|
assert guarded_request["target_scope"] == case.target
|
|
assert guarded_request["operation"] == case.operation
|
|
assert guarded_request["parameters"] == [
|
|
{"name": name, "value": value} for name, value in case.parameters.items()
|
|
]
|
|
|
|
capability_slug = case.capability.replace(".", "-").replace("_", "-")
|
|
vehicle_conversation_id = f"conv-matrix-{capability_slug}"
|
|
client = _ToolCallingCodexClient()
|
|
assistant = CodexAssistantService(client)
|
|
tool_results: list[dict[str, object]] = []
|
|
raw_results: list[dict[str, object]] = []
|
|
|
|
if case.expected_effect == "preset_apply":
|
|
setup_case = _CapabilityCase(
|
|
"seat.save_posture_preset",
|
|
"保存当前座椅姿态为矩阵姿态",
|
|
"passenger",
|
|
{"preset_name": "矩阵姿态"},
|
|
"save",
|
|
)
|
|
setup_result = vehicle_service.preview_native_can(
|
|
setup_case.message,
|
|
provider="local",
|
|
conversation_id=vehicle_conversation_id,
|
|
execution_mode="dry_run",
|
|
assistant_plan=setup_case.assistant_plan(),
|
|
)
|
|
assert setup_result["status"] == "succeeded"
|
|
|
|
def invoke_tool(request_token: str, _turn: ActiveCodexTurn) -> None:
|
|
tool_results.append(
|
|
assistant.execute_cockpit_request(
|
|
request_token,
|
|
proposed_plan,
|
|
)
|
|
)
|
|
|
|
def execute_control(
|
|
instruction: str,
|
|
progress: Callable[[dict[str, object]], None],
|
|
assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
assert instruction == case.message
|
|
result = vehicle_service.preview_native_can(
|
|
instruction,
|
|
provider="local",
|
|
conversation_id=vehicle_conversation_id,
|
|
execution_mode="dry_run",
|
|
on_commands_validated=progress,
|
|
assistant_plan=dict(assistant_plan),
|
|
)
|
|
raw_results.append(result)
|
|
return result
|
|
|
|
client.on_turn = invoke_tool
|
|
result, events = _stream_with_catalog(
|
|
assistant,
|
|
catalog,
|
|
case.message,
|
|
execute_control=execute_control,
|
|
conversation_id=f"conv-assistant-{capability_slug}",
|
|
)
|
|
|
|
assert result["route"] == "agent"
|
|
assert raw_results[0]["status"] == "succeeded"
|
|
assert tool_results[0]["verified"] is False
|
|
assert tool_results[0]["verification_status"] == "simulated"
|
|
safe_actions = tool_results[0]["actions"]
|
|
raw_actions = raw_results[0]["intent_ir"]["spec"]["actions"]
|
|
if case.expected_effect == "action":
|
|
assert case.capability in {action["capability"] for action in safe_actions}
|
|
elif case.expected_effect == "preset_save":
|
|
assert raw_actions == []
|
|
assert raw_results[0]["saved_presets"] == [{"name": "矩阵姿态", "target": "passenger"}]
|
|
else:
|
|
assert case.expected_effect == "preset_apply"
|
|
assert raw_results[0]["saved_presets"] == [{"name": "矩阵姿态", "target": "passenger"}]
|
|
assert {action["capability"] for action in raw_actions} == {
|
|
"seat.set_longitudinal_position",
|
|
"seat.set_cushion_tilt",
|
|
"seat.set_backrest_recline",
|
|
"seat.set_legrest_height",
|
|
"seat.set_legrest_extension",
|
|
}
|
|
action_event = next(event for event in events if event["type"] == "action.completed")
|
|
assert action_event["success"] is True
|
|
|
|
|
|
def test_every_catalog_mode_executes_through_the_deterministic_assistant_route(
|
|
vehicle_service: SeatDemoService,
|
|
catalog: AssistantCapabilityCatalog,
|
|
) -> None:
|
|
snapshot = catalog.snapshot()
|
|
modes = _repository_catalog_modes(snapshot["modes"])
|
|
|
|
for index, mode in enumerate(modes):
|
|
assert isinstance(mode, Mapping)
|
|
assert mode["assistant_callable"] is True
|
|
message = f"进入{mode['name']}"
|
|
client = _ToolCallingCodexClient()
|
|
assistant = CodexAssistantService(client)
|
|
raw_results: list[dict[str, object]] = []
|
|
tool_results: list[dict[str, object]] = []
|
|
|
|
def invoke_tool(
|
|
request_token: str,
|
|
_turn: ActiveCodexTurn,
|
|
*,
|
|
current_results: list[dict[str, object]] = tool_results,
|
|
current_assistant: CodexAssistantService = assistant,
|
|
) -> None:
|
|
current_results.append(current_assistant.execute_cockpit_request(request_token))
|
|
|
|
def execute_control(
|
|
instruction: str,
|
|
progress: Callable[[dict[str, object]], None],
|
|
*,
|
|
current_message: str = message,
|
|
current_index: int = index,
|
|
current_results: list[dict[str, object]] = raw_results,
|
|
) -> dict[str, object]:
|
|
assert instruction == current_message
|
|
result = vehicle_service.preview_native_can(
|
|
instruction,
|
|
provider="local",
|
|
conversation_id=f"conv-mode-matrix-{current_index:02d}",
|
|
execution_mode="dry_run",
|
|
on_commands_validated=progress,
|
|
)
|
|
current_results.append(result)
|
|
return result
|
|
|
|
client.on_turn = invoke_tool
|
|
result, events = _stream_with_catalog(
|
|
assistant,
|
|
catalog,
|
|
message,
|
|
execute_control=execute_control,
|
|
conversation_id=f"conv-assistant-mode-{index:02d}",
|
|
)
|
|
|
|
assert raw_results[0]["status"] == "succeeded", mode["id"]
|
|
assert raw_results[0]["matched_modes"][0]["id"] == mode["id"]
|
|
assert tool_results == []
|
|
assert client.turn_calls == 0
|
|
assert result["route"] == "mode_profile_execute"
|
|
assert result["success"] is True
|
|
assert events[0]["engine"] == "n7web-mode-profile-runner"
|
|
|
|
|
|
def test_every_catalog_mode_rejects_incompatible_explicit_seat_scopes(
|
|
vehicle_service: SeatDemoService,
|
|
catalog: AssistantCapabilityCatalog,
|
|
) -> None:
|
|
modes = _repository_catalog_modes(catalog.snapshot()["modes"])
|
|
|
|
for mode_index, mode in enumerate(modes):
|
|
assert isinstance(mode, Mapping)
|
|
profile = vehicle_service.mode_profiles.get_profile(str(mode["id"]), "denza_n7")
|
|
has_driver = any(
|
|
operation.domain == "seat" and operation.target_scope in {"driver", "front_left"}
|
|
for operation in profile.operations
|
|
)
|
|
for scope_index, scope in enumerate(
|
|
("主驾", "驾驶员", "后排", "二排", "前排", "所有座椅", "全车座椅")
|
|
):
|
|
if has_driver and scope in {"主驾", "驾驶员"}:
|
|
continue # Valid dual-seat subsets have a separate exact-action matrix.
|
|
message = f"请让{scope}进入{mode['name']}"
|
|
result = vehicle_service.preview_native_can(
|
|
message,
|
|
provider="local",
|
|
conversation_id=(
|
|
f"conv-mode-target-reject-{mode_index:02d}-{scope_index:02d}"
|
|
),
|
|
execution_mode="dry_run",
|
|
)
|
|
|
|
assert result["status"] in {"no_action", "needs_confirmation"}, (
|
|
mode["id"],
|
|
scope,
|
|
)
|
|
assert result["intent_ir"]["spec"]["actions"] == [], (
|
|
mode["id"],
|
|
scope,
|
|
)
|
|
assert any(
|
|
item.get("reason") == "mode_target_scope_mismatch"
|
|
for item in result["intent_ir"]["spec"].get("ambiguities", [])
|
|
), (mode["id"], scope)
|
|
|
|
passenger = vehicle_service.preview_native_can(
|
|
f"请让副驾进入{mode['name']}",
|
|
provider="local",
|
|
conversation_id=f"conv-mode-target-pass-{mode_index:02d}",
|
|
execution_mode="dry_run",
|
|
)
|
|
assert passenger["status"] == "succeeded", mode["id"]
|
|
assert passenger["intent_ir"]["spec"]["actions"], mode["id"]
|
|
|
|
|
|
def test_preview_and_process_share_mode_target_safety_gate(
|
|
vehicle_service: SeatDemoService,
|
|
) -> None:
|
|
message = "主驾开启零重力模式"
|
|
direct = vehicle_service.process(
|
|
message,
|
|
provider="local",
|
|
conversation_id="conv-mode-target-direct",
|
|
execution_mode="dry_run",
|
|
)
|
|
preview = vehicle_service.preview_native_can(
|
|
message,
|
|
provider="local",
|
|
conversation_id="conv-mode-target-preview",
|
|
execution_mode="dry_run",
|
|
)
|
|
|
|
assert direct["status"] in {"no_action", "needs_confirmation"}
|
|
assert preview["status"] in {"no_action", "needs_confirmation"}
|
|
assert direct["intent_ir"]["spec"]["actions"] == []
|
|
assert preview["intent_ir"]["spec"]["actions"] == []
|
|
assert {
|
|
item.get("reason")
|
|
for item in preview["intent_ir"]["spec"].get("ambiguities", [])
|
|
} == {"mode_target_scope_mismatch"}
|
|
|
|
|
|
def test_durable_nfc_peripheral_can_execute_after_bound_card_is_removed(
|
|
vehicle_service: SeatDemoService,
|
|
) -> None:
|
|
learned = {
|
|
"id": "star-projector-on",
|
|
"status": "learned",
|
|
"enabled": True,
|
|
"device_id": "star-projector",
|
|
"device_name": "星空投影仪",
|
|
"object_names": ["星空投影"],
|
|
"action": "开启",
|
|
"action_names": ["打开", "开启"],
|
|
"card_uid": "A1B2C3D4",
|
|
"slot": 7,
|
|
}
|
|
active_catalog = AssistantCapabilityCatalog(
|
|
vehicle_service,
|
|
peripheral_provider=lambda: [learned],
|
|
nfc_provider=lambda: {"card": {"uid": "a1b2c3d4"}},
|
|
)
|
|
detached_catalog = AssistantCapabilityCatalog(
|
|
vehicle_service,
|
|
peripheral_provider=lambda: [learned],
|
|
nfc_provider=lambda: {"card": None},
|
|
)
|
|
|
|
active_client = _ToolCallingCodexClient()
|
|
active_assistant = CodexAssistantService(active_client)
|
|
executed: list[str] = []
|
|
active_client.on_turn = lambda token, _turn: active_assistant.execute_cockpit_request(token)
|
|
_stream_with_catalog(
|
|
active_assistant,
|
|
active_catalog,
|
|
"打开星空投影仪",
|
|
execute_control=lambda instruction, _progress: (
|
|
executed.append(instruction)
|
|
or {
|
|
"status": "succeeded",
|
|
"execution_mode": "dry_run",
|
|
"utterance": instruction,
|
|
"infrared_actions": [
|
|
{
|
|
"capability_id": "star-projector-on",
|
|
"device_id": "star-projector",
|
|
"device_name": "星空投影仪",
|
|
"action": "开启",
|
|
"slot": 7,
|
|
"transport": "infrared",
|
|
}
|
|
],
|
|
}
|
|
),
|
|
conversation_id="conv-active-peripheral-01",
|
|
)
|
|
assert executed == ["打开星空投影仪"]
|
|
|
|
detached_client = _ToolCallingCodexClient()
|
|
detached_assistant = CodexAssistantService(detached_client)
|
|
detached_executed: list[str] = []
|
|
detached_client.on_turn = (
|
|
lambda token, _turn: detached_assistant.execute_cockpit_request(token)
|
|
)
|
|
_stream_with_catalog(
|
|
detached_assistant,
|
|
detached_catalog,
|
|
"打开星空投影仪",
|
|
execute_control=lambda instruction, _progress: (
|
|
detached_executed.append(instruction)
|
|
or {
|
|
"status": "succeeded",
|
|
"execution_mode": "dry_run",
|
|
"utterance": instruction,
|
|
"infrared_actions": [
|
|
{
|
|
"capability_id": "star-projector-on",
|
|
"device_id": "star-projector",
|
|
"device_name": "星空投影仪",
|
|
"action": "开启",
|
|
"slot": 7,
|
|
"transport": "infrared",
|
|
}
|
|
],
|
|
}
|
|
),
|
|
conversation_id="conv-detached-peripheral-01",
|
|
)
|
|
assert detached_executed == ["打开星空投影仪"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"message",
|
|
[
|
|
"整车锁车",
|
|
"打开车门",
|
|
"打开壁灯",
|
|
"打开物理天窗",
|
|
"打开前大灯",
|
|
"支持遮阳帘吗",
|
|
"请把“打开空调”翻译成英文,不要执行",
|
|
"帮我修改网页里的空调图标",
|
|
],
|
|
)
|
|
def test_unsupported_or_non_control_language_cannot_cross_the_execution_boundary(
|
|
catalog: AssistantCapabilityCatalog,
|
|
message: str,
|
|
) -> None:
|
|
client = _ToolCallingCodexClient()
|
|
assistant = CodexAssistantService(client)
|
|
errors: list[str] = []
|
|
|
|
def attempt_control(request_token: str, _turn: ActiveCodexTurn) -> None:
|
|
try:
|
|
assistant.execute_cockpit_request(request_token)
|
|
except CodexVehicleToolError as exc:
|
|
errors.append(str(exc))
|
|
|
|
client.on_turn = attempt_control
|
|
_stream_with_catalog(
|
|
assistant,
|
|
catalog,
|
|
message,
|
|
execute_control=lambda _instruction, _progress: pytest.fail(
|
|
"unsupported or informational request must not execute"
|
|
),
|
|
conversation_id=f"conv-rejection-{abs(hash(message)):x}",
|
|
)
|
|
|
|
assert errors == ["vehicle control was not authorized by the original user request"]
|
|
|
|
|
|
def test_capability_fast_route_works_when_codex_is_unavailable(
|
|
catalog: AssistantCapabilityCatalog,
|
|
) -> None:
|
|
client = _ToolCallingCodexClient(available=False)
|
|
assistant = CodexAssistantService(client)
|
|
answer = catalog.fast_answer("车端有哪些可控设备", catalog.snapshot())
|
|
assert answer is not None
|
|
|
|
result, events = _stream_with_catalog(
|
|
assistant,
|
|
catalog,
|
|
"车端有哪些可控设备",
|
|
execute_control=lambda _instruction, _progress: pytest.fail(
|
|
"capability fast route must never execute controls"
|
|
),
|
|
conversation_id="conv-fast-capability-route",
|
|
authoritative_answer=answer.answer,
|
|
)
|
|
|
|
assert result == {"route": "capability_catalog", "answer": answer.answer}
|
|
assert client.thread_calls == 0
|
|
assert client.turn_calls == 0
|
|
assert [event["type"] for event in events] == [
|
|
"message.accepted",
|
|
"work.started",
|
|
"answer.delta",
|
|
"work.completed",
|
|
"answer.completed",
|
|
]
|
|
assert events[0]["engine"] == "n7web-capability-registry"
|
|
assert events[-1]["route"] == "capability_catalog"
|
|
|
|
|
|
def test_hot_comfort_fast_route_reaches_real_climate_service(
|
|
vehicle_service: SeatDemoService,
|
|
catalog: AssistantCapabilityCatalog,
|
|
) -> None:
|
|
client = _ToolCallingCodexClient(available=False)
|
|
assistant = CodexAssistantService(client, fast_implicit_controls=True)
|
|
raw_results: list[dict[str, object]] = []
|
|
|
|
def execute_control(
|
|
instruction: str,
|
|
progress: Callable[[dict[str, object]], None],
|
|
) -> dict[str, object]:
|
|
result = vehicle_service.preview_native_can(
|
|
instruction,
|
|
provider="local",
|
|
conversation_id="conv-real-hot-comfort",
|
|
execution_mode="dry_run",
|
|
on_commands_validated=progress,
|
|
)
|
|
raw_results.append(result)
|
|
return result
|
|
|
|
result, events = _stream_with_catalog(
|
|
assistant,
|
|
catalog,
|
|
"车上有点热",
|
|
execute_control=execute_control,
|
|
conversation_id="conv-fast-hot-real-service",
|
|
)
|
|
|
|
assert client.thread_calls == 0
|
|
assert client.turn_calls == 0
|
|
assert result["route"] == "vehicle_control"
|
|
assert result["success"] is True
|
|
assert len(raw_results) == 1
|
|
assert raw_results[0]["status"] == "succeeded"
|
|
actions = raw_results[0]["intent_ir"]["spec"]["actions"]
|
|
capabilities = [action["capability"] for action in actions]
|
|
assert capabilities.count("climate.set_power") == 1
|
|
assert capabilities.count("climate.set_ac") == 0
|
|
assert capabilities.count("climate.set_temperature") == 3
|
|
assert capabilities.count("climate.set_fan_level") == 1
|
|
assert {
|
|
action["target"]["zone"]
|
|
for action in actions
|
|
if action["capability"] == "climate.set_temperature"
|
|
} == {"driver", "passenger", "rear"}
|
|
assert events[-1]["type"] == "answer.completed"
|
|
assert "23℃" in events[-1]["answer"]
|
|
|
|
|
|
def test_turn_bound_capability_query_has_token_and_per_turn_rate_limits(
|
|
catalog: AssistantCapabilityCatalog,
|
|
) -> None:
|
|
client = _ToolCallingCodexClient()
|
|
assistant = CodexAssistantService(client)
|
|
queries: list[str] = []
|
|
results: list[dict[str, object]] = []
|
|
limit_errors: list[str] = []
|
|
control_calls: list[str] = []
|
|
|
|
def query_capabilities(snapshot: Mapping[str, object], query: str) -> dict[str, object]:
|
|
queries.append(query)
|
|
return catalog.query(snapshot, query)
|
|
|
|
def use_tools(request_token: str, _turn: ActiveCodexTurn) -> None:
|
|
with pytest.raises(ValueError, match="160 characters"):
|
|
assistant.get_n7web_capabilities(request_token, "x" * 161)
|
|
for query in ("遮阳帘", "空调", "办公模式", "不存在的设备"):
|
|
results.append(assistant.get_n7web_capabilities(request_token, query))
|
|
try:
|
|
assistant.get_n7web_capabilities(request_token, "第五次查询")
|
|
except CodexVehicleToolError as exc:
|
|
limit_errors.append(str(exc))
|
|
assistant.execute_cockpit_request(request_token)
|
|
|
|
client.on_turn = use_tools
|
|
snapshot = catalog.snapshot()
|
|
events: list[dict[str, object]] = []
|
|
assistant.stream(
|
|
"打开空调",
|
|
conversation_id="conv-capability-query-limits",
|
|
principal="assistant-matrix-user",
|
|
history=[],
|
|
emit=lambda event: events.append(event),
|
|
execute_control=lambda instruction, _progress: (
|
|
control_calls.append(instruction)
|
|
or {
|
|
"status": "succeeded",
|
|
"execution_mode": "dry_run",
|
|
"utterance": instruction,
|
|
"intent_ir": {"spec": {"actions": []}},
|
|
}
|
|
),
|
|
capability_snapshot=snapshot,
|
|
capability_context=catalog.compact_context(snapshot),
|
|
control_terms=catalog.control_terms(snapshot),
|
|
query_capabilities=query_capabilities,
|
|
)
|
|
|
|
assert queries == ["遮阳帘", "空调", "办公模式", "不存在的设备"]
|
|
assert all(result["ok"] is True for result in results)
|
|
assert all(result["source"] == "n7web_runtime_registry" for result in results)
|
|
assert results[0]["catalog"]["matched"] is True
|
|
assert results[-1]["catalog"]["matched"] is False
|
|
assert limit_errors == ["capability query limit exceeded for this turn"]
|
|
assert control_calls == ["打开空调"]
|
|
|
|
expired_token = client.request_tokens[0]
|
|
with pytest.raises(CodexVehicleToolError, match="missing or expired"):
|
|
assistant.get_n7web_capabilities(expired_token, "空调")
|
|
with pytest.raises(CodexVehicleToolError, match="missing or expired"):
|
|
assistant.get_n7web_capabilities("unknown-token", "空调")
|