feat: make mode creation preview-only and cache-safe

This commit is contained in:
Miayui
2026-09-20 19:32:03 +08:00
parent bfff1b515c
commit 89707a42e2
28 changed files with 1375 additions and 165 deletions
@@ -28,6 +28,10 @@ from cognitive_agent.application.mode_profile_intent import (
parse_mode_profile_delete,
parse_mode_profile_update,
)
from cognitive_agent.application.mode_creation_seat_policy import (
ModeCreationSeatPolicyError,
project_seed_plan,
)
from cognitive_agent.application.mode_schedule import ModeSchedule
from cognitive_agent.application.mode_schedule_service import public_schedule_status
from cognitive_agent.application.scene_composer import (
@@ -39,6 +43,8 @@ from cognitive_agent.application.scene_composer import (
build_scene_output_schema,
normalize_scene_output,
scene_context_for_mode_creation,
scene_has_explicit_control_object,
scene_required_native_fallbacks,
scene_required_peripheral_refs,
scene_semantic_guidance,
)
@@ -963,9 +969,40 @@ class CodexAssistantService:
)
if str(capability_id) in live_scene_capabilities
]
include_default_bundle = (
not needs_mode_schedule(message)
and not scene_has_explicit_control_object(
message,
scene_context,
)
)
required_peripheral_refs = scene_required_peripheral_refs(
mode_parts[0],
scene_context,
include_default_bundle=include_default_bundle,
)
required_native_fallbacks = scene_required_native_fallbacks(
mode_parts[0],
scene_context,
include_default_bundle=include_default_bundle,
)
semantic_guidance["required_native_domains"] = list(dict.fromkeys(
str(item.get("domain") or "")
for item in required_native_fallbacks
if str(item.get("domain") or "")
))
semantic_guidance["required_native_controls"] = [
{
"capability": str(item.get("capability") or ""),
"target_scope": str(item.get("target_scope") or ""),
}
for item in required_native_fallbacks
]
semantic_guidance["native_fallback_requests"] = (
required_native_fallbacks
)
semantic_guidance["default_control_bundle_applied"] = (
include_default_bundle
)
raw_peripheral_actions = scene_context.get("peripheral_actions", [])
semantic_guidance["peripheral_policy"] = {
@@ -981,8 +1018,11 @@ class CodexAssistantService:
"one stable state action per device."
),
"required_reason": (
"Romantic and Mid-Autumn related scenes must include the "
"authorized flashing-light control."
"Name-only scenes must include the available authorized "
"flashing-light, ambient-light and light-strip controls."
if include_default_bundle and required_peripheral_refs
else "Romantic and Mid-Autumn related scenes must include the "
"authorized flashing-light and light-strip controls."
if required_peripheral_refs
else "No peripheral action is mandatory for this name."
),
@@ -1430,7 +1470,7 @@ class CodexAssistantService:
*,
maximum_requests: int = MAX_PLAN_REQUESTS,
) -> dict[str, object]:
"""Add mandatory opaque peripheral refs without exposing binding data."""
"""Enforce required native domains and opaque peripheral references."""
normalized = cls._copy_scene_plan(plan)
requests = normalized.get("requests")
@@ -1452,10 +1492,131 @@ class CodexAssistantService:
}
required_refs = cls._required_scene_peripheral_refs(scene_context)
missing_refs = [ref for ref in required_refs if ref not in existing_refs]
if len(requests) + len(missing_refs) > maximum_requests:
raise SceneCompositionError(
"required peripheral actions exceed the scene action limit"
semantics = scene_context.get("scene_semantics")
raw_native_fallbacks = (
semantics.get("native_fallback_requests", [])
if isinstance(semantics, Mapping)
else []
)
native_fallbacks = [
deepcopy(dict(item))
for item in (
raw_native_fallbacks
if isinstance(raw_native_fallbacks, Sequence)
and not isinstance(raw_native_fallbacks, (str, bytes, bytearray))
else []
)
if isinstance(item, Mapping)
]
flexible_native_values = {
"climate.set_temperature",
"climate.set_fan_level",
"climate.set_cycle_mode",
"roof.set_shade_position",
"seat.set_backrest_recline",
"seat.set_legrest_height",
}
def parameter_map(request: Mapping[str, object]) -> dict[str, object]:
raw_parameters = request.get("parameters", [])
return {
str(parameter.get("name") or ""): parameter.get("value")
for parameter in (
raw_parameters
if isinstance(raw_parameters, Sequence)
and not isinstance(raw_parameters, (str, bytes, bytearray))
else []
)
if isinstance(parameter, Mapping)
and str(parameter.get("name") or "")
}
def native_requirement_satisfied(
request: Mapping[str, object],
fallback: Mapping[str, object],
) -> bool:
capability = str(fallback.get("capability") or "")
target_scope = str(fallback.get("target_scope") or "")
if (
request.get("capability") != capability
or request.get("target_scope") != target_scope
):
return False
if capability in flexible_native_values:
return True
required_parameters = parameter_map(fallback)
actual_parameters = parameter_map(request)
return all(
actual_parameters.get(name) == value
for name, value in required_parameters.items()
)
missing_native: list[dict[str, object]] = []
replaced_native = 0
for fallback in native_fallbacks:
matching_indexes = [
index
for index, request in enumerate(requests)
if isinstance(request, Mapping)
and request.get("capability") == fallback.get("capability")
and request.get("target_scope") == fallback.get("target_scope")
]
if any(
native_requirement_satisfied(requests[index], fallback)
for index in matching_indexes
):
continue
if matching_indexes:
requests[matching_indexes[0]] = deepcopy(fallback)
replaced_native += 1
continue
missing_native.append(fallback)
native_changed = bool(missing_native or replaced_native)
metadata = normalized.get("metadata")
if (
native_changed
and isinstance(metadata, Mapping)
and metadata.get("source") == "scene_template"
):
scope_policy = scene_context.get("seat_scope_policy")
raw_selected_scopes = (
scope_policy.get("selected_scopes", [])
if isinstance(scope_policy, Mapping)
else []
)
selected_scopes = frozenset(
str(scope)
for scope in (
raw_selected_scopes
if isinstance(raw_selected_scopes, Sequence)
and not isinstance(
raw_selected_scopes,
(str, bytes, bytearray),
)
else []
)
if str(scope)
)
if selected_scopes:
try:
normalized = project_seed_plan(normalized, selected_scopes)
except ModeCreationSeatPolicyError as exc:
raise SceneCompositionError(str(exc)) from exc
projected_requests = normalized.get("requests")
if not isinstance(projected_requests, list):
raise SceneCompositionError(
"projected scene template requests must be an array"
)
requests = projected_requests
if (
len(requests) + len(missing_native) + len(missing_refs)
> maximum_requests
):
raise SceneCompositionError(
"required scene controls exceed the scene action limit"
)
requests.extend(missing_native)
requests.extend(
{
"domain": "self_extension",
@@ -1473,14 +1634,28 @@ class CodexAssistantService:
}
for capability_ref in missing_refs
)
if missing_refs:
if native_changed or missing_refs:
if native_changed:
# The trusted template remains the source seed, but adding a
# native domain makes this an ordinary fully guarded semantic
# composition. First project its seat variants to the user's
# selected scope above, then remove metadata because template
# metadata requires byte-identical base requests.
normalized.pop("metadata", None)
domains = normalized.get("detected_domains")
if isinstance(domains, list) and "self_extension" not in domains:
domains.append("self_extension")
if isinstance(domains, list):
for fallback in missing_native:
domain = str(fallback.get("domain") or "")
if domain and domain not in domains:
domains.append(domain)
if missing_refs and "self_extension" not in domains:
domains.append("self_extension")
summaries = normalized.get("decision_summary")
if isinstance(summaries, list):
summaries.append(
f"已按模式语义加入 {len(missing_refs)} 项必需的已授权外设动作。"
"已按模式语义补齐 "
f"{len(missing_native) + replaced_native} 项原生控制和 "
f"{len(missing_refs)} 项已授权外设动作。"
)
return normalized
@@ -1594,7 +1769,10 @@ class CodexAssistantService:
message_id=message_id,
emit=emit,
execute_control=execute_control,
assistant_plan=template_plan,
assistant_plan=self._with_required_scene_peripherals(
template_plan,
scene_context,
),
response_route="scene_template",
)
base_requests = template_plan.get("requests", [])
@@ -1627,7 +1805,14 @@ class CodexAssistantService:
0,
MAX_PLAN_REQUESTS
- len(base_requests)
- len(self._required_scene_peripheral_refs(scene_context)),
- len(self._required_scene_peripheral_refs(scene_context))
- len(
(
scene_context.get("scene_semantics") or {}
).get("native_fallback_requests", [])
if isinstance(scene_context.get("scene_semantics"), Mapping)
else []
),
)
augmentation_semantics = augmentation_context.get("scene_semantics")
if isinstance(augmentation_semantics, dict):
@@ -1654,6 +1839,7 @@ class CodexAssistantService:
deadline=deadline,
developer_instructions=SCENE_TEMPLATE_AUGMENTATION_INSTRUCTIONS,
dynamic_override=False,
enforce_required_controls=False,
)
proposed = augmentation.get("requests", [])
if not isinstance(proposed, list) or any(
@@ -1670,6 +1856,10 @@ class CodexAssistantService:
*deepcopy(base_requests),
*deepcopy(proposed),
]
combined = self._with_required_scene_peripherals(
combined,
scene_context,
)
return self._stream_direct_vehicle_control(
_VehicleAuthorization(message, "explicit_mode_profile_create"),
message_id=message_id,
@@ -1850,6 +2040,7 @@ class CodexAssistantService:
deadline: float,
developer_instructions: str | None = None,
dynamic_override: bool | None = None,
enforce_required_controls: bool = True,
) -> dict[str, object]:
start_structured_turn = getattr(self.client, "start_structured_turn", None)
if not callable(start_structured_turn):
@@ -2063,7 +2254,7 @@ class CodexAssistantService:
if not isinstance(raw_output, Mapping):
raise SceneCompositionError("scene composer output must be an object")
normalized = normalize_scene_output(raw_output, planner_context, dynamic=dynamic)
if not dynamic:
if not dynamic and enforce_required_controls:
normalized = self._with_required_scene_peripherals(
normalized,
planner_context,
@@ -2830,11 +3021,11 @@ class CodexAssistantService:
if draft.get("generation_source") == "registered_mode_profile":
return (
f"工程中已有“{name}”,已按当前车型载入预览并验证"
f"{operation_count}项设备动作。可以先试运行,确认后更新现有模式。"
f"{operation_count}项设备动作。创建阶段未下发,确认后可更新现有模式。"
)
return (
f"已按你提供的设置为现有“{name}”生成预览并验证"
f"{operation_count}项设备动作。可以先试运行,确认后更新现有模式。"
f"{operation_count}项设备动作。创建阶段未下发,确认后可更新现有模式。"
)
if (
isinstance(draft, Mapping)
@@ -2851,7 +3042,7 @@ class CodexAssistantService:
return (
f"已从常用模式模板快速生成“{name}”,并按当前工程能力验证了"
f"{operation_count}项设备动作。当前只是独立草稿,可以继续调整;"
"确认预览后可保存为新的功能模式,也可以先试运行。"
"创建阶段不会下发,确认预览后可保存为新的功能模式。"
)
if (
isinstance(draft, Mapping)
@@ -2867,9 +3058,9 @@ class CodexAssistantService:
)
return (
f"已理解“{name}”的场景意图,并从当前工程能力中组合、验证了"
f"{operation_count}项设备动作。确认预览后可保存为功能模式,也可以先试运行。"
f"{operation_count}项设备动作。创建阶段不会下发,确认预览后可保存。"
)
return f"已为你生成“{name}”预览。确认动作后可保存为功能模式,也可以先试运行。"
return f"已为你生成“{name}”预览。创建阶段不会下发,确认动作后可保存。"
@staticmethod
def _mode_profile_management_answer(
@@ -4073,7 +4264,9 @@ class CodexAssistantService:
if isinstance(intent_ir, dict):
spec = intent_ir.get("spec")
if isinstance(spec, dict) and isinstance(spec.get("actions"), list):
for item in spec["actions"][:96 if result.get("schedule") else 12]:
for item in spec["actions"][
:96 if result.get("schedule") else MAX_PLAN_REQUESTS
]:
if not isinstance(item, dict):
continue
safe_action = {
@@ -4101,7 +4294,7 @@ class CodexAssistantService:
infrared_actions: list[dict[str, object]] = []
raw_infrared = result.get("infrared_actions")
if isinstance(raw_infrared, list):
for item in raw_infrared[:12]:
for item in raw_infrared[:MAX_PLAN_REQUESTS]:
if not isinstance(item, dict):
continue
infrared_actions.append(
@@ -4152,7 +4345,7 @@ class CodexAssistantService:
for key in ("domain", "capability", "target_scope", "parameters")
if key in item
}
for item in mode_profile_changes[:12]
for item in mode_profile_changes[:MAX_PLAN_REQUESTS]
if isinstance(item, Mapping)
]
deleted_mode_profile = result.get("deleted_mode_profile")