feat: 完善 N7 智能座舱交互与控制链路
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user