"""Fail-closed language guards applied before semantic IntentIR generation.""" from __future__ import annotations import json import re from collections.abc import Mapping from copy import deepcopy from typing import Any CLAUSE_SEPARATOR = re.compile(r"[,,;;。!?!?\n]+") NEGATED_OPERATION = re.compile( r"(?:不要|别|不想|无需|不必|禁止)" r"(?:再|去|把|将|给我|帮我|立即|马上){0,2}" r"(?P打开|开启|启动|关闭|关掉|解锁|开锁|上锁|落锁|锁上|进入|" r"切换到|调用|恢复|保存|升高|降低|后仰|前倾|前移|后移)" ) PRESERVE_SEAT = re.compile(r"(?:不要|别|不想)(?:再)?动(?P主驾|副驾)") CONTROL_VERB = re.compile( r"打开|开启|启动|关闭|关掉|解锁|开锁|上锁|落锁|锁上|进入|切换到|" r"调用|恢复|保存|升高|降低|后仰|前倾|前移|后移|调到|调为|设置为" ) FUZZY_UNMAPPED_CONTROL = re.compile( r"(?P(?:座椅|靠背|腿托|空调|车窗|风口|灯光?)" r"(?:调|弄|搞)(?:得|到)?(?:舒服|合适|好)(?:一点|点|些)?)" ) OUTLET_MODE_DISABLE = re.compile( r"(?:关闭|关掉|停止|取消)(?:一下)?(?:空调)?(?:的)?" r"(?P吹面|吹脸|吹脚|除霜)(?:出风口|出风|模式)?|" r"(?P吹面|吹脸|吹脚|除霜)(?:出风口|出风|模式)?" r"(?:关闭|关掉|停止|取消)" ) POSITIVE_OUTLET_MODE = re.compile( r"(?:打开|开启|启用|切换到|切换为|改为|设为|设置为|调整为|调为)" r"(?:空调)?(?:的)?(?:吹面|吹脸|吹脚|除霜)" ) DRIVER_SEAT_THERMAL_MIDDLE = re.compile( r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}" r"(?:中等(?:档|级)?|中档|中级|中间档|适中|(?:调到|调为|调|设为|设置为|调整到|调整为)中)|" 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]: return {"text": match.group(0), "start": match.start(), "end": match.end()} def _target_from_text(text: str, anchor: int = 0) -> tuple[str | None, str | None]: candidates = [ (abs(match.start() - anchor), match.start(), domain, target) for pattern, domain, target in ( (r"左前", "window", "front_left"), (r"右前", "window", "front_right"), (r"左后", "window", "rear_left"), (r"右后", "window", "rear_right"), (r"主驾|驾驶位|司机位", "seat", "driver"), (r"副驾|副驾驶|乘客位", "seat", "passenger"), (r"后排", "door", "rear"), ) for match in re.finditer(pattern, text) ] if candidates: _, _, domain, target = min(candidates) return domain, target if "车窗" in text: return "window", "all" if re.search(r"全部|所有|四个|全车", text) else None if re.search(r"遮阳帘|天幕帘", text): return "roof", "vehicle" if "空调" in text: return "climate", "vehicle" if "氛围灯" in text: return "ambient_light", "vehicle" return None, None def _constraint(utterance: str, match: re.Match[str]) -> dict[str, Any]: clause = next( (item for item in CLAUSE_SEPARATOR.split(utterance) if match.group(0) in item), utterance, ) domain, target = _target_from_text(clause, match.start()) verb = match.group("verb") result: dict[str, Any] = { "type": "prohibit_action", "source_span": _span(utterance, match), } if domain: result["domain"] = domain if target: result["target"] = target parameters: dict[str, Any] = {} if domain == "window": result["capability"] = "window.set_motion" parameters["motion"] = "open" if verb in {"打开", "开启", "启动"} else "close" elif domain == "roof": result["capability"] = "roof.set_shade_motion" parameters["motion"] = "open" if verb in {"打开", "开启", "启动"} else "close" elif domain == "climate": result["capability"] = "climate.set_power" parameters["state"] = "on" if verb in {"打开", "开启", "启动"} else "off" elif domain == "ambient_light": result["capability"] = "ambient_light.set_power" parameters["state"] = "on" if verb in {"打开", "开启", "启动"} else "off" elif domain == "seat": if "通风" in clause: result["capability"] = "seat.set_ventilation" elif "加热" in clause: result["capability"] = "seat.set_heating" elif "靠背" in clause: result["capability"] = "seat.set_backrest_recline" if result.get("capability") in {"seat.set_ventilation", "seat.set_heating"}: parameters["state"] = "on" if verb in {"打开", "开启", "启动"} else "off" if parameters: result["parameters"] = parameters return result def _remove_seat_target(parsed: dict[str, Any], target: str) -> None: parsed["actions"] = [ action for action in parsed.get("actions", []) if action.get("target") != target ] for key in ("domain_actions", "native_can_actions", "deferred_requests", "model_requests"): parsed[key] = [ request for request in parsed.get(key, []) if not ( request.get("domain") == "seat" and request.get("target", request.get("target_scope")) == target ) ] def _remove_capability(parsed: dict[str, Any], capability: str) -> None: for key in ( "actions", "domain_actions", "native_can_actions", "deferred_requests", "model_requests", ): parsed[key] = [ request for request in parsed.get(key, []) if request.get("capability") != capability ] 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.""" result = deepcopy(parsed) 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( { "type": "prohibit_change", "domain": "seat", "target": target, "source_span": _span(utterance, match), } ) _remove_seat_target(result, target) negated = list(NEGATED_OPERATION.finditer(utterance)) if negated: constraints.extend(_constraint(utterance, item) for item in negated) for key in _EXECUTION_LIST_FIELDS: result[key] = [] result["ambiguities"] = [] if len(CONTROL_VERB.findall(utterance)) > len(negated): unresolved.append( { "text": utterance, "reason": "mixed_negated_and_positive_operations_require_atomic_confirmation", } ) result.setdefault("ambiguities", []).append( { "field": "utterance", "question": "同一句中同时包含否定和执行动作,请拆分或确认完整目标。", "reason": "mixed_negated_and_positive_operations", } ) result["decision_summary"] = [ *result.get("decision_summary", []), "检测到否定控制约束;未把“不要/不想”错误转换为相反动作。", ] result["confidence"] = min(float(result.get("confidence", 0)), 0.8) disabled_outlets = list(OUTLET_MODE_DISABLE.finditer(utterance)) if disabled_outlets and POSITIVE_OUTLET_MODE.search(utterance) is None: _remove_capability(result, "climate.set_outlet_mode") for match in disabled_outlets: feature = match.group("feature") or match.group("feature_after") constraints.append( { "type": "prohibit_action", "domain": "climate", "capability": "climate.set_outlet_mode", "parameters": {"disabled_feature": feature}, "source_span": _span(utterance, match), } ) unresolved.append( { "text": match.group(0), "reason": "outlet_feature_disable_requires_absolute_mode", } ) result["ambiguities"] = [ *result.get("ambiguities", []), { "field": "climate.outlet_mode", "question": ( "当前CAN白名单只能设置完整出风模式;请明确改为吹脚、除霜等目标模式。" ), "reason": "outlet_feature_disable_requires_absolute_mode", }, ] result["decision_summary"] = [ *result.get("decision_summary", []), "检测到关闭单一出风方向的请求;未将其错误转换为开启该出风模式。", ] result["confidence"] = min(float(result.get("confidence", 0)), 0.8) thermal_middle = DRIVER_SEAT_THERMAL_MIDDLE.search(utterance) if thermal_middle is not None and not re.search( r"副驾|副驾驶|乘客|右前", utterance ): feature = "通风" if "通风" in thermal_middle.group(0) else "加热" native_capability = ( "seat.set_driver_ventilation_native" if feature == "通风" else "seat.set_driver_heating_native" ) canonical_capability = ( "seat.set_ventilation" if feature == "通风" else "seat.set_heating" ) _remove_capability(result, native_capability) _remove_capability(result, canonical_capability) constraint = { "type": "unsupported_discrete_level", "domain": "seat", "capability": native_capability, "parameters": {"requested_level": "medium"}, "source_span": _span(utterance, thermal_middle), } if not any( item.get("type") == constraint["type"] and item.get("capability") == native_capability and item.get("parameters") == constraint["parameters"] for item in constraints ): constraints.append(constraint) unresolved.append( { "text": thermal_middle.group(0), "reason": "driver_seat_thermal_middle_level_unsupported", } ) result["ambiguities"] = [ *result.get("ambiguities", []), { "field": ( "seat.driver.ventilation.level" if feature == "通风" else "seat.driver.heating.level" ), "question": ( f"主驾座椅{feature}实车白名单只有低档和高档," "请明确选择低档或高档。" ), "reason": "driver_seat_thermal_middle_level_unsupported", }, ] result["decision_summary"] = [ *result.get("decision_summary", []), f"识别到主驾座椅{feature}中档请求;实车协议不支持中档,未降级为其他档位。", ] result["confidence"] = min(float(result.get("confidence", 0)), 0.8) if not result.get("actions") and not result.get("domain_actions"): for match in FUZZY_UNMAPPED_CONTROL.finditer(utterance): if not any(item.get("text") == match.group("text") for item in unresolved): unresolved.append( { "text": match.group("text"), "reason": "fuzzy_control_goal_not_resolved_to_absolute_target", } ) 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