fix: restore mode clicks and compose seat-aware assistant scenes

This commit is contained in:
vee
2026-09-12 13:33:38 +08:00
parent 76057be044
commit d09b5967f8
28 changed files with 1247 additions and 44 deletions
@@ -21,6 +21,7 @@ from cognitive_agent.application.assistant_plan_guard import (
)
from cognitive_agent.application.mode_profile_intent import (
normalize_mode_profile_name,
parse_mode_profile_create,
parse_mode_profile_detail_query,
)
from cognitive_agent.generated.can_semantic_protocol import CAN0_SUPPORTED_CAPABILITIES
@@ -428,6 +429,9 @@ _MODE_CAPABILITY_LABELS = {
"seat.set_legrest_extension": "座椅腿托伸展",
"seat.set_heating": "座椅加热",
"seat.set_ventilation": "座椅通风",
"seat.set_driver_ventilation_native": "座椅通风",
"seat.set_driver_heating_native": "座椅加热",
"seat.set_driver_massage_native": "座椅按摩",
"seat.start_dynamic_mode": "座椅动态模式",
"climate.set_power": "空调",
"climate.set_temperature": "空调温度",
@@ -575,6 +579,21 @@ def _mode_operation_text(raw: Mapping[str, object]) -> str:
}:
target_label = ""
subject = f"{target_label}{label}"
if capability in {"seat.set_driver_ventilation_native", "seat.set_driver_heating_native"}:
if parameters.get("state") == "off":
return f"{subject}关闭"
level = {1: "高档", 2: "低档"}.get(parameters.get("level", 2), "")
return f"{subject}开启{level}"
if capability == "seat.set_driver_massage_native":
if parameters.get("action") == "off":
return f"{subject}关闭"
if parameters.get("action") == "strength":
strength = {1: "轻柔", 2: "中等", 3: "较强"}.get(parameters.get("strength"), "")
return f"{subject}设为{strength}力度"
mode = {
"wave": "波浪", "pulse": "脉冲", "stretch": "舒展", "slack": "舒缓", "relax": "放松",
}.get(str(parameters.get("mode") or ""), "")
return f"{subject}设为{mode}模式"
if set(parameters) == {"state"}:
return f"{subject}{values[0]}"
if capability == "roof.set_shade_position" and parameters.get("position_pct") in {0, 100}:
@@ -1750,6 +1769,11 @@ class AssistantCapabilityCatalog:
normalized = "".join(text.lower().split())
if not normalized:
return None
# A creation request can contain both a device and the noun 功能模式.
# That noun is not evidence that the user is asking about capabilities.
# Use the guarded intent parser, so genuine questions remain answerable.
if parse_mode_profile_create(text) is not None:
return None
catalog = snapshot or self.snapshot()
unsupported_control = _unsupported_control_answer(
_canonical_control_text(text),
@@ -25,6 +25,8 @@ from collections.abc import Set as AbstractSet
from dataclasses import dataclass
from typing import Any
from cognitive_agent.application.semantic_ir import resolve_scene_seat_scopes
MAX_PLAN_BYTES = 24 * 1024
MAX_PLAN_REQUESTS = 12
MAX_PLAN_PARAMETERS = 12
@@ -39,6 +41,9 @@ SCENE_COMPOSABLE_CAPABILITIES = frozenset(
"seat.set_legrest_extension",
"seat.set_ventilation",
"seat.set_heating",
"seat.set_driver_ventilation_native",
"seat.set_driver_heating_native",
"seat.set_driver_massage_native",
"climate.set_power",
"climate.set_temperature",
"climate.set_fan_level",
@@ -1085,6 +1090,7 @@ def guard_assistant_plan(
fallback_intents: Sequence[Mapping[str, Any]] = (),
trusted_local: Mapping[str, Any] | None = None,
allow_scene_composition: bool = False,
scene_scope_utterance: str | None = None,
) -> GuardedAssistantPlan:
"""Authorize a Codex plan against the server-owned unresolved text.
@@ -1153,6 +1159,10 @@ def guard_assistant_plan(
parser_claims.update(_intent_claims(intent))
local_keys = _resolved_keys(trusted_local or {})
non_execution = bool(_NON_EXECUTION_PATTERN.search(text))
scene_seat_scopes = (
resolve_scene_seat_scopes(text if scene_scope_utterance is None else scene_scope_utterance)
if allow_scene_composition else None
)
accepted: list[dict[str, Any]] = []
rejected: list[dict[str, str]] = []
@@ -1204,6 +1214,24 @@ def guard_assistant_plan(
}
)
continue
if (
allow_scene_composition
and domain == "seat"
and scene_seat_scopes is not None
and (
"unsupported" in scene_seat_scopes
or {"front_left": "driver", "front_right": "passenger"}.get(
target_scope, target_scope
) not in scene_seat_scopes
)
):
rejected.append(
{
"field": f"requests[{index}].target_scope",
"question": "场景座椅动作超出了用户明确指定的座位范围,已拒绝生成该功能模式。",
}
)
continue
request_text = _relevant_text(capability, text)
if non_execution or (
not allow_scene_composition
@@ -34,6 +34,9 @@ _MODE_PROFILE_PURPOSE_WRAPPERS = (
r"^(?:可以|能够|能)?让(?:我们|我|人们?|乘员们?|乘客们?|大家)"
r"(?P<purpose>.+?)的(?:功能|场景|自定义)?模式$"
),
re.compile(
r"^(?P<purpose>(?:座椅|座位|乘坐|坐着).+?)的(?:功能|场景|自定义)?模式$"
),
)
_MODE_PROFILE_PURPOSE_INVALID = frozenset(
{"", "功能", "场景", "自定义", "使用", "日常使用", "合适", "需要"}
@@ -14,7 +14,7 @@ from cognitive_agent.application.assistant_plan_guard import (
MAX_PLAN_REQUESTS,
)
SCENE_PLANNER_VERSION = "n7web-scene-composer/v2"
SCENE_PLANNER_VERSION = "n7web-scene-composer/v3"
_MAX_OUTPUT_SCHEMA_BYTES = 64 * 1024
_JSON_PARAMETER_TYPES = frozenset({"boolean", "integer", "number", "string"})
@@ -46,6 +46,19 @@ For a commonly understood scene, infer conservative cabin settings. If the name
meaningless, unsafe, contradictory, or cannot be grounded in the supplied catalog,
return no requests and a short ambiguity. The application will independently check
the complete live registry, parameters, compiler, safety policy, and dry-run result.
The primary user need must be represented in the actions. For sitting comfort,
seating relaxation or rest, consider supported seat functions before auxiliary
climate and lights; climate and lights alone do not fulfil a seating request.
For an unqualified front-seat comfort scene, consider both driver and passenger
using each seat's own listed capabilities. Respect an explicitly named seat.
Driver heating/ventilation native levels use 2 for low and 1 for high; passenger
levels use 1 for low. Driver massage supports action=mode with one listed mode,
action=strength with strength, or action=off. Do not mix these action parameters.
Driver absolute axis positions, unlearned memory slots and passenger massage must
not be invented. Do not use timed axis movement as a substitute for an absolute
posture; leave unsupported axes unchanged. If the primary requested seat function
is unavailable, return an ambiguity rather than an unrelated cabin-only scene.
Return only the JSON object required by the output schema.
"""
@@ -179,6 +192,29 @@ def _capability_schema_branch(capability: Mapping[str, object]) -> dict[str, obj
}
for names in parameter_combinations
]
if capability_id == "seat.set_driver_massage_native":
# The native command discriminates mode, strength and off. Its YAML
# scalar contracts cannot express that dependency on their own.
parameter_object_branches = []
action_schema = value_schemas.get("action", {})
for action, dependent_name in (("off", None), ("mode", "mode"), ("strength", "strength")):
if action not in action_schema.get("enum", []):
continue
if dependent_name is not None and dependent_name not in value_schemas:
continue
properties = {"action": {"type": "string", "enum": [action]}}
if dependent_name is not None:
properties[dependent_name] = value_schemas[dependent_name]
parameter_object_branches.append(
{
"type": "object",
"additionalProperties": False,
"required": list(properties),
"properties": properties,
}
)
if not parameter_object_branches:
return None
return {
"type": "object",
"additionalProperties": False,
@@ -2195,6 +2195,7 @@ class SeatDemoService:
assistant_plan: dict[str, Any] | None = None,
allow_fuzzy_mode_profile_match: bool = True,
allow_scene_composition: bool = False,
scene_scope_utterance: str | None = None,
mode_profile_draft_context: dict[str, Any] | None = None,
) -> dict[str, Any]:
control_transport = self._validate_control_transport(control_transport)
@@ -2526,8 +2527,12 @@ class SeatDemoService:
model_parse_source=model_parse_source,
assistant_plan=assistant_plan,
allow_scene_composition=allow_scene_composition,
scene_scope_utterance=scene_scope_utterance,
)
scene_label_only = assistant_plan is not None and allow_scene_composition
dynamic_mode_stop = (
not scene_label_only and self._is_dynamic_mode_stop_request(utterance)
)
dynamic_mode_stop = self._is_dynamic_mode_stop_request(utterance)
if dynamic_mode_stop:
parsed = self._apply_dynamic_mode_stop_request(parsed, utterance)
# Reuse the same conservative clause segmentation for the final
@@ -2535,7 +2540,12 @@ class SeatDemoService:
# comma between independent controls can leak an early target
# (for example ``副驾``) into a later HVAC clause and can also
# hide otherwise valid window/temperature actions.
segmented_utterance = ",".join(
# Name-only scene plans have already been checked against exact
# targets by _interpret. Their synthesized 进入<name> label is not
# another atomic instruction: reparsing 座椅舒适模式 here applies
# the unscoped-seat driver default and discards valid passenger
# actions. Explicit user instructions never enter this branch.
segmented_utterance = "" if scene_label_only else ",".join(
self._split_intent_clauses(utterance)
)
relative_resolution = (
@@ -2617,7 +2627,12 @@ class SeatDemoService:
parsed,
excluded_control_signatures,
)
parsed = apply_language_safety_guards(parsed, utterance)
parsed = apply_language_safety_guards(
parsed,
utterance,
scene_composition=assistant_plan is not None and allow_scene_composition,
scene_scope_utterance=scene_scope_utterance,
)
parsed = self._partition_native_actions(
parsed,
promote_supported=(
@@ -5474,6 +5489,11 @@ class SeatDemoService:
and source_profile is None
and not requested_instruction
),
# A revision combines a server-loaded draft with changes that
# 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 None,
)
except NativeCanCompileError:
# Repository modes and model-composed scenes share the same native
@@ -7500,6 +7520,7 @@ class SeatDemoService:
model_parse_source: str = "cloud_model",
assistant_plan: dict[str, Any] | None = None,
allow_scene_composition: bool = False,
scene_scope_utterance: str | None = None,
) -> tuple[dict[str, Any], str, str | None]:
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
if assistant_plan is not None and allow_scene_composition:
@@ -7561,6 +7582,7 @@ class SeatDemoService:
],
trusted_local=trusted_local,
allow_scene_composition=allow_scene_composition,
scene_scope_utterance=scene_scope_utterance,
)
model = self.model_plan_adapter.adapt(
guarded.plan,
+48 -2
View File
@@ -71,6 +71,28 @@ _SEAT_UNSUPPORTED_SCOPE = re.compile(
r"左后|右后|前舱|后舱|左侧|右侧|左边|右边|"
r"所有座椅|全部座椅|全车座椅"
)
_SCENE_NON_SEAT_SCOPED_DEVICE = re.compile(
rf"(?:{_SEAT_DRIVER_SCOPE.pattern}|{_SEAT_PASSENGER_SCOPE.pattern}|"
rf"{_SEAT_UNSUPPORTED_SCOPE.pattern})\s*(?:的)?\s*"
r"(?=空调|温度|风量|出风|风口|除霜|车窗|窗户|车门|门锁|"
r"阅读灯|氛围灯|灯光|遮阳帘|天幕)",
re.IGNORECASE,
)
def resolve_scene_seat_scopes(utterance: str) -> frozenset[str] | None:
"""Keep a scene's explicit seat scope without borrowing an HVAC/light zone.
A name such as 主驾舒适模式 still constrains seat actions even when it
omits 座椅. But 副驾空调与座椅舒适模式 does not authorize restricting
every seat action to the passenger. Only remove scope tokens directly
attached to another device; all negation text remains untouched.
"""
scoped_text = _SCENE_NON_SEAT_SCOPED_DEVICE.sub(
"", utterance.replace("主副驾", "主驾和副驾")
)
return resolve_explicit_seat_scopes(scoped_text, assume_seat_context=True)
def resolve_explicit_seat_scopes(
@@ -356,7 +378,13 @@ def _remove_conflicting_controls(
result[field] = kept
def apply_language_safety_guards(parsed: dict[str, Any], utterance: str) -> dict[str, Any]:
def apply_language_safety_guards(
parsed: dict[str, Any],
utterance: str,
*,
scene_composition: bool = False,
scene_scope_utterance: str | None = None,
) -> dict[str, Any]:
"""Preserve negation and unresolved control language without producing CAN."""
result = deepcopy(parsed)
@@ -386,7 +414,25 @@ def apply_language_safety_guards(parsed: dict[str, Any], utterance: str) -> dict
]
result["confidence"] = min(float(result.get("confidence", 0)), 0.8)
explicit_seat_scopes = resolve_explicit_seat_scopes(utterance)
explicit_seat_scopes = (
resolve_scene_seat_scopes(
utterance if scene_scope_utterance is None else scene_scope_utterance
)
if scene_composition
else resolve_explicit_seat_scopes(utterance)
)
if scene_composition and not any(
isinstance(item, dict)
and (
item.get("domain") == "seat"
or str(item.get("capability", "")).startswith("seat.")
)
for key in _EXECUTION_LIST_FIELDS
for item in result.get(key, [])
):
# A rear-window ventilation scene is not a rear-seat request. The
# scene guard already checks every seat candidate before adaptation.
explicit_seat_scopes = None
if explicit_seat_scopes is not None:
removed = _filter_explicit_seat_scope(result, explicit_seat_scopes)
if removed: