feat: 完善 N7 智能座舱交互与控制链路

This commit is contained in:
vee
2026-09-07 15:34:59 +08:00
parent 32d179143f
commit c96970f068
100 changed files with 30088 additions and 1174 deletions
+298 -4
View File
@@ -2,7 +2,9 @@
from __future__ import annotations
import json
import re
from collections.abc import Mapping
from copy import deepcopy
from typing import Any
@@ -39,6 +41,96 @@ DRIVER_SEAT_THERMAL_MIDDLE = re.compile(
r"(?:中等(?:档|级)?|中档|中级|中间档|适中)"
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)"
)
META_NON_EXECUTION = re.compile(
r"复述|引用|翻译|改写|总结|解释|分析|造句|"
r"故事|剧本|小说|作文|角色|主角|假设|假如|如果|"
r"示例|举例|这句话|以下内容"
)
_EXECUTION_LIST_FIELDS = (
"actions",
"domain_actions",
"native_can_actions",
"deferred_requests",
"model_requests",
"preset_actions",
"preset_events",
)
_SEAT_CONTEXT = re.compile(
r"座椅|座位|靠背|椅背|坐盆|坐垫|腿托|脚托|"
r"座椅通风|座椅加热|按摩|座椅记忆|零重力|木马|摇篮"
)
_SEAT_DRIVER_SCOPE = re.compile(
r"\bdriver\b|\bfront[-_ ]?left\b|主驾|主驾驶|驾驶位|驾驶员|"
r"驾驶席|驾驶座(?:椅)?|司机位|司机座(?:椅)?|左前"
)
_SEAT_PASSENGER_SCOPE = re.compile(
r"\bpassenger\b|\bfront[-_ ]?right\b|副驾|副驾驶|乘客|乘员|右前"
)
_SEAT_UNSUPPORTED_SCOPE = re.compile(
r"前排|后排|后座|二排|第二排|中排|第三排|三排|"
r"左后|右后|前舱|后舱|左侧|右侧|左边|右边|"
r"所有座椅|全部座椅|全车座椅"
)
def resolve_explicit_seat_scopes(
utterance: str,
*,
assume_seat_context: bool = False,
) -> frozenset[str] | None:
"""Resolve scopes only from clauses that actually describe a seat action."""
scopes: set[str] = set()
relevant_clause_seen = False
clauses = [item for item in CLAUSE_SEPARATOR.split(utterance) if item.strip()]
for clause in clauses or [utterance]:
if not assume_seat_context and not _SEAT_CONTEXT.search(clause):
continue
relevant_clause_seen = True
if _SEAT_UNSUPPORTED_SCOPE.search(clause):
scopes.add("unsupported")
continue
if _SEAT_DRIVER_SCOPE.search(clause):
scopes.add("driver")
if _SEAT_PASSENGER_SCOPE.search(clause):
scopes.add("passenger")
if not relevant_clause_seen or not scopes:
return None
return frozenset(scopes)
def _filter_explicit_seat_scope(
result: dict[str, Any],
requested_scopes: frozenset[str],
) -> bool:
"""Remove seat operations outside the user's explicit scope."""
allowed: set[str] = set()
if "driver" in requested_scopes:
allowed.update(("driver", "front_left"))
if "passenger" in requested_scopes:
allowed.update(("passenger", "front_right"))
if "unsupported" in requested_scopes:
allowed.clear()
removed = False
for key in _EXECUTION_LIST_FIELDS:
kept: list[Any] = []
for item in result.get(key, []):
if not isinstance(item, dict):
kept.append(item)
continue
capability = str(item.get("capability", ""))
is_seat = item.get("domain") == "seat" or capability.startswith("seat.")
if not is_seat:
kept.append(item)
continue
target = str(item.get("target", item.get("target_scope", "")))
if target in allowed:
kept.append(item)
else:
removed = True
result[key] = kept
return removed
def _span(utterance: str, match: re.Match[str]) -> dict[str, Any]:
@@ -145,6 +237,125 @@ def _remove_capability(parsed: dict[str, Any], capability: str) -> None:
]
def _parameter_mapping(item: Mapping[str, Any]) -> dict[str, Any]:
parameters: object = item.get("parameters", {})
if isinstance(parameters, list):
return {
str(parameter.get("name")): parameter.get("value")
for parameter in parameters
if isinstance(parameter, dict) and parameter.get("name")
}
if isinstance(parameters, Mapping):
normalized = {str(name): value for name, value in parameters.items()}
if normalized:
return normalized
# The passenger-seat domain predates the capability-shaped actions used by
# the other domains. Normalize its legacy action fields here so the shared
# conflict gate runs before FunctionConfig compilation and execution.
legacy: dict[str, Any] = {}
for source, target in (
("state", "state"),
("requested_level", "level"),
("position_pct", "position_pct"),
("angle_deg", "angle_deg"),
):
if item.get(source) is not None:
legacy[target] = item.get(source)
return legacy
def _control_capability(item: Mapping[str, Any]) -> str:
capability = str(item.get("capability", ""))
if capability:
return capability
function = str(item.get("function", ""))
if function in {"ventilation", "heating"}:
return f"seat.set_{function}"
if item.get("action_type") == "position":
return {
"longitudinal": "seat.set_longitudinal",
"cushion_tilt": "seat.set_cushion_tilt",
"backrest_recline": "seat.set_backrest_recline",
"legrest_height": "seat.set_legrest_height",
}.get(str(item.get("axis", "")), "")
return ""
def _control_conflict_key(
item: Mapping[str, Any],
parameters: Mapping[str, Any],
) -> tuple[str, str, str, str]:
target: object = item.get("target", item.get("target_scope", ""))
if isinstance(target, Mapping):
target = target.get("zone", target.get("device", ""))
capability = _control_capability(item)
domain = str(item.get("domain", "")) or (
capability.split(".", 1)[0] if "." in capability else ""
)
selector = ""
if capability == "seat.set_driver_axis_native":
selector = f"axis:{parameters.get('axis', '')}"
elif capability == "seat.set_driver_massage_native":
selector = f"action:{parameters.get('action', '')}"
return domain, capability, str(target), selector
def _conflicting_control_keys(
result: dict[str, Any],
) -> set[tuple[str, str, str, str]]:
"""Find contradictory values, while allowing complementary subcommands.
Some capabilities intentionally emit more than one operation for the same
target: massage mode plus strength, or ambient colour plus brightness.
Those parameter sets are complementary. A conflict exists only when the
same setting parameter receives multiple values. ``action`` and ``axis``
select a subcommand and therefore are not themselves setting values.
"""
values: dict[tuple[str, str, str, str], dict[str, set[str]]] = {}
# ``model_requests`` is audit metadata and can still contain a built-in
# mode's superseded defaults. The executable action lists below have
# already applied explicit-user-value precedence, so conflict decisions
# must be based on those lists only.
for field in ("actions", "domain_actions", "native_can_actions"):
for item in result.get(field, []):
if not isinstance(item, dict) or not _control_capability(item):
continue
parameters = _parameter_mapping(item)
if not parameters:
continue
key = _control_conflict_key(item, parameters)
settings = values.setdefault(key, {})
for name, value in parameters.items():
if str(name) in {"action", "axis"}:
continue
settings.setdefault(str(name), set()).add(
json.dumps(value, ensure_ascii=False, sort_keys=True, default=str)
)
return {
key
for key, settings in values.items()
if any(len(candidates) > 1 for candidates in settings.values())
}
def _remove_conflicting_controls(
result: dict[str, Any],
conflicts: set[tuple[str, str, str, str]],
) -> None:
for field in _EXECUTION_LIST_FIELDS:
kept: list[Any] = []
for item in result.get(field, []):
if not isinstance(item, dict) or not _control_capability(item):
kept.append(item)
continue
key = _control_conflict_key(item, _parameter_mapping(item))
if key not in conflicts:
kept.append(item)
result[field] = kept
def apply_language_safety_guards(parsed: dict[str, Any], utterance: str) -> dict[str, Any]:
"""Preserve negation and unresolved control language without producing CAN."""
@@ -152,6 +363,60 @@ def apply_language_safety_guards(parsed: dict[str, Any], utterance: str) -> dict
constraints = list(result.get("semantic_constraints", []))
unresolved = list(result.get("unresolved_spans", []))
meta_match = META_NON_EXECUTION.search(utterance)
if meta_match is not None:
for key in _EXECUTION_LIST_FIELDS:
result[key] = []
constraints.append(
{
"type": "prohibit_meta_language_execution",
"source_span": _span(utterance, meta_match),
}
)
unresolved.append(
{
"text": utterance,
"reason": "meta_language_is_not_vehicle_authorization",
}
)
result["ambiguities"] = []
result["decision_summary"] = [
*result.get("decision_summary", []),
"检测到复述、翻译或创作类元语言;未将其中的控制词当作车辆授权。",
]
result["confidence"] = min(float(result.get("confidence", 0)), 0.8)
explicit_seat_scopes = resolve_explicit_seat_scopes(utterance)
if explicit_seat_scopes is not None:
removed = _filter_explicit_seat_scope(result, explicit_seat_scopes)
if removed:
constraints.append(
{
"type": "restrict_explicit_seat_scope",
"domain": "seat",
"target": ",".join(sorted(explicit_seat_scopes)),
}
)
result["decision_summary"] = [
*result.get("decision_summary", []),
"已移除与用户显式座位不一致的座椅动作,未扩大控制范围。",
]
if "unsupported" in explicit_seat_scopes:
unresolved.append(
{
"text": utterance,
"reason": "explicit_seat_scope_is_not_safely_callable",
}
)
result["ambiguities"] = [
*result.get("ambiguities", []),
{
"field": "target_scope",
"question": "当前请求的座位范围不能在不扩大目标的前提下执行。",
"reason": "explicit_seat_scope_is_not_safely_callable",
},
]
for match in PRESERVE_SEAT.finditer(utterance):
target = "driver" if match.group("target") == "主驾" else "passenger"
constraints.append(
@@ -167,10 +432,7 @@ def apply_language_safety_guards(parsed: dict[str, Any], utterance: str) -> dict
negated = list(NEGATED_OPERATION.finditer(utterance))
if negated:
constraints.extend(_constraint(utterance, item) for item in negated)
for key in (
"actions", "domain_actions", "native_can_actions", "deferred_requests",
"model_requests", "preset_actions", "preset_events",
):
for key in _EXECUTION_LIST_FIELDS:
result[key] = []
result["ambiguities"] = []
if len(CONTROL_VERB.findall(utterance)) > len(negated):
@@ -297,6 +559,38 @@ def apply_language_safety_guards(parsed: dict[str, Any], utterance: str) -> dict
}
)
conflicts = _conflicting_control_keys(result)
if conflicts:
_remove_conflicting_controls(result, conflicts)
for domain, capability, target, selector in sorted(conflicts):
constraint = {
"type": "reject_conflicting_absolute_values",
"domain": domain,
"capability": capability,
"target": target,
}
if selector:
constraint["selector"] = selector
constraints.append(constraint)
unresolved.append(
{
"text": utterance,
"reason": "conflicting_values_for_same_capability_target",
}
)
result["ambiguities"] = [
*result.get("ambiguities", []),
{
"field": "parameters",
"question": "同一设备目标包含互相冲突的设定值,已停止相关操作,请明确唯一值。",
"reason": "conflicting_values_for_same_capability_target",
},
]
result["decision_summary"] = [
*result.get("decision_summary", []),
"检测到同一能力与目标的冲突值,相关动作均未下发。",
]
result["semantic_constraints"] = constraints
result["unresolved_spans"] = unresolved
return result