1021 lines
33 KiB
Python
1021 lines
33 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.seat_service import SeatDemoService
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
@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"))
|
|
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_can_execute_through_the_assistant_tool_boundary(
|
|
vehicle_service: SeatDemoService,
|
|
catalog: AssistantCapabilityCatalog,
|
|
) -> None:
|
|
snapshot = catalog.snapshot()
|
|
modes = snapshot["modes"]
|
|
assert len(modes) == 13
|
|
|
|
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
|
|
_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[0]["verified"] is False
|
|
assert tool_results[0]["verification_status"] == "simulated"
|
|
assert tool_results[0]["actions"], mode["id"]
|
|
|
|
|
|
def test_every_catalog_mode_rejects_incompatible_explicit_seat_scopes(
|
|
vehicle_service: SeatDemoService,
|
|
catalog: AssistantCapabilityCatalog,
|
|
) -> None:
|
|
modes = catalog.snapshot()["modes"]
|
|
assert len(modes) == 13
|
|
|
|
for mode_index, mode in enumerate(modes):
|
|
assert isinstance(mode, Mapping)
|
|
for scope_index, scope in enumerate(
|
|
("主驾", "驾驶员", "后排", "二排", "前排", "所有座椅", "全车座椅")
|
|
):
|
|
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_active_nfc_peripheral_can_execute_but_inactive_peripheral_is_rejected(
|
|
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"}},
|
|
)
|
|
inactive_catalog = AssistantCapabilityCatalog(
|
|
vehicle_service,
|
|
peripheral_provider=lambda: [learned],
|
|
nfc_provider=lambda: {"card": {"uid": "different-card"}},
|
|
)
|
|
|
|
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 == ["打开星空投影仪"]
|
|
|
|
inactive_client = _ToolCallingCodexClient()
|
|
inactive_assistant = CodexAssistantService(inactive_client)
|
|
errors: list[str] = []
|
|
|
|
def attempt_inactive(token: str, _turn: ActiveCodexTurn) -> None:
|
|
try:
|
|
inactive_assistant.execute_cockpit_request(token)
|
|
except CodexVehicleToolError as exc:
|
|
errors.append(str(exc))
|
|
|
|
inactive_client.on_turn = attempt_inactive
|
|
_stream_with_catalog(
|
|
inactive_assistant,
|
|
inactive_catalog,
|
|
"打开星空投影仪",
|
|
execute_control=lambda _instruction, _progress: pytest.fail(
|
|
"inactive NFC peripheral must not execute"
|
|
),
|
|
conversation_id="conv-inactive-peripheral-01",
|
|
)
|
|
assert errors == ["vehicle control was not authorized by the original user request"]
|
|
|
|
|
|
@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", "空调")
|