feat(scenes): add deterministic romantic cinema template

This commit is contained in:
Miayui
2026-09-21 12:07:49 +08:00
parent b3bd90f748
commit ad0c1f4ed1
8 changed files with 453 additions and 23 deletions
@@ -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",
+122 -16
View File
@@ -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,9 +1445,22 @@ 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(
profile.name,
profile.instruction,
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(
@@ -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,