feat: integrate NFC infrared peripherals and orchestration

This commit is contained in:
vee
2026-08-25 11:45:45 +08:00
parent 9ac8fdface
commit 2529b4896a
19 changed files with 3818 additions and 117 deletions
+366
View File
@@ -16,7 +16,9 @@ from cognitive_agent.application.self_extension import (
SelfExtensionService,
SimulationGateway,
TcpGateway,
active_peripheral_plan_catalog,
build_gateway_command,
resolve_active_peripheral_command,
validate_slot,
)
from cognitive_agent.application.zcan_seat_executor import ZcanBridgeSeatExecutor
@@ -54,6 +56,250 @@ class SelfExtensionTests(unittest.TestCase):
self.assertFalse(result["physical_state_confirmed"])
self.assertEqual(len(result["device_response"]), 2)
def test_main_orchestration_resolves_only_live_bound_nfc_peripheral(self) -> None:
catalog = [
{
"id": "cap-third-party-light-on",
"status": "learned",
"device_id": "third-party-light",
"device_name": "第三方氛围灯",
"card_uid": "1A:53:DD:95",
"action": "开启",
"aliases": ["开灯"],
"slot": 9,
},
{
"id": "cap-third-party-light-off",
"status": "learned",
"device_id": "third-party-light",
"device_name": "第三方氛围灯",
"card_uid": "1A53DD95",
"action": "关闭",
"slot": 10,
},
]
present = {"card": {"action": "present", "uid": "1a53dd95"}}
resolved = resolve_active_peripheral_command(
"打开第三方氛围灯", catalog, present
)
self.assertIsNotNone(resolved)
self.assertEqual(resolved["id"], "cap-third-party-light-on")
self.assertEqual(resolved["slot"], 9)
self.assertEqual(resolved["card_uid"], "1A53DD95")
self.assertIsNone(
resolve_active_peripheral_command("打开氛围灯", catalog, present)
)
self.assertIsNone(
resolve_active_peripheral_command(
"打开第三方氛围灯", catalog, {"card": None}
)
)
self.assertIsNone(
resolve_active_peripheral_command(
"打开第三方氛围灯",
catalog,
{"card": {"uid": "FFFFFFFF"}},
)
)
def test_timed_phrase_selects_live_nfc_catalog_for_gpt_plan(self) -> None:
catalog = [
{
"id": "on",
"status": "learned",
"device_name": "#001 · 第三方氛围灯",
"card_uid": "1A53DD95",
"action": "开启灯光",
"slot": 1,
},
{
"id": "red",
"status": "learned",
"device_name": "#001 · 第三方氛围灯",
"card_uid": "1A53DD95",
"action": "红色灯光",
"slot": 2,
},
]
selected = active_peripheral_plan_catalog(
"打开第三方氛围灯,红灯闪烁5s后关闭",
catalog,
{"card": {"uid": "1A53DD95"}},
)
self.assertEqual([item["id"] for item in selected], ["on", "red"])
self.assertEqual(
active_peripheral_plan_catalog(
"打开第三方氛围灯",
catalog,
{"card": {"uid": "1A53DD95"}},
),
[],
)
def test_main_peripheral_result_uses_gscu_without_vehicle_can(self) -> None:
service = SelfExtensionService(SimulationGateway())
result = service.execute_peripheral_request(
"打开第三方氛围灯",
{
"id": "cap-third-party-light-on",
"device_id": "third-party-light",
"device_name": "第三方氛围灯",
"card_uid": "1A53DD95",
"action": "开启",
"slot": 9,
},
conversation_id="conv-main-extension",
conversation_turn=2,
execution_mode="hardware",
)
self.assertEqual(result["status"], "succeeded")
self.assertEqual(result["orchestration_route"], "nfc_peripheral")
self.assertEqual(result["peripheral"]["slot"], 9)
self.assertEqual(result["execution_report"]["spec"]["mode"], "hil")
operation = result["execution_report"]["spec"]["results"][0]
self.assertEqual(operation["domain"], "self_extension")
self.assertTrue(operation["telemetry"]["gateway_acknowledged"])
def test_main_peripheral_sequence_executes_validated_actions_and_delays(self) -> None:
service = SelfExtensionService(SimulationGateway())
catalog = [
{
"id": "red",
"status": "learned",
"device_id": "third-party-light",
"device_name": "第三方氛围灯",
"card_uid": "1A53DD95",
"action": "红色灯光",
"slot": 3,
},
{
"id": "blue",
"status": "learned",
"device_id": "third-party-light",
"device_name": "第三方氛围灯",
"card_uid": "1A53DD95",
"action": "蓝色灯光",
"slot": 4,
},
{
"id": "off",
"status": "learned",
"device_id": "third-party-light",
"device_name": "第三方氛围灯",
"card_uid": "1A53DD95",
"action": "关闭灯光",
"slot": 5,
},
]
plan_result = {
"model": "gpt-5.6-sol",
"summary": "红灯保持五秒后切换蓝灯",
"steps": [
{"kind": "peripheral", "capability_id": "red"},
{"kind": "delay", "delay_ms": 5000},
{"kind": "peripheral", "capability_id": "blue"},
],
}
with patch("cognitive_agent.application.self_extension.time.sleep") as sleep:
result = service.execute_peripheral_plan_request(
"第三方氛围灯红灯闪烁5秒后切换蓝灯",
plan_result,
catalog,
conversation_id="conv-main-extension-sequence",
execution_mode="hardware",
)
self.assertEqual(result["status"], "succeeded")
self.assertEqual(
result["orchestration_route"], "nfc_peripheral_sequence"
)
self.assertEqual(result["sequence"]["step_count"], 3)
self.assertEqual(result["sequence"]["action_count"], 2)
self.assertEqual(result["sequence"]["emit_count"], 2)
self.assertEqual(result["sequence"]["total_delay_ms"], 5000)
self.assertEqual(result["sequence"]["steady_blink_fallbacks"], 1)
self.assertEqual(
[item["status"] for item in result["operation_results"]],
["succeeded", "succeeded", "succeeded"],
)
sleep.assert_any_call(5.0)
self.assertNotIn(0.5, [call.args[0] for call in sleep.call_args_list])
def test_color_sequence_never_cycles_the_power_off_capability(self) -> None:
service = SelfExtensionService(SimulationGateway())
catalog = [
{
"id": capability_id,
"status": "learned",
"device_id": "third-party-light",
"device_name": "第三方氛围灯",
"card_uid": "1A53DD95",
"action": action,
"slot": slot,
}
for capability_id, action, slot in (
("on", "开启灯光", 1),
("off", "关闭灯光", 2),
("red", "红色灯光", 3),
("green", "绿色灯光", 4),
("blue", "蓝色灯光", 5),
("white", "白色灯光", 6),
)
]
plan_result = {
"model": "gpt-5.6-sol",
"summary": "按顺序显示四种颜色",
"steps": [
{"kind": "peripheral", "capability_id": "on"},
{"kind": "peripheral", "capability_id": "red"},
{"kind": "delay", "delay_ms": 5000},
{"kind": "peripheral", "capability_id": "blue"},
{"kind": "delay", "delay_ms": 5000},
{"kind": "peripheral", "capability_id": "green"},
{"kind": "delay", "delay_ms": 5000},
{"kind": "peripheral", "capability_id": "white"},
{"kind": "delay", "delay_ms": 5000},
{"kind": "peripheral", "capability_id": "off"},
],
}
with patch("cognitive_agent.application.self_extension.time.sleep") as sleep:
result = service.execute_peripheral_plan_request(
(
"打开第三方氛围灯,红灯闪烁5s后,蓝光闪烁5s后,"
"绿灯闪烁5s后,白色闪烁5s,最后关闭第三方氛围灯"
),
plan_result,
catalog,
conversation_id="conv-color-sequence-no-power-cycle",
execution_mode="hardware",
)
self.assertEqual(result["status"], "succeeded")
self.assertEqual(result["sequence"]["emit_count"], 6)
self.assertEqual(result["sequence"]["steady_blink_fallbacks"], 4)
self.assertEqual(result["sequence"]["total_delay_ms"], 20_000)
self.assertEqual(
[
operation["telemetry"]["slot"]
for operation in result["operation_results"]
if operation["telemetry"].get("slot")
],
[1, 3, 5, 4, 6, 2],
)
self.assertEqual(
[call.args[0] for call in sleep.call_args_list].count(5.0),
4,
)
self.assertNotIn(0.5, [call.args[0] for call in sleep.call_args_list])
def test_plan_only_accepts_learned_catalog_capabilities(self) -> None:
service = SelfExtensionService(SimulationGateway())
proposed = {
@@ -117,6 +363,122 @@ class SelfExtensionTests(unittest.TestCase):
self.assertEqual(result["steps"][0]["capability_id"], "cap-light-on")
self.assertEqual(result["steps"][1]["command"], "light reading all on")
def test_plan_maps_short_planner_ids_back_to_opaque_capability_ids(self) -> None:
service = SelfExtensionService(SimulationGateway())
opaque_id = "cap-b0ab02a2-3640-4468-8cdc-3bb7164c7078"
def planned_response(
_utterance: str,
peripherals: list[dict[str, object]],
_vehicle_devices: list[dict[str, object]],
_api_key: str,
) -> str:
self.assertEqual(peripherals[0]["id"], "peripheral_1")
self.assertNotIn(opaque_id, [item["id"] for item in peripherals])
return json.dumps(
{
"summary": "切换红灯",
"needs_confirmation": False,
"question": "",
"steps": [
{
"kind": "peripheral",
"capability_id": "peripheral_1",
"command": "红色灯光",
"label": "第三方氛围灯 · 红色灯光",
"delay_ms": 0,
}
],
},
ensure_ascii=False,
)
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
with patch.object(
service,
"_request_gpt_plan",
side_effect=planned_response,
):
result = service.plan(
{
"utterance": "第三方氛围灯切换红灯",
"peripherals": [
{
"id": opaque_id,
"status": "learned",
"device_name": "第三方氛围灯",
"action": "红色灯光",
"risk": "low",
}
],
"vehicle_devices": [],
}
)
self.assertEqual(result["steps"][0]["capability_id"], opaque_id)
def test_plan_recovers_full_color_sequence_when_gpt_returns_no_steps(self) -> None:
service = SelfExtensionService(SimulationGateway())
actions = (
("on", "开启灯光", 1),
("off", "关闭灯光", 2),
("red", "红色灯光", 3),
("green", "绿色灯光", 4),
("blue", "蓝色灯光", 5),
("white", "白色灯光", 6),
)
proposed = {
"summary": "模型未能生成动作",
"needs_confirmation": False,
"question": "",
"steps": [],
}
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
with patch.object(
service,
"_request_gpt_plan",
return_value=json.dumps(proposed, ensure_ascii=False),
):
result = service.plan(
{
"utterance": (
"打开第三方氛围灯,红灯闪烁5s后,蓝光闪烁5s后,"
"绿灯闪烁5s后,白色闪烁5s,最后关闭第三方氛围灯"
),
"peripherals": [
{
"id": capability_id,
"status": "learned",
"device_id": "third-party-light",
"device_name": "第三方氛围灯",
"action": action,
"aliases": [action],
"slot": slot,
"risk": "low",
}
for capability_id, action, slot in actions
],
"vehicle_devices": [],
}
)
self.assertEqual(
[
step.get("capability_id")
for step in result["steps"]
if step["kind"] == "peripheral"
],
["on", "red", "blue", "green", "white", "off"],
)
self.assertEqual(
[
step.get("delay_ms")
for step in result["steps"]
if step["kind"] == "delay"
],
[5000, 5000, 5000, 5000],
)
def test_plan_rejects_vehicle_command_outside_current_catalog(self) -> None:
service = SelfExtensionService(SimulationGateway())
proposed = {
@@ -235,6 +597,10 @@ class SelfExtensionTests(unittest.TestCase):
]["items"]["properties"]["command"]
self.assertEqual(command_schema["type"], "string")
self.assertEqual(command_schema["maxLength"], 300)
capability_schema = request_payload["text"]["format"]["schema"]["properties"][
"steps"
]["items"]["properties"]["capability_id"]
self.assertEqual(capability_schema["enum"], [""])
def test_plan_accepts_bounded_passenger_position_for_declared_ch0_executor(
self,