feat: integrate learned peripherals into mode creation

Expose safe NFC-bound infrared actions to semantic scene composition, require flashing-light actions for romantic and Mid-Autumn modes, and preserve mixed CAN/IR mode flows.

Fix NFC device reassignment and retry extension/toy MQTT publication when an active card's binding catalog changes.
This commit is contained in:
Miayui
2026-09-20 16:37:19 +08:00
parent 904f5f8a6c
commit bfff1b515c
25 changed files with 3073 additions and 102 deletions
+258
View File
@@ -15,6 +15,7 @@ from cognitive_agent.application.codex_assistant import (
CodexAssistantService,
_mode_profile_create_parts,
)
from cognitive_agent.application.scene_composer import scene_semantic_guidance
CONTROL_TERMS = ("空调", "座椅", "车窗", "遮阳帘", "氛围灯", "阅读灯", "出风口")
@@ -488,6 +489,213 @@ def test_exact_scene_template_hit_skips_codex_and_previews_a_fresh_plan() -> Non
assert events[0]["engine"] == "n7web-scene-template"
def test_exact_scene_template_keeps_native_base_and_adds_only_selected_ir_ref() -> None:
client = StructuredSceneClient()
capability_ref = "ir:0123456789abcdef01234567"
client.structured_answers.put(
json.dumps(
{
"requests": [{"capability_ref": capability_ref}],
"ambiguities": [],
"confidence": 0.93,
},
ensure_ascii=False,
)
)
assistant = CodexAssistantService(client)
template = _scene_template()
context = _scene_context()
context["peripheral_actions"] = [
{
"ref": capability_ref,
"device_label": "氛围灯带",
"action_label": "设置为红色",
"semantic_tags": ["浪漫", "红色", "灯光"],
}
]
context["device_inventory"] = [
{
"kind": "learned_infrared",
"id": "romance-light",
"label": "氛围灯带",
"action": "设置为红色",
"availability": "available",
}
]
observed: list[dict[str, object]] = []
def execute(
_instruction: str,
_progress: Callable[[dict[str, object]], None],
assistant_plan: Mapping[str, object],
) -> dict[str, object]:
observed.append(deepcopy(dict(assistant_plan)))
return _preview_result("观影模式")
result, _events = _invoke(
assistant,
"构建一个观影模式",
execute,
conversation_id="conv-scene-template-ir-augmentation",
catalog=_catalog_with_templates(template),
scene_context=context,
)
assert result["success"] is True
assert result["route"] == "scene_template"
assert len(client.structured_calls) == 1
assert not client.legacy_calls
assert observed[0]["requests"][:2] == template["plan"]["requests"]
assert observed[0]["requests"][2] == {
"domain": "self_extension",
"capability": "infrared.emit",
"target_scope": capability_ref,
"operation": "emit",
"duration_ms": None,
"parameters": [{"name": "capability_ref", "value": capability_ref}],
"reason": (
"Scene Composer selected a server-issued learned-device reference; "
"runtime binding resolution is still required."
),
}
assert "learned-device augmentation" in client.thread_instructions[0]
def _scene_context_with_flashing_light() -> dict[str, object]:
context = _scene_context()
context["peripheral_actions"] = [
{
"ref": "ir:flash-open-000000000001",
"device_label": "闪烁灯",
"object_names": ["闪烁灯", "闪光灯"],
"action_label": "打开",
"action_names": ["打开", "开启", "启动"],
"semantic_tags": ["闪烁灯", "灯光"],
},
{
"ref": "ir:flash-red-0000000000002",
"device_label": "闪烁灯",
"object_names": ["闪烁灯", "闪光灯"],
"action_label": "切换为红色",
"action_names": ["切换为红色", "设置为红色"],
"semantic_tags": ["闪烁灯", "红色", "灯光"],
},
{
"ref": "ir:flash-close-00000000003",
"device_label": "闪烁灯",
"object_names": ["闪烁灯", "闪光灯"],
"action_label": "关闭",
"action_names": ["关闭", "熄灭"],
"semantic_tags": ["闪烁灯", "灯光"],
},
]
context["device_inventory"] = [
{
"kind": "learned_infrared",
"id": "toy-light-001",
"label": "闪烁灯",
"availability": "available",
}
]
return context
def _peripheral_refs(plan: Mapping[str, object]) -> list[str]:
return [
str(parameter.get("value") or "")
for request in plan.get("requests", [])
if isinstance(request, Mapping)
and request.get("domain") == "self_extension"
for parameter in request.get("parameters", [])
if isinstance(parameter, Mapping)
and parameter.get("name") == "capability_ref"
]
def test_romantic_template_always_adds_authorized_flashing_light() -> None:
client = StructuredSceneClient()
client.structured_answers.put(
json.dumps(
{"requests": [], "ambiguities": [], "confidence": 0.82},
ensure_ascii=False,
)
)
assistant = CodexAssistantService(client)
template = _scene_template(
template_id="romantic",
name="浪漫模式",
aliases=["约会模式"],
)
observed: list[dict[str, object]] = []
def execute(
_instruction: str,
_progress: Callable[[dict[str, object]], None],
assistant_plan: Mapping[str, object],
) -> dict[str, object]:
observed.append(deepcopy(dict(assistant_plan)))
return _preview_result("浪漫模式")
result, _events = _invoke(
assistant,
"创建浪漫模式",
execute,
conversation_id="conv-romantic-required-flashing-light",
catalog=_catalog_with_templates(template),
scene_context=_scene_context_with_flashing_light(),
)
assert result["success"] is True
assert observed[0]["requests"][:2] == template["plan"]["requests"]
assert _peripheral_refs(observed[0]) == [
"ir:flash-open-000000000001",
"ir:flash-red-0000000000002",
]
assert "ir:flash-close-00000000003" not in _peripheral_refs(observed[0])
def test_mid_autumn_cloud_composition_receives_all_abilities_and_adds_flashing_light() -> None:
client = StructuredSceneClient()
assistant = CodexAssistantService(client)
observed: list[dict[str, object]] = []
scene_context = _scene_context_with_flashing_light()
def execute(
_instruction: str,
_progress: Callable[[dict[str, object]], None],
assistant_plan: Mapping[str, object],
) -> dict[str, object]:
observed.append(deepcopy(dict(assistant_plan)))
return _preview_result("中秋欢快模式")
result, _events = _invoke(
assistant,
"创建中秋欢快模式",
execute,
conversation_id="conv-mid-autumn-cloud-peripherals",
catalog=_catalog_with_templates(_scene_template()),
scene_context=scene_context,
)
assert result["success"] is True
assert result["route"] == "scene_composer"
call_context = client.structured_calls[0]["application_context"]
assert len(call_context["capabilities"]) == len(scene_context["capabilities"])
assert len(call_context["peripheral_actions"]) == 3
assert call_context["scene_semantics"]["family"] == "festival_celebration"
assert call_context["scene_semantics"]["peripheral_policy"][
"required_refs"
] == [
"ir:flash-open-000000000001",
"ir:flash-red-0000000000002",
]
assert "as many relevant" in client.thread_instructions[0]
assert _peripheral_refs(observed[0]) == [
"ir:flash-open-000000000001",
"ir:flash-red-0000000000002",
]
def test_scene_template_miss_falls_through_to_structured_scene_composer() -> None:
client = StructuredSceneClient()
assistant = CodexAssistantService(client)
@@ -517,6 +725,44 @@ def test_scene_template_miss_falls_through_to_structured_scene_composer() -> Non
assert events[0]["engine"] == "codex-app-server-scene-composer"
def test_learning_mode_uses_grounded_free_composition_without_device_names() -> None:
client = StructuredSceneClient()
assistant = CodexAssistantService(client)
observed: list[dict[str, object]] = []
def execute(
instruction: str,
_progress: Callable[[dict[str, object]], None],
assistant_plan: Mapping[str, object],
) -> dict[str, object]:
assert instruction == "创建学习模式"
observed.append(deepcopy(dict(assistant_plan)))
return _preview_result("学习模式")
result, _events = _invoke(
assistant,
"创建学习模式",
execute,
conversation_id="conv-learning-mode-free-composition",
catalog=_catalog_with_templates(_scene_template()),
scene_context=_scene_context(),
)
assert result["success"] is True
assert result["route"] == "scene_composer"
assert len(client.structured_calls) == 1
context = client.structured_calls[0]["application_context"]
semantics = context["scene_semantics"]
assert semantics["family"] == "focused_learning"
assert semantics["grounded"] is True
assert "climate.set_temperature" in semantics["preferred_capability_ids"]
output_schema = client.structured_calls[0]["output_schema"]
assert output_schema["properties"]["requests"]["minItems"] == 1
assert output_schema["properties"]["ambiguities"]["maxItems"] == 0
assert "学习/自习/专注" in client.thread_instructions[0]
assert observed and observed[0]["requests"]
def test_registered_mode_match_takes_precedence_over_same_named_template() -> None:
client = StructuredSceneClient()
assistant = CodexAssistantService(client)
@@ -856,6 +1102,18 @@ def test_unknown_mode_uses_one_structured_turn_then_local_dry_run() -> None:
"unresolved_clauses": ["构建一个星夜模式"],
"cloud_input_utterance": "构建一个星夜模式",
}
expected_semantics = scene_semantic_guidance("星夜模式")
expected_semantics["peripheral_policy"] = {
"available_action_count": 0,
"required_refs": [],
"selection_goal": (
"Include as many semantically relevant scene-safe peripheral devices "
"as are useful; normally choose activation plus at most one stable "
"state action per device."
),
"required_reason": "No peripheral action is mandatory for this name.",
}
expected_context["scene_semantics"] = expected_semantics
assert call["application_context"] == expected_context
assert call["output_schema"]["type"] == "object"
assert call["output_schema"]["additionalProperties"] is False