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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user