fix: restore mode clicks and compose seat-aware assistant scenes
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user