feat(scenes): add deterministic romantic cinema template
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
id: romantic-cinema
|
||||
name: 浪漫影院模式
|
||||
aliases: [浪漫观影模式, 浪漫电影模式, 约会影院模式]
|
||||
description: 完整保留观影模式的座椅、温控、遮光和车内灯光功能,并在运行时确定性加入当前已授权的外设灯光控制。
|
||||
icon: heart
|
||||
operations:
|
||||
- {domain: seat, capability: seat.set_driver_massage_native, target_scope: driver, operation: set, parameters: {action: mode, mode: relax}, reason: 完整保留影院模式的主驾放松按摩;不移动主驾座椅。}
|
||||
- {domain: seat, capability: seat.set_ventilation, target_scope: passenger, operation: set, parameters: {state: "on", level: 1}, reason: 完整保留影院模式的副驾低档通风。}
|
||||
- {domain: seat, capability: seat.set_backrest_recline, target_scope: passenger, operation: set, parameters: {position_pct: 55}, reason: 完整保留影院模式的副驾靠背姿态。}
|
||||
- {domain: seat, capability: seat.set_legrest_height, target_scope: passenger, operation: set, parameters: {position_pct: 45}, reason: 完整保留影院模式的副驾腿部支撑。}
|
||||
- {domain: climate, capability: climate.set_power, target_scope: vehicle, operation: set, parameters: {state: "on"}, reason: 完整保留影院模式的座舱恒温。}
|
||||
- {domain: climate, capability: climate.set_temperature, target_scope: vehicle, operation: set, parameters: {temperature_c: 24}, reason: 完整保留影院模式的舒适中性温度。}
|
||||
- {domain: climate, capability: climate.set_fan_level, target_scope: vehicle, operation: set, parameters: {level: 1}, reason: 完整保留影院模式的低噪声风量。}
|
||||
- {domain: roof, capability: roof.set_shade_position, target_scope: vehicle, operation: set, parameters: {position_pct: 0}, reason: 完整保留影院模式的遮光设置。}
|
||||
- {domain: ambient_light, capability: ambient_light.set_mode, target_scope: vehicle, operation: set, parameters: {mode: cinema}, reason: 完整保留影院模式的车内影院氛围。}
|
||||
- {domain: interior_light, capability: interior_light.set_indoor_power, target_scope: vehicle, operation: set, parameters: {state: "off"}, reason: 完整保留影院模式的室内主照明设置。}
|
||||
- {domain: interior_light, capability: interior_light.set_reading_power, target_scope: all, operation: set, parameters: {state: "off"}, reason: 完整保留影院模式的阅读灯设置。}
|
||||
@@ -48,7 +48,10 @@ from cognitive_agent.application.scene_composer import (
|
||||
scene_required_peripheral_refs,
|
||||
scene_semantic_guidance,
|
||||
)
|
||||
from cognitive_agent.application.scene_templates import normalize_scene_template_name
|
||||
from cognitive_agent.application.scene_templates import (
|
||||
PRESERVE_BASE_EXTERNAL_LIGHT_TEMPLATE_IDS,
|
||||
normalize_scene_template_name,
|
||||
)
|
||||
from cognitive_agent.application.schedule_intent import is_schedule_stop, needs_mode_schedule
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
@@ -1034,15 +1037,53 @@ class CodexAssistantService:
|
||||
if mode_parts
|
||||
else None
|
||||
)
|
||||
scene_template_plan = (
|
||||
candidate_scene_template_plan = (
|
||||
_catalog_exact_scene_template_plan(catalog, mode_parts[0])
|
||||
if mode_parts and not mode_parts[1] and exact_mode_matches == 0
|
||||
if mode_parts and not mode_parts[1]
|
||||
else None
|
||||
)
|
||||
candidate_scene_template_metadata = (
|
||||
candidate_scene_template_plan.get("metadata")
|
||||
if isinstance(candidate_scene_template_plan, Mapping)
|
||||
else None
|
||||
)
|
||||
candidate_scene_template_id = (
|
||||
str(candidate_scene_template_metadata.get("template_id") or "")
|
||||
if isinstance(candidate_scene_template_metadata, Mapping)
|
||||
else ""
|
||||
)
|
||||
scene_template_plan = (
|
||||
candidate_scene_template_plan
|
||||
if exact_mode_matches == 0
|
||||
or candidate_scene_template_id
|
||||
in PRESERVE_BASE_EXTERNAL_LIGHT_TEMPLATE_IDS
|
||||
else None
|
||||
)
|
||||
scene_template_metadata = (
|
||||
scene_template_plan.get("metadata")
|
||||
if isinstance(scene_template_plan, Mapping)
|
||||
else None
|
||||
)
|
||||
preserve_base_external_lights = bool(
|
||||
isinstance(scene_template_metadata, Mapping)
|
||||
and str(scene_template_metadata.get("template_id") or "")
|
||||
in PRESERVE_BASE_EXTERNAL_LIGHT_TEMPLATE_IDS
|
||||
)
|
||||
if preserve_base_external_lights:
|
||||
# The template already owns every native cinema action. Do not let
|
||||
# generic romantic-scene defaults replace or append native actions;
|
||||
# only the deterministic, NFC-authorized light refs may be added.
|
||||
scene_semantics = scene_context.get("scene_semantics")
|
||||
if isinstance(scene_semantics, dict):
|
||||
scene_semantics["required_native_domains"] = []
|
||||
scene_semantics["required_native_controls"] = []
|
||||
scene_semantics["native_fallback_requests"] = []
|
||||
scene_semantics["default_control_bundle_applied"] = False
|
||||
dynamic_scene_request = bool(mode_parts and needs_mode_schedule(message))
|
||||
scene_peripheral_actions = scene_context.get("peripheral_actions", [])
|
||||
template_augmentation_preview = bool(
|
||||
scene_template_plan is not None
|
||||
and not preserve_base_external_lights
|
||||
and not dynamic_scene_request
|
||||
and isinstance(scene_peripheral_actions, list)
|
||||
and scene_peripheral_actions
|
||||
|
||||
@@ -33,6 +33,10 @@ _DOCUMENT_FIELDS = frozenset(
|
||||
{"id", "name", "aliases", "description", "icon", "operations", "schedule"}
|
||||
)
|
||||
|
||||
# These templates keep their repository-backed native plan byte-for-byte and
|
||||
# only add currently authorized external-light references at request time.
|
||||
PRESERVE_BASE_EXTERNAL_LIGHT_TEMPLATE_IDS = frozenset({"romantic-cinema"})
|
||||
|
||||
|
||||
def normalize_scene_template_name(value: object) -> str:
|
||||
"""Normalize an exact user-facing name without enabling fuzzy matching."""
|
||||
@@ -289,6 +293,7 @@ class SceneTemplateRegistry:
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PRESERVE_BASE_EXTERNAL_LIGHT_TEMPLATE_IDS",
|
||||
"SceneTemplate",
|
||||
"SceneTemplateRegistry",
|
||||
"normalize_scene_template_name",
|
||||
|
||||
@@ -116,7 +116,10 @@ from cognitive_agent.application.native_can_runtime import ( # noqa: E402
|
||||
NativeCanExecutor,
|
||||
NativeCanRuntime,
|
||||
)
|
||||
from cognitive_agent.application.scene_templates import SceneTemplateRegistry # noqa: E402
|
||||
from cognitive_agent.application.scene_templates import ( # noqa: E402
|
||||
PRESERVE_BASE_EXTERNAL_LIGHT_TEMPLATE_IDS,
|
||||
SceneTemplateRegistry,
|
||||
)
|
||||
from cognitive_agent.application.schedule_intent import ( # noqa: E402
|
||||
is_schedule_stop,
|
||||
needs_mode_schedule,
|
||||
@@ -745,14 +748,14 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
merged[existing] = action
|
||||
return merged
|
||||
|
||||
def _n7_cinema_base_scene(
|
||||
def _n7_cinema_base_profile(
|
||||
self,
|
||||
incoming_scene: Mapping[str, Any],
|
||||
) -> dict[str, Any] | None:
|
||||
) -> ModeProfile | None:
|
||||
if self._is_n7_cinema_base_scene(incoming_scene):
|
||||
return None
|
||||
vehicle_id = self.vehicle_profiles.default().id
|
||||
candidates: list[tuple[int, dict[str, Any]]] = []
|
||||
candidates: list[tuple[int, ModeProfile]] = []
|
||||
for public_profile in self.mode_profiles.public_modes(vehicle_id):
|
||||
try:
|
||||
profile = self.mode_profiles.get_profile(
|
||||
@@ -774,12 +777,26 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
):
|
||||
continue
|
||||
priority = 0 if str(source_scene.get("id") or "") == "cinema" else 1
|
||||
candidates.append((priority, deepcopy(dict(source_scene))))
|
||||
candidates.append((priority, profile))
|
||||
if not candidates:
|
||||
return None
|
||||
candidates.sort(key=lambda item: (item[0], str(item[1].get("id") or "")))
|
||||
candidates.sort(key=lambda item: (item[0], item[1].id))
|
||||
return candidates[0][1]
|
||||
|
||||
def _n7_cinema_base_scene(
|
||||
self,
|
||||
incoming_scene: Mapping[str, Any],
|
||||
) -> dict[str, Any] | None:
|
||||
profile = self._n7_cinema_base_profile(incoming_scene)
|
||||
if profile is None:
|
||||
return None
|
||||
source_scene = (
|
||||
profile.command_flow.get("source_scene")
|
||||
if isinstance(profile.command_flow, Mapping)
|
||||
else None
|
||||
)
|
||||
return deepcopy(dict(source_scene)) if isinstance(source_scene, Mapping) else None
|
||||
|
||||
@staticmethod
|
||||
def _is_n7_cinema_video_launch_action(action: Mapping[str, Any]) -> bool:
|
||||
parameters = action.get("parameters")
|
||||
@@ -1428,10 +1445,23 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
|
||||
requests = self.mode_profiles.requests_for_profiles([profile])
|
||||
if profile.sync_origin != MQTT_MODE_SCENE_INGRESS_TOPIC:
|
||||
transfer_actions = self._local_cinema_n7_transfer_actions(
|
||||
stored_transfer_actions = (
|
||||
profile.command_flow.get("n7_transfer_actions")
|
||||
if isinstance(profile.command_flow, Mapping)
|
||||
else None
|
||||
)
|
||||
transfer_actions = (
|
||||
[
|
||||
deepcopy(dict(action))
|
||||
for action in stored_transfer_actions[:256]
|
||||
if isinstance(action, Mapping)
|
||||
]
|
||||
if isinstance(stored_transfer_actions, list)
|
||||
else self._local_cinema_n7_transfer_actions(
|
||||
profile.name,
|
||||
profile.instruction,
|
||||
)
|
||||
)
|
||||
transfer_selectors = {
|
||||
self._n7_transfer_operation_selector(
|
||||
action.get("domain"),
|
||||
@@ -2020,8 +2050,22 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
raise ValueError("动态模式更新必须保留完整时间轴,请按阶段重新生成预览。")
|
||||
if schedule is not None:
|
||||
operations = [deepcopy(op) for phase in schedule.phases for op in phase.operations]
|
||||
source_template_id = (
|
||||
str(source_draft.get("source_template_id") or "").strip()
|
||||
if isinstance(source_draft, Mapping)
|
||||
else ""
|
||||
)
|
||||
preview_transfer_candidates = source_result.get("n7_transfer_candidates")
|
||||
n7_transfer_actions = (
|
||||
self._local_cinema_n7_transfer_actions(
|
||||
[
|
||||
deepcopy(dict(action))
|
||||
for action in preview_transfer_candidates[:256]
|
||||
if isinstance(action, Mapping)
|
||||
]
|
||||
if schedule is None
|
||||
and source_template_id in PRESERVE_BASE_EXTERNAL_LIGHT_TEMPLATE_IDS
|
||||
and isinstance(preview_transfer_candidates, list)
|
||||
else self._local_cinema_n7_transfer_actions(
|
||||
clean_name,
|
||||
clean_instruction,
|
||||
)
|
||||
@@ -6966,13 +7010,34 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
mode_name,
|
||||
vehicle_id=vehicle_profile.id,
|
||||
)
|
||||
if scene_template is not None and existing_profile is not None:
|
||||
preserve_base_external_lights = bool(
|
||||
scene_template is not None
|
||||
and scene_template.id in PRESERVE_BASE_EXTERNAL_LIGHT_TEMPLATE_IDS
|
||||
)
|
||||
if (
|
||||
scene_template is not None
|
||||
and existing_profile is not None
|
||||
and not preserve_base_external_lights
|
||||
):
|
||||
raise ValueError("同名功能模式已创建,请重新提交以载入最新模式。")
|
||||
source_profile = (
|
||||
existing_profile
|
||||
if not requested_instruction and source_draft is None
|
||||
and not preserve_base_external_lights
|
||||
else None
|
||||
)
|
||||
if (
|
||||
source_profile is None
|
||||
and source_draft is None
|
||||
and not requested_instruction
|
||||
and preserve_base_external_lights
|
||||
):
|
||||
source_profile = self._n7_cinema_base_profile(
|
||||
{
|
||||
"id": scene_template.id,
|
||||
"name": mode_name,
|
||||
}
|
||||
)
|
||||
if source_profile is None and source_draft is None and not requested_instruction and scene_template is None:
|
||||
source_name = mode_creation_source_name(mode_name)
|
||||
if source_name != mode_name:
|
||||
@@ -7000,7 +7065,18 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
# of appending a model prompt to the user's utterance. The latter was
|
||||
# being parsed as a second control clause on the assistant's local fast
|
||||
# path, turning a valid nine-action mode into an incomplete draft.
|
||||
source_utterance = requested_instruction or f"进入{draft_mode_name}"
|
||||
draft_instruction = requested_instruction or f"进入{draft_mode_name}"
|
||||
inherited_n7_cinema_profile = bool(
|
||||
source_profile is not None
|
||||
and scene_template is not None
|
||||
and scene_template.id in PRESERVE_BASE_EXTERNAL_LIGHT_TEMPLATE_IDS
|
||||
and source_profile.sync_origin == MQTT_MODE_SCENE_INGRESS_TOPIC
|
||||
)
|
||||
source_utterance = (
|
||||
f"进入{source_profile.name}"
|
||||
if inherited_n7_cinema_profile
|
||||
else draft_instruction
|
||||
)
|
||||
if requested_instruction and selected_seats is not None and len(selected_seats) == 1:
|
||||
# Qualify only explicit seat-device clauses, never an HVAC/window
|
||||
# zone or unrelated control. General one-shot controls keep their
|
||||
@@ -7056,7 +7132,19 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
self._persist(result)
|
||||
return result
|
||||
try:
|
||||
if scene_template is not None and selected_seats is not None:
|
||||
preserve_unscoped_template_seats = bool(
|
||||
scene_template is not None
|
||||
and scene_template.id in PRESERVE_BASE_EXTERNAL_LIGHT_TEMPLATE_IDS
|
||||
and resolve_explicit_seat_scopes(
|
||||
utterance,
|
||||
assume_seat_context=True,
|
||||
) is None
|
||||
)
|
||||
if (
|
||||
scene_template is not None
|
||||
and selected_seats is not None
|
||||
and not preserve_unscoped_template_seats
|
||||
):
|
||||
assistant_plan = project_seed_plan(assistant_plan, selected_seats)
|
||||
source_result = self._process_once(
|
||||
source_utterance,
|
||||
@@ -7070,7 +7158,9 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
direct_mode_profile_id=(
|
||||
source_profile.id if source_profile is not None else None
|
||||
),
|
||||
mode_creation_scopes=selected_seats,
|
||||
mode_creation_scopes=(
|
||||
None if preserve_unscoped_template_seats else selected_seats
|
||||
),
|
||||
assistant_plan=assistant_plan,
|
||||
# ``source_utterance`` is synthesized for a name-only, unknown
|
||||
# scene. It must not fall back to substring matching (for example
|
||||
@@ -7086,7 +7176,11 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
# already passed the ordinary text/target authorization path.
|
||||
# Its old display name cannot veto an explicitly requested new
|
||||
# seat. This override is internal, never accepted from payloads.
|
||||
scene_scope_utterance="" if source_draft is not None else utterance,
|
||||
scene_scope_utterance=(
|
||||
""
|
||||
if source_draft is not None or preserve_unscoped_template_seats
|
||||
else utterance
|
||||
),
|
||||
)
|
||||
except ModeCreationSeatPolicyError as exc:
|
||||
return self._mode_creation_seat_failure(utterance, conversation_id, vehicle_profile, str(exc))
|
||||
@@ -7219,6 +7313,16 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
typed_source_actions = [
|
||||
action for action in source_actions if isinstance(action, dict)
|
||||
] if isinstance(source_actions, list) else []
|
||||
if (
|
||||
inherited_n7_cinema_profile
|
||||
and source_result.get("status") == "partially_succeeded"
|
||||
and typed_source_actions
|
||||
and source_result.get("n7_transfer_candidates")
|
||||
):
|
||||
# N7-only actions are intentionally deferred during a dry-run. They
|
||||
# remain part of the received cinema base and are persisted as
|
||||
# transfer actions, so their presence is not a partial draft.
|
||||
source_result["status"] = "succeeded"
|
||||
if (
|
||||
source_result.get("status") == "succeeded"
|
||||
and generation_source
|
||||
@@ -7228,6 +7332,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
"scene_template_copy",
|
||||
}
|
||||
and assistant_plan is not None
|
||||
and not inherited_n7_cinema_profile
|
||||
and (
|
||||
self._scene_plan_signatures(assistant_plan)
|
||||
!= self._scene_action_signatures(typed_source_actions)
|
||||
@@ -7372,9 +7477,10 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
if source_draft is not None
|
||||
else 0
|
||||
)
|
||||
source_result["utterance"] = draft_instruction
|
||||
source_result["mode_profile_draft"] = {
|
||||
"name": draft_mode_name,
|
||||
"instruction": source_utterance,
|
||||
"instruction": draft_instruction,
|
||||
"source_request_id": source_request_id,
|
||||
"vehicle_id": vehicle_profile.id,
|
||||
"domains": domains,
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -65,6 +65,204 @@ def _learned_light_power_on() -> dict[str, object]:
|
||||
}
|
||||
|
||||
|
||||
def _received_cinema_scene() -> dict[str, object]:
|
||||
actions = [
|
||||
{
|
||||
"type": "window_position",
|
||||
"at_ms": 0,
|
||||
"domain": "window",
|
||||
"capability": "window.set_position",
|
||||
"target_scope": "front_left",
|
||||
"operation": "set",
|
||||
"parameters": {"position_pct": 0},
|
||||
},
|
||||
{
|
||||
"type": "climate_temperature",
|
||||
"at_ms": 0,
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_temperature",
|
||||
"target_scope": "vehicle",
|
||||
"operation": "set",
|
||||
"parameters": {"temperature_c": 25},
|
||||
},
|
||||
{
|
||||
"type": "seat_preset",
|
||||
"at_ms": 0,
|
||||
"domain": "local_vehicle",
|
||||
"capability": "local_scene.seat_preset",
|
||||
"target_scope": "vehicle",
|
||||
"operation": "set",
|
||||
"parameters": {"targets_pulse": [246, 112, 405, 342]},
|
||||
},
|
||||
{
|
||||
"type": "android_app_launch",
|
||||
"at_ms": 3000,
|
||||
"domain": "android",
|
||||
"capability": "android.launch_app",
|
||||
"target_scope": "headunit",
|
||||
"operation": "launch",
|
||||
"parameters": {
|
||||
"package_name": "com.byd.videoplay",
|
||||
"activity_name": "com.byd.videoplay.activity.WelComeActivity",
|
||||
},
|
||||
},
|
||||
]
|
||||
return {
|
||||
"id": "cinema",
|
||||
"label": "影院模式",
|
||||
"name": "影院模式",
|
||||
"description": "接收到的车机影院模式。",
|
||||
"icon": "film",
|
||||
"aliases": ["观影模式"],
|
||||
"origin": "headunit",
|
||||
"deletable": True,
|
||||
"behavior": "static",
|
||||
"domains": ["window", "climate", "local_vehicle", "android"],
|
||||
"operation_count": len(actions),
|
||||
"scope": {"common": False, "vehicle_ids": ["denza_n7"]},
|
||||
"profile_revision": "received-cinema-v1",
|
||||
"actions": actions,
|
||||
}
|
||||
|
||||
|
||||
def test_romantic_cinema_template_preserves_cinema_and_adds_authorized_light(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service = SeatDemoService(REPOSITORY_ROOT)
|
||||
service.artifact_root = tmp_path / "runs"
|
||||
service.mode_profiles = ModeProfileRegistry(
|
||||
REPOSITORY_ROOT / "config" / "modes",
|
||||
user_root=tmp_path / "modes",
|
||||
)
|
||||
extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=tmp_path / "infrared.sqlite3",
|
||||
)
|
||||
extension.sync_peripheral_catalog([_learned_light_power_on()])
|
||||
received_cinema = _received_cinema_scene()
|
||||
service.import_n7_mode_scene(received_cinema)
|
||||
stale_profile = service.mode_profiles.save_mode(
|
||||
"浪漫影院模式",
|
||||
[
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_temperature",
|
||||
"target_scope": "vehicle",
|
||||
"operation": "set",
|
||||
"parameters": {"temperature_c": 30},
|
||||
}
|
||||
],
|
||||
vehicle_id="denza_n7",
|
||||
instruction="进入浪漫影院模式",
|
||||
)
|
||||
catalog = AssistantCapabilityCatalog(
|
||||
service,
|
||||
peripheral_provider=extension.list_peripheral_routing_capabilities,
|
||||
)
|
||||
handler = object.__new__(SeatDemoHandler)
|
||||
handler.service = service
|
||||
handler.extension = extension
|
||||
handler.nfc = SimpleNamespace(snapshot=lambda: {"card": None})
|
||||
handler.vehicle_display_sync = VehicleDisplaySync()
|
||||
client = MagicMock()
|
||||
client.available = True
|
||||
assistant = CodexAssistantService(client)
|
||||
snapshot = catalog.snapshot(vehicle_id="denza_n7")
|
||||
result: dict[str, object] = {}
|
||||
|
||||
def execute_control(
|
||||
instruction: str,
|
||||
progress,
|
||||
assistant_plan: dict[str, object],
|
||||
) -> dict[str, object]:
|
||||
nonlocal result
|
||||
arguments = {
|
||||
"utterance": instruction,
|
||||
"provider": "local",
|
||||
"conversation_id": "conv-romantic-cinema-template",
|
||||
"execution_mode": "hardware",
|
||||
"vehicle_id": "denza_n7",
|
||||
"simulation_fault": "normal",
|
||||
"hardware_access_bypass": True,
|
||||
"reasoning_effort": "low",
|
||||
"control_transport": "wired_can",
|
||||
"assistant_plan": assistant_plan,
|
||||
}
|
||||
result = handler._preview_mode_with_scene_peripherals(
|
||||
"/api/requests/preview",
|
||||
arguments,
|
||||
assistant_plan,
|
||||
on_commands_validated=progress,
|
||||
)
|
||||
return result
|
||||
|
||||
try:
|
||||
with patch.object(
|
||||
extension,
|
||||
"execute_gateway",
|
||||
side_effect=AssertionError("mode creation must not emit infrared"),
|
||||
):
|
||||
response = assistant.stream(
|
||||
"创建浪漫影院模式",
|
||||
conversation_id="conv-romantic-cinema-template",
|
||||
principal="romantic-cinema-template-test",
|
||||
history=[],
|
||||
emit=lambda _event: True,
|
||||
execute_control=execute_control,
|
||||
capability_snapshot=snapshot,
|
||||
capability_context=catalog.compact_context(snapshot),
|
||||
scene_capability_context=catalog.scene_composition_context(snapshot),
|
||||
control_terms=catalog.control_terms(snapshot),
|
||||
)
|
||||
|
||||
assert response["success"] is True
|
||||
assert response["route"] == "scene_template"
|
||||
client.start_structured_turn.assert_not_called()
|
||||
client.start_turn.assert_not_called()
|
||||
assert result["status"] == "succeeded"
|
||||
assert result["mode_profile_draft"]["generation_source"] == "scene_template_copy"
|
||||
assert result["mode_profile_draft"]["existing_mode_id"] == stale_profile.id
|
||||
assert result["mode_profile_draft"]["operation_count"] == 3
|
||||
|
||||
native_actions = {
|
||||
(
|
||||
action["capability"],
|
||||
action["target"].get("instance") or action["target"].get("zone"),
|
||||
): action["parameters"]
|
||||
for action in result["intent_ir"]["spec"]["actions"]
|
||||
if action["domain"] != "self_extension"
|
||||
}
|
||||
assert native_actions == {
|
||||
(action["capability"], action["target_scope"]): action["parameters"]
|
||||
for action in received_cinema["actions"]
|
||||
if action["domain"] in {"window", "climate"}
|
||||
}
|
||||
assert result["n7_transfer_candidates"] == received_cinema["actions"][2:]
|
||||
assert [action["slot"] for action in result["infrared_actions"]] == [1]
|
||||
|
||||
draft = result["mode_profile_draft"]
|
||||
saved = service.save_mode_profile_card(
|
||||
stale_profile.id,
|
||||
draft["name"],
|
||||
draft["instruction"],
|
||||
draft["source_request_id"],
|
||||
draft["vehicle_id"],
|
||||
)
|
||||
profile = service.get_mode_profile_card(saved["mode_profile"]["id"], "denza_n7")
|
||||
saved_selectors = {
|
||||
(operation.capability, operation.target_scope)
|
||||
for operation in profile.operations
|
||||
}
|
||||
assert saved_selectors == {
|
||||
(action["capability"], action["target_scope"])
|
||||
for action in received_cinema["actions"]
|
||||
} | {("infrared.emit", "romance-light")}
|
||||
_local_requests, transfer_actions = service._mode_requests_and_n7_transfers(profile)
|
||||
assert transfer_actions == received_cinema["actions"][2:]
|
||||
finally:
|
||||
service.close()
|
||||
|
||||
|
||||
def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
@@ -165,7 +165,7 @@ def test_all_production_scene_templates_complete_the_full_local_dry_run(
|
||||
templates = snapshot["scene_templates"]
|
||||
|
||||
assert isinstance(templates, list)
|
||||
assert len(templates) == 16
|
||||
assert len(templates) == 17
|
||||
assert all(template["assistant_callable"] is True for template in templates)
|
||||
|
||||
with patch.object(
|
||||
@@ -211,7 +211,9 @@ def test_all_production_scene_templates_complete_the_full_local_dry_run(
|
||||
else:
|
||||
expected_requests = [
|
||||
request for request in expected_requests
|
||||
if request["domain"] != "seat" or request["target_scope"] == "passenger"
|
||||
if template["id"] == "romantic-cinema"
|
||||
or request["domain"] != "seat"
|
||||
or request["target_scope"] == "passenger"
|
||||
]
|
||||
assert draft["operation_count"] == len(expected_requests)
|
||||
expected = {
|
||||
@@ -246,6 +248,23 @@ def test_all_production_scene_templates_complete_the_full_local_dry_run(
|
||||
assert not list(user_root.rglob("*.yaml"))
|
||||
|
||||
|
||||
def test_romantic_cinema_template_is_an_exact_copy_of_all_cinema_functions(
|
||||
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
||||
) -> None:
|
||||
service, _catalog, _user_root = hybrid_service
|
||||
cinema = service.scene_templates.get_template("cinema")
|
||||
romantic_cinema = service.scene_templates.get_template("romantic-cinema")
|
||||
|
||||
assert len(cinema.operations) == len(romantic_cinema.operations) == 11
|
||||
assert [
|
||||
(operation.domain, operation.capability, operation.target_scope, operation.parameters)
|
||||
for operation in romantic_cinema.operations
|
||||
] == [
|
||||
(operation.domain, operation.capability, operation.target_scope, operation.parameters)
|
||||
for operation in cinema.operations
|
||||
]
|
||||
|
||||
|
||||
def test_viewing_template_seeds_a_complete_unbound_preview_without_codex(
|
||||
hybrid_service: tuple[SeatDemoService, AssistantCapabilityCatalog, Path],
|
||||
) -> None:
|
||||
|
||||
@@ -372,7 +372,7 @@ def test_common_scene_templates_are_read_only_callable_catalog_entries(
|
||||
templates = snapshot["scene_templates"]
|
||||
|
||||
assert snapshot["source_status"]["scene_templates"] == "ready"
|
||||
assert len(templates) == 16
|
||||
assert len(templates) == 17
|
||||
assert {item["name"] for item in templates} >= {
|
||||
"观影模式",
|
||||
"露营模式",
|
||||
|
||||
@@ -701,6 +701,50 @@ def test_romantic_template_always_adds_authorized_decorative_lights() -> None:
|
||||
assert "ir:flash-close-00000000003" not in _peripheral_refs(observed[0])
|
||||
|
||||
|
||||
def test_romantic_cinema_template_keeps_native_base_and_adds_lights_without_model() -> None:
|
||||
client = StructuredSceneClient()
|
||||
assistant = CodexAssistantService(client)
|
||||
template = _scene_template(
|
||||
template_id="romantic-cinema",
|
||||
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-cinema-deterministic",
|
||||
catalog=_catalog_with_templates(template),
|
||||
scene_context=_scene_context_with_flashing_light(),
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["route"] == "scene_template"
|
||||
assert client.structured_calls == []
|
||||
assert client.legacy_calls == []
|
||||
native_requests = [
|
||||
request
|
||||
for request in observed[0]["requests"]
|
||||
if request.get("domain") != "self_extension"
|
||||
]
|
||||
assert native_requests == template["plan"]["requests"]
|
||||
assert _peripheral_refs(observed[0]) == [
|
||||
"ir:flash-open-000000000001",
|
||||
"ir:ambient-open-0000000001",
|
||||
"ir:strip-open-000000000001",
|
||||
]
|
||||
|
||||
|
||||
def test_mid_autumn_cloud_composition_adds_required_native_and_external_controls() -> None:
|
||||
client = StructuredSceneClient()
|
||||
assistant = CodexAssistantService(client)
|
||||
|
||||
Reference in New Issue
Block a user