6490 lines
262 KiB
Python
Executable File
6490 lines
262 KiB
Python
Executable File
"""Application service for the stage-one seat single-domain closed loop."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import re
|
||
import sys
|
||
import time
|
||
import uuid
|
||
from collections.abc import Callable
|
||
from concurrent.futures import ThreadPoolExecutor
|
||
from copy import deepcopy
|
||
from datetime import UTC, datetime
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
import yaml
|
||
|
||
|
||
REPOSITORY_ROOT = Path(__file__).resolve().parents[3]
|
||
SEAT_SOURCE = REPOSITORY_ROOT / "domains" / "seat" / "src"
|
||
if str(SEAT_SOURCE) not in sys.path:
|
||
sys.path.insert(0, str(SEAT_SOURCE))
|
||
|
||
from seat_domain import ( # noqa: E402
|
||
HardwareSeatExecutor,
|
||
SIMULATED_HARDWARE_FAULTS,
|
||
SeatCanMatrix,
|
||
SeatCompiler,
|
||
T2GSeatCompiler,
|
||
SimulatedHardwareSeatExecutor,
|
||
VirtualSeatExecutor,
|
||
build_function_config,
|
||
build_function_preset,
|
||
build_intent_ir,
|
||
parse_local_intent,
|
||
validate_semantics,
|
||
)
|
||
|
||
from cognitive_agent.application.assistant_plan_guard import ( # noqa: E402
|
||
guard_assistant_plan,
|
||
)
|
||
from cognitive_agent.application.capability_registry import CapabilityRegistry # noqa: E402
|
||
from cognitive_agent.application.conversation_state import ConversationStateStore # noqa: E402
|
||
from cognitive_agent.application.domain_runtime import ConfiguredDomainRuntime # noqa: E402
|
||
from cognitive_agent.application.mode_profile import ( # noqa: E402
|
||
ModeProfile,
|
||
ModeProfileRegistry,
|
||
)
|
||
from cognitive_agent.application.model_plan_adapter import ( # noqa: E402
|
||
ModelPlanAdapter,
|
||
APPLY_PRESET_CAPABILITY,
|
||
DYNAMIC_MODE_STOP_CAPABILITY,
|
||
SAVE_PRESET_CAPABILITY,
|
||
SEAT_CAPABILITIES,
|
||
SEAT_MOTION_CAPABILITIES,
|
||
)
|
||
from cognitive_agent.application.mqtt_ota_seat_executor import ( # noqa: E402
|
||
MqttNativeSeatExecutor,
|
||
)
|
||
from cognitive_agent.application.native_can_intent import ( # noqa: E402
|
||
DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN,
|
||
merge_native_can_intent,
|
||
)
|
||
from cognitive_agent.application.direct_can_control import ( # noqa: E402
|
||
build_direct_can_intent,
|
||
)
|
||
from cognitive_agent.application.native_can_relative import ( # noqa: E402
|
||
NativeCanRelativeResolver,
|
||
)
|
||
from cognitive_agent.application.semantic_ir import ( # noqa: E402
|
||
apply_language_safety_guards,
|
||
resolve_explicit_seat_scopes,
|
||
)
|
||
from cognitive_agent.application.native_can_feedback import ( # noqa: E402
|
||
VehicleSafetyPreflightError,
|
||
authoritative_physical_ch1_feedback_state,
|
||
decode_feedback,
|
||
observe_vehicle_safety,
|
||
require_stationary_vehicle,
|
||
)
|
||
from cognitive_agent.application.native_can_runtime import ( # noqa: E402
|
||
NativeCanExecutor,
|
||
NativeCanRuntime,
|
||
UNVERIFIED_NATIVE_CAPABILITIES,
|
||
)
|
||
from cognitive_agent.application.seat_calibration import position_pct_to_angle # noqa: E402
|
||
from cognitive_agent.application.vehicle_profile import ( # noqa: E402
|
||
VehicleProfile,
|
||
VehicleProfileRegistry,
|
||
)
|
||
from cognitive_agent.application.vehicle_state_monitor import ( # noqa: E402
|
||
OBSERVABLE_IDS,
|
||
REQUIRED_BODY_IDS,
|
||
REQUIRED_SEAT_IDS,
|
||
VehicleStateMonitor,
|
||
)
|
||
from cognitive_agent.application.zcan_seat_executor import ( # noqa: E402
|
||
ZcanBridgeSeatExecutor,
|
||
)
|
||
from cognitive_agent.infrastructure.llm.openai_responses import ( # noqa: E402
|
||
OpenAIIntentError,
|
||
OpenAIIntentInterpreter,
|
||
REASONING_EFFORTS,
|
||
)
|
||
from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
|
||
HardwareSafetyError,
|
||
PHYSICAL_BENCH_COORDINATOR,
|
||
SeatHardwareSettings,
|
||
VehicleCanSettings,
|
||
VehicleMqttSettings,
|
||
)
|
||
from cognitive_agent.infrastructure.can.gateway import ( # noqa: E402
|
||
ZlgCanBridgeProcess,
|
||
ZlgChannelConfig,
|
||
)
|
||
from cognitive_agent.infrastructure.can.mqtt_transport import ( # noqa: E402
|
||
MqttCanTransport,
|
||
)
|
||
from cognitive_agent.infrastructure.can.access import ( # noqa: E402
|
||
PHYSICAL_BENCH_ACCESS,
|
||
)
|
||
from cognitive_agent.runtime.contracts import ContractValidator # noqa: E402
|
||
|
||
|
||
def _now() -> str:
|
||
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
|
||
|
||
|
||
def _clean_vehicle_number(value: float | None) -> int | float | None:
|
||
if value is None:
|
||
return None
|
||
return int(value) if float(value).is_integer() else round(float(value), 1)
|
||
|
||
|
||
CONVERSATION_ID_PATTERN = re.compile(r"^conv-[a-z0-9-]{6,80}$")
|
||
MODE_EDITOR_REQUEST_ID_PATTERN = re.compile(r"^req-[a-z0-9-]{6,100}$")
|
||
SEAT_SCOPE_PATTERN = re.compile(r"\bseats?\b|座椅|坐椅", re.IGNORECASE)
|
||
DRIVER_SEAT_SCOPE_PATTERN = re.compile(
|
||
r"\bdriver\b|\bfront[-_ ]?left\b|\bleft[-_ ]?front\b|"
|
||
r"主驾|主驾驶(?:座|座椅|席)?|驾驶位|驾驶员|驾驶席|驾驶座(?:椅)?|"
|
||
r"司机位|司机座(?:椅)?|左前",
|
||
re.IGNORECASE,
|
||
)
|
||
PASSENGER_SEAT_SCOPE_PATTERN = re.compile(
|
||
r"\bpassenger\b|\bfront[-_ ]?right\b|\bright[-_ ]?front\b|副驾|乘客|乘员|右前",
|
||
re.IGNORECASE,
|
||
)
|
||
WIRED_CAN_TRANSPORT = "wired_can"
|
||
WIRELESS_MQTT_TRANSPORT = "wireless_mqtt"
|
||
VEHICLE_CONTROL_TRANSPORTS = frozenset(
|
||
{WIRED_CAN_TRANSPORT, WIRELESS_MQTT_TRANSPORT}
|
||
)
|
||
|
||
|
||
def _wireless_mqtt_publish_preflight() -> dict[str, Any]:
|
||
"""Describe the non-physical gate used before an approved MQTT publish."""
|
||
|
||
return {
|
||
"transport": WIRELESS_MQTT_TRANSPORT,
|
||
"status": "delegated",
|
||
"policy": "mqtt_whitelist_publish",
|
||
"control_gate": "capability_parameter_and_frame_whitelist",
|
||
"required_vehicle_frames": [],
|
||
}
|
||
|
||
|
||
EXECUTION_VERIFICATION_STATUSES = frozenset(
|
||
{"verified_success", "submitted_unverified", "failed_or_blocked"}
|
||
)
|
||
|
||
|
||
def _operation_verification_status(item: dict[str, Any]) -> str:
|
||
"""Classify one operation without conflating Tx and physical feedback."""
|
||
|
||
telemetry = item.get("telemetry", {})
|
||
explicit = str(telemetry.get("verification_status") or "")
|
||
if explicit in EXECUTION_VERIFICATION_STATUSES:
|
||
return explicit
|
||
if item.get("status") != "succeeded":
|
||
return "failed_or_blocked"
|
||
if (
|
||
telemetry.get("physical_effect_confirmed") is True
|
||
or telemetry.get("feedback_frame") is not None
|
||
or telemetry.get("hardware_feedback") is True
|
||
or telemetry.get("firmware_verified") is True
|
||
):
|
||
return "verified_success"
|
||
if (
|
||
telemetry.get("command_frame") is not None
|
||
or telemetry.get("delivery_status") in {"submitted", "queued", "published"}
|
||
):
|
||
return "submitted_unverified"
|
||
# Dry-run and control-plane operations are complete at their own evidence
|
||
# level and do not claim that a physical actuator moved.
|
||
return "verified_success"
|
||
|
||
|
||
def _annotate_execution_report(report: dict[str, Any]) -> tuple[str, bool]:
|
||
"""Add task-level evidence and normalize submission success.
|
||
|
||
Returns ``(verification_status, partially_succeeded)``. Support steps such
|
||
as firmware identity checks do not turn a failed requested operation into a
|
||
partially successful user task.
|
||
"""
|
||
|
||
spec = report.setdefault("spec", {})
|
||
results = [item for item in spec.get("results", []) if isinstance(item, dict)]
|
||
operation_results = [
|
||
item
|
||
for item in results
|
||
if item.get("ref") not in {"seat-controller-firmware"}
|
||
] or results
|
||
statuses_by_ref: dict[str, list[str]] = {}
|
||
for item in operation_results:
|
||
statuses_by_ref.setdefault(str(item.get("ref") or "operation"), []).append(
|
||
_operation_verification_status(item)
|
||
)
|
||
statuses = [
|
||
(
|
||
"failed_or_blocked"
|
||
if "failed_or_blocked" in values
|
||
else "submitted_unverified"
|
||
if "submitted_unverified" in values
|
||
else "verified_success"
|
||
)
|
||
for values in statuses_by_ref.values()
|
||
]
|
||
failed = sum(status == "failed_or_blocked" for status in statuses)
|
||
accepted = len(statuses) - failed
|
||
partially_succeeded = bool(failed and accepted)
|
||
verification_status = (
|
||
"failed_or_blocked"
|
||
if failed
|
||
else "submitted_unverified"
|
||
if any(status == "submitted_unverified" for status in statuses)
|
||
else "verified_success"
|
||
)
|
||
spec["verification_status"] = verification_status
|
||
spec["status"] = (
|
||
"partially_succeeded"
|
||
if partially_succeeded
|
||
else "failed"
|
||
if failed
|
||
else "succeeded"
|
||
)
|
||
return verification_status, partially_succeeded
|
||
|
||
|
||
def _has_mixed_driver_passenger_seat_scope(utterance: str) -> bool:
|
||
"""Detect one request that crosses the CH1-driver and CH0-passenger boundary."""
|
||
|
||
return bool(
|
||
SEAT_SCOPE_PATTERN.search(utterance)
|
||
and DRIVER_SEAT_SCOPE_PATTERN.search(utterance)
|
||
and PASSENGER_SEAT_SCOPE_PATTERN.search(utterance)
|
||
)
|
||
|
||
|
||
SAVE_MODE_PROFILE_NAME = re.compile(
|
||
r"(?:固化为|保存为长期|保存成长期|长期保存为|记为长期|命名为)"
|
||
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
|
||
)
|
||
CREATE_MODE_PROFILE_NAME = re.compile(
|
||
r"^\s*(?:(?:请|麻烦)\s*(?:帮我|帮忙|给我)?\s*|"
|
||
r"(?:帮我|帮忙|给我)\s*)?"
|
||
r"(?:构建|创建|新建|新增|生成|添加)\s*(?:一下)?\s*"
|
||
r"(?:一个|个|一套|套)?\s*"
|
||
r"(?:名为|叫做|叫)?\s*[“\"'「『]?"
|
||
r"(?P<name>[^,。,.!!::\s“”\"'「」『』]{1,64}?模式)"
|
||
r"[”\"'」』]?"
|
||
r"\s*(?:配置)?(?:并(?:添加|加入)(?:到)?功能模式(?:栏)?)?"
|
||
r"(?:[,,::]\s*(?P<instruction>[\s\S]{1,800}))?[。.!!]?\s*$"
|
||
)
|
||
|
||
# ``、`` is used both as a command separator (``车窗关闭、空调24度``)
|
||
# and as an enumeration separator (``主驾、副驾车窗关闭``). Treat it as a
|
||
# boundary only when both sides already look like self-contained controls;
|
||
# otherwise keep it intact for the target/feature parsers below.
|
||
CONTROL_CLAUSE_SUBJECT_PATTERN = re.compile(
|
||
r"座椅|座位|靠背|椅背|坐盆|坐垫|腿托|脚托|滑轨|"
|
||
r"通风|加热|按摩|零重力|半躺|女王副驾|休闲躺椅|"
|
||
r"遮阳帘|天幕帘|天幕|车窗|窗户|"
|
||
r"灯光|室内灯|车内灯|阅读灯|氛围灯|"
|
||
r"空调|压缩机|温度|风量|风速|内循环|外循环|"
|
||
r"车门|门锁|后备箱|前大灯|近光灯|远光灯|雾灯|转向灯|双闪|"
|
||
r"出风口"
|
||
)
|
||
CONTROL_CLAUSE_PREDICATE_PATTERN = re.compile(
|
||
r"打开|开启|启动|关闭|关掉|关上|停止|暂停|取消|"
|
||
r"调整|调节|调到|调成|调为|调至|设置|设为|设到|切换|"
|
||
r"移动|移到|移至|前移|后移|上移|下移|升高|降低|抬起|放倒|"
|
||
r"调直|立直|竖直|归零|复位|锁上|上锁|落锁|解锁|"
|
||
r"零重力|半躺|最大|最小|最高|最低|最前|最后|到底|"
|
||
r"(?:\d+(?:\.\d+)?|[零一二三四五六七八九十百两半]+)"
|
||
r"(?:%|%|度|℃|档|级|秒|毫秒)"
|
||
)
|
||
UPDATE_MODE_PROFILE_PREFIX = re.compile(
|
||
r"^(?:请(?:帮我)?|帮我)?(?:修改一下|修改|调整|更改|更新|改一下)"
|
||
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
|
||
r"(?:[,。,.!!\s]*(?P<change>.*))?$"
|
||
)
|
||
UPDATE_MODE_PROFILE_BA = re.compile(
|
||
r"^(?:请(?:帮我)?|帮我)?(?:把|将)"
|
||
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
|
||
r"的(?P<change>.+)$"
|
||
)
|
||
RESTORE_MODE_PROFILE_PREFIX = re.compile(
|
||
r"^(?:请(?:帮我)?|帮我)?(?:恢复|还原|重置|初始化)"
|
||
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
|
||
r"(?:的)?(?:初始配置|初始参数|默认配置|原始配置|出厂配置)?$"
|
||
)
|
||
RESTORE_MODE_PROFILE_SUFFIX = re.compile(
|
||
r"^(?:请(?:帮我)?|帮我)?(?:把|将)?"
|
||
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
|
||
r"(?:恢复|还原|重置|初始化)(?:到|为)?"
|
||
r"(?:初始配置|初始参数|默认配置|原始配置|出厂配置)?$"
|
||
)
|
||
SEAT_AXIS_TO_CAPABILITY = {
|
||
axis: capability for capability, axis in SEAT_MOTION_CAPABILITIES.items()
|
||
}
|
||
SEAT_FUNCTION_TO_CAPABILITY = {
|
||
function: capability for capability, function in SEAT_CAPABILITIES.items()
|
||
}
|
||
DYNAMIC_MODE_STOP_WORDS = ("停止", "关闭", "结束", "取消", "停下", "退出")
|
||
DYNAMIC_MODE_WORDS = (
|
||
"动态模式",
|
||
"动态座椅",
|
||
"木马模式",
|
||
"木马",
|
||
"摇摇马",
|
||
"骑木马",
|
||
"rocking horse",
|
||
"动态摇篮",
|
||
"摇篮模式",
|
||
)
|
||
|
||
|
||
class SeatDemoService:
|
||
def __init__(self, repository_root: Path = REPOSITORY_ROOT):
|
||
self.root = repository_root
|
||
self.domain_root = repository_root / "domains" / "seat"
|
||
self.contracts = ContractValidator(repository_root)
|
||
self.matrix = SeatCanMatrix(
|
||
[
|
||
self.domain_root / "can" / "seat-thermal-matrix.yaml",
|
||
self.domain_root / "can" / "seat-motion-virtual-matrix.yaml",
|
||
]
|
||
)
|
||
self.compiler = SeatCompiler(self.domain_root / "can" / "signal-mapping.yaml")
|
||
self.executor = VirtualSeatExecutor(self.matrix)
|
||
self.simulated_hardware_executor = SimulatedHardwareSeatExecutor(self.matrix)
|
||
self.simulated_hardware_enabled = os.getenv(
|
||
"COGNITIVE_AGENT_SIMULATED_HARDWARE_ENABLED", "false"
|
||
).strip().lower() in {"1", "true", "yes", "on"}
|
||
self.hardware_matrix = SeatCanMatrix(
|
||
[
|
||
self.domain_root / "can" / "seat-thermal-hardware-matrix.yaml",
|
||
self.domain_root / "can" / "seat-motion-hardware-matrix.yaml",
|
||
]
|
||
)
|
||
self.hardware_compiler = T2GSeatCompiler(
|
||
self.domain_root / "can" / "signal-mapping.hardware.yaml",
|
||
repository_root / "platforms" / "t2g_seat_bench" / "calibration" / "seat.yaml",
|
||
)
|
||
self.hardware_settings = SeatHardwareSettings.from_env(repository_root)
|
||
self.vehicle_can_settings = VehicleCanSettings.from_env(repository_root)
|
||
self.vehicle_mqtt_settings = VehicleMqttSettings.from_env()
|
||
self._vehicle_state_monitor = (
|
||
VehicleStateMonitor(self.vehicle_can_settings)
|
||
if self.vehicle_can_settings.backend == "zlgcan-linux"
|
||
and self.vehicle_can_settings.state_configured
|
||
and not self.vehicle_can_settings.configured
|
||
else None
|
||
)
|
||
self._vehicle_mqtt_transport = MqttCanTransport(
|
||
self.vehicle_mqtt_settings
|
||
)
|
||
self.native_can_runtime = NativeCanRuntime(repository_root)
|
||
self.native_can_relative_resolver = NativeCanRelativeResolver(repository_root)
|
||
self.hardware_execution = PHYSICAL_BENCH_COORDINATOR
|
||
self.hardware_access = PHYSICAL_BENCH_ACCESS
|
||
# Shared by the short-lived hardware executor instances. A process must
|
||
# establish a fresh physical four-axis reference before trusting stored
|
||
# Hall counts, and a firmware replacement invalidates it again.
|
||
self._seat_position_memory_state = {"synchronized": False}
|
||
self.openai = OpenAIIntentInterpreter()
|
||
self.domain_runtimes = {
|
||
domain: ConfiguredDomainRuntime(repository_root, domain)
|
||
for domain in ("climate", "roof", "ambient_light")
|
||
}
|
||
self.capability_registry = CapabilityRegistry(
|
||
repository_root, {"seat", *self.domain_runtimes}
|
||
)
|
||
self.capability_context = self.capability_registry.snapshot()
|
||
self.semantic_capability_metadata = {
|
||
capability["name"]: {
|
||
"risk_level": capability.get("risk_level", "high"),
|
||
"confirmation_policy": capability.get(
|
||
"confirmation_policy", "on_ambiguity"
|
||
),
|
||
}
|
||
for domain in self.capability_context["domains"]
|
||
for capability in domain["capabilities"]
|
||
}
|
||
self.model_plan_adapter = ModelPlanAdapter(self.capability_context)
|
||
self.vehicle_profiles = VehicleProfileRegistry(repository_root / "config" / "vehicles")
|
||
configured_mode_root = os.getenv(
|
||
"COGNITIVE_AGENT_MODE_PROFILE_ROOT", ""
|
||
).strip()
|
||
self.mode_profile_user_root = (
|
||
Path(configured_mode_root).expanduser()
|
||
if configured_mode_root
|
||
else repository_root / "artifacts" / "mode-profiles"
|
||
)
|
||
if not self.mode_profile_user_root.is_absolute():
|
||
self.mode_profile_user_root = repository_root / self.mode_profile_user_root
|
||
self.mode_profiles = ModeProfileRegistry(
|
||
repository_root / "config" / "modes",
|
||
user_root=self.mode_profile_user_root,
|
||
)
|
||
self._artifact_root = repository_root / "artifacts" / "runs"
|
||
self.conversations = ConversationStateStore(
|
||
preset_root=repository_root / "artifacts" / "user-presets"
|
||
)
|
||
|
||
@property
|
||
def artifact_root(self) -> Path:
|
||
return self._artifact_root
|
||
|
||
@artifact_root.setter
|
||
def artifact_root(self, value: Path) -> None:
|
||
self._artifact_root = Path(value)
|
||
if hasattr(self, "conversations"):
|
||
self.conversations.preset_root = self._artifact_root / "user-presets"
|
||
|
||
@property
|
||
def driver_only_ch1_execution_active(self) -> bool:
|
||
"""Return whether deployment policy authorizes only the CH1 vehicle path."""
|
||
|
||
return bool(
|
||
not self.hardware_settings.enabled
|
||
and self.vehicle_can_settings.configured
|
||
and self.vehicle_can_settings.channel_is_allowed
|
||
and self.vehicle_can_settings.channel == 1
|
||
)
|
||
|
||
def _driver_only_vehicle_execution_active(self, transport: str) -> bool:
|
||
if self.hardware_settings.enabled:
|
||
return False
|
||
if transport == WIRELESS_MQTT_TRANSPORT:
|
||
return self.vehicle_mqtt_settings.configured
|
||
return self.driver_only_ch1_execution_active
|
||
|
||
@staticmethod
|
||
def _validate_control_transport(transport: str) -> str:
|
||
normalized = str(transport).strip().lower()
|
||
if normalized not in VEHICLE_CONTROL_TRANSPORTS:
|
||
raise ValueError(
|
||
"control_transport must be wired_can or wireless_mqtt"
|
||
)
|
||
return normalized
|
||
|
||
def runtime_info(self) -> dict[str, Any]:
|
||
domain_runtime_info = [
|
||
runtime.runtime_info() for runtime in self.domain_runtimes.values()
|
||
]
|
||
default_vehicle = self.vehicle_profiles.default()
|
||
hardware_info = self.hardware_settings.public_info()
|
||
hardware_info.update(self.hardware_execution.public_info())
|
||
hardware_info.update(self.hardware_access.public_info())
|
||
vehicle_can_info = self.vehicle_can_settings.public_info()
|
||
if self._vehicle_state_monitor is not None:
|
||
state_sync_info = self._vehicle_state_monitor.public_info()
|
||
if self.hardware_settings.configured:
|
||
# Keep the slower legacy cadence when the same page also polls
|
||
# the physical passenger-seat path through the shared adapter.
|
||
state_sync_info["poll_interval_ms"] = 5_000
|
||
vehicle_can_info["state_sync"] = state_sync_info
|
||
elif self.vehicle_can_settings.state_configured:
|
||
vehicle_can_info["state_sync"] = {
|
||
"mode": "on_demand_shared_adapter",
|
||
"state": "ready",
|
||
"read_only": True,
|
||
"poll_interval_ms": 5_000,
|
||
"snapshot_freshness_ms": 5_000,
|
||
"capture_sequence": None,
|
||
"observed_frame_ids": [],
|
||
"last_error": None,
|
||
}
|
||
vehicle_mqtt_info = self._vehicle_mqtt_transport.public_info()
|
||
return {
|
||
"service": "cabin-cognitive-orchestrator",
|
||
"domain": "cabin",
|
||
"platform": "reference_vehicle",
|
||
"execution_mode": os.getenv(
|
||
"COGNITIVE_AGENT_EXECUTION_MODE", "dry_run"
|
||
).strip(),
|
||
"execution_modes": ["dry_run", "simulated_hardware", "hardware"],
|
||
"hardware_execution_allowed": (
|
||
self.hardware_settings.configured
|
||
or self.vehicle_can_settings.configured
|
||
or self.vehicle_mqtt_settings.configured
|
||
),
|
||
"hardware": hardware_info,
|
||
"vehicle_can": vehicle_can_info,
|
||
"vehicle_mqtt": vehicle_mqtt_info,
|
||
"vehicle_transports": {
|
||
"default": WIRED_CAN_TRANSPORT,
|
||
"selection_scope": "browser_request",
|
||
"options": [
|
||
{
|
||
"id": WIRED_CAN_TRANSPORT,
|
||
"label": "有线控制",
|
||
**vehicle_can_info,
|
||
"target_policy": (
|
||
"dual_channel_wired_can0_passenger_can1_vehicle"
|
||
if self.hardware_settings.configured
|
||
and self.vehicle_can_settings.configured
|
||
else vehicle_can_info.get("target_policy")
|
||
),
|
||
"channels": {
|
||
"seat": 0,
|
||
"vehicle": self.vehicle_can_settings.channel,
|
||
},
|
||
},
|
||
{
|
||
"id": WIRELESS_MQTT_TRANSPORT,
|
||
"label": "无线控制",
|
||
**vehicle_mqtt_info,
|
||
},
|
||
],
|
||
},
|
||
"vehicle_safety_policy": {
|
||
"required_channel": 1,
|
||
"required_gear": "P",
|
||
"maximum_speed_kmh": 0.05,
|
||
"fresh_feedback_ids": ["0x342", "0x4EB"],
|
||
"preflight": "read_only_before_every_physical_request",
|
||
},
|
||
"multi_channel_can": {
|
||
"adapter_ownership": "single_process",
|
||
"seat_channel": (
|
||
self.hardware_settings.channel
|
||
if self.hardware_settings.enabled
|
||
else None
|
||
),
|
||
"vehicle_channel": self.vehicle_can_settings.channel,
|
||
"channels_must_be_distinct": bool(
|
||
self.hardware_settings.enabled
|
||
and self.vehicle_can_settings.enabled
|
||
),
|
||
"active_policy": (
|
||
"dual_channel_wired_can0_passenger_can1_vehicle"
|
||
if self.hardware_settings.configured
|
||
and self.vehicle_can_settings.configured
|
||
else "ch1_body_can_driver_only"
|
||
if self.vehicle_can_settings.configured
|
||
else "ch1_body_can_read_only"
|
||
if self.vehicle_can_settings.state_configured
|
||
else "ch0_passenger_only"
|
||
if self.hardware_settings.configured
|
||
else "virtual_only"
|
||
),
|
||
"seat_roles": {
|
||
"passenger": {"zone": "front_right", "channel": 0},
|
||
"driver": {"zone": "front_left", "channel": 1},
|
||
},
|
||
},
|
||
"seat_position_memory_synchronized": bool(
|
||
self._seat_position_memory_state.get("synchronized", False)
|
||
),
|
||
"simulated_hardware": {
|
||
"enabled": self.simulated_hardware_enabled,
|
||
"default_fault": "normal",
|
||
"fault_scenarios": list(SIMULATED_HARDWARE_FAULTS),
|
||
"physical_can_opened": False,
|
||
},
|
||
"openai_available": self.openai.available,
|
||
"openai_model": self.openai.model,
|
||
"openai_models": self.openai.model_catalog,
|
||
"openai_reasoning_effort_default": self.openai.reasoning_effort,
|
||
"openai_reasoning_efforts": list(REASONING_EFFORTS),
|
||
"openai_responses_endpoint": self.openai.responses_endpoint,
|
||
"conversation_memory": True,
|
||
"default_vehicle_id": default_vehicle.id,
|
||
"vehicle": default_vehicle.public_dict(),
|
||
"vehicles": self.vehicle_profiles.public_profiles(),
|
||
"modes": self.mode_profiles.public_modes(default_vehicle.id),
|
||
"mode_profile_storage": "persistent_yaml",
|
||
"mode_profile_user_root": str(self.mode_profile_user_root),
|
||
"preset_storage": "conversation_durable",
|
||
"inferred_level_range": [1, 3],
|
||
"motion_position_range_pct": [0, 100],
|
||
"capability_domains": [
|
||
{
|
||
"domain": item["domain"],
|
||
"availability": item["availability"],
|
||
}
|
||
for item in self.capability_context["domains"]
|
||
],
|
||
"capabilities": [
|
||
capability
|
||
for domain in self.capability_context["domains"]
|
||
for capability in domain["capabilities"]
|
||
],
|
||
"matrix": {
|
||
"messages": len(self.matrix.data["messages"])
|
||
+ sum(item["matrix"]["messages"] for item in domain_runtime_info),
|
||
"signals": sum(len(item["signals"]) for item in self.matrix.data["messages"])
|
||
+ sum(item["matrix"]["signals"] for item in domain_runtime_info),
|
||
"source": {
|
||
"seat": self.matrix.data["source"],
|
||
**{
|
||
item["domain"]: item["matrix"]["source"]
|
||
for item in domain_runtime_info
|
||
},
|
||
},
|
||
"limitations": self.matrix.data["limitations"]
|
||
+ [
|
||
limitation
|
||
for item in domain_runtime_info
|
||
for limitation in item["matrix"]["limitations"]
|
||
],
|
||
},
|
||
"domain_runtimes": domain_runtime_info,
|
||
}
|
||
|
||
def delete_user_mode_profile(
|
||
self,
|
||
mode_id: str,
|
||
vehicle_id: str | None = None,
|
||
) -> dict[str, Any]:
|
||
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
|
||
profile = self.mode_profiles.delete_user_mode(mode_id, resolved_vehicle_id)
|
||
return {
|
||
"status": "succeeded",
|
||
"deleted_mode_profile": profile.public_dict(),
|
||
"vehicle_id": resolved_vehicle_id,
|
||
}
|
||
|
||
def get_mode_profile_card(
|
||
self,
|
||
mode_id: str,
|
||
vehicle_id: str | None = None,
|
||
) -> ModeProfile:
|
||
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
|
||
return self.mode_profiles.get_profile(mode_id, resolved_vehicle_id)
|
||
|
||
def execute_mode_profile_card(
|
||
self,
|
||
mode_id: str,
|
||
*,
|
||
conversation_id: str | None = None,
|
||
execution_mode: str = "dry_run",
|
||
approval_token: str | None = None,
|
||
vehicle_id: str | None = None,
|
||
simulation_fault: str = "normal",
|
||
hardware_client_id: str | None = None,
|
||
hardware_access_bypass: bool = True,
|
||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||
) -> dict[str, Any]:
|
||
"""Execute a persisted mode by ID without interpreting natural language."""
|
||
|
||
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
|
||
profile = self.mode_profiles.get_profile(mode_id, resolved_vehicle_id)
|
||
if not profile.command_flow or not profile.command_flow.get("plans"):
|
||
raise ValueError("该模式尚未生成可直接执行的控制指令流。")
|
||
result = self.process(
|
||
profile.instruction or f"进入{profile.name}",
|
||
provider="local",
|
||
conversation_id=conversation_id,
|
||
execution_mode=execution_mode,
|
||
approval_token=approval_token,
|
||
vehicle_id=resolved_vehicle_id,
|
||
simulation_fault=simulation_fault,
|
||
hardware_client_id=hardware_client_id,
|
||
hardware_access_bypass=hardware_access_bypass,
|
||
control_transport=control_transport,
|
||
_direct_mode_profile_id=profile.id,
|
||
)
|
||
public_profile = profile.public_dict()
|
||
result["provider_used"] = "saved_mode_command_flow"
|
||
result["utterance"] = profile.name
|
||
result["matched_modes"] = [public_profile]
|
||
result["mode_profile"] = public_profile
|
||
result["orchestration_route"] = "saved_mode_command_flow"
|
||
result["decision_summary"] = [
|
||
f"直接载入已保存的模式“{profile.name}”,未再次解析自然语言。",
|
||
*list(result.get("decision_summary") or []),
|
||
]
|
||
self._persist(result)
|
||
return result
|
||
|
||
def save_mode_profile_card(
|
||
self,
|
||
mode_id: str,
|
||
name: str,
|
||
instruction: str,
|
||
source_request_id: str,
|
||
vehicle_id: str | None = None,
|
||
) -> dict[str, Any]:
|
||
"""Persist one edited mode from a server-generated control flow."""
|
||
|
||
return self._persist_mode_profile_card(
|
||
mode_id,
|
||
name,
|
||
instruction,
|
||
source_request_id,
|
||
vehicle_id,
|
||
)
|
||
|
||
def create_mode_profile_card(
|
||
self,
|
||
name: str,
|
||
instruction: str,
|
||
source_request_id: str,
|
||
vehicle_id: str | None = None,
|
||
) -> dict[str, Any]:
|
||
"""Create one user mode from a server-generated control flow."""
|
||
|
||
return self._persist_mode_profile_card(
|
||
None,
|
||
name,
|
||
instruction,
|
||
source_request_id,
|
||
vehicle_id,
|
||
)
|
||
|
||
def _persist_mode_profile_card(
|
||
self,
|
||
mode_id: str | None,
|
||
name: str,
|
||
instruction: str,
|
||
source_request_id: str,
|
||
vehicle_id: str | None = None,
|
||
) -> dict[str, Any]:
|
||
"""Validate and persist a created or edited mode command flow."""
|
||
|
||
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
|
||
clean_mode_id = str(mode_id or "").strip()
|
||
clean_name = str(name).strip()
|
||
clean_instruction = str(instruction).strip()
|
||
clean_request_id = str(source_request_id).strip()
|
||
if not clean_name:
|
||
raise ValueError("模式名称不能为空。")
|
||
if len(clean_name) > 64:
|
||
raise ValueError("模式名称不能超过 64 个字符。")
|
||
if not clean_instruction:
|
||
raise ValueError("自然语言指令不能为空。")
|
||
if len(clean_instruction) > 1000:
|
||
raise ValueError("自然语言指令不能超过 1000 个字符。")
|
||
if not MODE_EDITOR_REQUEST_ID_PATTERN.fullmatch(clean_request_id):
|
||
raise ValueError("控制指令流来源请求编号无效。")
|
||
|
||
visible_profiles = self.mode_profiles._profiles_for_vehicle( # noqa: SLF001
|
||
resolved_vehicle_id
|
||
)
|
||
profile = (
|
||
next(
|
||
(item for item in visible_profiles if item.id == clean_mode_id),
|
||
None,
|
||
)
|
||
if clean_mode_id
|
||
else None
|
||
)
|
||
if clean_mode_id and profile is None:
|
||
raise KeyError(clean_mode_id)
|
||
normalized_name = re.sub(r"\s+", "", clean_name).casefold()
|
||
if any(
|
||
(profile is None or item.id != profile.id)
|
||
and re.sub(r"\s+", "", item.name).casefold() == normalized_name
|
||
for item in visible_profiles
|
||
):
|
||
raise ValueError("该模式名称已被其他模式使用。")
|
||
|
||
result_path = self.artifact_root / clean_request_id / "result.json"
|
||
if not result_path.is_file():
|
||
raise ValueError("未找到“生成指令”按钮产生的控制指令流,请重新生成。")
|
||
try:
|
||
source_result = json.loads(result_path.read_text(encoding="utf-8"))
|
||
except (OSError, json.JSONDecodeError) as exc:
|
||
raise ValueError("控制指令流来源结果无法读取,请重新生成。") from exc
|
||
if not isinstance(source_result, dict) or source_result.get(
|
||
"request_id"
|
||
) != clean_request_id:
|
||
raise ValueError("控制指令流来源结果不完整,请重新生成。")
|
||
if source_result.get("status") != "succeeded":
|
||
raise ValueError("控制指令流没有完整通过校验,请重新生成。")
|
||
source_vehicle = source_result.get("vehicle")
|
||
source_vehicle_id = (
|
||
str(source_vehicle.get("id") or "").strip()
|
||
if isinstance(source_vehicle, dict)
|
||
else ""
|
||
)
|
||
if source_vehicle_id and source_vehicle_id != resolved_vehicle_id:
|
||
raise ValueError("控制指令流不属于当前车型,请重新生成。")
|
||
source_draft = source_result.get("mode_profile_draft")
|
||
if isinstance(source_draft, dict):
|
||
draft_request_id = str(source_draft.get("source_request_id") or "").strip()
|
||
draft_vehicle_id = str(source_draft.get("vehicle_id") or "").strip()
|
||
draft_instruction = str(source_draft.get("instruction") or "").strip()
|
||
draft_name = str(source_draft.get("name") or "").strip()
|
||
if draft_request_id != clean_request_id:
|
||
raise ValueError("功能模式草稿与控制指令流不匹配,请重新生成。")
|
||
if draft_vehicle_id and draft_vehicle_id != resolved_vehicle_id:
|
||
raise ValueError("功能模式草稿不属于当前车型,请重新生成。")
|
||
if draft_instruction and draft_instruction != clean_instruction:
|
||
raise ValueError("自然语言指令已修改,请重新生成模式预览。")
|
||
if draft_name and draft_name != clean_name:
|
||
raise ValueError("功能模式名称已修改,请重新生成模式预览。")
|
||
recorded_instruction = str(source_result.get("utterance", "")).strip()
|
||
if not recorded_instruction:
|
||
source_spans = {
|
||
str(item.get("source_span", "")).strip()
|
||
for item in (
|
||
(source_result.get("intent_ir") or {}).get("spec", {}).get(
|
||
"requests", []
|
||
)
|
||
)
|
||
if isinstance(item, dict) and str(item.get("source_span", "")).strip()
|
||
}
|
||
if len(source_spans) == 1:
|
||
recorded_instruction = next(iter(source_spans))
|
||
if recorded_instruction and recorded_instruction != clean_instruction:
|
||
raise ValueError("自然语言指令已修改,请重新点击“生成指令”。")
|
||
|
||
plans = source_result.get("command_plans") or {}
|
||
configs = source_result.get("function_configs") or {}
|
||
if not isinstance(plans, dict) or not isinstance(configs, dict):
|
||
raise ValueError("解析结果中没有可永久化的控制指令流。")
|
||
executable_plan_keys = {
|
||
key
|
||
for key, plan in plans.items()
|
||
if isinstance(plan, dict)
|
||
and isinstance(plan.get("spec"), dict)
|
||
and isinstance(plan["spec"].get("commands"), list)
|
||
and plan["spec"]["commands"]
|
||
}
|
||
if not executable_plan_keys:
|
||
raise ValueError("请先点击“生成指令”并成功生成控制指令流。")
|
||
|
||
operations: list[dict[str, Any]] = []
|
||
signatures: set[tuple[str, str, str, str]] = set()
|
||
for key in plans:
|
||
if key not in executable_plan_keys:
|
||
continue
|
||
config = configs.get(key)
|
||
if not isinstance(config, dict):
|
||
continue
|
||
spec = config.get("spec") or {}
|
||
domain = str(spec.get("domain", "")).strip()
|
||
for operation in spec.get("operations", []):
|
||
if not isinstance(operation, dict):
|
||
continue
|
||
capability = str(operation.get("capability", "")).strip()
|
||
operation_target = operation.get("target") or {}
|
||
target = str(
|
||
operation_target.get("instance")
|
||
or operation_target.get("zone")
|
||
or ""
|
||
).strip()
|
||
parameters = operation.get("parameters") or {}
|
||
if not domain or not capability or not target or not isinstance(
|
||
parameters, dict
|
||
):
|
||
continue
|
||
signature = (
|
||
domain,
|
||
capability,
|
||
target,
|
||
json.dumps(parameters, ensure_ascii=False, sort_keys=True),
|
||
)
|
||
if signature in signatures:
|
||
continue
|
||
signatures.add(signature)
|
||
operations.append(
|
||
{
|
||
"domain": domain,
|
||
"capability": capability,
|
||
"target_scope": target,
|
||
"operation": "set",
|
||
"parameters": deepcopy(parameters),
|
||
"reason": f"由模式编辑器请求 {clean_request_id} 固化。",
|
||
}
|
||
)
|
||
if not operations:
|
||
raise ValueError("控制指令流没有关联到可复用的模式操作。")
|
||
|
||
command_flow = {
|
||
"schema": "cognitive-agent-mode-command-flow/v1",
|
||
"source_request_id": clean_request_id,
|
||
"generated_at": _now(),
|
||
"execution_mode": source_result.get("execution_mode"),
|
||
"control_transport": source_result.get("control_transport"),
|
||
"function_configs": {
|
||
key: deepcopy(configs[key])
|
||
for key in plans
|
||
if key in executable_plan_keys and key in configs
|
||
},
|
||
"plans": {key: deepcopy(plans[key]) for key in plans if key in executable_plan_keys},
|
||
# A mode card stores only the validated, deterministic plan. Live
|
||
# delivery evidence belongs to each later execution, not to the
|
||
# generation workflow.
|
||
"execution_steps": [],
|
||
}
|
||
saved = self.mode_profiles.save_mode(
|
||
clean_name,
|
||
operations,
|
||
mode_id=profile.id if profile is not None else None,
|
||
aliases=list(profile.aliases) if profile is not None else [],
|
||
vehicle_id=resolved_vehicle_id,
|
||
description=(
|
||
profile.description
|
||
if profile is not None
|
||
else "由自然语言生成的自定义模式。"
|
||
),
|
||
icon=profile.icon if profile is not None else "bookmark-plus",
|
||
instruction=clean_instruction,
|
||
command_flow=command_flow,
|
||
timeline=profile.timeline if profile is not None else None,
|
||
)
|
||
public_profile = next(
|
||
(
|
||
item
|
||
for item in self.mode_profiles.public_modes(resolved_vehicle_id)
|
||
if item["id"] == saved.id
|
||
),
|
||
saved.public_dict(),
|
||
)
|
||
return {
|
||
"status": "succeeded",
|
||
"mode_profile": public_profile,
|
||
"vehicle_id": resolved_vehicle_id,
|
||
"storage": "persistent_yaml",
|
||
"source_request_id": clean_request_id,
|
||
}
|
||
|
||
def synchronize_hardware_state(
|
||
self,
|
||
conversation_id: str,
|
||
vehicle_id: str | None = None,
|
||
*,
|
||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||
hardware_client_id: str | None = None,
|
||
hardware_access_bypass: bool = True,
|
||
) -> dict[str, Any]:
|
||
"""Read the bench only when no physical execution currently owns it."""
|
||
|
||
control_transport = self._validate_control_transport(control_transport)
|
||
mqtt_selected = control_transport == WIRELESS_MQTT_TRANSPORT
|
||
if hardware_access_bypass:
|
||
# Wireless CAN0 is a thread-safe view over the persistent MQTT
|
||
# subscription. HardwareSeatExecutor still serializes this seat
|
||
# read with seat actuation through its adapter lock, while an
|
||
# absent CAN0 stream must not block the independent cached CAN1
|
||
# dashboard or make control requests fail with hardware_busy.
|
||
if mqtt_selected:
|
||
return self._synchronize_hardware_state_once(
|
||
conversation_id,
|
||
vehicle_id,
|
||
control_transport=control_transport,
|
||
)
|
||
with self.hardware_execution.reserve():
|
||
return self._synchronize_hardware_state_once(
|
||
conversation_id,
|
||
vehicle_id,
|
||
control_transport=control_transport,
|
||
)
|
||
authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id)
|
||
if mqtt_selected:
|
||
with self.hardware_access.use(authorized_client_id):
|
||
return self._synchronize_hardware_state_once(
|
||
conversation_id,
|
||
vehicle_id,
|
||
control_transport=control_transport,
|
||
)
|
||
with self.hardware_execution.reserve():
|
||
with self.hardware_access.use(authorized_client_id):
|
||
return self._synchronize_hardware_state_once(
|
||
conversation_id,
|
||
vehicle_id,
|
||
control_transport=control_transport,
|
||
)
|
||
|
||
def _synchronize_hardware_state_once(
|
||
self,
|
||
conversation_id: str,
|
||
vehicle_id: str | None = None,
|
||
*,
|
||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||
) -> dict[str, Any]:
|
||
"""Read the bench and project it into the current passenger conversation."""
|
||
|
||
if not CONVERSATION_ID_PATTERN.fullmatch(conversation_id):
|
||
raise ValueError(
|
||
"conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens"
|
||
)
|
||
control_transport = self._validate_control_transport(control_transport)
|
||
mqtt_selected = control_transport == WIRELESS_MQTT_TRANSPORT
|
||
wired_state_bridge = None
|
||
if mqtt_selected:
|
||
if not self.vehicle_mqtt_settings.state_configured:
|
||
raise HardwareSafetyError("the MQTT CAN0 seat relay is not configured")
|
||
bus_factory = lambda: self._vehicle_mqtt_transport.bus(
|
||
channel=0,
|
||
replay_latest=True,
|
||
)
|
||
else:
|
||
if not self.hardware_settings.configured:
|
||
raise HardwareSafetyError(
|
||
"the passenger-seat CAN0 hardware runtime is not configured"
|
||
)
|
||
if (
|
||
self.hardware_settings.native_passenger_can0
|
||
and self.vehicle_can_settings.adapter_configured
|
||
):
|
||
wired_state_bridge = self.vehicle_can_settings.bridge(
|
||
[
|
||
ZlgChannelConfig(
|
||
channel=0,
|
||
frame_format="classic_can",
|
||
arbitration_bitrate=self.hardware_settings.bitrate,
|
||
data_bitrate=self.vehicle_can_settings.data_bitrate,
|
||
can_fd_iso=True,
|
||
termination_enabled=(
|
||
self.hardware_settings.termination_enabled
|
||
),
|
||
)
|
||
],
|
||
monitor_only=True,
|
||
)
|
||
bus_factory = lambda: wired_state_bridge.channel_bus(0)
|
||
else:
|
||
bus_factory = None
|
||
vehicle_profile = self.vehicle_profiles.get(vehicle_id)
|
||
try:
|
||
physical_state = self._new_hardware_executor(
|
||
bus_factory,
|
||
remote_transport=mqtt_selected,
|
||
).read_physical_state()
|
||
finally:
|
||
if wired_state_bridge is not None:
|
||
wired_state_bridge.close()
|
||
if mqtt_selected:
|
||
physical_state["source"] = "wireless_mqtt_ch0"
|
||
context = self.conversations.synchronize_physical_passenger(
|
||
conversation_id,
|
||
physical_state,
|
||
)
|
||
context = self._context_for_vehicle(context, vehicle_profile)
|
||
return {
|
||
"status": "succeeded",
|
||
"conversation_id": conversation_id,
|
||
"conversation_turn": context["turn_count"],
|
||
"source": "wireless_mqtt_ch0" if mqtt_selected else "physical_can",
|
||
"control_transport": control_transport,
|
||
"captured_at": physical_state.get("captured_at"),
|
||
"complete": bool(physical_state.get("complete")),
|
||
"missing": list(physical_state.get("missing", [])),
|
||
"physical_state": physical_state,
|
||
"actuator_state": context["devices"],
|
||
}
|
||
|
||
def synchronize_vehicle_state(
|
||
self,
|
||
conversation_id: str,
|
||
vehicle_id: str | None = None,
|
||
*,
|
||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||
hardware_client_id: str | None = None,
|
||
hardware_access_bypass: bool = True,
|
||
) -> dict[str, Any]:
|
||
"""Passively receive CH1 driver-seat feedback without transmitting CAN."""
|
||
|
||
control_transport = self._validate_control_transport(control_transport)
|
||
mqtt_selected = control_transport == WIRELESS_MQTT_TRANSPORT
|
||
|
||
if hardware_access_bypass:
|
||
# The MQTT snapshot is already decoded and protected by the
|
||
# transport condition lock. It is independent from physical
|
||
# execution ownership and should remain readable during CAN0
|
||
# waits or an in-flight command.
|
||
if mqtt_selected:
|
||
return self._synchronize_vehicle_state_once(
|
||
conversation_id,
|
||
vehicle_id,
|
||
control_transport=control_transport,
|
||
)
|
||
with self.hardware_execution.reserve():
|
||
return self._synchronize_vehicle_state_once(
|
||
conversation_id,
|
||
vehicle_id,
|
||
control_transport=control_transport,
|
||
)
|
||
authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id)
|
||
if mqtt_selected:
|
||
with self.hardware_access.use(authorized_client_id):
|
||
return self._synchronize_vehicle_state_once(
|
||
conversation_id,
|
||
vehicle_id,
|
||
control_transport=control_transport,
|
||
)
|
||
with self.hardware_execution.reserve():
|
||
with self.hardware_access.use(authorized_client_id):
|
||
return self._synchronize_vehicle_state_once(
|
||
conversation_id,
|
||
vehicle_id,
|
||
control_transport=control_transport,
|
||
)
|
||
|
||
def start(self) -> None:
|
||
if self._vehicle_state_monitor is not None:
|
||
self._vehicle_state_monitor.start()
|
||
self._vehicle_mqtt_transport.start()
|
||
|
||
def close(self) -> None:
|
||
if self._vehicle_state_monitor is not None:
|
||
self._vehicle_state_monitor.close()
|
||
self._vehicle_mqtt_transport.close()
|
||
|
||
def _synchronize_vehicle_state_once(
|
||
self,
|
||
conversation_id: str,
|
||
vehicle_id: str | None = None,
|
||
*,
|
||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||
) -> dict[str, Any]:
|
||
if not CONVERSATION_ID_PATTERN.fullmatch(conversation_id):
|
||
raise ValueError(
|
||
"conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens"
|
||
)
|
||
control_transport = self._validate_control_transport(control_transport)
|
||
mqtt_selected = control_transport == WIRELESS_MQTT_TRANSPORT
|
||
if mqtt_selected:
|
||
if not self.vehicle_mqtt_settings.state_configured:
|
||
raise HardwareSafetyError("the MQTT vehicle CAN relay is not configured")
|
||
elif not self.vehicle_can_settings.state_configured:
|
||
raise HardwareSafetyError("the CH1 vehicle CAN runtime is not configured")
|
||
|
||
vehicle_profile = self.vehicle_profiles.get(vehicle_id)
|
||
shared_runtime_enabled = (
|
||
self.vehicle_can_settings.backend != "zlgcan-linux"
|
||
and self.vehicle_can_settings.configured
|
||
and os.getenv(
|
||
"COGNITIVE_AGENT_SHARED_CAN_RUNTIME_ENABLED",
|
||
os.getenv(
|
||
"COGNITIVE_AGENT_SEAT_SHARED_ZCANPRO_BRIDGE_ENABLED", "true"
|
||
),
|
||
).strip().lower() in {"1", "true", "yes", "on"}
|
||
)
|
||
bridge = None
|
||
capture_sequence: int | None = None
|
||
if mqtt_selected:
|
||
snapshot = self._vehicle_mqtt_transport.snapshot()
|
||
feedback_states = list(snapshot.feedback_states)
|
||
observed_ids = set(snapshot.observed_ids)
|
||
captured_at = snapshot.captured_at
|
||
capture_sequence = snapshot.capture_sequence
|
||
elif self._vehicle_state_monitor is not None:
|
||
snapshot = self._vehicle_state_monitor.snapshot()
|
||
feedback_states = list(snapshot.feedback_states)
|
||
observed_ids = set(snapshot.observed_ids)
|
||
captured_at = snapshot.captured_at
|
||
capture_sequence = snapshot.capture_sequence
|
||
elif shared_runtime_enabled:
|
||
from cognitive_agent.application.self_repair.service import BRIDGE_MANAGER
|
||
|
||
if not hasattr(BRIDGE_MANAGER, "channel_bus_factory"):
|
||
raise HardwareSafetyError(
|
||
"CH1 feedback requires the standalone multi-channel CAN runtime"
|
||
)
|
||
bus = BRIDGE_MANAGER.channel_bus_factory(
|
||
self.vehicle_can_settings.channel
|
||
)()
|
||
else:
|
||
bridge = self.vehicle_can_settings.bridge(
|
||
[self.vehicle_can_settings.channel_config()],
|
||
monitor_only=True,
|
||
)
|
||
bus = bridge.channel_bus(self.vehicle_can_settings.channel)
|
||
|
||
if not mqtt_selected and self._vehicle_state_monitor is None:
|
||
feedback_states = []
|
||
observed_ids = set()
|
||
captured_at = None
|
||
# Keep driver-seat completeness compatible with the existing endpoint,
|
||
# while passively collecting every recognized CH1 body-state frame for
|
||
# the whole-vehicle dashboard during the same bounded receive window.
|
||
deadline = (
|
||
time.monotonic()
|
||
+ self.vehicle_can_settings.feedback_timeout_ms / 1000
|
||
)
|
||
try:
|
||
while time.monotonic() < deadline and observed_ids != OBSERVABLE_IDS:
|
||
remaining_ms = max(1, int((deadline - time.monotonic()) * 1000))
|
||
frame = bus.receive(remaining_ms, include_fd=True)
|
||
if frame is None:
|
||
break
|
||
if frame.channel != self.vehicle_can_settings.channel:
|
||
continue
|
||
if frame.arbitration_id not in OBSERVABLE_IDS or len(frame.data) < 8:
|
||
continue
|
||
decoded = decode_feedback(frame)
|
||
if not any(
|
||
key in decoded
|
||
for key in ("actuator_state", "domain_state", "vehicle_safety")
|
||
):
|
||
# 0x4EB is multiplexed. Only subtype 0x07 carries the
|
||
# verified speed signal, so unrelated subframes must not
|
||
# make the body-state snapshot look complete.
|
||
continue
|
||
frame_id = str(decoded.get("frame_id", ""))
|
||
decoded.update(
|
||
{
|
||
"valid": True,
|
||
"feedback_frame_id": frame_id,
|
||
"captured_at": _now(),
|
||
"verification": {
|
||
"authoritative": True,
|
||
"channel": 1,
|
||
"simulated": False,
|
||
"read_only": True,
|
||
"frame_id": frame_id,
|
||
"payload_size": len(frame.data),
|
||
},
|
||
}
|
||
)
|
||
feedback_states.append(decoded)
|
||
observed_ids.add(frame.arbitration_id)
|
||
captured_at = str(decoded["captured_at"])
|
||
finally:
|
||
if bridge is not None:
|
||
bridge.close()
|
||
else:
|
||
bus.close()
|
||
|
||
context = self.conversations.synchronize_physical_ch1_feedback(
|
||
conversation_id,
|
||
feedback_states,
|
||
)
|
||
context = self._context_for_vehicle(context, vehicle_profile)
|
||
missing = [
|
||
f"0x{frame_id:03X}"
|
||
for frame_id in sorted(REQUIRED_SEAT_IDS - observed_ids)
|
||
]
|
||
body_state_missing = [
|
||
f"0x{frame_id:03X}"
|
||
for frame_id in sorted(REQUIRED_BODY_IDS - observed_ids)
|
||
]
|
||
return {
|
||
"status": "succeeded",
|
||
"conversation_id": conversation_id,
|
||
"conversation_turn": context["turn_count"],
|
||
"source": (
|
||
"wireless_mqtt_ch1" if mqtt_selected else "physical_can_ch1"
|
||
),
|
||
"control_transport": control_transport,
|
||
"read_only": True,
|
||
"captured_at": captured_at or _now(),
|
||
"capture_sequence": capture_sequence,
|
||
"complete": not missing,
|
||
"missing": missing,
|
||
"body_state_complete": not body_state_missing,
|
||
"body_state_missing": body_state_missing,
|
||
"observed_frame_ids": [f"0x{frame_id:03X}" for frame_id in sorted(observed_ids)],
|
||
"actuator_state": context["devices"],
|
||
"domain_state": context["domain_devices"],
|
||
}
|
||
|
||
def _new_hardware_executor(
|
||
self,
|
||
bus_factory: Any | None = None,
|
||
*,
|
||
remote_transport: bool = False,
|
||
) -> HardwareSeatExecutor | MqttNativeSeatExecutor | ZcanBridgeSeatExecutor:
|
||
shared_bridge_enabled = os.getenv(
|
||
"COGNITIVE_AGENT_SHARED_CAN_RUNTIME_ENABLED",
|
||
os.getenv("COGNITIVE_AGENT_SEAT_SHARED_ZCANPRO_BRIDGE_ENABLED", "true"),
|
||
).strip().lower() in {"1", "true", "yes", "on"}
|
||
if (
|
||
bus_factory is None
|
||
and shared_bridge_enabled
|
||
and self.hardware_settings.backend != "zlgcan-linux"
|
||
):
|
||
# Import lazily so the ordinary dry-run service does not initialize
|
||
# any self-repair runtime state. Both modules then share the one
|
||
# process that is allowed to own the Windows ZLG adapter.
|
||
from cognitive_agent.application.self_repair.service import (
|
||
BRIDGE_MANAGER,
|
||
PENDING_DIR,
|
||
REQUIRED_BRIDGE_VERSION,
|
||
STATUS_PATH,
|
||
)
|
||
manager = BRIDGE_MANAGER
|
||
manager_snapshot = manager.snapshot()
|
||
if (
|
||
manager_snapshot.get("backend") == "standalone_zlg"
|
||
or Path(str(manager_snapshot.get("zcan_exe") or "")).is_file()
|
||
):
|
||
return ZcanBridgeSeatExecutor(
|
||
self.hardware_matrix,
|
||
manager,
|
||
STATUS_PATH,
|
||
PENDING_DIR,
|
||
required_version=REQUIRED_BRIDGE_VERSION,
|
||
auto_home=self.hardware_settings.auto_home_enabled,
|
||
)
|
||
resolved_bus_factory = bus_factory or self.hardware_settings.bus_factory()
|
||
executor = HardwareSeatExecutor(
|
||
self.hardware_matrix,
|
||
resolved_bus_factory,
|
||
thermal_timeout_ms=self.hardware_settings.thermal_timeout_ms,
|
||
ack_timeout_ms=self.hardware_settings.ack_timeout_ms,
|
||
axis_maximum_x100={
|
||
capability: int(
|
||
self.hardware_compiler.protocol["normalized_position_max"]
|
||
)
|
||
for capability in self.hardware_compiler.axes
|
||
},
|
||
motor_max_hall={
|
||
int(axis["motor_selector"]): int(axis["default_max_hall_count"])
|
||
for axis in self.hardware_compiler.axes.values()
|
||
},
|
||
stop_all_motor_selector=int(
|
||
self.hardware_compiler.protocol["stop_all_motor_selector"]
|
||
),
|
||
auto_home=(
|
||
self.hardware_settings.auto_home_enabled
|
||
if not remote_transport
|
||
else False
|
||
),
|
||
home_timeout_s=self.hardware_settings.home_timeout_s,
|
||
firmware_guard=(
|
||
None
|
||
if remote_transport
|
||
or self.hardware_settings.native_passenger_can0
|
||
else self.hardware_settings.firmware_guard(
|
||
resolved_bus_factory,
|
||
allow_programming=not remote_transport,
|
||
)
|
||
),
|
||
synchronize_position_before_first_motion=(
|
||
self.hardware_settings.position_sync_enabled
|
||
if not remote_transport
|
||
else False
|
||
),
|
||
position_sync_state=self._seat_position_memory_state,
|
||
require_thermal_feedback=not remote_transport,
|
||
feedback_required=not remote_transport,
|
||
)
|
||
if remote_transport:
|
||
return MqttNativeSeatExecutor(executor, resolved_bus_factory)
|
||
return executor
|
||
|
||
def _new_linux_wired_dual_channel_bridge(self) -> ZlgCanBridgeProcess:
|
||
"""Open CAN0 passenger classic CAN and CAN1 body CAN FD under one owner."""
|
||
|
||
if not self.hardware_settings.native_passenger_can0:
|
||
raise HardwareSafetyError(
|
||
"the shared Linux dual-channel bridge requires native passenger CAN0"
|
||
)
|
||
if not self.vehicle_can_settings.configured:
|
||
raise HardwareSafetyError(
|
||
"the shared Linux dual-channel bridge requires configured vehicle CAN1"
|
||
)
|
||
if (
|
||
self.hardware_settings.device_type
|
||
!= self.vehicle_can_settings.device_type
|
||
or self.hardware_settings.device_index
|
||
!= self.vehicle_can_settings.device_index
|
||
or self.hardware_settings.zlg_dll.resolve()
|
||
!= self.vehicle_can_settings.zlg_dll.resolve()
|
||
):
|
||
raise HardwareSafetyError(
|
||
"CAN0 and CAN1 must use the same Linux ZLG device and SDK library"
|
||
)
|
||
return self.vehicle_can_settings.bridge(
|
||
[
|
||
ZlgChannelConfig(
|
||
channel=0,
|
||
frame_format="classic_can",
|
||
arbitration_bitrate=self.hardware_settings.bitrate,
|
||
data_bitrate=self.vehicle_can_settings.data_bitrate,
|
||
can_fd_iso=True,
|
||
termination_enabled=(
|
||
self.hardware_settings.termination_enabled
|
||
),
|
||
),
|
||
self.vehicle_can_settings.channel_config(),
|
||
]
|
||
)
|
||
|
||
def preview_native_can(
|
||
self,
|
||
utterance: str,
|
||
provider: str = "auto",
|
||
conversation_id: str | None = None,
|
||
execution_mode: str = "dry_run",
|
||
approval_token: str | None = None,
|
||
vehicle_id: str | None = None,
|
||
simulation_fault: str = "normal",
|
||
*,
|
||
hardware_client_id: str | None = None,
|
||
hardware_access_bypass: bool = True,
|
||
reasoning_effort: str | None = None,
|
||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||
assistant_plan: dict[str, Any] | None = None,
|
||
) -> dict[str, Any]:
|
||
"""Validate and execute deterministic controls before hosted parsing."""
|
||
|
||
utterance = utterance.strip()
|
||
if not utterance:
|
||
raise ValueError("utterance must not be empty")
|
||
if provider not in {"auto", "openai", "local"}:
|
||
raise ValueError("provider must be auto, openai, or local")
|
||
conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}"
|
||
if not CONVERSATION_ID_PATTERN.fullmatch(conversation_id):
|
||
raise ValueError(
|
||
"conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens"
|
||
)
|
||
vehicle_profile = self.vehicle_profiles.get(vehicle_id)
|
||
control_plane_request = any(
|
||
matcher(utterance) is not None
|
||
for matcher in (
|
||
self._parse_mode_profile_save_name,
|
||
self._parse_mode_profile_create_name,
|
||
self._parse_mode_profile_restore_name,
|
||
self._parse_mode_profile_update,
|
||
)
|
||
)
|
||
if control_plane_request:
|
||
result = self.process(
|
||
utterance,
|
||
provider=provider,
|
||
conversation_id=conversation_id,
|
||
execution_mode=execution_mode,
|
||
approval_token=approval_token,
|
||
vehicle_id=vehicle_profile.id,
|
||
simulation_fault=simulation_fault,
|
||
hardware_client_id=hardware_client_id,
|
||
hardware_access_bypass=hardware_access_bypass,
|
||
reasoning_effort=reasoning_effort,
|
||
control_transport=control_transport,
|
||
_on_commands_validated=on_commands_validated,
|
||
_assistant_plan=assistant_plan,
|
||
)
|
||
result.update(
|
||
{
|
||
"phase": "control_plane_complete",
|
||
"preview_request_id": result["request_id"],
|
||
"unresolved_clauses": list(
|
||
result.get("unresolved_clauses") or []
|
||
),
|
||
"supplement_required": False,
|
||
}
|
||
)
|
||
self._persist(result)
|
||
return result
|
||
context = self._context_for_vehicle(
|
||
self.conversations.snapshot(conversation_id),
|
||
vehicle_profile,
|
||
)
|
||
clause_results = self._local_clause_results(utterance, context)
|
||
unresolved_clauses = [
|
||
clause
|
||
for clause, _, status in clause_results
|
||
if status != "complete"
|
||
]
|
||
started = time.perf_counter()
|
||
|
||
def publish_local_commands(validated: dict[str, Any]) -> None:
|
||
if on_commands_validated is None:
|
||
return
|
||
event = deepcopy(validated)
|
||
event.update(
|
||
{
|
||
"phase": "deterministic_commands_validated",
|
||
"provider_requested": provider,
|
||
"unresolved_clauses": unresolved_clauses,
|
||
"supplement_required": bool(
|
||
provider != "local" and unresolved_clauses
|
||
),
|
||
}
|
||
)
|
||
on_commands_validated(event)
|
||
|
||
result = self.process(
|
||
utterance,
|
||
provider="local",
|
||
conversation_id=conversation_id,
|
||
execution_mode=execution_mode,
|
||
approval_token=approval_token,
|
||
vehicle_id=vehicle_profile.id,
|
||
simulation_fault=simulation_fault,
|
||
hardware_client_id=hardware_client_id,
|
||
hardware_access_bypass=hardware_access_bypass,
|
||
reasoning_effort=reasoning_effort,
|
||
control_transport=control_transport,
|
||
_local_preview=True,
|
||
_allow_empty_control_result=True,
|
||
_on_commands_validated=publish_local_commands,
|
||
_assistant_plan=assistant_plan,
|
||
)
|
||
elapsed_ms = round((time.perf_counter() - started) * 1000, 3)
|
||
supplement_required = bool(
|
||
provider != "local" and unresolved_clauses
|
||
)
|
||
result.update(
|
||
{
|
||
"phase": "deterministic_preview",
|
||
"provider_requested": provider,
|
||
"preview_request_id": result["request_id"],
|
||
"preview_elapsed_ms": elapsed_ms,
|
||
"unresolved_clauses": unresolved_clauses,
|
||
"supplement_required": supplement_required,
|
||
}
|
||
)
|
||
result["decision_summary"] = [
|
||
*result.get("decision_summary", []),
|
||
(
|
||
"本地规则阶段已完成白名单校验并先行处理 "
|
||
f"{sum(len((config.get('spec') or {}).get('operations', [])) for config in result.get('function_configs', {}).values())} 条 CAN 控制指令。"
|
||
),
|
||
]
|
||
self._persist(result)
|
||
manifest = {
|
||
"version": 1,
|
||
"preview_request_id": result["request_id"],
|
||
"utterance": utterance,
|
||
"provider": provider,
|
||
"conversation_id": conversation_id,
|
||
"execution_mode": execution_mode,
|
||
"vehicle_id": vehicle_profile.id,
|
||
"simulation_fault": simulation_fault,
|
||
"hardware_client_id": hardware_client_id,
|
||
"reasoning_effort": reasoning_effort,
|
||
"control_transport": control_transport,
|
||
"unresolved_clauses": unresolved_clauses,
|
||
"preview_control_signatures": result.get(
|
||
"parsed_control_signatures", []
|
||
),
|
||
}
|
||
self._atomic_write(
|
||
self.artifact_root / result["request_id"] / "preview.json",
|
||
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
|
||
)
|
||
return result
|
||
|
||
def supplement_native_can(
|
||
self,
|
||
preview_request_id: str,
|
||
*,
|
||
provider: str | None = None,
|
||
approval_token: str | None = None,
|
||
hardware_client_id: str | None = None,
|
||
hardware_access_bypass: bool = True,
|
||
reasoning_effort: str | None = None,
|
||
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||
) -> dict[str, Any]:
|
||
"""Parse only unresolved clauses and merge them with the local result."""
|
||
|
||
if not MODE_EDITOR_REQUEST_ID_PATTERN.fullmatch(preview_request_id):
|
||
raise ValueError("invalid preview request id")
|
||
preview_dir = self.artifact_root / preview_request_id
|
||
try:
|
||
manifest = json.loads(
|
||
(preview_dir / "preview.json").read_text(encoding="utf-8")
|
||
)
|
||
preview = json.loads(
|
||
(preview_dir / "result.json").read_text(encoding="utf-8")
|
||
)
|
||
except (OSError, json.JSONDecodeError) as exc:
|
||
raise ValueError("native CAN preview not found") from exc
|
||
if (
|
||
not isinstance(manifest, dict)
|
||
or manifest.get("version") != 1
|
||
or manifest.get("preview_request_id") != preview_request_id
|
||
or not isinstance(preview, dict)
|
||
or preview.get("request_id") != preview_request_id
|
||
):
|
||
raise ValueError("invalid native CAN preview manifest")
|
||
selected_provider = str(manifest.get("provider") or "auto")
|
||
if provider is not None and provider != selected_provider:
|
||
raise ValueError("supplement provider does not match preview")
|
||
selected_effort = manifest.get("reasoning_effort")
|
||
if reasoning_effort is not None and reasoning_effort != selected_effort:
|
||
raise ValueError("supplement reasoning effort does not match preview")
|
||
expected_client_id = manifest.get("hardware_client_id")
|
||
if hardware_client_id != expected_client_id:
|
||
raise ValueError("supplement hardware client does not match preview")
|
||
unresolved_clauses = [
|
||
str(item).strip()
|
||
for item in manifest.get("unresolved_clauses", [])
|
||
if str(item).strip()
|
||
]
|
||
if not unresolved_clauses:
|
||
raise ValueError("native CAN preview has no unresolved clauses")
|
||
excluded_signatures = {
|
||
tuple(item)
|
||
for item in manifest.get("preview_control_signatures", [])
|
||
if isinstance(item, list)
|
||
}
|
||
|
||
def publish_cloud_commands(validated: dict[str, Any]) -> None:
|
||
if on_commands_validated is None:
|
||
return
|
||
event = self._merge_native_can_phases(preview, validated)
|
||
event.update(
|
||
{
|
||
"phase": "supplement_commands_validated",
|
||
"preview_request_id": preview_request_id,
|
||
"supplement_of": preview_request_id,
|
||
"supplement_required": False,
|
||
}
|
||
)
|
||
on_commands_validated(event)
|
||
|
||
supplement = self.process(
|
||
",".join(unresolved_clauses),
|
||
provider=selected_provider,
|
||
conversation_id=str(manifest["conversation_id"]),
|
||
execution_mode=str(manifest.get("execution_mode") or "dry_run"),
|
||
approval_token=approval_token,
|
||
vehicle_id=str(manifest["vehicle_id"]),
|
||
simulation_fault=str(manifest.get("simulation_fault") or "normal"),
|
||
hardware_client_id=hardware_client_id,
|
||
hardware_access_bypass=hardware_access_bypass,
|
||
reasoning_effort=(
|
||
str(selected_effort) if selected_effort is not None else None
|
||
),
|
||
control_transport=str(
|
||
manifest.get("control_transport") or WIRED_CAN_TRANSPORT
|
||
),
|
||
_excluded_control_signatures=excluded_signatures,
|
||
_model_parse_source="cloud_supplement",
|
||
_allow_empty_control_result=True,
|
||
_on_commands_validated=publish_cloud_commands,
|
||
)
|
||
merged = self._merge_native_can_phases(preview, supplement)
|
||
merged.update(
|
||
{
|
||
"phase": "two_stage_complete",
|
||
"utterance": str(
|
||
preview.get("utterance")
|
||
if preview.get("orchestration_route") == "unified_can_infrared"
|
||
else manifest["utterance"]
|
||
),
|
||
"provider_requested": selected_provider,
|
||
"preview_request_id": preview_request_id,
|
||
"supplement_of": preview_request_id,
|
||
"supplement_request_id": supplement["request_id"],
|
||
"supplement_required": False,
|
||
"unresolved_clauses": unresolved_clauses,
|
||
}
|
||
)
|
||
self._persist(merged)
|
||
return merged
|
||
|
||
@staticmethod
|
||
def _merge_native_can_phases(
|
||
preview: dict[str, Any],
|
||
supplement: dict[str, Any],
|
||
) -> dict[str, Any]:
|
||
"""Create one durable result without collapsing same-domain phases."""
|
||
|
||
merged = deepcopy(supplement)
|
||
|
||
def phase_documents(
|
||
local_documents: dict[str, Any] | None,
|
||
cloud_documents: dict[str, Any] | None,
|
||
) -> dict[str, Any]:
|
||
return {
|
||
**{
|
||
f"local_{key}": deepcopy(value)
|
||
for key, value in (local_documents or {}).items()
|
||
},
|
||
**{
|
||
f"cloud_{key}": deepcopy(value)
|
||
for key, value in (cloud_documents or {}).items()
|
||
},
|
||
}
|
||
|
||
configs = phase_documents(
|
||
preview.get("function_configs"),
|
||
supplement.get("function_configs"),
|
||
)
|
||
plans = phase_documents(
|
||
preview.get("command_plans"),
|
||
supplement.get("command_plans"),
|
||
)
|
||
merged["function_configs"] = configs
|
||
merged["command_plans"] = plans
|
||
merged["function_config"] = next(iter(configs.values()), None)
|
||
merged["command_plan"] = next(iter(plans.values()), None)
|
||
merged["decision_summary"] = [
|
||
*preview.get("decision_summary", []),
|
||
*supplement.get("decision_summary", []),
|
||
]
|
||
merged["trace_events"] = [
|
||
*preview.get("trace_events", []),
|
||
*supplement.get("trace_events", []),
|
||
]
|
||
merged["validation"] = [
|
||
*preview.get("validation", []),
|
||
*supplement.get("validation", []),
|
||
]
|
||
merged["parsed_control_signatures"] = [
|
||
*preview.get("parsed_control_signatures", []),
|
||
*supplement.get("parsed_control_signatures", []),
|
||
]
|
||
merged["phase_execution_reports"] = {
|
||
"local_rule": deepcopy(preview.get("execution_report")),
|
||
"cloud_supplement": deepcopy(supplement.get("execution_report")),
|
||
**deepcopy(preview.get("phase_execution_reports") or {}),
|
||
}
|
||
merged["operation_results"] = [
|
||
*deepcopy(preview.get("operation_results") or []),
|
||
*deepcopy(supplement.get("operation_results") or []),
|
||
]
|
||
if preview.get("orchestration_route") == "unified_can_infrared":
|
||
merged["orchestration_route"] = "unified_can_infrared"
|
||
for key in (
|
||
"infrared_actions",
|
||
"peripheral",
|
||
"control_transports",
|
||
):
|
||
merged[key] = deepcopy(preview.get(key))
|
||
merged["provider_used"] = (
|
||
f"{supplement.get('provider_used') or 'cloud_supplement'}"
|
||
"+infrared_local_rules"
|
||
)
|
||
if supplement.get("execution_report") is None:
|
||
merged["execution_report"] = deepcopy(preview.get("execution_report"))
|
||
successful = {"succeeded", "partially_succeeded", "no_action"}
|
||
preview_ok = preview.get("status") in successful
|
||
supplement_ok = supplement.get("status") in successful
|
||
has_any_commands = bool(configs and plans)
|
||
if preview_ok and supplement_ok:
|
||
merged["status"] = "succeeded" if has_any_commands else "no_action"
|
||
elif preview_ok and preview.get("function_configs"):
|
||
merged["status"] = "partially_succeeded"
|
||
merged["local_preview"] = {
|
||
"request_id": preview.get("request_id"),
|
||
"elapsed_ms": preview.get("preview_elapsed_ms"),
|
||
"command_count": sum(
|
||
len((config.get("spec") or {}).get("operations", []))
|
||
for config in preview.get("function_configs", {}).values()
|
||
),
|
||
}
|
||
return merged
|
||
|
||
def process_can_commands(
|
||
self,
|
||
commands: object,
|
||
conversation_id: str | None = None,
|
||
execution_mode: str = "dry_run",
|
||
approval_token: str | None = None,
|
||
vehicle_id: str | None = None,
|
||
simulation_fault: str = "normal",
|
||
*,
|
||
hardware_client_id: str | None = None,
|
||
hardware_access_bypass: bool = True,
|
||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||
) -> dict[str, Any]:
|
||
"""Compile and execute an exact UI command batch without intent parsing."""
|
||
|
||
resolved_conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}"
|
||
if not CONVERSATION_ID_PATTERN.fullmatch(resolved_conversation_id):
|
||
raise ValueError(
|
||
"conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens"
|
||
)
|
||
vehicle_profile = self.vehicle_profiles.get(vehicle_id)
|
||
context = self._context_for_vehicle(
|
||
self.conversations.snapshot(resolved_conversation_id),
|
||
vehicle_profile,
|
||
)
|
||
direct_can_batch = build_direct_can_intent(commands, context)
|
||
normalized_commands, _parsed = direct_can_batch
|
||
return self.process(
|
||
";".join(normalized_commands),
|
||
provider="local",
|
||
conversation_id=resolved_conversation_id,
|
||
execution_mode=execution_mode,
|
||
approval_token=approval_token,
|
||
vehicle_id=vehicle_id,
|
||
simulation_fault=simulation_fault,
|
||
hardware_client_id=hardware_client_id,
|
||
hardware_access_bypass=hardware_access_bypass,
|
||
control_transport=control_transport,
|
||
_direct_can_batch=direct_can_batch,
|
||
)
|
||
|
||
def _adapt_speech_understanding(
|
||
self,
|
||
understanding: dict[str, Any],
|
||
context: dict[str, Any],
|
||
vehicle_profile: Any,
|
||
) -> dict[str, Any]:
|
||
"""Revalidate a server-owned speech result into the normal control IR.
|
||
|
||
Speech is allowed to skip a second natural-language parse, but it never
|
||
skips the authoritative capability catalog or ModelPlanAdapter. Native
|
||
CAN actions and configured-domain/seat actions are routed through the
|
||
same structures used by the rest of one-sentence orchestration.
|
||
"""
|
||
|
||
if understanding.get("execution_eligible") is not True:
|
||
raise ValueError("speech understanding is not execution eligible")
|
||
commands = understanding.get("commands")
|
||
if not isinstance(commands, list) or not commands:
|
||
raise ValueError("speech understanding has no executable commands")
|
||
|
||
native_actions: list[dict[str, Any]] = []
|
||
model_requests: list[dict[str, Any]] = []
|
||
detected_domains: list[str] = []
|
||
|
||
def add_payload(payload: object, source_text: str) -> None:
|
||
if not isinstance(payload, dict):
|
||
raise ValueError("speech command has no validated executor payload")
|
||
domain = str(payload.get("domain", ""))
|
||
capability = str(payload.get("capability", ""))
|
||
target = str(payload.get("target", ""))
|
||
operation = str(payload.get("operation", "set"))
|
||
parameters = payload.get("parameters")
|
||
if not domain or not capability or not target or not isinstance(parameters, dict):
|
||
raise ValueError("speech executor payload is incomplete")
|
||
|
||
definitions = tuple(
|
||
definition
|
||
for definition in self.capability_registry.find(capability)
|
||
if definition.domain == domain
|
||
)
|
||
if len(definitions) != 1:
|
||
raise ValueError(
|
||
f"speech capability is not uniquely registered: {domain}.{capability}"
|
||
)
|
||
if domain not in detected_domains:
|
||
detected_domains.append(domain)
|
||
# Catalog availability describes where a capability is declared,
|
||
# not necessarily which executor owns it. Roof, climate and
|
||
# ambient capabilities are declared in domain catalogs but some
|
||
# are byte-exact NativeCanRuntime operations. Keep this routing in
|
||
# lockstep with CapabilityResolver._executor_binding().
|
||
if capability in self.native_can_runtime.supported_capabilities:
|
||
native_actions.append(
|
||
{
|
||
"domain": domain,
|
||
"capability": capability,
|
||
"target": target,
|
||
"operation": operation,
|
||
"parameters": deepcopy(parameters),
|
||
"parse_source": "speech_semantic",
|
||
"source_span": source_text,
|
||
"reason": "语音语义层已按真实能力目录解析并由服务端重新校验。",
|
||
}
|
||
)
|
||
return
|
||
model_requests.append(
|
||
{
|
||
"domain": domain,
|
||
"capability": capability,
|
||
"target_scope": target,
|
||
"operation": operation,
|
||
"parameters": [
|
||
{"name": name, "value": value}
|
||
for name, value in parameters.items()
|
||
],
|
||
"reason": "语音语义层已按真实能力目录解析并由服务端重新校验。",
|
||
}
|
||
)
|
||
|
||
for command in commands:
|
||
if not isinstance(command, dict):
|
||
raise ValueError("speech command must be an object")
|
||
if (
|
||
command.get("status") != "active"
|
||
or command.get("negated") is True
|
||
or command.get("resolution_status") != "executable"
|
||
or command.get("validation_issues")
|
||
):
|
||
raise ValueError("speech command failed execution invariants")
|
||
source_text = str(command.get("source_text") or understanding.get("raw_text") or "")
|
||
payload = command.get("executor_payload")
|
||
if command.get("mode_profile_id"):
|
||
if not isinstance(payload, dict) or not isinstance(payload.get("operations"), list):
|
||
raise ValueError("speech mode command has no validated operations")
|
||
for operation_payload in payload["operations"]:
|
||
add_payload(operation_payload, source_text)
|
||
else:
|
||
add_payload(payload, source_text)
|
||
|
||
plan = {
|
||
"detected_domains": detected_domains,
|
||
"requests": model_requests,
|
||
"ambiguities": [],
|
||
"decision_summary": [
|
||
"复用服务端语音理解结果;未对 cleanText 进行第二次自然语言解析。"
|
||
],
|
||
"confidence": min(
|
||
float(command.get("confidence", 0.0))
|
||
for command in commands
|
||
if isinstance(command, dict)
|
||
),
|
||
}
|
||
parsed = self.model_plan_adapter.adapt(
|
||
plan,
|
||
context=context,
|
||
vehicle_profile=vehicle_profile,
|
||
utterance=str(understanding.get("raw_text") or ""),
|
||
)
|
||
parsed["native_can_actions"] = native_actions
|
||
parsed.setdefault("semantic_constraints", [])
|
||
parsed.setdefault("unresolved_spans", [])
|
||
if parsed.get("ambiguities") or parsed.get("deferred_requests"):
|
||
raise ValueError("speech command no longer passes capability resolution")
|
||
mapped_count = (
|
||
len(parsed.get("actions", ()))
|
||
+ len(parsed.get("domain_actions", ()))
|
||
+ len(parsed.get("native_can_actions", ()))
|
||
)
|
||
expected_count = len(model_requests) + len(native_actions)
|
||
if mapped_count != expected_count:
|
||
raise ValueError("speech command mapping lost a validated capability mutation")
|
||
return parsed
|
||
|
||
def process(
|
||
self,
|
||
utterance: str,
|
||
provider: str = "auto",
|
||
conversation_id: str | None = None,
|
||
execution_mode: str = "dry_run",
|
||
approval_token: str | None = None,
|
||
vehicle_id: str | None = None,
|
||
simulation_fault: str = "normal",
|
||
*,
|
||
hardware_client_id: str | None = None,
|
||
hardware_access_bypass: bool = True,
|
||
reasoning_effort: str | None = None,
|
||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||
_local_preview: bool = False,
|
||
_excluded_control_signatures: set[tuple[Any, ...]] | None = None,
|
||
_model_parse_source: str = "cloud_model",
|
||
_allow_empty_control_result: bool = False,
|
||
_on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||
_direct_can_batch: tuple[list[str], dict[str, Any]] | None = None,
|
||
_direct_mode_profile_id: str | None = None,
|
||
_speech_understanding: dict[str, Any] | None = None,
|
||
_assistant_plan: dict[str, Any] | None = None,
|
||
) -> dict[str, Any]:
|
||
control_transport = self._validate_control_transport(control_transport)
|
||
# Creating a reusable scene is a configuration workflow. Synthesize
|
||
# and validate it in dry-run even when the console currently targets
|
||
# the real bench, otherwise virtual domains such as climate or ambient
|
||
# light would be rejected before the profile can be persisted.
|
||
if self._parse_mode_profile_create_name(utterance.strip()) is not None:
|
||
execution_mode = "dry_run"
|
||
if execution_mode != "hardware":
|
||
return self._process_once(
|
||
utterance,
|
||
provider,
|
||
conversation_id,
|
||
execution_mode,
|
||
approval_token,
|
||
vehicle_id,
|
||
simulation_fault,
|
||
reasoning_effort,
|
||
control_transport,
|
||
_local_preview,
|
||
_excluded_control_signatures,
|
||
_model_parse_source,
|
||
_allow_empty_control_result,
|
||
_on_commands_validated,
|
||
_direct_can_batch,
|
||
_direct_mode_profile_id,
|
||
_speech_understanding,
|
||
assistant_plan=_assistant_plan,
|
||
)
|
||
if control_transport == WIRELESS_MQTT_TRANSPORT:
|
||
# The head unit owns both physical CAN channels in wireless mode.
|
||
# Local T2G/ZLG hardware access, firmware profiles and the local
|
||
# physical-bench lease are unrelated to MQTT relay execution.
|
||
return self._process_once(
|
||
utterance,
|
||
provider,
|
||
conversation_id,
|
||
execution_mode,
|
||
approval_token,
|
||
vehicle_id,
|
||
simulation_fault,
|
||
reasoning_effort,
|
||
control_transport,
|
||
_local_preview,
|
||
_excluded_control_signatures,
|
||
_model_parse_source,
|
||
_allow_empty_control_result,
|
||
_on_commands_validated,
|
||
_direct_can_batch,
|
||
_direct_mode_profile_id,
|
||
_speech_understanding,
|
||
assistant_plan=_assistant_plan,
|
||
)
|
||
if hardware_access_bypass or self._driver_only_vehicle_execution_active(
|
||
control_transport
|
||
):
|
||
with self.hardware_execution.reserve():
|
||
return self._process_once(
|
||
utterance,
|
||
provider,
|
||
conversation_id,
|
||
execution_mode,
|
||
approval_token,
|
||
vehicle_id,
|
||
simulation_fault,
|
||
reasoning_effort,
|
||
control_transport,
|
||
_local_preview,
|
||
_excluded_control_signatures,
|
||
_model_parse_source,
|
||
_allow_empty_control_result,
|
||
_on_commands_validated,
|
||
_direct_can_batch,
|
||
_direct_mode_profile_id,
|
||
_speech_understanding,
|
||
assistant_plan=_assistant_plan,
|
||
)
|
||
authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id)
|
||
with self.hardware_execution.reserve():
|
||
with self.hardware_access.use(authorized_client_id):
|
||
return self._process_once(
|
||
utterance,
|
||
provider,
|
||
conversation_id,
|
||
execution_mode,
|
||
approval_token,
|
||
vehicle_id,
|
||
simulation_fault,
|
||
reasoning_effort,
|
||
control_transport,
|
||
_local_preview,
|
||
_excluded_control_signatures,
|
||
_model_parse_source,
|
||
_allow_empty_control_result,
|
||
_on_commands_validated,
|
||
_direct_can_batch,
|
||
_direct_mode_profile_id,
|
||
_speech_understanding,
|
||
assistant_plan=_assistant_plan,
|
||
)
|
||
|
||
def _process_once(
|
||
self,
|
||
utterance: str,
|
||
provider: str = "auto",
|
||
conversation_id: str | None = None,
|
||
execution_mode: str = "dry_run",
|
||
approval_token: str | None = None,
|
||
vehicle_id: str | None = None,
|
||
simulation_fault: str = "normal",
|
||
reasoning_effort: str | None = None,
|
||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||
local_preview: bool = False,
|
||
excluded_control_signatures: set[tuple[Any, ...]] | None = None,
|
||
model_parse_source: str = "cloud_model",
|
||
allow_empty_control_result: bool = False,
|
||
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||
direct_can_batch: tuple[list[str], dict[str, Any]] | None = None,
|
||
direct_mode_profile_id: str | None = None,
|
||
speech_understanding: dict[str, Any] | None = None,
|
||
assistant_plan: dict[str, Any] | None = None,
|
||
) -> dict[str, Any]:
|
||
control_transport = self._validate_control_transport(control_transport)
|
||
utterance = utterance.strip()
|
||
if not utterance:
|
||
raise ValueError("utterance must not be empty")
|
||
if len(utterance) > 1000:
|
||
raise ValueError("utterance must be at most 1000 characters")
|
||
if provider not in {"auto", "openai", "local"}:
|
||
raise ValueError("provider must be auto, openai, or local")
|
||
effective_reasoning_effort = (
|
||
self.openai.reasoning_effort
|
||
if reasoning_effort is None
|
||
else self.openai.validate_reasoning_effort(reasoning_effort)
|
||
)
|
||
if execution_mode not in {"dry_run", "simulated_hardware", "hardware"}:
|
||
raise ValueError(
|
||
"execution_mode must be dry_run, simulated_hardware, or hardware"
|
||
)
|
||
simulated_hardware_request = execution_mode == "simulated_hardware"
|
||
if simulated_hardware_request and not self.simulated_hardware_enabled:
|
||
raise HardwareSafetyError(
|
||
"simulated hardware is disabled; set "
|
||
"COGNITIVE_AGENT_SIMULATED_HARDWARE_ENABLED=true"
|
||
)
|
||
if simulation_fault not in SIMULATED_HARDWARE_FAULTS:
|
||
raise ValueError(
|
||
"simulation_fault must be one of "
|
||
+ ", ".join(SIMULATED_HARDWARE_FAULTS)
|
||
)
|
||
mode_save_name = self._parse_mode_profile_save_name(utterance)
|
||
mode_create_name = self._parse_mode_profile_create_name(utterance)
|
||
mode_restore_name = self._parse_mode_profile_restore_name(utterance)
|
||
mode_update = self._parse_mode_profile_update(utterance)
|
||
control_plane_request = (
|
||
mode_save_name is not None
|
||
or mode_restore_name is not None
|
||
or mode_update is not None
|
||
)
|
||
hardware_request = execution_mode == "hardware"
|
||
if hardware_request and _has_mixed_driver_passenger_seat_scope(utterance):
|
||
raise HardwareSafetyError(
|
||
"hardware mode refuses a mixed CH1 driver/front-left and "
|
||
"CH0 passenger/front-right seat request"
|
||
)
|
||
conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}"
|
||
if not CONVERSATION_ID_PATTERN.fullmatch(conversation_id):
|
||
raise ValueError("conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens")
|
||
vehicle_profile = self.vehicle_profiles.get(vehicle_id)
|
||
direct_mode_profile = (
|
||
self.mode_profiles.get_profile(
|
||
direct_mode_profile_id,
|
||
vehicle_profile.id,
|
||
)
|
||
if direct_mode_profile_id
|
||
else None
|
||
)
|
||
|
||
if mode_create_name is not None:
|
||
return self._process_mode_profile_create(
|
||
utterance,
|
||
provider,
|
||
conversation_id,
|
||
execution_mode,
|
||
approval_token,
|
||
vehicle_profile,
|
||
simulation_fault,
|
||
mode_create_name,
|
||
control_transport,
|
||
assistant_plan=assistant_plan,
|
||
)
|
||
|
||
request_id = f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}"
|
||
context = self._context_for_vehicle(
|
||
self.conversations.snapshot(conversation_id),
|
||
vehicle_profile,
|
||
)
|
||
context["mode_profiles"] = self.mode_profiles.public_modes(vehicle_profile.id)
|
||
matched_modes = (
|
||
[direct_mode_profile]
|
||
if direct_mode_profile is not None
|
||
else []
|
||
if direct_can_batch is not None or speech_understanding is not None
|
||
else self.mode_profiles.match_text(
|
||
utterance,
|
||
vehicle_id=vehicle_profile.id,
|
||
)
|
||
)
|
||
traces: list[dict[str, Any]] = []
|
||
result: dict[str, Any] = {
|
||
"request_id": request_id,
|
||
"utterance": utterance,
|
||
"conversation_id": conversation_id,
|
||
"conversation_turn": context["turn_count"],
|
||
"status": "processing",
|
||
"provider_requested": provider,
|
||
"reasoning_effort_requested": effective_reasoning_effort,
|
||
"reasoning_effort_used": None,
|
||
"execution_mode": execution_mode,
|
||
"control_transport": control_transport,
|
||
"hardware_execution_allowed": (
|
||
hardware_request and not control_plane_request
|
||
),
|
||
"simulated_hardware": simulated_hardware_request,
|
||
"simulation_fault": simulation_fault if simulated_hardware_request else None,
|
||
"vehicle": vehicle_profile.public_dict(),
|
||
"matched_modes": [
|
||
profile.public_dict() for profile in matched_modes
|
||
],
|
||
"mode_profile": None,
|
||
"decision_summary": [],
|
||
"deferred_requests": [],
|
||
"model_requests": [],
|
||
"intent_ir": None,
|
||
"function_config": None,
|
||
"function_configs": {},
|
||
"scene_config": None,
|
||
"preset_config": None,
|
||
"validation": [],
|
||
"command_plan": None,
|
||
"command_plans": {},
|
||
"execution_report": None,
|
||
"actuator_state": context["devices"],
|
||
"domain_state": context["domain_devices"],
|
||
"saved_presets": [
|
||
{"name": item["name"], "target": item["target"]}
|
||
for item in context.get("presets", [])
|
||
],
|
||
"trace_events": traces,
|
||
"can_commands": (
|
||
list(direct_can_batch[0]) if direct_can_batch is not None else []
|
||
),
|
||
"speech_understanding": (
|
||
{
|
||
"utterance_id": speech_understanding.get("utterance_id"),
|
||
"raw_text": speech_understanding.get("raw_text"),
|
||
"clean_text": speech_understanding.get("clean_text"),
|
||
"semantic_status": speech_understanding.get("semantic_status"),
|
||
}
|
||
if speech_understanding is not None
|
||
else None
|
||
),
|
||
}
|
||
|
||
started = time.perf_counter()
|
||
if mode_save_name is not None:
|
||
return self._process_mode_profile_save(
|
||
utterance,
|
||
request_id,
|
||
conversation_id,
|
||
context,
|
||
mode_save_name,
|
||
vehicle_profile,
|
||
result,
|
||
traces,
|
||
)
|
||
if mode_restore_name is not None:
|
||
return self._process_mode_profile_restore(
|
||
utterance,
|
||
request_id,
|
||
conversation_id,
|
||
mode_restore_name,
|
||
vehicle_profile,
|
||
result,
|
||
traces,
|
||
)
|
||
if mode_update is not None:
|
||
mode_name, change_text = mode_update
|
||
return self._process_mode_profile_update(
|
||
utterance,
|
||
request_id,
|
||
provider,
|
||
conversation_id,
|
||
context,
|
||
mode_name,
|
||
change_text,
|
||
vehicle_profile,
|
||
result,
|
||
traces,
|
||
)
|
||
if direct_mode_profile is not None:
|
||
mode_requests = self.mode_profiles.requests_for_profiles(
|
||
[direct_mode_profile]
|
||
)
|
||
detected_domains = list(
|
||
dict.fromkeys(
|
||
str(item.get("domain") or "")
|
||
for item in mode_requests
|
||
if str(item.get("domain") or "")
|
||
)
|
||
)
|
||
parsed = self.model_plan_adapter.adapt(
|
||
{
|
||
"detected_domains": detected_domains or ["seat"],
|
||
"requests": mode_requests,
|
||
"ambiguities": [],
|
||
"decision_summary": [
|
||
f"已载入模式“{direct_mode_profile.name}”保存的结构化操作。"
|
||
],
|
||
"confidence": 1.0,
|
||
},
|
||
context,
|
||
vehicle_profile,
|
||
)
|
||
saved_native_actions: list[dict[str, Any]] = []
|
||
for request in mode_requests:
|
||
parameters = {
|
||
str(item.get("name")): deepcopy(item.get("value"))
|
||
for item in request.get("parameters", [])
|
||
if isinstance(item, dict) and str(item.get("name") or "")
|
||
}
|
||
action = {
|
||
"domain": str(request.get("domain") or ""),
|
||
"capability": str(request.get("capability") or ""),
|
||
"target": str(request.get("target_scope") or "vehicle"),
|
||
"parameters": parameters,
|
||
"operation": str(request.get("operation") or "set"),
|
||
"reason": str(request.get("reason") or ""),
|
||
"execution_backend": "native_can",
|
||
"parse_source": "saved_mode_command_flow",
|
||
}
|
||
if self.native_can_runtime.supports(action):
|
||
saved_native_actions.append(action)
|
||
parsed["native_can_actions"] = [
|
||
*list(parsed.get("native_can_actions", [])),
|
||
*saved_native_actions,
|
||
]
|
||
provider_used = "saved_mode_command_flow"
|
||
provider_notice = None
|
||
elif direct_can_batch is not None:
|
||
parsed = deepcopy(direct_can_batch[1])
|
||
provider_used = "direct_can_control"
|
||
provider_notice = None
|
||
elif speech_understanding is not None:
|
||
parsed = self._adapt_speech_understanding(
|
||
speech_understanding,
|
||
context,
|
||
vehicle_profile,
|
||
)
|
||
parsed = self._mark_control_parse_source(
|
||
parsed,
|
||
"speech_semantic",
|
||
overwrite=True,
|
||
)
|
||
provider_used = "speech_semantic"
|
||
provider_notice = "使用服务端缓存并重新校验的语音结构化意图。"
|
||
else:
|
||
parsed, provider_used, provider_notice = self._interpret(
|
||
utterance,
|
||
provider,
|
||
context,
|
||
vehicle_profile,
|
||
reasoning_effort=reasoning_effort,
|
||
local_preview=local_preview,
|
||
model_parse_source=model_parse_source,
|
||
assistant_plan=assistant_plan,
|
||
)
|
||
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
|
||
# native pass. Without this normalization a Chinese enumeration
|
||
# 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(
|
||
self._split_intent_clauses(utterance)
|
||
)
|
||
relative_resolution = (
|
||
self.native_can_relative_resolver.resolve(
|
||
segmented_utterance,
|
||
context,
|
||
)
|
||
if self.native_can_relative_resolver.matches(
|
||
segmented_utterance
|
||
)
|
||
else None
|
||
)
|
||
native_merge_utterance = segmented_utterance
|
||
if any(
|
||
request.get("capability") in {SAVE_PRESET_CAPABILITY, APPLY_PRESET_CAPABILITY}
|
||
for request in parsed.get("model_requests", [])
|
||
):
|
||
# These registered capabilities currently have one executable
|
||
# target (the passenger-seat posture store). Keep that
|
||
# adapter-resolved target from being discarded by the native
|
||
# parser's general unscoped-seat => driver default. The
|
||
# original utterance is still used by the language safety
|
||
# guards below.
|
||
native_merge_utterance = f"副驾座椅,{utterance}"
|
||
parsed = merge_native_can_intent(
|
||
parsed,
|
||
native_merge_utterance,
|
||
relative_actions=(
|
||
relative_resolution.actions if relative_resolution else None
|
||
),
|
||
relative_ambiguities=(
|
||
relative_resolution.ambiguities if relative_resolution else None
|
||
),
|
||
relative_claimed_controls=(
|
||
relative_resolution.claimed_controls if relative_resolution else None
|
||
),
|
||
)
|
||
if not dynamic_mode_stop:
|
||
# Apply repository-backed modes after the final native-CAN
|
||
# merge. Otherwise that merge can discard a valid passenger
|
||
# mode operation while enforcing its generic unscoped-seat
|
||
# CH1 default.
|
||
parsed = self._apply_mode_profiles(
|
||
parsed,
|
||
matched_modes,
|
||
context,
|
||
vehicle_profile,
|
||
utterance=utterance,
|
||
)
|
||
parsed = self._mark_control_parse_source(parsed, "local_rule")
|
||
if direct_mode_profile is not None:
|
||
parsed = self._mark_control_parse_source(
|
||
parsed,
|
||
"saved_mode_command_flow",
|
||
)
|
||
if local_preview:
|
||
# A preview may discard ordinary model-supplement questions, but
|
||
# target and safety conflicts are execution gates. Keeping these
|
||
# makes the assistant path no less strict than normal process().
|
||
parsed["ambiguities"] = [
|
||
item
|
||
for item in parsed.get("ambiguities", [])
|
||
if str(item.get("field", ""))
|
||
in {"target", "target_scope", "mode_profile", "risk", "safety", "utterance"}
|
||
or str(item.get("reason", "")).startswith(
|
||
("mode_target_", "safety_", "risk_", "mixed_")
|
||
)
|
||
]
|
||
parsed["unresolved_spans"] = [
|
||
item
|
||
for item in parsed.get("unresolved_spans", [])
|
||
if str(item.get("reason", "")).startswith(
|
||
("mode_target_", "safety_", "risk_", "mixed_")
|
||
)
|
||
]
|
||
parsed["model_requests"] = []
|
||
if excluded_control_signatures:
|
||
parsed = self._filter_excluded_controls(
|
||
parsed,
|
||
excluded_control_signatures,
|
||
)
|
||
parsed = apply_language_safety_guards(parsed, utterance)
|
||
parsed = self._partition_native_actions(
|
||
parsed,
|
||
promote_supported=(
|
||
direct_mode_profile is not None
|
||
or (
|
||
hardware_request
|
||
and (
|
||
self.vehicle_mqtt_settings.enabled
|
||
if control_transport == WIRELESS_MQTT_TRANSPORT
|
||
else self.vehicle_can_settings.enabled
|
||
)
|
||
)
|
||
),
|
||
)
|
||
result["parsed_control_signatures"] = [
|
||
list(self._control_signature(action))
|
||
for action in [
|
||
*parsed.get("actions", []),
|
||
*parsed.get("domain_actions", []),
|
||
*parsed.get("native_can_actions", []),
|
||
]
|
||
]
|
||
has_control = any(
|
||
parsed.get(key)
|
||
for key in (
|
||
"actions",
|
||
"domain_actions",
|
||
"native_can_actions",
|
||
"preset_actions",
|
||
)
|
||
)
|
||
if (
|
||
allow_empty_control_result
|
||
and not has_control
|
||
and not parsed.get("ambiguities")
|
||
):
|
||
result["provider_used"] = provider_used
|
||
result["decision_summary"] = [
|
||
*parsed.get("decision_summary", []),
|
||
"当前阶段没有新增的已验证 CAN 控制指令。",
|
||
]
|
||
result["deferred_requests"] = parsed.get("deferred_requests", [])
|
||
result["model_requests"] = parsed.get("model_requests", [])
|
||
result["intent_ir"] = build_intent_ir(
|
||
parsed,
|
||
utterance,
|
||
request_id,
|
||
provider_used,
|
||
self.semantic_capability_metadata,
|
||
)
|
||
result["validation"] = self.contracts.validate(result["intent_ir"])
|
||
result["status"] = "no_action"
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"intent_parse",
|
||
"succeeded",
|
||
"当前阶段未产生新的白名单控制动作。",
|
||
started,
|
||
{"provider": provider_used, "control_count": 0},
|
||
domain="orchestration",
|
||
)
|
||
self._persist(result)
|
||
return result
|
||
if hardware_request and not control_plane_request:
|
||
if (
|
||
control_transport == WIRELESS_MQTT_TRANSPORT
|
||
and parsed.get("domain_actions")
|
||
and (parsed.get("actions") or parsed.get("native_can_actions"))
|
||
):
|
||
parsed = self._defer_nonwhitelisted_wireless_actions(parsed)
|
||
if parsed.get("domain_actions"):
|
||
unsupported = sorted(
|
||
{
|
||
str(action.get("capability", "unknown"))
|
||
for action in parsed["domain_actions"]
|
||
}
|
||
)
|
||
raise HardwareSafetyError(
|
||
"hardware mode refuses virtual or unverified vehicle actions: "
|
||
+ ", ".join(unsupported)
|
||
)
|
||
if not parsed.get("actions") and not parsed.get("native_can_actions"):
|
||
raise HardwareSafetyError(
|
||
"hardware mode requires at least one verified seat or vehicle CAN operation"
|
||
)
|
||
if parsed.get("actions") and parsed.get("native_can_actions"):
|
||
if (
|
||
control_transport == WIRED_CAN_TRANSPORT
|
||
and self.hardware_settings.backend
|
||
not in {"zlgcan", "zlgcan-linux"}
|
||
):
|
||
raise HardwareSafetyError(
|
||
"linked seat/vehicle execution requires a shared ZLG backend"
|
||
)
|
||
if control_transport == WIRED_CAN_TRANSPORT:
|
||
try:
|
||
seat_channel = int(self.hardware_settings.channel, 0)
|
||
except ValueError as exc:
|
||
raise HardwareSafetyError(
|
||
"ZLG seat CAN channel must be an integer"
|
||
) from exc
|
||
if seat_channel == self.vehicle_can_settings.channel:
|
||
raise HardwareSafetyError(
|
||
"seat and vehicle CAN must use different adapter channels"
|
||
)
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"intent_parse",
|
||
"succeeded",
|
||
f"使用 {provider_used} 完成结构化意图抽取。",
|
||
started,
|
||
{
|
||
"provider": provider_used,
|
||
"confidence": parsed["confidence"],
|
||
"conversation_turn": context["turn_count"],
|
||
},
|
||
)
|
||
if provider_notice:
|
||
parsed["decision_summary"].append(provider_notice)
|
||
result["provider_used"] = provider_used
|
||
if "openai:" in provider_used:
|
||
result["reasoning_effort_used"] = effective_reasoning_effort
|
||
result["decision_summary"] = parsed["decision_summary"]
|
||
result["deferred_requests"] = parsed.get("deferred_requests", [])
|
||
result["model_requests"] = parsed.get("model_requests", [])
|
||
|
||
if (
|
||
parsed.get("semantic_constraints")
|
||
and not parsed.get("actions")
|
||
and not parsed.get("domain_actions")
|
||
and not parsed.get("native_can_actions")
|
||
and not parsed.get("ambiguities")
|
||
and not parsed.get("unresolved_spans")
|
||
):
|
||
intent_ir = build_intent_ir(
|
||
parsed,
|
||
utterance,
|
||
request_id,
|
||
provider_used,
|
||
self.semantic_capability_metadata,
|
||
)
|
||
result["intent_ir"] = intent_ir
|
||
result["validation"] = self.contracts.validate(intent_ir)
|
||
result["status"] = "no_action"
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"safety_check",
|
||
"succeeded",
|
||
"检测到禁止或保持约束;未生成、编译或发送任何 CAN 动作。",
|
||
time.perf_counter(),
|
||
{"constraint_count": len(intent_ir["spec"]["constraints"]), "execution_eligible": False},
|
||
domain="orchestration",
|
||
)
|
||
self._persist(result)
|
||
return result
|
||
|
||
context_started = time.perf_counter()
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"context_resolve",
|
||
"succeeded",
|
||
(
|
||
f"已将会话 {conversation_id} 的第 {context['turn_count']} 轮执行状态"
|
||
"、域能力注册表和长期模式配置作为模型上下文;"
|
||
"本地层仅注入已命中的长期模式动作。"
|
||
),
|
||
context_started,
|
||
{
|
||
"conversation_id": conversation_id,
|
||
"turn_count": context["turn_count"],
|
||
"last_target": context["last_target"],
|
||
"state_before": context["devices"],
|
||
"capability_domains": [
|
||
item["domain"] for item in self.capability_context["domains"]
|
||
],
|
||
"matched_modes": [profile.id for profile in matched_modes],
|
||
},
|
||
)
|
||
|
||
route_started = time.perf_counter()
|
||
detected_domains = parsed.get("detected_domains", ["seat"])
|
||
seat_only = detected_domains == ["seat"]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"domain_route",
|
||
"succeeded",
|
||
(
|
||
"仅识别到 seat 领域,使用已验证的座椅单域闭环。"
|
||
if seat_only
|
||
else (
|
||
f"识别到 {','.join(detected_domains)} 领域,"
|
||
"按领域边界生成独立 FunctionConfig。"
|
||
)
|
||
),
|
||
route_started,
|
||
{
|
||
"domains": detected_domains,
|
||
"mode": "single_domain" if len(detected_domains) == 1 else "cross_domain",
|
||
},
|
||
)
|
||
|
||
if parsed.get("domain_actions") or parsed.get("native_can_actions") or any(
|
||
domain != "seat" for domain in detected_domains
|
||
):
|
||
return self._process_orchestrated(
|
||
utterance,
|
||
request_id,
|
||
provider_used,
|
||
conversation_id,
|
||
context,
|
||
parsed,
|
||
result,
|
||
traces,
|
||
hardware_request=hardware_request,
|
||
simulated_hardware_request=simulated_hardware_request,
|
||
simulation_fault=simulation_fault,
|
||
vehicle_profile=vehicle_profile,
|
||
control_transport=control_transport,
|
||
approval_token=approval_token,
|
||
on_commands_validated=on_commands_validated,
|
||
)
|
||
|
||
intent_ir = build_intent_ir(
|
||
parsed,
|
||
utterance,
|
||
request_id,
|
||
provider_used,
|
||
self.semantic_capability_metadata,
|
||
)
|
||
result["intent_ir"] = intent_ir
|
||
generation_started = time.perf_counter()
|
||
function_config = build_function_config(parsed, request_id, provider_used)
|
||
preset_config = None
|
||
if parsed.get("preset_actions"):
|
||
preset_action = parsed["preset_actions"][0]
|
||
preset_config = build_function_preset(
|
||
preset_action["target"],
|
||
preset_action["preset_name"],
|
||
context["devices"][preset_action["target"]],
|
||
request_id,
|
||
provider_used,
|
||
)
|
||
result["function_config"] = function_config
|
||
result["function_configs"] = {"seat": function_config} if function_config else {}
|
||
result["preset_config"] = preset_config
|
||
generated_document = function_config or preset_config
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"function_generate",
|
||
"succeeded" if generated_document else "waiting_confirmation",
|
||
(
|
||
"已生成座椅 FunctionConfig。"
|
||
if function_config
|
||
else (
|
||
"已从当前姿态生成 FunctionPreset。"
|
||
if preset_config
|
||
else "没有足够信息生成可执行配置。"
|
||
)
|
||
),
|
||
generation_started,
|
||
{
|
||
"operation_count": len(parsed["actions"]),
|
||
"preset_operation_count": (
|
||
len(preset_config["spec"]["operations"]) if preset_config else 0
|
||
),
|
||
},
|
||
)
|
||
|
||
schema_started = time.perf_counter()
|
||
validation = self.contracts.validate(intent_ir)
|
||
if function_config is not None:
|
||
validation.extend(self.contracts.validate(function_config))
|
||
if preset_config is not None:
|
||
validation.extend(self.contracts.validate(preset_config))
|
||
schema_errors = [issue for issue in validation if issue["severity"] == "error"]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"schema_validate",
|
||
"failed" if schema_errors else "succeeded",
|
||
f"公共与领域 Schema 校验{'发现错误' if schema_errors else '通过'}。",
|
||
schema_started,
|
||
{"error_count": len(schema_errors)},
|
||
)
|
||
|
||
semantic_started = time.perf_counter()
|
||
semantic_issues = validate_semantics(parsed, function_config)
|
||
validation.extend(semantic_issues)
|
||
if hardware_request:
|
||
self._adapt_validation_for_hardware(validation)
|
||
semantic_errors = [issue for issue in semantic_issues if issue["severity"] == "error"]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"domain_validate",
|
||
"failed" if semantic_errors else "succeeded",
|
||
"座椅语义校验阻止了不受支持的请求。" if semantic_errors else "座椅能力和目标映射校验通过。",
|
||
semantic_started,
|
||
{"error_count": len(semantic_errors), "warning_count": len(semantic_issues) - len(semantic_errors)},
|
||
)
|
||
result["validation"] = validation
|
||
|
||
all_errors = [issue for issue in validation if issue["severity"] == "error"]
|
||
if all_errors:
|
||
if any(issue["code"] == "AMBIGUOUS_INTENT" for issue in all_errors):
|
||
result["status"] = "needs_confirmation"
|
||
else:
|
||
result["status"] = "validation_failed"
|
||
self._persist(result)
|
||
return result
|
||
|
||
if parsed.get("deferred_requests") and not parsed.get("actions"):
|
||
result["status"] = "capability_unavailable"
|
||
self._persist(result)
|
||
return result
|
||
|
||
if preset_config is not None and not parsed.get("actions"):
|
||
preset_action = parsed["preset_actions"][0]
|
||
compile_started = time.perf_counter()
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"compile",
|
||
"succeeded",
|
||
"FunctionPreset 属于控制平面数据,不生成 CAN 命令。",
|
||
compile_started,
|
||
{
|
||
"preset_name": preset_action["preset_name"],
|
||
"command_count": 0,
|
||
},
|
||
)
|
||
safety_started = time.perf_counter()
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"safety_check",
|
||
"succeeded",
|
||
"预设仅保存已验证的五轴目标,不改变硬件发送边界。",
|
||
safety_started,
|
||
{"hardware_execution_allowed": False},
|
||
)
|
||
execute_started = time.perf_counter()
|
||
execution_report = {
|
||
"apiVersion": "cognitive-agent/v1alpha1",
|
||
"kind": "ExecutionReport",
|
||
"metadata": {
|
||
"id": f"report-{request_id}",
|
||
"name": "Seat posture preset save report",
|
||
"version": "1.0.0",
|
||
"request_id": request_id,
|
||
"labels": {"control_plane": "preset_store"},
|
||
},
|
||
"spec": {
|
||
"mode": "dry_run",
|
||
"status": "succeeded",
|
||
"started_at": _now(),
|
||
"finished_at": _now(),
|
||
"results": [
|
||
{
|
||
"ref": f"save-{preset_action['target']}-posture-preset",
|
||
"domain": "seat",
|
||
"status": "succeeded",
|
||
"message": (
|
||
f"已将 {preset_action['target']} 当前五轴姿态保存为"
|
||
f"“{preset_action['preset_name']}”。"
|
||
),
|
||
"telemetry": {
|
||
"operation": "save_posture_preset",
|
||
"target": preset_action["target"],
|
||
"preset_name": preset_action["preset_name"],
|
||
"captured_axis_count": len(
|
||
preset_config["spec"]["operations"]
|
||
),
|
||
"storage_scope": "conversation_durable",
|
||
"can_command_emitted": False,
|
||
},
|
||
}
|
||
],
|
||
},
|
||
}
|
||
report_issues = self.contracts.validate(execution_report)
|
||
validation.extend(report_issues)
|
||
result["execution_report"] = execution_report
|
||
if report_issues:
|
||
result["status"] = "execution_report_invalid"
|
||
self._persist(result)
|
||
return result
|
||
updated_context = self.conversations.save_preset(
|
||
conversation_id,
|
||
utterance,
|
||
preset_action["target"],
|
||
preset_action["preset_name"],
|
||
preset_config,
|
||
parsed["decision_summary"],
|
||
)
|
||
updated_context = self._context_for_vehicle(
|
||
updated_context,
|
||
vehicle_profile,
|
||
)
|
||
result["status"] = "succeeded"
|
||
result["conversation_turn"] = updated_context["turn_count"]
|
||
result["actuator_state"] = updated_context["devices"]
|
||
result["saved_presets"] = [
|
||
{"name": item["name"], "target": item["target"]}
|
||
for item in updated_context["presets"]
|
||
]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"execute",
|
||
"succeeded",
|
||
"当前姿态已写入会话预设仓库;未生成 CAN 帧。",
|
||
execute_started,
|
||
{
|
||
"mode": "control_plane",
|
||
"preset_name": preset_action["preset_name"],
|
||
},
|
||
)
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"state_update",
|
||
"succeeded",
|
||
f"预设“{preset_action['preset_name']}”已持久化,可在后续指令中直接调用。",
|
||
time.perf_counter(),
|
||
{
|
||
"preset_count": len(updated_context["presets"]),
|
||
"storage_scope": "conversation_durable",
|
||
},
|
||
)
|
||
self._persist(result)
|
||
return result
|
||
|
||
compile_started = time.perf_counter()
|
||
try:
|
||
if hardware_request:
|
||
if control_transport != WIRELESS_MQTT_TRANSPORT:
|
||
self.hardware_settings.validate_function_config(function_config)
|
||
command_plan = self.hardware_compiler.compile(
|
||
function_config, context["devices"]
|
||
)
|
||
else:
|
||
command_plan = self.compiler.compile(function_config)
|
||
except ValueError as exc:
|
||
compile_issue = {
|
||
"severity": "error",
|
||
"code": "CAN_COMPILE_REJECTED",
|
||
"path": "spec.operations",
|
||
"message": str(exc),
|
||
}
|
||
validation.append(compile_issue)
|
||
result["validation"] = validation
|
||
result["status"] = "compile_failed"
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"compile",
|
||
"failed",
|
||
f"座椅 CAN 编译被安全约束阻止:{exc}",
|
||
compile_started,
|
||
{"error": str(exc), "hardware_request": hardware_request},
|
||
)
|
||
self._persist(result)
|
||
return result
|
||
plan_issues = self.contracts.validate(command_plan)
|
||
validation.extend(plan_issues)
|
||
result["command_plan"] = command_plan
|
||
result["command_plans"] = {"seat": command_plan}
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"compile",
|
||
"failed" if plan_issues else "succeeded",
|
||
"配置已确定性编译为座椅 CAN 语义信号计划。",
|
||
compile_started,
|
||
{
|
||
"messages": [
|
||
command["message"] for command in command_plan["spec"]["commands"]
|
||
],
|
||
"command_count": len(command_plan["spec"]["commands"]),
|
||
"signal_count": sum(
|
||
len(command["signals"])
|
||
for command in command_plan["spec"]["commands"]
|
||
),
|
||
},
|
||
)
|
||
if plan_issues:
|
||
result["status"] = "compile_failed"
|
||
self._persist(result)
|
||
return result
|
||
|
||
self._emit_validated_commands(result, on_commands_validated)
|
||
|
||
if hardware_request:
|
||
self._authorize_compiled_hardware_delivery(
|
||
parsed,
|
||
approval_token,
|
||
control_transport,
|
||
)
|
||
|
||
safety_started = time.perf_counter()
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"safety_check",
|
||
"succeeded",
|
||
(
|
||
"无线 MQTT CAN0 座椅语义白名单与固件原生 0x300 报文白名单已通过;"
|
||
"本地 T2G 固件 profile、USB-CAN 与物理台架连接不参与校验。"
|
||
if hardware_request
|
||
and control_transport == WIRELESS_MQTT_TRANSPORT
|
||
else (
|
||
"硬件启用、授权令牌、固件 profile 与目标座椅配置门禁已通过;"
|
||
if self.hardware_settings.approval_required
|
||
else "硬件启用、调试免令牌、固件 profile 与目标座椅配置门禁已通过;"
|
||
)
|
||
+ "板端 Position_Valid 将在发送绝对移动命令前实时检查。"
|
||
if hardware_request
|
||
else (
|
||
"模拟台架隔离门禁已通过;仅生成故障注入反馈,物理 CAN、ZLG 与 IAR 路径均未打开。"
|
||
if simulated_hardware_request
|
||
else "平台、矩阵和策略均限制为 dry-run;硬件发送路径未启用。"
|
||
)
|
||
),
|
||
safety_started,
|
||
{
|
||
"hardware_execution_allowed": hardware_request,
|
||
"simulated_hardware": simulated_hardware_request,
|
||
"simulation_fault": (
|
||
simulation_fault if simulated_hardware_request else None
|
||
),
|
||
"position_calibration_gate": (
|
||
"remote_native_can0_feedback"
|
||
if hardware_request
|
||
and control_transport == WIRELESS_MQTT_TRANSPORT
|
||
else "runtime_can_preflight"
|
||
if hardware_request
|
||
else "not_applicable"
|
||
),
|
||
"mapping_status": (
|
||
"mqtt_can0_native_firmware_whitelist"
|
||
if hardware_request
|
||
and control_transport == WIRELESS_MQTT_TRANSPORT
|
||
else "protocol_hil_verified_combined_firmware_hil_pending"
|
||
if hardware_request
|
||
else "draft"
|
||
),
|
||
"firmware_profiles": (
|
||
sorted(self.hardware_settings.profiles)
|
||
if hardware_request
|
||
and control_transport != WIRELESS_MQTT_TRANSPORT
|
||
else []
|
||
),
|
||
},
|
||
)
|
||
|
||
execute_started = time.perf_counter()
|
||
if hardware_request:
|
||
mqtt_selected = control_transport == WIRELESS_MQTT_TRANSPORT
|
||
vehicle_safety_preflight = None
|
||
wired_bridge = None
|
||
try:
|
||
if mqtt_selected:
|
||
vehicle_safety_preflight = _wireless_mqtt_publish_preflight()
|
||
hardware_executor = self._new_hardware_executor(
|
||
lambda: self._vehicle_mqtt_transport.bus(channel=0),
|
||
remote_transport=True,
|
||
)
|
||
elif (
|
||
self.hardware_settings.native_passenger_can0
|
||
and self.vehicle_can_settings.configured
|
||
):
|
||
wired_bridge = self._new_linux_wired_dual_channel_bridge()
|
||
vehicle_safety_preflight = observe_vehicle_safety(
|
||
wired_bridge.channel_bus(1),
|
||
timeout_ms=min(
|
||
5_000,
|
||
max(
|
||
1_000,
|
||
self.vehicle_can_settings.feedback_timeout_ms,
|
||
),
|
||
),
|
||
required_channel=1,
|
||
)
|
||
require_stationary_vehicle(vehicle_safety_preflight)
|
||
hardware_executor = self._new_hardware_executor(
|
||
lambda: wired_bridge.channel_bus(0)
|
||
)
|
||
else:
|
||
hardware_executor = self._new_hardware_executor()
|
||
execution_report = hardware_executor.execute(
|
||
command_plan, function_config, context["devices"]
|
||
)
|
||
except VehicleSafetyPreflightError as exc:
|
||
raise HardwareSafetyError(
|
||
"CH1 vehicle safety preflight refused passenger CAN0 execution: "
|
||
f"{exc}"
|
||
) from exc
|
||
finally:
|
||
if wired_bridge is not None:
|
||
wired_bridge.close()
|
||
if vehicle_safety_preflight is not None:
|
||
for item in execution_report["spec"].get("results", []):
|
||
item.setdefault("telemetry", {})[
|
||
"vehicle_safety_preflight"
|
||
] = vehicle_safety_preflight
|
||
elif simulated_hardware_request:
|
||
execution_report = self.simulated_hardware_executor.execute(
|
||
command_plan,
|
||
function_config,
|
||
context["devices"],
|
||
fault_scenario=simulation_fault,
|
||
)
|
||
else:
|
||
execution_report = self.executor.execute(
|
||
command_plan, function_config, context["devices"]
|
||
)
|
||
execution_outcome, execution_partially_succeeded = (
|
||
_annotate_execution_report(execution_report)
|
||
)
|
||
report_issues = self.contracts.validate(execution_report)
|
||
validation.extend(report_issues)
|
||
result["execution_report"] = execution_report
|
||
result["execution_outcome"] = execution_outcome
|
||
execution_succeeded = (
|
||
not report_issues
|
||
and execution_report["spec"]["status"] == "succeeded"
|
||
)
|
||
if report_issues:
|
||
result["status"] = "execution_report_invalid"
|
||
elif execution_partially_succeeded:
|
||
result["status"] = "partially_succeeded"
|
||
elif not execution_succeeded:
|
||
result["status"] = "execution_failed"
|
||
elif parsed.get("deferred_requests"):
|
||
result["status"] = "partially_succeeded"
|
||
else:
|
||
result["status"] = "succeeded"
|
||
execution_results = execution_report["spec"]["results"]
|
||
stop_all_attempted = any(
|
||
item.get("telemetry", {}).get("stop_all_attempted")
|
||
for item in execution_results
|
||
)
|
||
firmware_gate_failed = any(
|
||
item.get("telemetry", {}).get("error_type")
|
||
== "FirmwareProgrammingError"
|
||
for item in execution_results
|
||
)
|
||
failed_execution = next(
|
||
(item for item in execution_results if item.get("status") == "failed"),
|
||
None,
|
||
)
|
||
failed_telemetry = (
|
||
failed_execution.get("telemetry", {}) if failed_execution else {}
|
||
)
|
||
hardware_failure_summary = (
|
||
f"真实 CAN 执行失败:{failed_execution['message']}"
|
||
if failed_execution
|
||
else "真实 CAN 执行失败,未获得满足目标的物理反馈。"
|
||
)
|
||
if stop_all_attempted:
|
||
hardware_failure_summary += " 移动路径已尝试发送 STOP_ALL。"
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"execute",
|
||
"succeeded" if execution_succeeded else "failed",
|
||
(
|
||
"真实 CAN 命令已提交;当前反馈窗口未确认车辆效果,状态以实时 CAN 回读为准。"
|
||
if hardware_request
|
||
and execution_succeeded
|
||
and execution_outcome == "submitted_unverified"
|
||
else "真实 CAN 命令已发送,并收到 IAR 固件的实际状态回读。"
|
||
if hardware_request and execution_succeeded
|
||
else (
|
||
(
|
||
"固件身份验证或自动烧录失败;未发送座椅控制帧。"
|
||
if firmware_gate_failed
|
||
else hardware_failure_summary
|
||
)
|
||
if hardware_request
|
||
else (
|
||
"模拟台架已生成隔离的 HIL 反馈,未访问物理 CAN。"
|
||
if simulated_hardware_request and execution_succeeded
|
||
else (
|
||
f"模拟台架故障已触发:{failed_execution['message']}"
|
||
if simulated_hardware_request and failed_execution
|
||
else "虚拟 CAN 已编码命令帧并生成模拟回读。"
|
||
)
|
||
)
|
||
)
|
||
),
|
||
execute_started,
|
||
{
|
||
"mode": execution_report["spec"]["mode"],
|
||
"result_count": len(execution_report["spec"]["results"]),
|
||
"firmware_gate_failed": firmware_gate_failed,
|
||
"stop_all_attempted": stop_all_attempted,
|
||
"motor_result": failed_telemetry.get("motor_result_name"),
|
||
"failure_category": failed_telemetry.get("failure_category"),
|
||
"hall_before": failed_telemetry.get("hall_before"),
|
||
"hall_after": failed_telemetry.get("hall_after"),
|
||
},
|
||
)
|
||
if execution_succeeded:
|
||
execution_notices = [
|
||
str(item.get("telemetry", {}).get("user_notice"))
|
||
for item in execution_results
|
||
if item.get("telemetry", {}).get("user_notice")
|
||
]
|
||
for notice in execution_notices:
|
||
if notice not in parsed["decision_summary"]:
|
||
parsed["decision_summary"].append(notice)
|
||
state_actions = (
|
||
self._hardware_feedback_actions(
|
||
parsed["actions"], function_config, execution_report
|
||
)
|
||
if hardware_request
|
||
else parsed["actions"]
|
||
)
|
||
updated_context = self.conversations.apply_successful_actions(
|
||
conversation_id,
|
||
utterance,
|
||
state_actions,
|
||
parsed["decision_summary"],
|
||
)
|
||
physical_state = execution_report.get("spec", {}).get(
|
||
"physical_state"
|
||
)
|
||
if hardware_request and isinstance(physical_state, dict):
|
||
updated_context = self.conversations.synchronize_physical_passenger(
|
||
conversation_id,
|
||
physical_state,
|
||
)
|
||
updated_context = self._context_for_vehicle(
|
||
updated_context,
|
||
vehicle_profile,
|
||
)
|
||
result["conversation_turn"] = updated_context["turn_count"]
|
||
result["actuator_state"] = updated_context["devices"]
|
||
result["domain_state"] = updated_context["domain_devices"]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"state_update",
|
||
"succeeded",
|
||
f"会话状态已更新为第 {updated_context['turn_count']} 轮,可用于下一条指令。",
|
||
time.perf_counter(),
|
||
{"state_after": updated_context["devices"]},
|
||
)
|
||
elif hardware_request:
|
||
physical_state = execution_report.get("spec", {}).get(
|
||
"physical_state"
|
||
)
|
||
if isinstance(physical_state, dict) and (
|
||
physical_state.get("position")
|
||
or physical_state.get("ventilation")
|
||
or physical_state.get("heating")
|
||
):
|
||
updated_context = self.conversations.synchronize_physical_passenger(
|
||
conversation_id,
|
||
physical_state,
|
||
)
|
||
updated_context = self._context_for_vehicle(
|
||
updated_context,
|
||
vehicle_profile,
|
||
)
|
||
result["actuator_state"] = updated_context["devices"]
|
||
result["domain_state"] = updated_context["domain_devices"]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"state_sync",
|
||
"succeeded",
|
||
"真实执行未完成,但已按 CAN 物理回读同步副驾状态。",
|
||
time.perf_counter(),
|
||
{"state_after": updated_context["devices"]},
|
||
)
|
||
telemetry = next(
|
||
(
|
||
item.get("telemetry", {})
|
||
for item in execution_report["spec"]["results"]
|
||
if item.get("telemetry", {}).get("feedback_frame")
|
||
),
|
||
execution_report["spec"]["results"][0].get("telemetry", {}),
|
||
)
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"telemetry",
|
||
"succeeded" if execution_succeeded else "failed",
|
||
(
|
||
"IAR 固件反馈帧中的实际状态与请求目标一致。"
|
||
if hardware_request and execution_succeeded
|
||
else (
|
||
(
|
||
f"未获得满足目标的真实硬件反馈:"
|
||
f"{failed_execution['message']}"
|
||
if failed_execution
|
||
else "未获得满足目标的真实硬件反馈。"
|
||
)
|
||
if hardware_request
|
||
else (
|
||
"模拟台架反馈与请求目标一致,物理 CAN 始终关闭。"
|
||
if simulated_hardware_request and execution_succeeded
|
||
else (
|
||
f"模拟台架故障反馈:{failed_execution['message']}"
|
||
if simulated_hardware_request and failed_execution
|
||
else "虚构反馈帧中的最终状态与请求目标一致。"
|
||
)
|
||
)
|
||
)
|
||
),
|
||
time.perf_counter(),
|
||
{
|
||
"command_frame": telemetry.get("command_frame", {}).get("data_hex"),
|
||
"feedback_frame": telemetry.get("feedback_frame", {}).get("data_hex"),
|
||
"simulated": not hardware_request,
|
||
"simulation_fault": (
|
||
simulation_fault if simulated_hardware_request else None
|
||
),
|
||
},
|
||
)
|
||
self._persist(result)
|
||
return result
|
||
|
||
@staticmethod
|
||
def _adapt_validation_for_hardware(
|
||
validation: list[dict[str, Any]],
|
||
) -> None:
|
||
"""Replace reference-vehicle dry-run warnings with HIL bench semantics."""
|
||
|
||
hardware_messages = {
|
||
"LEVEL_COMMAND_SIMULATED": (
|
||
"LEVEL_COMMAND_T2G_MAPPED",
|
||
"目标档位已映射到统一 T2G 固件的 0x29E 档位信号;"
|
||
"执行结果以 0x4FA 物理 CAN 回读为准。",
|
||
),
|
||
"SYNTHETIC_MOTION_CAN": (
|
||
"T2G_MOTION_PROTOCOL_MAPPED",
|
||
"参考车型的虚拟运动矩阵未用于本次 HIL;座椅移动通过统一 T2G 固件的 "
|
||
"0x300 命令和 0x301/0x302 物理反馈闭环执行。",
|
||
),
|
||
"DRIVER_MOTION_INTERLOCK_NOT_IMPLEMENTED": (
|
||
"BENCH_ONLY_NO_VEHICLE_INTERLOCKS",
|
||
"当前真实移动路径仅限独立 T2G 台架;尚未接入车速、挡位、乘员和防夹信号,"
|
||
"不得直接用于车载座椅系统。",
|
||
),
|
||
"COMBINED_THERMAL_SIMULATED": (
|
||
"COMBINED_THERMAL_T2G_MAPPED",
|
||
"加热与通风组合目标由统一 T2G 固件执行并以 0x4FA 物理反馈确认;"
|
||
"接入完整座椅总成前仍需补充热安全互锁验证。",
|
||
),
|
||
"VIRTUAL_EXECUTION_ONLY": (
|
||
"REFERENCE_MATRIX_OVERRIDDEN_BY_T2G_BENCH",
|
||
"参考车型矩阵元数据仍不完整;本次 HIL 仅使用仓库内已验证的 "
|
||
"T2G 统一固件协议、身份门禁和物理反馈闭环。",
|
||
),
|
||
}
|
||
for issue in validation:
|
||
replacement = hardware_messages.get(str(issue.get("code")))
|
||
if replacement is not None:
|
||
issue["code"], issue["message"] = replacement
|
||
|
||
@staticmethod
|
||
def _hardware_feedback_actions(
|
||
actions: list[dict[str, Any]],
|
||
function_config: dict[str, Any],
|
||
execution_report: dict[str, Any],
|
||
) -> list[dict[str, Any]]:
|
||
"""Project authoritative physical feedback into conversation/UI state."""
|
||
feedback_by_ref = {
|
||
item["ref"]: item.get("telemetry", {})
|
||
for item in execution_report["spec"]["results"]
|
||
if _operation_verification_status(item) == "verified_success"
|
||
}
|
||
updated: list[dict[str, Any]] = []
|
||
operations = function_config["spec"]["operations"]
|
||
for source_action, operation in zip(actions, operations, strict=True):
|
||
telemetry = feedback_by_ref.get(operation["id"])
|
||
if telemetry is None:
|
||
# Submission alone is not an authoritative vehicle-state
|
||
# update. The asynchronous CAN/MQTT state monitor will apply
|
||
# the change if and when the vehicle reports it.
|
||
continue
|
||
action = deepcopy(source_action)
|
||
if action.get("action_type") == "position":
|
||
if telemetry.get("effective_position_pct") is not None:
|
||
action["position_pct"] = int(
|
||
telemetry["effective_position_pct"]
|
||
)
|
||
if telemetry.get("position_valid") is not None:
|
||
action["position_valid"] = bool(
|
||
telemetry["position_valid"]
|
||
)
|
||
if telemetry.get("motion_mode") in {
|
||
"relative_timed_duty",
|
||
"timed_user_request",
|
||
}:
|
||
action["position_estimated"] = True
|
||
action["feedback_basis"] = (
|
||
"timed_endpoint_or_hall_confirmed_open_loop"
|
||
if telemetry.get("motion_mode") == "timed_user_request"
|
||
else "relative_hall_confirmed_open_loop"
|
||
)
|
||
updated.append(action)
|
||
continue
|
||
raw = telemetry.get("feedback_value")
|
||
if raw is not None:
|
||
raw_value = int(raw)
|
||
action["state"] = "off" if raw_value <= 1 else "on"
|
||
action["requested_level"] = max(0, raw_value - 1)
|
||
updated.append(action)
|
||
|
||
physical_state = execution_report.get("spec", {}).get("physical_state")
|
||
if isinstance(physical_state, dict):
|
||
positions = physical_state.get("position", {})
|
||
by_axis = {
|
||
action.get("axis"): action
|
||
for action in updated
|
||
if action.get("action_type") == "position"
|
||
}
|
||
for axis, feedback in positions.items():
|
||
if not isinstance(feedback, dict) or feedback.get("position_pct") is None:
|
||
continue
|
||
action = by_axis.get(axis)
|
||
if action is None:
|
||
action = {
|
||
"domain": "seat",
|
||
"target": "passenger",
|
||
"action_type": "position",
|
||
"axis": axis,
|
||
}
|
||
updated.append(action)
|
||
by_axis[axis] = action
|
||
action["position_pct"] = int(feedback["position_pct"])
|
||
if feedback.get("position_valid") is not None:
|
||
action["position_valid"] = bool(feedback["position_valid"])
|
||
action["feedback_basis"] = "physical_can_snapshot"
|
||
|
||
by_function = {
|
||
action.get("function"): action
|
||
for action in updated
|
||
if action.get("function") in {"ventilation", "heating"}
|
||
}
|
||
for feature in ("ventilation", "heating"):
|
||
feedback = physical_state.get(feature)
|
||
if not isinstance(feedback, dict):
|
||
continue
|
||
action = by_function.get(feature)
|
||
if action is None:
|
||
action = {
|
||
"domain": "seat",
|
||
"target": "passenger",
|
||
"function": feature,
|
||
}
|
||
updated.append(action)
|
||
by_function[feature] = action
|
||
action["state"] = (
|
||
"on" if feedback.get("state") == "on" else "off"
|
||
)
|
||
action["requested_level"] = int(feedback.get("level", 0))
|
||
for action in updated:
|
||
action["physical_target_alias"] = "t2g_primary_seat"
|
||
return updated
|
||
|
||
@staticmethod
|
||
def _physical_ch1_feedback_states(
|
||
reports: dict[str, dict[str, Any]],
|
||
) -> list[dict[str, Any]]:
|
||
"""Return only fully verified decoded CH1 feedback state patches."""
|
||
|
||
states: list[dict[str, Any]] = []
|
||
for report in reports.values():
|
||
for item in report.get("spec", {}).get("results", []):
|
||
feedback_state = authoritative_physical_ch1_feedback_state(item)
|
||
if feedback_state is not None:
|
||
states.append(feedback_state)
|
||
return states
|
||
|
||
@staticmethod
|
||
def _parse_mode_profile_save_name(utterance: str) -> str | None:
|
||
explicit = SAVE_MODE_PROFILE_NAME.search(utterance)
|
||
if explicit is not None:
|
||
return explicit.group("name").strip()
|
||
|
||
save_like = any(word in utterance for word in ("保存", "记住", "记为"))
|
||
mode_context = any(
|
||
word in utterance
|
||
for word in ("这个模式", "这个配置", "当前模式", "长期", "固化")
|
||
)
|
||
if not save_like or not mode_context:
|
||
return None
|
||
generic = re.search(
|
||
r"(?:保存为|保存成|记为|命名为)(?P<name>[^,。,.!!\s]{1,64}?模式)",
|
||
utterance,
|
||
)
|
||
return generic.group("name").strip() if generic is not None else None
|
||
|
||
@staticmethod
|
||
def _parse_mode_profile_create_name(utterance: str) -> str | None:
|
||
explicit = CREATE_MODE_PROFILE_NAME.search(utterance)
|
||
return explicit.group("name").strip() if explicit is not None else None
|
||
|
||
@staticmethod
|
||
def _parse_mode_profile_update(utterance: str) -> tuple[str, str] | None:
|
||
explicit = UPDATE_MODE_PROFILE_PREFIX.search(utterance)
|
||
if explicit is not None:
|
||
change_text = str(explicit.group("change") or "").strip()
|
||
return explicit.group("name").strip(), change_text
|
||
|
||
inline = UPDATE_MODE_PROFILE_BA.search(utterance)
|
||
if inline is None:
|
||
return None
|
||
change_text = inline.group("change").strip("的").strip()
|
||
if not any(word in change_text for word in ("改", "调", "设", "更新", "变")):
|
||
return None
|
||
return inline.group("name").strip(), change_text
|
||
|
||
@staticmethod
|
||
def _parse_mode_profile_restore_name(utterance: str) -> str | None:
|
||
explicit = RESTORE_MODE_PROFILE_PREFIX.search(utterance)
|
||
if explicit is not None:
|
||
return explicit.group("name").strip()
|
||
suffix = RESTORE_MODE_PROFILE_SUFFIX.search(utterance)
|
||
return suffix.group("name").strip() if suffix is not None else None
|
||
|
||
def _apply_mode_profiles(
|
||
self,
|
||
parsed: dict[str, Any],
|
||
matched_modes: list[ModeProfile],
|
||
context: dict[str, Any],
|
||
vehicle_profile: VehicleProfile,
|
||
*,
|
||
utterance: str,
|
||
) -> dict[str, Any]:
|
||
if not matched_modes:
|
||
return parsed
|
||
if self._contains_seat_preset_request(parsed):
|
||
return parsed
|
||
if len(matched_modes) > 1:
|
||
rejected = deepcopy(parsed)
|
||
for key in (
|
||
"actions",
|
||
"domain_actions",
|
||
"preset_actions",
|
||
"preset_events",
|
||
"deferred_requests",
|
||
"model_requests",
|
||
):
|
||
rejected[key] = []
|
||
rejected["ambiguities"] = [
|
||
{
|
||
"field": "mode_profile",
|
||
"question": "一条请求只能执行一个座椅功能模式,请确认要执行的模式。",
|
||
}
|
||
]
|
||
rejected["decision_summary"] = [
|
||
"同一条请求命中多个功能模式,已停止生成控制指令以避免模式冲突。"
|
||
]
|
||
return rejected
|
||
|
||
target_conflict = self._explicit_mode_target_conflict(
|
||
matched_modes[0],
|
||
utterance,
|
||
)
|
||
if target_conflict is not None:
|
||
rejected = deepcopy(parsed)
|
||
for key in (
|
||
"actions",
|
||
"domain_actions",
|
||
"native_can_actions",
|
||
"preset_actions",
|
||
"preset_events",
|
||
"deferred_requests",
|
||
"model_requests",
|
||
):
|
||
rejected[key] = []
|
||
requested_scope, supported_scopes = target_conflict
|
||
rejected["ambiguities"] = [
|
||
*rejected.get("ambiguities", []),
|
||
{
|
||
"field": "mode_profile",
|
||
"question": (
|
||
f"该模式的座椅目标为 {', '.join(supported_scopes)},"
|
||
f"与用户指定的 {requested_scope} 不一致,请重新确认。"
|
||
),
|
||
"reason": "mode_target_scope_mismatch",
|
||
},
|
||
]
|
||
rejected["unresolved_spans"] = [
|
||
*rejected.get("unresolved_spans", []),
|
||
{
|
||
"text": utterance,
|
||
"reason": "mode_target_scope_mismatch",
|
||
},
|
||
]
|
||
rejected["decision_summary"] = [
|
||
*rejected.get("decision_summary", []),
|
||
"用户显式座位与功能模式登记目标不一致,已停止所有模式动作。",
|
||
]
|
||
return rejected
|
||
|
||
parsed = self._apply_implicit_mode_target_policy(
|
||
parsed,
|
||
matched_modes,
|
||
utterance,
|
||
)
|
||
|
||
safety_ambiguities = [
|
||
item
|
||
for item in parsed.get("ambiguities", [])
|
||
if item.get("field") != "capability"
|
||
]
|
||
if safety_ambiguities:
|
||
return parsed
|
||
|
||
mode_requests = self.mode_profiles.requests_for_profiles(matched_modes)
|
||
detected_domains = []
|
||
for request in mode_requests:
|
||
if request["domain"] not in detected_domains:
|
||
detected_domains.append(request["domain"])
|
||
mode_plan = {
|
||
"detected_domains": detected_domains or ["seat"],
|
||
"requests": mode_requests,
|
||
"ambiguities": [],
|
||
"decision_summary": [
|
||
"命中长期模式配置:"
|
||
+ "、".join(profile.name for profile in matched_modes)
|
||
+ "。"
|
||
],
|
||
"confidence": max(float(parsed.get("confidence", 0)), 0.97),
|
||
}
|
||
mode_parsed = self.model_plan_adapter.adapt(
|
||
mode_plan,
|
||
context,
|
||
vehicle_profile,
|
||
)
|
||
parsed_has_explicit_request = bool(
|
||
parsed.get("actions")
|
||
or parsed.get("domain_actions")
|
||
or parsed.get("preset_actions")
|
||
or parsed.get("preset_events")
|
||
or parsed.get("model_requests")
|
||
)
|
||
merged = deepcopy(parsed)
|
||
merged["actions"] = self._merge_control_actions(
|
||
mode_parsed.get("actions", []),
|
||
parsed.get("actions", []),
|
||
)
|
||
merged["domain_actions"] = self._merge_control_actions(
|
||
mode_parsed.get("domain_actions", []),
|
||
parsed.get("domain_actions", []),
|
||
)
|
||
for key in (
|
||
"preset_actions",
|
||
"preset_events",
|
||
"deferred_requests",
|
||
"ambiguities",
|
||
"model_requests",
|
||
):
|
||
parsed_values = parsed.get(key, [])
|
||
if key == "ambiguities" and not parsed_has_explicit_request:
|
||
# A local parser does not know arbitrary named modes and emits
|
||
# a generic unsupported-capability ambiguity. A unique,
|
||
# repository-backed mode match is authoritative for that case.
|
||
parsed_values = []
|
||
merged[key] = self._merge_unique(
|
||
mode_parsed.get(key, []),
|
||
parsed_values,
|
||
)
|
||
merged["decision_summary"] = [
|
||
*mode_parsed.get("decision_summary", []),
|
||
*parsed.get("decision_summary", []),
|
||
]
|
||
merged["detected_domains"] = self._merge_domains(
|
||
mode_parsed.get("detected_domains", []),
|
||
parsed.get("detected_domains", []),
|
||
)
|
||
merged["confidence"] = max(
|
||
float(mode_parsed.get("confidence", 0)),
|
||
float(parsed.get("confidence", 0)),
|
||
)
|
||
return merged
|
||
|
||
@staticmethod
|
||
def _explicit_mode_target_conflict(
|
||
profile: ModeProfile,
|
||
utterance: str,
|
||
) -> tuple[str, tuple[str, ...]] | None:
|
||
"""Reject a named mode when its persisted seat scope contradicts text."""
|
||
|
||
aliases = (profile.id, profile.name, *profile.aliases)
|
||
mode_clause = next(
|
||
(
|
||
clause
|
||
for clause in re.split(r"[,,;;。!?!?\n]+", utterance)
|
||
if any(
|
||
re.sub(r"\s+", "", alias.casefold())
|
||
in re.sub(r"\s+", "", clause.casefold())
|
||
for alias in aliases
|
||
if alias
|
||
)
|
||
),
|
||
utterance,
|
||
)
|
||
requested = resolve_explicit_seat_scopes(
|
||
mode_clause,
|
||
assume_seat_context=True,
|
||
)
|
||
if requested is None:
|
||
return None
|
||
|
||
normalized_scopes = {
|
||
"driver" if operation.target_scope == "front_left" else
|
||
"passenger" if operation.target_scope == "front_right" else
|
||
"rear" if operation.target_scope in {"rear", "rear_left", "rear_right"} else
|
||
operation.target_scope
|
||
for operation in profile.operations
|
||
if operation.domain == "seat"
|
||
}
|
||
if requested == normalized_scopes:
|
||
return None
|
||
return ",".join(sorted(requested)), tuple(sorted(normalized_scopes))
|
||
|
||
@staticmethod
|
||
def _apply_implicit_mode_target_policy(
|
||
parsed: dict[str, Any],
|
||
matched_modes: list[ModeProfile],
|
||
utterance: str,
|
||
) -> dict[str, Any]:
|
||
"""Let one matched mode own only an omitted seat target.
|
||
|
||
The native CH1 parser can add an implicit-driver ambiguity before or
|
||
after mode injection. A unique repository-backed mode already carries
|
||
authoritative target scopes on every operation, so that one tagged
|
||
default is redundant. Explicit scopes and all other safety questions
|
||
remain untouched.
|
||
"""
|
||
|
||
if len(matched_modes) != 1 or (
|
||
DRIVER_SEAT_SCOPE_PATTERN.search(utterance)
|
||
or PASSENGER_SEAT_SCOPE_PATTERN.search(utterance)
|
||
):
|
||
return parsed
|
||
ambiguities = list(parsed.get("ambiguities", []))
|
||
filtered = [
|
||
item
|
||
for item in ambiguities
|
||
if item.get("reason") != "implicit_driver_default_unresolved"
|
||
]
|
||
if len(filtered) == len(ambiguities):
|
||
return parsed
|
||
resolved = deepcopy(parsed)
|
||
resolved["ambiguities"] = filtered
|
||
return resolved
|
||
|
||
@staticmethod
|
||
def _is_dynamic_mode_stop_request(utterance: str) -> bool:
|
||
compact = re.sub(r"\s+", "", utterance)
|
||
if any(negation in compact for negation in ("不要停止", "无需停止", "别停止")):
|
||
return False
|
||
return any(word in compact for word in DYNAMIC_MODE_STOP_WORDS) and any(
|
||
word in compact for word in DYNAMIC_MODE_WORDS
|
||
)
|
||
|
||
def _apply_dynamic_mode_stop_request(
|
||
self,
|
||
parsed: dict[str, Any],
|
||
utterance: str,
|
||
) -> dict[str, Any]:
|
||
"""Make explicit stop language authoritative over a matched start profile."""
|
||
|
||
stopped = deepcopy(parsed)
|
||
stopped["actions"] = [
|
||
action
|
||
for action in stopped.get("actions", [])
|
||
if action.get("action_type") not in {"dynamic_mode", "dynamic_mode_stop"}
|
||
]
|
||
stopped["actions"].append(
|
||
{"action_type": "dynamic_mode_stop", "target": "passenger"}
|
||
)
|
||
stopped["model_requests"] = [
|
||
request
|
||
for request in stopped.get("model_requests", [])
|
||
if request.get("capability")
|
||
not in {"seat.start_dynamic_mode", DYNAMIC_MODE_STOP_CAPABILITY}
|
||
]
|
||
stopped["model_requests"].append(
|
||
{
|
||
"domain": "seat",
|
||
"capability": DYNAMIC_MODE_STOP_CAPABILITY,
|
||
"target_scope": "passenger",
|
||
"operation": "deactivate",
|
||
"parameters": [],
|
||
"reason": "用户明确要求停止动态座椅循环。",
|
||
"source_span": utterance,
|
||
}
|
||
)
|
||
stopped["ambiguities"] = [
|
||
item
|
||
for item in stopped.get("ambiguities", [])
|
||
if item.get("field") != "capability"
|
||
]
|
||
stopped["detected_domains"] = self._merge_domains(
|
||
["seat"], stopped.get("detected_domains", [])
|
||
)
|
||
stopped["decision_summary"] = [
|
||
"识别到停止动态座椅模式指令;不会重新启动同名长期模式。",
|
||
*stopped.get("decision_summary", []),
|
||
]
|
||
stopped["confidence"] = max(float(stopped.get("confidence", 0)), 0.99)
|
||
return stopped
|
||
|
||
@staticmethod
|
||
def _contains_seat_preset_request(parsed: dict[str, Any]) -> bool:
|
||
capabilities = {
|
||
request.get("capability")
|
||
for request in parsed.get("model_requests", [])
|
||
if isinstance(request, dict)
|
||
}
|
||
return bool(
|
||
parsed.get("preset_actions")
|
||
or parsed.get("preset_events")
|
||
or SAVE_PRESET_CAPABILITY in capabilities
|
||
or APPLY_PRESET_CAPABILITY in capabilities
|
||
)
|
||
|
||
@classmethod
|
||
def _merge_control_actions(
|
||
cls,
|
||
mode_items: list[dict[str, Any]],
|
||
parsed_items: list[dict[str, Any]],
|
||
) -> list[dict[str, Any]]:
|
||
parsed_controls = {
|
||
cls._control_signature(item)
|
||
for item in parsed_items
|
||
}
|
||
candidates = [
|
||
*parsed_items,
|
||
*(
|
||
item
|
||
for item in mode_items
|
||
if cls._control_signature(item) not in parsed_controls
|
||
),
|
||
]
|
||
return cls._merge_unique([], candidates)
|
||
|
||
@staticmethod
|
||
def _control_signature(action: dict[str, Any]) -> tuple[Any, ...]:
|
||
if action.get("domain"):
|
||
return (
|
||
action.get("domain"),
|
||
action.get("capability"),
|
||
action.get("target"),
|
||
)
|
||
if action.get("action_type") == "position":
|
||
return (
|
||
"seat",
|
||
"position",
|
||
action.get("target"),
|
||
action.get("axis"),
|
||
)
|
||
if action.get("action_type") == "dynamic_mode_stop":
|
||
return (
|
||
"seat",
|
||
"dynamic_mode_stop",
|
||
action.get("target"),
|
||
)
|
||
if action.get("action_type") == "dynamic_mode":
|
||
return (
|
||
"seat",
|
||
"dynamic_mode",
|
||
action.get("target"),
|
||
action.get("mode_id"),
|
||
)
|
||
return (
|
||
"seat",
|
||
"function",
|
||
action.get("target"),
|
||
action.get("function"),
|
||
)
|
||
|
||
@staticmethod
|
||
def _merge_unique(
|
||
first: list[dict[str, Any]],
|
||
second: list[dict[str, Any]],
|
||
) -> list[dict[str, Any]]:
|
||
merged = []
|
||
seen: set[str] = set()
|
||
for item in [*first, *second]:
|
||
signature = json.dumps(item, ensure_ascii=False, sort_keys=True)
|
||
if signature in seen:
|
||
continue
|
||
seen.add(signature)
|
||
merged.append(item)
|
||
return merged
|
||
|
||
@staticmethod
|
||
def _merge_domains(
|
||
first: list[str],
|
||
second: list[str],
|
||
) -> list[str]:
|
||
domains = []
|
||
for domain in [*first, *second]:
|
||
if domain not in domains:
|
||
domains.append(domain)
|
||
return domains or ["seat"]
|
||
|
||
def _process_mode_profile_update(
|
||
self,
|
||
utterance: str,
|
||
request_id: str,
|
||
provider: str,
|
||
conversation_id: str,
|
||
context: dict[str, Any],
|
||
mode_name: str,
|
||
change_text: str,
|
||
vehicle_profile: VehicleProfile,
|
||
result: dict[str, Any],
|
||
traces: list[dict[str, Any]],
|
||
) -> dict[str, Any]:
|
||
started = time.perf_counter()
|
||
result["provider_used"] = "mode_profile_registry"
|
||
result["decision_summary"] = [
|
||
f"识别为长期模式修改请求,目标模式为“{mode_name}”。"
|
||
]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"intent_parse",
|
||
"succeeded",
|
||
"识别到长期模式配置修改请求;本次不调用 CAN 编译路径。",
|
||
started,
|
||
{"mode_name": mode_name, "change_text": change_text},
|
||
domain="mode",
|
||
)
|
||
|
||
profile = self._resolve_single_mode_profile(
|
||
mode_name,
|
||
vehicle_profile,
|
||
result,
|
||
traces,
|
||
request_id,
|
||
)
|
||
if profile is None:
|
||
self._persist(result)
|
||
return result
|
||
if not change_text:
|
||
result["status"] = "needs_confirmation"
|
||
result["validation"] = [
|
||
{
|
||
"severity": "error",
|
||
"code": "MODE_PROFILE_UPDATE_MISSING_CHANGE",
|
||
"path": "utterance",
|
||
"message": "请说明要把长期模式中的哪些参数改成什么目标值。",
|
||
}
|
||
]
|
||
self._persist(result)
|
||
return result
|
||
|
||
parsed, provider_used, provider_notice = self._interpret(
|
||
change_text,
|
||
provider,
|
||
context,
|
||
vehicle_profile,
|
||
)
|
||
result["provider_used"] = f"mode_profile_registry+{provider_used}"
|
||
result["decision_summary"].extend(parsed.get("decision_summary", []))
|
||
if provider_notice:
|
||
result["decision_summary"].append(provider_notice)
|
||
result["deferred_requests"] = parsed.get("deferred_requests", [])
|
||
result["model_requests"] = parsed.get("model_requests", [])
|
||
|
||
source_actions = [
|
||
*parsed.get("actions", []),
|
||
*parsed.get("domain_actions", []),
|
||
]
|
||
mode_operations = self._mode_operations_from_actions(source_actions)
|
||
if not mode_operations:
|
||
validation = [
|
||
{
|
||
"severity": "error",
|
||
"code": "NO_MODE_PROFILE_UPDATE_OPERATIONS",
|
||
"path": "spec.operations",
|
||
"message": "没有从修改说明中得到可写入长期模式库的可执行配置。",
|
||
}
|
||
]
|
||
validation.extend(
|
||
{
|
||
"severity": "error",
|
||
"code": "AMBIGUOUS_INTENT",
|
||
"path": ambiguity.get("field", "utterance"),
|
||
"message": ambiguity.get("question", "修改说明不完整。"),
|
||
}
|
||
for ambiguity in parsed.get("ambiguities", [])
|
||
)
|
||
result["validation"] = validation
|
||
result["status"] = (
|
||
"capability_unavailable"
|
||
if parsed.get("deferred_requests") and not parsed.get("ambiguities")
|
||
else "needs_confirmation"
|
||
)
|
||
self._persist(result)
|
||
return result
|
||
|
||
updated_profile = self.mode_profiles.update_mode(profile, mode_operations)
|
||
profile_public = updated_profile.public_dict()
|
||
source_path = self._display_path(Path(updated_profile.source))
|
||
backup_path = self._display_path(
|
||
Path(updated_profile.source).with_suffix(".yaml.initial.bak")
|
||
)
|
||
result["mode_profile"] = {
|
||
**profile_public,
|
||
"source": source_path,
|
||
"initial_backup_source": backup_path,
|
||
}
|
||
result["matched_modes"] = [profile_public]
|
||
result["model_requests"] = self.mode_profiles.requests_for_profiles(
|
||
[updated_profile]
|
||
)
|
||
result["decision_summary"].append(
|
||
f"已更新长期模式“{updated_profile.name}”,原始配置备份保留为初始化来源。"
|
||
)
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"function_generate",
|
||
"succeeded",
|
||
"已将修改后的可执行配置写回长期模式 YAML。",
|
||
time.perf_counter(),
|
||
{
|
||
"mode_profile_id": updated_profile.id,
|
||
"operation_count": len(updated_profile.operations),
|
||
"updated_operation_count": len(mode_operations),
|
||
"source": source_path,
|
||
"initial_backup_source": backup_path,
|
||
},
|
||
domain="mode",
|
||
)
|
||
|
||
execution_report = self._mode_profile_control_report(
|
||
request_id,
|
||
"update_mode_profile",
|
||
updated_profile,
|
||
f"已更新长期模式“{updated_profile.name}”。",
|
||
{
|
||
"updated_operation_count": len(mode_operations),
|
||
"source": source_path,
|
||
"initial_backup_source": backup_path,
|
||
},
|
||
)
|
||
validation = self.contracts.validate(execution_report)
|
||
result["validation"] = validation
|
||
result["execution_report"] = execution_report
|
||
if validation:
|
||
result["status"] = "execution_report_invalid"
|
||
self._persist(result)
|
||
return result
|
||
|
||
updated_context = self.conversations.record_mode_profile_event(
|
||
conversation_id,
|
||
utterance,
|
||
result["mode_profile"],
|
||
source_actions,
|
||
result["decision_summary"],
|
||
"mode_profile_update",
|
||
)
|
||
result["status"] = (
|
||
"partially_succeeded"
|
||
if parsed.get("deferred_requests")
|
||
else "succeeded"
|
||
)
|
||
result["conversation_turn"] = updated_context["turn_count"]
|
||
result["actuator_state"] = updated_context["devices"]
|
||
result["domain_state"] = updated_context["domain_devices"]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"execute",
|
||
"succeeded",
|
||
"长期模式修改属于控制平面操作,未生成或发送 CAN 帧。",
|
||
time.perf_counter(),
|
||
{
|
||
"mode": "control_plane",
|
||
"mode_profile_id": updated_profile.id,
|
||
"can_command_emitted": False,
|
||
},
|
||
domain="mode",
|
||
)
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"state_update",
|
||
"succeeded",
|
||
f"长期模式“{updated_profile.name}”已更新,可被后续自然语言请求命中。",
|
||
time.perf_counter(),
|
||
{
|
||
"turn_count": updated_context["turn_count"],
|
||
"mode_profile_id": updated_profile.id,
|
||
},
|
||
domain="mode",
|
||
)
|
||
self._persist(result)
|
||
return result
|
||
|
||
def _process_mode_profile_restore(
|
||
self,
|
||
utterance: str,
|
||
request_id: str,
|
||
conversation_id: str,
|
||
mode_name: str,
|
||
vehicle_profile: VehicleProfile,
|
||
result: dict[str, Any],
|
||
traces: list[dict[str, Any]],
|
||
) -> dict[str, Any]:
|
||
started = time.perf_counter()
|
||
result["provider_used"] = "mode_profile_registry"
|
||
result["decision_summary"] = [
|
||
f"识别为长期模式初始化请求,目标模式为“{mode_name}”。"
|
||
]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"intent_parse",
|
||
"succeeded",
|
||
"识别到长期模式初始化请求;本次不调用模型或 CAN 编译路径。",
|
||
started,
|
||
{"mode_name": mode_name},
|
||
domain="mode",
|
||
)
|
||
|
||
profile = self._resolve_single_mode_profile(
|
||
mode_name,
|
||
vehicle_profile,
|
||
result,
|
||
traces,
|
||
request_id,
|
||
)
|
||
if profile is None:
|
||
self._persist(result)
|
||
return result
|
||
|
||
try:
|
||
restored_profile = self.mode_profiles.restore_initial_mode(profile)
|
||
except ValueError as exc:
|
||
result["status"] = "needs_confirmation"
|
||
result["validation"] = [
|
||
{
|
||
"severity": "error",
|
||
"code": "MODE_PROFILE_INITIAL_BACKUP_MISSING",
|
||
"path": "mode_profile.initial_backup",
|
||
"message": str(exc),
|
||
}
|
||
]
|
||
self._persist(result)
|
||
return result
|
||
|
||
profile_public = restored_profile.public_dict()
|
||
source_path = self._display_path(Path(restored_profile.source))
|
||
backup_path = self._display_path(
|
||
Path(restored_profile.source).with_suffix(".yaml.initial.bak")
|
||
)
|
||
result["mode_profile"] = {
|
||
**profile_public,
|
||
"source": source_path,
|
||
"initial_backup_source": backup_path,
|
||
}
|
||
result["matched_modes"] = [profile_public]
|
||
result["model_requests"] = self.mode_profiles.requests_for_profiles(
|
||
[restored_profile]
|
||
)
|
||
result["decision_summary"].append(
|
||
f"已将长期模式“{restored_profile.name}”恢复为初始配置。"
|
||
)
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"function_generate",
|
||
"succeeded",
|
||
"已从初始备份恢复长期模式 YAML。",
|
||
time.perf_counter(),
|
||
{
|
||
"mode_profile_id": restored_profile.id,
|
||
"operation_count": len(restored_profile.operations),
|
||
"source": source_path,
|
||
"initial_backup_source": backup_path,
|
||
},
|
||
domain="mode",
|
||
)
|
||
|
||
execution_report = self._mode_profile_control_report(
|
||
request_id,
|
||
"restore_mode_profile",
|
||
restored_profile,
|
||
f"已恢复长期模式“{restored_profile.name}”。",
|
||
{
|
||
"source": source_path,
|
||
"initial_backup_source": backup_path,
|
||
},
|
||
)
|
||
validation = self.contracts.validate(execution_report)
|
||
result["validation"] = validation
|
||
result["execution_report"] = execution_report
|
||
if validation:
|
||
result["status"] = "execution_report_invalid"
|
||
self._persist(result)
|
||
return result
|
||
|
||
updated_context = self.conversations.record_mode_profile_event(
|
||
conversation_id,
|
||
utterance,
|
||
result["mode_profile"],
|
||
[],
|
||
result["decision_summary"],
|
||
"mode_profile_restore",
|
||
)
|
||
result["status"] = "succeeded"
|
||
result["conversation_turn"] = updated_context["turn_count"]
|
||
result["actuator_state"] = updated_context["devices"]
|
||
result["domain_state"] = updated_context["domain_devices"]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"execute",
|
||
"succeeded",
|
||
"长期模式初始化属于控制平面操作,未生成或发送 CAN 帧。",
|
||
time.perf_counter(),
|
||
{
|
||
"mode": "control_plane",
|
||
"mode_profile_id": restored_profile.id,
|
||
"can_command_emitted": False,
|
||
},
|
||
domain="mode",
|
||
)
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"state_update",
|
||
"succeeded",
|
||
f"长期模式“{restored_profile.name}”已恢复,可被后续自然语言请求命中。",
|
||
time.perf_counter(),
|
||
{
|
||
"turn_count": updated_context["turn_count"],
|
||
"mode_profile_id": restored_profile.id,
|
||
},
|
||
domain="mode",
|
||
)
|
||
self._persist(result)
|
||
return result
|
||
|
||
def _resolve_single_mode_profile(
|
||
self,
|
||
mode_name: str,
|
||
vehicle_profile: VehicleProfile,
|
||
result: dict[str, Any],
|
||
traces: list[dict[str, Any]],
|
||
request_id: str,
|
||
) -> ModeProfile | None:
|
||
matches = self.mode_profiles.match_text(
|
||
mode_name,
|
||
vehicle_id=vehicle_profile.id,
|
||
)
|
||
result["matched_modes"] = [profile.public_dict() for profile in matches]
|
||
if len(matches) == 1:
|
||
return matches[0]
|
||
result["status"] = "needs_confirmation"
|
||
result["validation"] = [
|
||
{
|
||
"severity": "error",
|
||
"code": (
|
||
"MODE_PROFILE_NOT_FOUND"
|
||
if not matches
|
||
else "MODE_PROFILE_AMBIGUOUS"
|
||
),
|
||
"path": "utterance.mode_profile",
|
||
"message": (
|
||
f"未在车型 {vehicle_profile.id} 的长期模式库中找到“{mode_name}”。"
|
||
if not matches
|
||
else f"“{mode_name}”命中了多个长期模式,请指定更完整名称。"
|
||
),
|
||
}
|
||
]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"context_resolve",
|
||
"failed",
|
||
result["validation"][0]["message"],
|
||
time.perf_counter(),
|
||
{
|
||
"mode_name": mode_name,
|
||
"vehicle_id": vehicle_profile.id,
|
||
"match_count": len(matches),
|
||
},
|
||
domain="mode",
|
||
)
|
||
return None
|
||
|
||
def _mode_profile_control_report(
|
||
self,
|
||
request_id: str,
|
||
operation: str,
|
||
profile: ModeProfile,
|
||
message: str,
|
||
telemetry: dict[str, Any],
|
||
) -> dict[str, Any]:
|
||
return {
|
||
"apiVersion": "cognitive-agent/v1alpha1",
|
||
"kind": "ExecutionReport",
|
||
"metadata": {
|
||
"id": f"report-{request_id}",
|
||
"name": "Mode profile control report",
|
||
"version": "1.0.0",
|
||
"request_id": request_id,
|
||
"labels": {"control_plane": "mode_profile_store"},
|
||
},
|
||
"spec": {
|
||
"mode": "dry_run",
|
||
"status": "succeeded",
|
||
"started_at": _now(),
|
||
"finished_at": _now(),
|
||
"results": [
|
||
{
|
||
"ref": f"{operation}-{profile.id}",
|
||
"domain": "mode",
|
||
"status": "succeeded",
|
||
"message": message,
|
||
"telemetry": {
|
||
"operation": operation,
|
||
"mode_profile_id": profile.id,
|
||
"operation_count": len(profile.operations),
|
||
"storage_scope": "repository_config",
|
||
"can_command_emitted": False,
|
||
**telemetry,
|
||
},
|
||
}
|
||
],
|
||
},
|
||
}
|
||
|
||
def _display_path(self, path: Path) -> str:
|
||
try:
|
||
return str(path.relative_to(self.root))
|
||
except ValueError:
|
||
return str(path)
|
||
|
||
@staticmethod
|
||
def _mode_feedback_text(value: object, *, limit: int = 180) -> str:
|
||
text = re.sub(r"[\x00-\x1f\x7f]+", " ", str(value or ""))
|
||
return re.sub(r"\s+", " ", text).strip(" ,,。;;::")[:limit]
|
||
|
||
@classmethod
|
||
def _mode_failure_issue(
|
||
cls,
|
||
clause: str,
|
||
parsed: dict[str, Any],
|
||
) -> dict[str, str]:
|
||
display_clause = cls._mode_feedback_text(
|
||
re.sub(r"^(?:副驾座椅|主驾座椅)[,,]", "", clause),
|
||
limit=120,
|
||
)
|
||
ambiguities = [
|
||
item
|
||
for item in parsed.get("ambiguities", [])
|
||
if isinstance(item, dict) and str(item.get("field") or "") != "capability"
|
||
]
|
||
ambiguity = ambiguities[0] if ambiguities else None
|
||
field = str(ambiguity.get("field") or "") if ambiguity else ""
|
||
question = cls._mode_feedback_text(
|
||
ambiguity.get("question") if ambiguity else "",
|
||
limit=220,
|
||
)
|
||
# Parser questions may contain both the diagnosis and a concrete
|
||
# correction. Split those parts so every public failure has the same
|
||
# reason/next-step contract.
|
||
reason_from_question = question
|
||
next_from_question = ""
|
||
for separator in (";请", "。请"):
|
||
if separator in question:
|
||
reason_from_question, suffix = question.split(separator, 1)
|
||
next_from_question = f"请{suffix}"
|
||
break
|
||
|
||
if reason_from_question and not re.search(
|
||
r"(?:CAN|CH[01]|JSON|schema|0x[0-9a-f]+|内部路径)",
|
||
reason_from_question,
|
||
re.IGNORECASE,
|
||
):
|
||
reason = f"设置“{display_clause}”未通过:{reason_from_question}"
|
||
else:
|
||
reason = (
|
||
f"设置“{display_clause}”未被识别为当前支持的座舱控制动作"
|
||
)
|
||
|
||
if next_from_question:
|
||
next_step = next_from_question
|
||
elif "temperature" in field or "温度" in display_clause:
|
||
next_step = (
|
||
"请只保留一个18至32℃的整数温度,例如“空调调整到24度”,然后重试"
|
||
)
|
||
elif field == "target" or "target_scope" in field:
|
||
next_step = (
|
||
"请改用当前支持的副驾座椅目标,并明确部件与位置,例如"
|
||
"“副驾腿托调整到80%”"
|
||
)
|
||
elif "position" in field or re.search(
|
||
r"座椅|靠背|椅背|坐垫|坐盆|腿托|脚托|滑轨",
|
||
display_clause,
|
||
):
|
||
next_step = (
|
||
"请按“副驾 + 座椅部件 + 方向或0到100%位置”重述,例如"
|
||
"“副驾腿托向上调整到最大位置”或“副驾腿托调整到80%”"
|
||
)
|
||
else:
|
||
next_step = (
|
||
"请按“设备 + 动作 + 明确目标”重述,例如“关闭所有车窗”、"
|
||
"“副驾靠背调整到80%”或“空调调整到24度”"
|
||
)
|
||
return {
|
||
"clause": display_clause,
|
||
"reason": reason,
|
||
"next_step": cls._mode_feedback_text(next_step, limit=260),
|
||
}
|
||
|
||
@classmethod
|
||
def _mode_creation_failure_from_clauses(
|
||
cls,
|
||
clause_results: list[tuple[str, dict[str, Any], str]],
|
||
unresolved_clauses: list[str],
|
||
) -> dict[str, Any]:
|
||
unresolved = set(unresolved_clauses)
|
||
issues = [
|
||
cls._mode_failure_issue(clause, parsed)
|
||
for clause, parsed, status in clause_results
|
||
if status != "complete" and clause in unresolved
|
||
][:3]
|
||
if not issues:
|
||
issues = [
|
||
cls._mode_failure_issue(clause, {})
|
||
for clause in unresolved_clauses[:3]
|
||
]
|
||
reason = ";".join(dict.fromkeys(item["reason"] for item in issues))
|
||
next_step = ";".join(
|
||
dict.fromkeys(item["next_step"] for item in issues)
|
||
)
|
||
return {
|
||
"code": "MODE_PROFILE_DRAFT_INCOMPLETE",
|
||
"reason": reason or "部分座舱设置没有转换为可保存的控制动作",
|
||
"next_step": next_step
|
||
or "请将未识别设置改写为明确的设备、动作和目标值后重试",
|
||
"unresolved_clauses": [item["clause"] for item in issues],
|
||
"issues": issues,
|
||
}
|
||
|
||
def _process_mode_profile_create(
|
||
self,
|
||
utterance: str,
|
||
provider: str,
|
||
conversation_id: str,
|
||
execution_mode: str,
|
||
approval_token: str | None,
|
||
vehicle_profile: VehicleProfile,
|
||
simulation_fault: str,
|
||
mode_name: str,
|
||
control_transport: str,
|
||
assistant_plan: dict[str, Any] | None = None,
|
||
) -> dict[str, Any]:
|
||
"""Build and verify a named-mode draft for explicit user confirmation.
|
||
|
||
A request such as ``构建一个K歌模式`` used to be interpreted as an
|
||
ordinary temporary scene. Generate the scene through the normal
|
||
dry-run path, but leave persistence to the existing mode-card API so
|
||
the assistant can show the exact actions before the user saves them.
|
||
"""
|
||
|
||
create_match = CREATE_MODE_PROFILE_NAME.fullmatch(utterance)
|
||
requested_instruction = (
|
||
str(create_match.group("instruction") or "").strip(" ,,::。.!!")
|
||
if create_match is not None
|
||
else ""
|
||
)
|
||
source_utterance = requested_instruction or (
|
||
f"进入{mode_name},请根据该场景名称使用当前可控原子能力构建并执行场景"
|
||
)
|
||
source_result = self._process_once(
|
||
source_utterance,
|
||
provider,
|
||
conversation_id,
|
||
"dry_run",
|
||
approval_token,
|
||
vehicle_profile.id,
|
||
simulation_fault,
|
||
control_transport=control_transport,
|
||
assistant_plan=assistant_plan,
|
||
)
|
||
draft_context = self._context_for_vehicle(
|
||
self.conversations.snapshot(conversation_id),
|
||
vehicle_profile,
|
||
)
|
||
draft_context["mode_profiles"] = self.mode_profiles.public_modes(
|
||
vehicle_profile.id
|
||
)
|
||
local_clause_results = self._local_clause_results(
|
||
source_utterance,
|
||
draft_context,
|
||
)
|
||
unresolved_draft_clauses = [
|
||
clause
|
||
for clause, _parsed, status in local_clause_results
|
||
if status != "complete"
|
||
and not self.mode_profiles.match_text(
|
||
clause,
|
||
vehicle_id=vehicle_profile.id,
|
||
)
|
||
]
|
||
if provider == "local" and assistant_plan is None and unresolved_draft_clauses:
|
||
source_result["status"] = "needs_confirmation"
|
||
source_result["unresolved_clauses"] = unresolved_draft_clauses
|
||
source_result["mode_creation_failure"] = (
|
||
self._mode_creation_failure_from_clauses(
|
||
local_clause_results,
|
||
unresolved_draft_clauses,
|
||
)
|
||
)
|
||
source_result["supplement_required"] = False
|
||
source_result["validation"] = [
|
||
*list(source_result.get("validation", [])),
|
||
{
|
||
"severity": "error",
|
||
"code": "MODE_PROFILE_DRAFT_INCOMPLETE",
|
||
"path": "mode_profile_draft.instruction",
|
||
"message": (
|
||
"以下模式设置尚未完整解析:"
|
||
+ ";".join(unresolved_draft_clauses)
|
||
),
|
||
},
|
||
]
|
||
source_result["decision_summary"] = [
|
||
f"未能完整构建“{mode_name}”,因此没有生成可保存草稿。",
|
||
*list(source_result.get("decision_summary", [])),
|
||
"请补充为当前支持的明确设备动作后重试。",
|
||
]
|
||
self._persist(source_result)
|
||
return source_result
|
||
if source_result.get("status") != "succeeded":
|
||
source_result["mode_creation_failure"] = {
|
||
"code": "MODE_PROFILE_ACTION_VALIDATION_FAILED",
|
||
"reason": "场景动作未通过车辆能力或安全校验",
|
||
"next_step": (
|
||
"请检查每项设备目标是否在当前支持范围内;仍失败时可分条输入后重试"
|
||
),
|
||
"unresolved_clauses": [],
|
||
"issues": [],
|
||
}
|
||
source_result["decision_summary"] = [
|
||
f"未能完成“{mode_name}”的场景动作验证,因此没有创建长期模式。",
|
||
*list(source_result.get("decision_summary", [])),
|
||
]
|
||
self._persist(source_result)
|
||
return source_result
|
||
|
||
intent_ir = source_result.get("intent_ir")
|
||
intent_spec = intent_ir.get("spec") if isinstance(intent_ir, dict) else None
|
||
raw_intent_actions = (
|
||
intent_spec.get("actions", []) if isinstance(intent_spec, dict) else []
|
||
)
|
||
reusable_actions = [
|
||
item for item in raw_intent_actions if isinstance(item, dict)
|
||
] if isinstance(raw_intent_actions, list) else []
|
||
if not reusable_actions:
|
||
source_result["status"] = "needs_confirmation"
|
||
source_result["mode_creation_failure"] = {
|
||
"code": "MODE_PROFILE_DRAFT_EMPTY",
|
||
"reason": "没有识别到可保存的座舱控制动作",
|
||
"next_step": (
|
||
"请按“设备 + 动作 + 明确目标”补充,例如“关闭所有车窗,空调调整到24度”"
|
||
),
|
||
"unresolved_clauses": [],
|
||
"issues": [],
|
||
}
|
||
source_result["validation"] = [
|
||
{
|
||
"severity": "error",
|
||
"code": "MODE_PROFILE_DRAFT_EMPTY",
|
||
"path": "intent_ir.spec.actions",
|
||
"message": "没有生成可保存的功能模式动作,请补充更明确的座舱设置。",
|
||
}
|
||
]
|
||
source_result["decision_summary"] = [
|
||
f"未能为“{mode_name}”生成可保存的场景动作。",
|
||
*list(source_result.get("decision_summary", [])),
|
||
]
|
||
self._persist(source_result)
|
||
return source_result
|
||
|
||
domains = list(
|
||
dict.fromkeys(
|
||
str(item.get("domain") or "")
|
||
for item in reusable_actions
|
||
if str(item.get("domain") or "")
|
||
)
|
||
)
|
||
source_request_id = str(source_result.get("request_id") or "")
|
||
source_result["mode_profile_draft"] = {
|
||
"name": mode_name,
|
||
"instruction": source_utterance,
|
||
"source_request_id": source_request_id,
|
||
"vehicle_id": vehicle_profile.id,
|
||
"domains": domains,
|
||
"operation_count": len(reusable_actions),
|
||
}
|
||
source_result["mode_creation"] = {
|
||
"requested_utterance": utterance,
|
||
"source_utterance": source_utterance,
|
||
"source_request_id": source_request_id,
|
||
"source_status": source_result.get("status"),
|
||
"source_provider": source_result.get("provider_used"),
|
||
"operation_count": len(reusable_actions),
|
||
}
|
||
source_result["orchestration_route"] = "mode_profile_create_preview"
|
||
source_result["decision_summary"] = [
|
||
f"已构建并验证“{mode_name}”场景。",
|
||
*list(source_result.get("decision_summary", [])),
|
||
"请确认动作后保存为功能模式,或先进行一次模拟试运行。",
|
||
]
|
||
# Overwrite the persisted source result with the draft metadata. The
|
||
# mode-card endpoint later re-reads this server-owned artifact and
|
||
# refuses any client-side instruction or request-id substitution.
|
||
self._persist(source_result)
|
||
return source_result
|
||
|
||
def _process_mode_profile_save(
|
||
self,
|
||
utterance: str,
|
||
request_id: str,
|
||
conversation_id: str,
|
||
context: dict[str, Any],
|
||
mode_name: str,
|
||
vehicle_profile: VehicleProfile,
|
||
result: dict[str, Any],
|
||
traces: list[dict[str, Any]],
|
||
) -> dict[str, Any]:
|
||
started = time.perf_counter()
|
||
result["provider_used"] = "mode_profile_registry"
|
||
result["decision_summary"] = [
|
||
f"识别为长期模式固化请求,目标名称为“{mode_name}”。"
|
||
]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"intent_parse",
|
||
"succeeded",
|
||
"识别到长期模式配置保存请求;不调用 CAN 编译路径。",
|
||
started,
|
||
{"mode_name": mode_name},
|
||
domain="mode",
|
||
)
|
||
|
||
source_actions, mode_operations = self._last_mode_source(context)
|
||
if not mode_operations:
|
||
result["status"] = "needs_confirmation"
|
||
result["validation"] = [
|
||
{
|
||
"severity": "error",
|
||
"code": "NO_RECENT_MODE_SOURCE",
|
||
"path": "conversation.recent_turns",
|
||
"message": "当前会话没有可固化为长期模式的最近成功动作。",
|
||
}
|
||
]
|
||
self._persist(result)
|
||
return result
|
||
|
||
profile = self.mode_profiles.save_mode(
|
||
mode_name,
|
||
mode_operations,
|
||
vehicle_id=vehicle_profile.id,
|
||
)
|
||
profile_public = profile.public_dict()
|
||
profile_path = Path(profile.source)
|
||
try:
|
||
source_path = str(profile_path.relative_to(self.root))
|
||
except ValueError:
|
||
source_path = str(profile_path)
|
||
result["mode_profile"] = {
|
||
**profile_public,
|
||
"source": source_path,
|
||
}
|
||
result["matched_modes"] = [profile_public]
|
||
result["decision_summary"].append(
|
||
f"已将最近一次成功动作固化为长期模式“{profile.name}”。"
|
||
)
|
||
result["model_requests"] = self.mode_profiles.requests_for_profiles([profile])
|
||
generation_started = time.perf_counter()
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"function_generate",
|
||
"succeeded",
|
||
"已写入长期模式 YAML;该配置将在后续请求中重新生成 FunctionConfig。",
|
||
generation_started,
|
||
{
|
||
"mode_profile_id": profile.id,
|
||
"operation_count": len(profile.operations),
|
||
"source": source_path,
|
||
},
|
||
domain="mode",
|
||
)
|
||
execution_report = {
|
||
"apiVersion": "cognitive-agent/v1alpha1",
|
||
"kind": "ExecutionReport",
|
||
"metadata": {
|
||
"id": f"report-{request_id}",
|
||
"name": "Mode profile save report",
|
||
"version": "1.0.0",
|
||
"request_id": request_id,
|
||
"labels": {"control_plane": "mode_profile_store"},
|
||
},
|
||
"spec": {
|
||
"mode": "dry_run",
|
||
"status": "succeeded",
|
||
"started_at": _now(),
|
||
"finished_at": _now(),
|
||
"results": [
|
||
{
|
||
"ref": f"save-{profile.id}",
|
||
"domain": "mode",
|
||
"status": "succeeded",
|
||
"message": f"已保存长期模式“{profile.name}”。",
|
||
"telemetry": {
|
||
"operation": "save_mode_profile",
|
||
"mode_profile_id": profile.id,
|
||
"operation_count": len(profile.operations),
|
||
"storage_scope": "repository_config",
|
||
"source": source_path,
|
||
"can_command_emitted": False,
|
||
},
|
||
}
|
||
],
|
||
},
|
||
}
|
||
validation = self.contracts.validate(execution_report)
|
||
result["validation"] = validation
|
||
result["execution_report"] = execution_report
|
||
if validation:
|
||
result["status"] = "execution_report_invalid"
|
||
self._persist(result)
|
||
return result
|
||
|
||
updated_context = self.conversations.save_mode_profile(
|
||
conversation_id,
|
||
utterance,
|
||
result["mode_profile"],
|
||
source_actions,
|
||
result["decision_summary"],
|
||
)
|
||
result["status"] = "succeeded"
|
||
result["conversation_turn"] = updated_context["turn_count"]
|
||
result["actuator_state"] = updated_context["devices"]
|
||
result["domain_state"] = updated_context["domain_devices"]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"execute",
|
||
"succeeded",
|
||
"长期模式保存属于控制平面操作,未生成或发送 CAN 帧。",
|
||
time.perf_counter(),
|
||
{
|
||
"mode": "control_plane",
|
||
"mode_profile_id": profile.id,
|
||
"can_command_emitted": False,
|
||
},
|
||
domain="mode",
|
||
)
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"state_update",
|
||
"succeeded",
|
||
f"长期模式“{profile.name}”已登记,可被后续自然语言请求命中。",
|
||
time.perf_counter(),
|
||
{
|
||
"turn_count": updated_context["turn_count"],
|
||
"mode_profile_id": profile.id,
|
||
},
|
||
domain="mode",
|
||
)
|
||
self._persist(result)
|
||
return result
|
||
|
||
def _last_mode_source(
|
||
self,
|
||
context: dict[str, Any],
|
||
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||
for turn in reversed(context.get("recent_turns", [])):
|
||
actions = turn.get("actions", [])
|
||
if not isinstance(actions, list):
|
||
continue
|
||
operations = self._mode_operations_from_actions(actions)
|
||
if operations:
|
||
return deepcopy(actions), operations
|
||
return [], []
|
||
|
||
def _mode_operations_from_actions(
|
||
self,
|
||
actions: list[dict[str, Any]],
|
||
) -> list[dict[str, Any]]:
|
||
operations = []
|
||
for action in actions:
|
||
operation = self._mode_operation_from_action(action)
|
||
if operation is not None:
|
||
operations.append(operation)
|
||
return operations
|
||
|
||
def _mode_operation_from_action(
|
||
self,
|
||
action: dict[str, Any],
|
||
) -> dict[str, Any] | None:
|
||
domain = action.get("domain")
|
||
if domain in self.domain_runtimes:
|
||
return {
|
||
"domain": domain,
|
||
"capability": action["capability"],
|
||
"target_scope": action["target"],
|
||
"operation": action.get("operation", "set"),
|
||
"parameters": dict(action.get("parameters", {})),
|
||
"reason": action.get("reason", "用户固化的跨域模式动作。"),
|
||
}
|
||
if action.get("action_type") == "position":
|
||
capability = SEAT_AXIS_TO_CAPABILITY.get(str(action.get("axis")))
|
||
if capability is None:
|
||
return None
|
||
parameters = {"position_pct": action["position_pct"]}
|
||
if action.get("angle_deg") is not None:
|
||
parameters["angle_deg"] = action["angle_deg"]
|
||
return {
|
||
"domain": "seat",
|
||
"capability": capability,
|
||
"target_scope": action["target"],
|
||
"operation": "set",
|
||
"parameters": parameters,
|
||
"reason": "用户固化的座椅位置模式动作。",
|
||
}
|
||
if action.get("action_type") == "dynamic_mode_stop":
|
||
return {
|
||
"domain": "seat",
|
||
"capability": DYNAMIC_MODE_STOP_CAPABILITY,
|
||
"target_scope": action["target"],
|
||
"operation": "deactivate",
|
||
"parameters": {},
|
||
"reason": "用户固化的动态座椅停止动作。",
|
||
}
|
||
if action.get("action_type") == "dynamic_mode":
|
||
return {
|
||
"domain": "seat",
|
||
"capability": "seat.start_dynamic_mode",
|
||
"target_scope": action["target"],
|
||
"operation": "set",
|
||
"parameters": {"mode_id": int(action["mode_id"])},
|
||
"reason": "用户固化的固件原生动态座椅模式。",
|
||
}
|
||
function = action.get("function")
|
||
capability = SEAT_FUNCTION_TO_CAPABILITY.get(str(function))
|
||
if capability is None:
|
||
return None
|
||
parameters = {"state": action.get("state")}
|
||
if action.get("requested_level") is not None:
|
||
parameters["level"] = action["requested_level"]
|
||
return {
|
||
"domain": "seat",
|
||
"capability": capability,
|
||
"target_scope": action["target"],
|
||
"operation": "set",
|
||
"parameters": parameters,
|
||
"reason": "用户固化的座椅舒适功能模式动作。",
|
||
}
|
||
|
||
def _partition_native_actions(
|
||
self, parsed: dict[str, Any], *, promote_supported: bool = False
|
||
) -> dict[str, Any]:
|
||
"""Promote verified CAN_DEV capabilities and leave unsupported domains virtual."""
|
||
|
||
result = deepcopy(parsed)
|
||
candidate_native_actions = list(result.get("native_can_actions", []))
|
||
native_actions = [
|
||
action
|
||
for action in candidate_native_actions
|
||
if self.native_can_runtime.supports(action)
|
||
]
|
||
native_signatures = {
|
||
(
|
||
str(action.get("domain")),
|
||
str(action.get("capability")),
|
||
str(action.get("target", action.get("target_scope", "vehicle"))),
|
||
json.dumps(action.get("parameters", {}), sort_keys=True, ensure_ascii=False),
|
||
)
|
||
for action in candidate_native_actions
|
||
}
|
||
virtual_actions: list[dict[str, Any]] = []
|
||
demoted_signatures: set[tuple[str, str, str, str]] = set()
|
||
for action in candidate_native_actions:
|
||
if str(action.get("capability")) not in UNVERIFIED_NATIVE_CAPABILITIES:
|
||
continue
|
||
demoted = dict(action)
|
||
demoted["execution_backend"] = "configured_domain"
|
||
virtual_actions.append(demoted)
|
||
demoted_signatures.add(
|
||
(
|
||
str(action.get("domain")),
|
||
str(action.get("capability")),
|
||
str(action.get("target", action.get("target_scope", "vehicle"))),
|
||
json.dumps(action.get("parameters", {}), sort_keys=True, ensure_ascii=False),
|
||
)
|
||
)
|
||
for action in result.get("domain_actions", []):
|
||
signature = (
|
||
str(action.get("domain")),
|
||
str(action.get("capability")),
|
||
str(action.get("target", action.get("target_scope", "vehicle"))),
|
||
json.dumps(action.get("parameters", {}), sort_keys=True, ensure_ascii=False),
|
||
)
|
||
if signature in native_signatures:
|
||
continue
|
||
if promote_supported and self.native_can_runtime.supports(action):
|
||
promoted = dict(action)
|
||
promoted["execution_backend"] = "native_can"
|
||
native_actions.append(promoted)
|
||
else:
|
||
virtual_actions.append(action)
|
||
|
||
deduplicated: list[dict[str, Any]] = []
|
||
signatures: set[tuple[str, str, str, str]] = set()
|
||
for action in native_actions:
|
||
signature = (
|
||
str(action.get("domain")),
|
||
str(action.get("capability")),
|
||
str(action.get("target", action.get("target_scope", "vehicle"))),
|
||
json.dumps(action.get("parameters", {}), sort_keys=True, ensure_ascii=False),
|
||
)
|
||
if signature not in signatures:
|
||
signatures.add(signature)
|
||
deduplicated.append(action)
|
||
|
||
result["domain_actions"] = virtual_actions
|
||
result["native_can_actions"] = deduplicated
|
||
detected_domains = list(result.get("detected_domains", []))
|
||
if result.get("actions") and "seat" not in detected_domains:
|
||
detected_domains.insert(0, "seat")
|
||
for action in [*virtual_actions, *deduplicated]:
|
||
if action["domain"] not in detected_domains:
|
||
detected_domains.append(action["domain"])
|
||
result["detected_domains"] = detected_domains
|
||
deferred_requests = [
|
||
item
|
||
for item in result.get("deferred_requests", [])
|
||
if not (
|
||
item.get("execution_backend") == "native_can"
|
||
and (
|
||
self.native_can_runtime.supports(item)
|
||
or (
|
||
str(item.get("domain")),
|
||
str(item.get("capability")),
|
||
str(item.get("target", item.get("target_scope", "vehicle"))),
|
||
json.dumps(item.get("parameters", {}), sort_keys=True, ensure_ascii=False),
|
||
)
|
||
in demoted_signatures
|
||
)
|
||
)
|
||
]
|
||
for item in deferred_requests:
|
||
if item.get("capability") == "door.set_vehicle_lock":
|
||
item["execution_backend"] = "unsupported"
|
||
item["rejection_code"] = "CH1_DOOR_LOCK_CONTROL_UNSUPPORTED"
|
||
item["reason"] = (
|
||
"Current CH1 whole-vehicle door-lock control is unsupported; "
|
||
"lock feedback is observational only and no verified physical "
|
||
"command will be built."
|
||
)
|
||
result["deferred_requests"] = deferred_requests
|
||
return result
|
||
|
||
@staticmethod
|
||
def _defer_nonwhitelisted_wireless_actions(
|
||
parsed: dict[str, Any],
|
||
) -> dict[str, Any]:
|
||
"""Keep approved MQTT actions executable when a model adds virtual ones."""
|
||
|
||
result = deepcopy(parsed)
|
||
deferred_requests = list(result.get("deferred_requests", []))
|
||
existing = {
|
||
(
|
||
str(item.get("domain", "")),
|
||
str(item.get("capability", "")),
|
||
str(item.get("target", item.get("target_scope", "vehicle"))),
|
||
)
|
||
for item in deferred_requests
|
||
}
|
||
for action in result.get("domain_actions", []):
|
||
target = str(
|
||
action.get("target", action.get("target_scope", "vehicle"))
|
||
)
|
||
signature = (
|
||
str(action.get("domain", "unknown")),
|
||
str(action.get("capability", "unknown")),
|
||
target,
|
||
)
|
||
if signature in existing:
|
||
continue
|
||
parameters = dict(action.get("parameters", {}))
|
||
requested_value: Any = parameters
|
||
if len(parameters) == 1:
|
||
requested_value = next(iter(parameters.values()))
|
||
deferred_requests.append(
|
||
{
|
||
"domain": signature[0],
|
||
"capability": signature[1],
|
||
"target_scope": target,
|
||
"desired_change": str(action.get("operation", "set")),
|
||
"requested_value": requested_value,
|
||
"parameters": parameters,
|
||
"reason": (
|
||
"该模型动作不在无线 MQTT CAN 控制白名单中,已单独跳过;"
|
||
"同一句中的白名单动作继续发布。"
|
||
),
|
||
"execution_backend": "unsupported",
|
||
"rejection_code": "WIRELESS_MQTT_CONTROL_NOT_WHITELISTED",
|
||
}
|
||
)
|
||
existing.add(signature)
|
||
result["domain_actions"] = []
|
||
result["deferred_requests"] = deferred_requests
|
||
return result
|
||
|
||
@staticmethod
|
||
def _emit_validated_commands(
|
||
result: dict[str, Any],
|
||
callback: Callable[[dict[str, Any]], None] | None,
|
||
) -> None:
|
||
"""Expose fully compiled commands before physical execution begins."""
|
||
|
||
if callback is None:
|
||
return
|
||
event = deepcopy(result)
|
||
event["status"] = "processing"
|
||
event["phase"] = "commands_validated"
|
||
event["execution_report"] = None
|
||
callback(event)
|
||
|
||
def _authorize_compiled_hardware_delivery(
|
||
self,
|
||
parsed: dict[str, Any],
|
||
approval_token: str | None,
|
||
control_transport: str,
|
||
) -> None:
|
||
"""Authorize physical transports after commands are safe to display.
|
||
|
||
Parsing, schema validation and deterministic CAN compilation do not
|
||
transmit frames. Keeping transport readiness checks after the
|
||
``commands_validated`` callback lets every UI target show the same
|
||
parsed commands, while these gates still run before any executor can
|
||
open a CAN or MQTT transmission path.
|
||
"""
|
||
|
||
if parsed.get("actions"):
|
||
(
|
||
self.vehicle_mqtt_settings
|
||
if control_transport == WIRELESS_MQTT_TRANSPORT
|
||
else self.hardware_settings
|
||
).authorize(approval_token)
|
||
if parsed.get("native_can_actions"):
|
||
(
|
||
self.vehicle_mqtt_settings
|
||
if control_transport == WIRELESS_MQTT_TRANSPORT
|
||
else self.vehicle_can_settings
|
||
).authorize(approval_token)
|
||
|
||
def _process_orchestrated(
|
||
self,
|
||
utterance: str,
|
||
request_id: str,
|
||
provider_used: str,
|
||
conversation_id: str,
|
||
context: dict[str, Any],
|
||
parsed: dict[str, Any],
|
||
result: dict[str, Any],
|
||
traces: list[dict[str, Any]],
|
||
*,
|
||
hardware_request: bool = False,
|
||
simulated_hardware_request: bool = False,
|
||
simulation_fault: str = "normal",
|
||
vehicle_profile: VehicleProfile | None = None,
|
||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||
approval_token: str | None = None,
|
||
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||
) -> dict[str, Any]:
|
||
"""Run isolated adapters under the selected physical-bus policy."""
|
||
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
||
generation_started = time.perf_counter()
|
||
intent_ir = build_intent_ir(
|
||
parsed,
|
||
utterance,
|
||
request_id,
|
||
provider_used,
|
||
self.semantic_capability_metadata,
|
||
)
|
||
intent_actions = [
|
||
*parsed.get("domain_actions", []),
|
||
*parsed.get("native_can_actions", []),
|
||
]
|
||
for action in intent_actions:
|
||
if action in parsed.get("native_can_actions", []):
|
||
capability = self.native_can_runtime.capabilities[action["capability"]]
|
||
else:
|
||
capability = self.domain_runtimes[action["domain"]].capabilities[
|
||
action["capability"]
|
||
]
|
||
intent_ir["spec"]["requests"].append(
|
||
{
|
||
"domain": action["domain"],
|
||
"goal": (
|
||
f"{action['operation']} {action['capability']} "
|
||
f"for {action['target']}"
|
||
),
|
||
"target": {
|
||
"device": capability["target_devices"][0],
|
||
"zone": action["target"],
|
||
},
|
||
"parameters": {
|
||
"capability": action["capability"],
|
||
**action["parameters"],
|
||
},
|
||
"source_span": utterance,
|
||
}
|
||
)
|
||
result["intent_ir"] = intent_ir
|
||
|
||
if intent_ir["spec"]["requires_confirmation"]:
|
||
validation = self.contracts.validate(intent_ir)
|
||
validation.extend(
|
||
{
|
||
"severity": "error",
|
||
"code": "AMBIGUOUS_INTENT",
|
||
"path": item.get("field", "utterance"),
|
||
"message": item.get("question", "控制目标需要确认。"),
|
||
}
|
||
for item in intent_ir["spec"]["ambiguities"]
|
||
)
|
||
validation.extend(
|
||
{
|
||
"severity": "error",
|
||
"code": "UNRESOLVED_UTTERANCE_SPAN",
|
||
"path": "utterance",
|
||
"message": (
|
||
f"未解析原文“{item['text']}”:{item['reason']}。"
|
||
),
|
||
}
|
||
for item in intent_ir["spec"]["unresolved_spans"]
|
||
)
|
||
result["validation"] = validation
|
||
result["status"] = "needs_confirmation"
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"safety_check",
|
||
"failed",
|
||
"语义意图存在歧义或未覆盖片段;未进入 CAN 编译与执行。",
|
||
time.perf_counter(),
|
||
{
|
||
"ambiguity_count": len(intent_ir["spec"]["ambiguities"]),
|
||
"unresolved_span_count": len(intent_ir["spec"]["unresolved_spans"]),
|
||
"execution_eligible": False,
|
||
},
|
||
domain="orchestration",
|
||
)
|
||
self._persist(result)
|
||
return result
|
||
|
||
configs: dict[str, dict[str, Any]] = {}
|
||
if parsed.get("actions"):
|
||
seat_config = build_function_config(parsed, request_id, provider_used)
|
||
if seat_config is not None:
|
||
configs["seat"] = seat_config
|
||
domain_actions: dict[str, list[dict[str, Any]]] = {}
|
||
for action in parsed.get("domain_actions", []):
|
||
domain_actions.setdefault(action["domain"], []).append(action)
|
||
for domain in ("climate", "roof", "ambient_light"):
|
||
if domain in domain_actions:
|
||
configs[domain] = self.domain_runtimes[domain].build_function_config(
|
||
domain_actions[domain],
|
||
request_id,
|
||
provider_used,
|
||
)
|
||
|
||
native_config_keys: set[str] = set()
|
||
native_actions_by_domain: dict[str, list[dict[str, Any]]] = {}
|
||
for action in parsed.get("native_can_actions", []):
|
||
native_actions_by_domain.setdefault(action["domain"], []).append(action)
|
||
for domain, actions in native_actions_by_domain.items():
|
||
key = f"native_{domain}"
|
||
native_config_keys.add(key)
|
||
configs[key] = self.native_can_runtime.build_function_config(
|
||
domain,
|
||
actions,
|
||
request_id,
|
||
provider_used,
|
||
)
|
||
|
||
result["function_configs"] = configs
|
||
result["function_config"] = (
|
||
configs.get("seat") or next(iter(configs.values()), None)
|
||
)
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"function_generate",
|
||
"succeeded" if configs else "waiting_confirmation",
|
||
(
|
||
f"已按领域边界生成 {len(configs)} 份 FunctionConfig。"
|
||
if configs
|
||
else "没有足够的绝对目标生成可执行配置。"
|
||
),
|
||
generation_started,
|
||
{
|
||
"domains": list(configs),
|
||
"operation_count": sum(
|
||
len(config["spec"]["operations"]) for config in configs.values()
|
||
),
|
||
},
|
||
domain="orchestration",
|
||
)
|
||
|
||
validation_started = time.perf_counter()
|
||
validation = self.contracts.validate(intent_ir)
|
||
for config in configs.values():
|
||
validation.extend(self.contracts.validate(config))
|
||
if parsed.get("actions"):
|
||
validation.extend(validate_semantics(parsed, configs.get("seat")))
|
||
else:
|
||
for ambiguity in parsed.get("ambiguities", []):
|
||
validation.append(
|
||
{
|
||
"severity": "error",
|
||
"code": "AMBIGUOUS_INTENT",
|
||
"path": ambiguity["field"],
|
||
"message": ambiguity["question"],
|
||
}
|
||
)
|
||
if not native_config_keys:
|
||
validation.append(
|
||
{
|
||
"severity": "warning",
|
||
"code": "VIRTUAL_EXECUTION_ONLY",
|
||
"path": "platform.reference_vehicle",
|
||
"message": "当前三域矩阵仅允许虚拟执行,禁止实车下发。",
|
||
}
|
||
)
|
||
if not configs and not parsed.get("ambiguities") and not parsed.get(
|
||
"deferred_requests"
|
||
):
|
||
validation.append(
|
||
{
|
||
"severity": "error",
|
||
"code": "NO_EXECUTABLE_OPERATION",
|
||
"path": "spec.operations",
|
||
"message": "模型没有生成任何可执行领域操作。",
|
||
}
|
||
)
|
||
if hardware_request:
|
||
self._adapt_validation_for_hardware(validation)
|
||
result["validation"] = validation
|
||
errors = [issue for issue in validation if issue["severity"] == "error"]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"schema_validate",
|
||
"failed" if errors else "succeeded",
|
||
f"公共和领域 Schema 校验{'发现错误' if errors else '通过'}。",
|
||
validation_started,
|
||
{"error_count": len(errors), "document_count": len(configs) + 1},
|
||
domain="orchestration",
|
||
)
|
||
if errors:
|
||
result["status"] = (
|
||
"needs_confirmation"
|
||
if any(issue["code"] == "AMBIGUOUS_INTENT" for issue in errors)
|
||
else "validation_failed"
|
||
)
|
||
self._persist(result)
|
||
return result
|
||
if parsed.get("deferred_requests") and not configs:
|
||
result["status"] = "capability_unavailable"
|
||
self._persist(result)
|
||
return result
|
||
|
||
compile_started = time.perf_counter()
|
||
plans: dict[str, dict[str, Any]] = {}
|
||
wireless_hardware_request = (
|
||
hardware_request
|
||
and control_transport == WIRELESS_MQTT_TRANSPORT
|
||
)
|
||
for domain, config in configs.items():
|
||
if domain == "seat":
|
||
if hardware_request:
|
||
if not wireless_hardware_request:
|
||
self.hardware_settings.validate_function_config(config)
|
||
plans[domain] = self.hardware_compiler.compile(
|
||
config,
|
||
context["devices"],
|
||
)
|
||
else:
|
||
plans[domain] = self.compiler.compile(config)
|
||
elif domain in native_config_keys:
|
||
plans[domain] = self.native_can_runtime.compile(config)
|
||
else:
|
||
runtime = self.domain_runtimes[domain]
|
||
plans[domain] = runtime.compile(config)
|
||
validation.extend(self.contracts.validate(plans[domain]))
|
||
result["command_plans"] = plans
|
||
result["command_plan"] = plans.get("seat") or next(
|
||
iter(plans.values()), None
|
||
)
|
||
|
||
scene_config = (
|
||
self._build_scene_config(request_id, configs, plans)
|
||
if len(configs) > 1
|
||
else None
|
||
)
|
||
result["scene_config"] = scene_config
|
||
if scene_config is not None:
|
||
validation.extend(self.contracts.validate(scene_config))
|
||
compile_errors = [
|
||
issue for issue in validation if issue["severity"] == "error"
|
||
]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"compile",
|
||
"failed" if compile_errors else "succeeded",
|
||
(
|
||
f"{len(configs)} 份配置已编译为相互隔离的 CAN 命令计划"
|
||
+ (",并生成 SceneConfig。" if scene_config else "。")
|
||
),
|
||
compile_started,
|
||
{
|
||
"domains": list(plans),
|
||
"command_count": sum(
|
||
len(plan["spec"]["commands"]) for plan in plans.values()
|
||
),
|
||
"scene_generated": scene_config is not None,
|
||
},
|
||
domain="orchestration",
|
||
)
|
||
if compile_errors:
|
||
result["status"] = "compile_failed"
|
||
self._persist(result)
|
||
return result
|
||
|
||
self._emit_validated_commands(result, on_commands_validated)
|
||
|
||
if hardware_request:
|
||
self._authorize_compiled_hardware_delivery(
|
||
parsed,
|
||
approval_token,
|
||
control_transport,
|
||
)
|
||
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"safety_check",
|
||
"succeeded",
|
||
(
|
||
"所有动作已通过无线 MQTT 能力、参数及 CAN 帧白名单门禁。"
|
||
if (
|
||
hardware_request
|
||
and control_transport == WIRELESS_MQTT_TRANSPORT
|
||
)
|
||
else "所有动作已通过 CH1 车身 CAN 白名单和物理反馈门禁。"
|
||
if hardware_request and native_config_keys and "seat" not in configs
|
||
else "副驾 T2G 台架动作已通过独立硬件安全门。"
|
||
if hardware_request
|
||
else (
|
||
"模拟台架仅对座椅结果注入故障;全部领域的物理发送路径保持关闭。"
|
||
if simulated_hardware_request
|
||
else "所有领域均为 dry-run;矩阵缺少实车完整性信息,硬件发送保持禁用。"
|
||
)
|
||
),
|
||
time.perf_counter(),
|
||
{
|
||
"hardware_execution_allowed": hardware_request,
|
||
"simulated_hardware": simulated_hardware_request,
|
||
"simulation_fault": (
|
||
simulation_fault if simulated_hardware_request else None
|
||
),
|
||
"hardware_domains": (
|
||
[configs[key]["spec"]["domain"] for key in native_config_keys]
|
||
+ (["seat"] if "seat" in configs else [])
|
||
if hardware_request
|
||
else []
|
||
),
|
||
"virtual_domains": [
|
||
domain
|
||
for domain in configs
|
||
if not hardware_request
|
||
],
|
||
"domains": list(configs),
|
||
},
|
||
domain="orchestration",
|
||
)
|
||
|
||
execute_started = time.perf_counter()
|
||
execution_started_at = _now()
|
||
reports = self._execute_orchestrated_reports(
|
||
configs,
|
||
plans,
|
||
native_config_keys,
|
||
context,
|
||
hardware_request=hardware_request,
|
||
simulated_hardware_request=simulated_hardware_request,
|
||
simulation_fault=simulation_fault,
|
||
control_transport=control_transport,
|
||
)
|
||
for domain_report in reports.values():
|
||
_annotate_execution_report(domain_report)
|
||
report_results = [
|
||
item
|
||
for domain_report in reports.values()
|
||
for item in domain_report["spec"]["results"]
|
||
]
|
||
operation_results = [
|
||
item
|
||
for item in report_results
|
||
if item.get("ref") != "seat-controller-firmware"
|
||
]
|
||
operation_domains = {
|
||
str(item.get("domain")) for item in operation_results
|
||
}
|
||
successful_domains = {
|
||
domain
|
||
for domain in operation_domains
|
||
if all(
|
||
_operation_verification_status(item) != "failed_or_blocked"
|
||
for item in operation_results
|
||
if str(item.get("domain")) == domain
|
||
)
|
||
}
|
||
failed_domains = {
|
||
str(item.get("domain"))
|
||
for item in operation_results
|
||
if _operation_verification_status(item) == "failed_or_blocked"
|
||
}
|
||
execution_succeeded = not failed_domains
|
||
execution_partially_succeeded = bool(successful_domains and failed_domains)
|
||
execution_outcome = (
|
||
"failed_or_blocked"
|
||
if failed_domains
|
||
else "submitted_unverified"
|
||
if any(
|
||
_operation_verification_status(item) == "submitted_unverified"
|
||
for item in operation_results
|
||
)
|
||
else "verified_success"
|
||
)
|
||
verified_domains = {
|
||
domain
|
||
for domain in operation_domains
|
||
if all(
|
||
_operation_verification_status(item) == "verified_success"
|
||
for item in operation_results
|
||
if str(item.get("domain")) == domain
|
||
)
|
||
}
|
||
physical_state = (
|
||
reports.get("seat", {}).get("spec", {}).get("physical_state")
|
||
if hardware_request
|
||
else None
|
||
)
|
||
simulated_seat = simulated_hardware_request and "seat" in reports
|
||
report_spec: dict[str, Any] = {
|
||
"mode": "hil" if hardware_request or simulated_seat else "dry_run",
|
||
"status": (
|
||
"partially_succeeded"
|
||
if execution_partially_succeeded
|
||
else "succeeded"
|
||
if execution_succeeded
|
||
else "failed"
|
||
),
|
||
"verification_status": execution_outcome,
|
||
"started_at": execution_started_at,
|
||
"finished_at": _now(),
|
||
"results": report_results,
|
||
}
|
||
if isinstance(physical_state, dict):
|
||
report_spec["physical_state"] = physical_state
|
||
execution_report = {
|
||
"apiVersion": "cognitive-agent/v1alpha1",
|
||
"kind": "ExecutionReport",
|
||
"metadata": {
|
||
"id": f"report-{request_id}",
|
||
"name": (
|
||
"Cabin orchestration mixed HIL execution report"
|
||
if hardware_request
|
||
else (
|
||
"Cabin orchestration simulated hardware execution report"
|
||
if simulated_seat
|
||
else "Cabin orchestration virtual execution report"
|
||
)
|
||
),
|
||
"version": "1.0.0",
|
||
"request_id": request_id,
|
||
"labels": {
|
||
"simulation": "mixed" if hardware_request else "true",
|
||
"seat_feedback": (
|
||
"wireless_mqtt_ch0"
|
||
if wireless_hardware_request and "seat" in configs
|
||
else "wireless_mqtt_ch1"
|
||
if wireless_hardware_request and native_config_keys
|
||
else "physical_can_ch1"
|
||
if hardware_request and native_config_keys and "seat" not in configs
|
||
else "physical_can"
|
||
if hardware_request
|
||
else (
|
||
"simulated_hardware"
|
||
if simulated_seat
|
||
else "simulated"
|
||
)
|
||
),
|
||
"fault_scenario": (
|
||
simulation_fault if simulated_seat else "none"
|
||
),
|
||
"orchestration": "cross_domain"
|
||
if len(configs) > 1
|
||
else "single_domain",
|
||
},
|
||
},
|
||
"spec": report_spec,
|
||
}
|
||
report_issues = self.contracts.validate(execution_report)
|
||
validation.extend(report_issues)
|
||
result["execution_report"] = execution_report
|
||
result["execution_outcome"] = execution_outcome
|
||
result["validation"] = validation
|
||
if report_issues:
|
||
result["status"] = "execution_report_invalid"
|
||
elif execution_partially_succeeded:
|
||
result["status"] = "partially_succeeded"
|
||
elif not execution_succeeded:
|
||
result["status"] = "execution_failed"
|
||
elif parsed.get("deferred_requests"):
|
||
result["status"] = "partially_succeeded"
|
||
else:
|
||
result["status"] = "succeeded"
|
||
failed_execution = next(
|
||
(item for item in report_results if item.get("status") == "failed"),
|
||
None,
|
||
)
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"execute",
|
||
"succeeded" if execution_succeeded and not report_issues else "failed",
|
||
(
|
||
"无线 MQTT CAN0/CAN1 白名单控制帧已提交;"
|
||
"车辆实际状态以状态话题回读为准。"
|
||
if wireless_hardware_request and execution_succeeded
|
||
else "有线 CAN0/CAN1 白名单控制帧已提交;"
|
||
"当前反馈窗口未确认车辆效果,状态以实时 CAN 回读为准。"
|
||
if hardware_request
|
||
and execution_succeeded
|
||
and execution_outcome == "submitted_unverified"
|
||
else "CH1 车身 CAN 动作已按目标字段物理回读闭环执行。"
|
||
if hardware_request and native_config_keys and execution_succeeded
|
||
else "副驾座椅已按独立 T2G 物理 CAN 回读闭环执行。"
|
||
if hardware_request and execution_succeeded
|
||
else (
|
||
(
|
||
"跨域指令部分执行成功;成功域="
|
||
f"{','.join(sorted(successful_domains))},失败域="
|
||
f"{','.join(sorted(failed_domains))}:"
|
||
f"{failed_execution['message']}"
|
||
)
|
||
if execution_partially_succeeded and failed_execution
|
||
else f"真实 CAN 执行失败:{failed_execution['message']}"
|
||
if hardware_request and failed_execution
|
||
else (
|
||
f"模拟台架已执行 {len(report_results)} 个领域操作,物理 CAN 未打开。"
|
||
if simulated_seat and execution_succeeded
|
||
else (
|
||
f"模拟台架故障已触发:{failed_execution['message']}"
|
||
if simulated_seat and failed_execution
|
||
else (
|
||
"本次没有座椅动作,故障注入未应用;"
|
||
f"已虚拟执行 {len(report_results)} 个领域操作。"
|
||
if simulated_hardware_request
|
||
else f"已虚拟执行 {len(report_results)} 个领域操作并汇总反馈。"
|
||
)
|
||
)
|
||
)
|
||
)
|
||
),
|
||
execute_started,
|
||
{
|
||
"mode": execution_report["spec"]["mode"],
|
||
"simulation": execution_report["metadata"]["labels"]["simulation"],
|
||
"domains": list(configs),
|
||
"result_count": len(report_results),
|
||
"failed_domain": (
|
||
failed_execution.get("domain") if failed_execution else None
|
||
),
|
||
"successful_domains": sorted(successful_domains),
|
||
"failed_domains": sorted(failed_domains),
|
||
"partially_succeeded": execution_partially_succeeded,
|
||
"execution_outcome": execution_outcome,
|
||
},
|
||
domain="orchestration",
|
||
)
|
||
|
||
if (execution_succeeded or execution_partially_succeeded) and not report_issues:
|
||
seat_actions = (
|
||
parsed.get("actions", [])
|
||
if execution_succeeded or "seat" in successful_domains
|
||
else []
|
||
)
|
||
if hardware_request and seat_actions and "seat" in reports:
|
||
seat_actions = self._hardware_feedback_actions(
|
||
seat_actions,
|
||
configs["seat"],
|
||
reports["seat"],
|
||
)
|
||
all_actions = [
|
||
*seat_actions,
|
||
*[
|
||
action
|
||
for action in parsed.get("domain_actions", [])
|
||
if (
|
||
not hardware_request
|
||
and (
|
||
execution_succeeded
|
||
or str(action.get("domain")) in successful_domains
|
||
)
|
||
)
|
||
or (
|
||
hardware_request
|
||
and str(action.get("domain")) in verified_domains
|
||
)
|
||
],
|
||
*[
|
||
{
|
||
**action,
|
||
**(
|
||
{"feedback_source": "physical_can_ch1"}
|
||
if hardware_request
|
||
else {}
|
||
),
|
||
}
|
||
for action in parsed.get("native_can_actions", [])
|
||
if not hardware_request
|
||
and (
|
||
execution_succeeded
|
||
or str(action.get("domain")) in successful_domains
|
||
)
|
||
],
|
||
]
|
||
updated_context = self.conversations.apply_successful_actions(
|
||
conversation_id,
|
||
utterance,
|
||
all_actions,
|
||
parsed["decision_summary"],
|
||
)
|
||
if hardware_request and isinstance(physical_state, dict):
|
||
updated_context = self.conversations.synchronize_physical_passenger(
|
||
conversation_id,
|
||
physical_state,
|
||
)
|
||
if hardware_request:
|
||
physical_ch1_feedback = self._physical_ch1_feedback_states(reports)
|
||
if physical_ch1_feedback:
|
||
updated_context = self.conversations.synchronize_physical_ch1_feedback(
|
||
conversation_id,
|
||
physical_ch1_feedback,
|
||
)
|
||
updated_context = self._context_for_vehicle(
|
||
updated_context,
|
||
vehicle_profile,
|
||
)
|
||
result["conversation_turn"] = updated_context["turn_count"]
|
||
result["actuator_state"] = updated_context["devices"]
|
||
result["domain_state"] = updated_context["domain_devices"]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"state_update",
|
||
"succeeded",
|
||
(
|
||
f"跨域会话状态已更新为第 {updated_context['turn_count']} 轮。"
|
||
if execution_succeeded
|
||
else "仅成功设备已写入会话状态;失败设备保持执行前状态。"
|
||
),
|
||
time.perf_counter(),
|
||
{
|
||
"seat_state": updated_context["devices"],
|
||
"domain_state": updated_context["domain_devices"],
|
||
"successful_domains": sorted(successful_domains),
|
||
"failed_domains": sorted(failed_domains),
|
||
},
|
||
domain="orchestration",
|
||
)
|
||
elif hardware_request and isinstance(physical_state, dict) and (
|
||
physical_state.get("position")
|
||
or physical_state.get("ventilation")
|
||
or physical_state.get("heating")
|
||
):
|
||
updated_context = self.conversations.synchronize_physical_passenger(
|
||
conversation_id,
|
||
physical_state,
|
||
)
|
||
updated_context = self._context_for_vehicle(
|
||
updated_context,
|
||
vehicle_profile,
|
||
)
|
||
result["actuator_state"] = updated_context["devices"]
|
||
result["domain_state"] = updated_context["domain_devices"]
|
||
self._trace(
|
||
traces,
|
||
request_id,
|
||
"state_sync",
|
||
"succeeded",
|
||
"跨域真实执行未完成,但已按 CAN 回读同步副驾物理状态。",
|
||
time.perf_counter(),
|
||
{"seat_state": updated_context["devices"].get("passenger", {})},
|
||
domain="orchestration",
|
||
)
|
||
self._persist(result)
|
||
return result
|
||
|
||
def _execute_orchestrated_reports(
|
||
self,
|
||
configs: dict[str, dict[str, Any]],
|
||
plans: dict[str, dict[str, Any]],
|
||
native_config_keys: set[str],
|
||
context: dict[str, Any],
|
||
*,
|
||
hardware_request: bool,
|
||
simulated_hardware_request: bool,
|
||
simulation_fault: str,
|
||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||
) -> dict[str, dict[str, Any]]:
|
||
reports: dict[str, dict[str, Any]] = {}
|
||
bridge = None
|
||
seat_bus_factory = None
|
||
native_bus_factory = None
|
||
vehicle_safety_preflight: dict[str, Any] | None = None
|
||
mqtt_selected = control_transport == WIRELESS_MQTT_TRANSPORT
|
||
|
||
wired_native_passenger = bool(
|
||
not mqtt_selected
|
||
and "seat" in configs
|
||
and self.hardware_settings.native_passenger_can0
|
||
and self.vehicle_can_settings.configured
|
||
)
|
||
if hardware_request and (
|
||
native_config_keys
|
||
or (mqtt_selected and "seat" in configs)
|
||
or wired_native_passenger
|
||
):
|
||
shared_runtime_enabled = (
|
||
not mqtt_selected
|
||
and self.vehicle_can_settings.backend != "zlgcan-linux"
|
||
and os.getenv(
|
||
"COGNITIVE_AGENT_SHARED_CAN_RUNTIME_ENABLED",
|
||
os.getenv(
|
||
"COGNITIVE_AGENT_SEAT_SHARED_ZCANPRO_BRIDGE_ENABLED", "true"
|
||
),
|
||
).strip().lower() in {"1", "true", "yes", "on"}
|
||
)
|
||
if mqtt_selected:
|
||
native_bus_factory = lambda: self._vehicle_mqtt_transport.bus(channel=1)
|
||
if "seat" in configs:
|
||
seat_bus_factory = lambda: self._vehicle_mqtt_transport.bus(channel=0)
|
||
elif shared_runtime_enabled:
|
||
from cognitive_agent.application.self_repair.service import (
|
||
BRIDGE_MANAGER,
|
||
)
|
||
|
||
if not hasattr(BRIDGE_MANAGER, "channel_bus_factory"):
|
||
raise HardwareSafetyError(
|
||
"vehicle CAN requires the standalone multi-channel runtime; "
|
||
"the ZCANPRO compatibility backend owns CH0 only"
|
||
)
|
||
native_bus_factory = BRIDGE_MANAGER.channel_bus_factory(
|
||
self.vehicle_can_settings.channel
|
||
)
|
||
# Seat commands continue through the high-level request bridge
|
||
# on CH0; passing a raw bus factory here would bypass its one-
|
||
# owner queue and allow self-repair to race orchestration.
|
||
seat_bus_factory = None
|
||
elif wired_native_passenger:
|
||
bridge = self._new_linux_wired_dual_channel_bridge()
|
||
native_bus_factory = lambda: bridge.channel_bus(1)
|
||
seat_bus_factory = lambda: bridge.channel_bus(0)
|
||
else:
|
||
channel_configs = [self.vehicle_can_settings.channel_config()]
|
||
if "seat" in configs:
|
||
seat_channel = int(self.hardware_settings.channel, 0)
|
||
channel_configs.insert(
|
||
0,
|
||
ZlgChannelConfig(
|
||
channel=seat_channel,
|
||
frame_format="classic_can",
|
||
arbitration_bitrate=self.hardware_settings.bitrate,
|
||
data_bitrate=self.vehicle_can_settings.data_bitrate,
|
||
can_fd_iso=True,
|
||
),
|
||
)
|
||
bridge = self.vehicle_can_settings.bridge(channel_configs)
|
||
native_bus_factory = lambda: bridge.channel_bus(
|
||
self.vehicle_can_settings.channel
|
||
)
|
||
if "seat" in configs:
|
||
seat_channel = int(self.hardware_settings.channel, 0)
|
||
seat_bus_factory = lambda: bridge.channel_bus(seat_channel)
|
||
|
||
if mqtt_selected:
|
||
# The head unit is the physical CAN owner. Once an action has
|
||
# passed the capability/parameter compiler, MqttCanTransport
|
||
# applies the final channel, ID and frame-format allow-list at
|
||
# publish time. Missing 0x342/0x4EB state frames must not
|
||
# suppress an otherwise approved MQTT command.
|
||
vehicle_safety_preflight = _wireless_mqtt_publish_preflight()
|
||
else:
|
||
# The wired bridge is open but no control frame has been
|
||
# submitted yet. Require two independent, fresh CH1 status
|
||
# frames before entering any actuator executor; CH0 traffic is
|
||
# ignored by the observer.
|
||
try:
|
||
vehicle_safety_preflight = observe_vehicle_safety(
|
||
native_bus_factory(),
|
||
timeout_ms=min(
|
||
5_000,
|
||
max(1_000, self.vehicle_can_settings.feedback_timeout_ms),
|
||
),
|
||
required_channel=1,
|
||
)
|
||
require_stationary_vehicle(vehicle_safety_preflight)
|
||
except VehicleSafetyPreflightError as exc:
|
||
if bridge is not None:
|
||
bridge.close()
|
||
raise HardwareSafetyError(
|
||
"CH1 vehicle safety preflight refused physical execution: "
|
||
f"{exc}"
|
||
) from exc
|
||
except Exception:
|
||
if bridge is not None:
|
||
bridge.close()
|
||
raise
|
||
|
||
try:
|
||
def execute_native_configs() -> dict[str, dict[str, Any]]:
|
||
native_reports: dict[str, dict[str, Any]] = {}
|
||
for key in sorted(native_config_keys):
|
||
if not hardware_request:
|
||
native_reports[key] = self.native_can_runtime.execute_virtual(
|
||
plans[key], configs[key]
|
||
)
|
||
continue
|
||
executor = NativeCanExecutor(
|
||
native_bus_factory,
|
||
feedback_timeout_ms=(
|
||
self.vehicle_mqtt_settings.feedback_timeout_ms
|
||
if control_transport == WIRELESS_MQTT_TRANSPORT
|
||
else self.vehicle_can_settings.feedback_timeout_ms
|
||
),
|
||
feedback_required=not mqtt_selected,
|
||
)
|
||
native_reports[key] = executor.execute(
|
||
self.native_can_runtime,
|
||
plans[key],
|
||
configs[key],
|
||
)
|
||
native_reports[key]["spec"]["vehicle_safety_preflight"] = (
|
||
vehicle_safety_preflight
|
||
)
|
||
for item in native_reports[key]["spec"].get("results", []):
|
||
item.setdefault("telemetry", {})[
|
||
"vehicle_safety_preflight"
|
||
] = vehicle_safety_preflight
|
||
return native_reports
|
||
|
||
def execute_seat_config() -> dict[str, Any]:
|
||
config = configs["seat"]
|
||
if hardware_request:
|
||
seat_executor = (
|
||
self._new_hardware_executor(
|
||
seat_bus_factory,
|
||
remote_transport=mqtt_selected,
|
||
)
|
||
if seat_bus_factory is not None
|
||
else self._new_hardware_executor()
|
||
)
|
||
return seat_executor.execute(
|
||
plans["seat"], config, context["devices"]
|
||
)
|
||
if simulated_hardware_request:
|
||
return self.simulated_hardware_executor.execute(
|
||
plans["seat"],
|
||
config,
|
||
context["devices"],
|
||
fault_scenario=simulation_fault,
|
||
)
|
||
return self.executor.execute(
|
||
plans["seat"], config, context["devices"]
|
||
)
|
||
|
||
if hardware_request and native_config_keys and "seat" in configs:
|
||
# Both channel views share one adapter owner; executor threads let
|
||
# the two channels start within normal USB scheduling jitter.
|
||
with ThreadPoolExecutor(max_workers=2) as pool:
|
||
native_future = pool.submit(execute_native_configs)
|
||
seat_future = pool.submit(execute_seat_config)
|
||
reports.update(native_future.result())
|
||
reports["seat"] = seat_future.result()
|
||
else:
|
||
reports.update(execute_native_configs())
|
||
if "seat" in configs:
|
||
reports["seat"] = execute_seat_config()
|
||
|
||
for domain, config in configs.items():
|
||
if domain in native_config_keys or domain == "seat":
|
||
continue
|
||
reports[domain] = self.domain_runtimes[domain].execute(
|
||
plans[domain], config
|
||
)
|
||
finally:
|
||
if bridge is not None:
|
||
bridge.close()
|
||
return reports
|
||
|
||
@staticmethod
|
||
def _build_scene_config(
|
||
request_id: str,
|
||
configs: dict[str, dict[str, Any]],
|
||
plans: dict[str, dict[str, Any]],
|
||
) -> dict[str, Any]:
|
||
function_refs = []
|
||
steps = []
|
||
for domain in configs:
|
||
reference_id = f"{domain}_function"
|
||
function_refs.append(
|
||
{
|
||
"id": reference_id,
|
||
"domain": domain,
|
||
"config_ref": f"functions/{domain}.yaml",
|
||
"sha256": plans[domain]["spec"]["source"]["config_sha256"],
|
||
}
|
||
)
|
||
steps.append(
|
||
{
|
||
"id": f"execute_{domain}",
|
||
"function_ref": reference_id,
|
||
"start_offset_ms": 0,
|
||
"failure_policy": "abort" if domain == "seat" else "continue",
|
||
}
|
||
)
|
||
return {
|
||
"apiVersion": "cognitive-agent/v1alpha1",
|
||
"kind": "SceneConfig",
|
||
"metadata": {
|
||
"id": f"scene-{request_id}",
|
||
"name": "Generated cabin scene",
|
||
"version": "1.0.0",
|
||
"request_id": request_id,
|
||
"generated_at": _now(),
|
||
"labels": {"execution_scope": "virtual_only"},
|
||
},
|
||
"spec": {
|
||
"function_refs": function_refs,
|
||
"steps": steps,
|
||
"execution": {
|
||
"mode": "parallel",
|
||
"default_failure_policy": "continue",
|
||
"max_parallelism": min(4, len(configs)),
|
||
"scene_timeout_ms": 30000,
|
||
},
|
||
},
|
||
}
|
||
|
||
@staticmethod
|
||
def _split_intent_clauses(utterance: str) -> list[str]:
|
||
def looks_like_control(value: str) -> bool:
|
||
return bool(
|
||
CONTROL_CLAUSE_SUBJECT_PATTERN.search(value)
|
||
and CONTROL_CLAUSE_PREDICATE_PATTERN.search(value)
|
||
)
|
||
|
||
def split_independent_controls(
|
||
value: str,
|
||
separator_pattern: str,
|
||
) -> list[str]:
|
||
tokens = re.split(f"({separator_pattern})", value)
|
||
current = tokens[0].strip()
|
||
separated: list[str] = []
|
||
for index in range(1, len(tokens), 2):
|
||
separator = tokens[index]
|
||
next_item = tokens[index + 1].strip()
|
||
if looks_like_control(current) and looks_like_control(next_item):
|
||
if current:
|
||
separated.append(current)
|
||
current = next_item
|
||
else:
|
||
current = f"{current}{separator}{next_item}".strip()
|
||
if current:
|
||
separated.append(current)
|
||
return separated
|
||
|
||
coarse_fragments = [
|
||
item.strip()
|
||
for item in re.split(r"[,,。;;::!!??\r\n]+", utterance)
|
||
if item.strip()
|
||
]
|
||
fragments: list[str] = []
|
||
for coarse_fragment in coarse_fragments:
|
||
list_fragments = split_independent_controls(coarse_fragment, r"、")
|
||
for list_fragment in list_fragments:
|
||
fragments.extend(
|
||
split_independent_controls(
|
||
list_fragment,
|
||
r"然后|随后|接着|最后|同时|并且|以及|再|并",
|
||
)
|
||
)
|
||
|
||
clauses: list[str] = []
|
||
for fragment in fragments:
|
||
fragment = re.sub(
|
||
r"^(?:然后|随后|接着|最后|同时|并且|以及|并)",
|
||
"",
|
||
fragment,
|
||
).strip()
|
||
fragment = re.sub(
|
||
r"^再(?=(?:打开|开启|启动|关闭|关掉|关上|停止|暂停|取消))",
|
||
"",
|
||
fragment,
|
||
).strip()
|
||
if not fragment:
|
||
continue
|
||
# A massage mode/strength phrase may omit both "主驾" and "按摩"
|
||
# after the first clause. Keep the whole request atomic so the ON
|
||
# fallback does not emit relax before an explicit later mode.
|
||
massage_continuation = bool(
|
||
clauses
|
||
and "按摩" in clauses[-1]
|
||
and DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN.search(fragment)
|
||
)
|
||
# An exclusion is a qualifier of the control immediately before it,
|
||
# not an independent command. Keeping both pieces together lets
|
||
# the deterministic window parser remove the excluded targets
|
||
# before the command list is finalized.
|
||
exclusion_qualifier = bool(
|
||
clauses and re.search(r"除外|除去|不包括|不含", fragment)
|
||
)
|
||
if massage_continuation or exclusion_qualifier:
|
||
clauses[-1] = f"{clauses[-1]},{fragment}"
|
||
else:
|
||
clauses.append(fragment)
|
||
return clauses or [utterance.strip()]
|
||
|
||
@staticmethod
|
||
def _local_clause_status(parsed: dict[str, Any], clause: str) -> str:
|
||
"""Classify one deterministic parse as complete, partial, or unparsed."""
|
||
|
||
controls = any(
|
||
parsed.get(key)
|
||
for key in (
|
||
"actions",
|
||
"domain_actions",
|
||
"native_can_actions",
|
||
"preset_actions",
|
||
"preset_events",
|
||
"semantic_constraints",
|
||
)
|
||
)
|
||
real_ambiguities = [
|
||
item
|
||
for item in parsed.get("ambiguities", [])
|
||
if str(item.get("field", "")) != "capability"
|
||
]
|
||
unresolved = bool(parsed.get("unresolved_spans"))
|
||
# Scene/fuzzy-goal language may contain valid atomic controls and still
|
||
# ask for additional orchestration. Keep the trusted local controls but
|
||
# give only that clause to the model for the uncovered meaning.
|
||
fuzzy_goal = bool(
|
||
re.search(
|
||
r"观影|影院|看电影|休息|睡眠|露营|播放|音乐|场景|"
|
||
r"舒适(?:点|一些)?|凉快(?:点|一些)?|暖和(?:点|一些)?",
|
||
clause,
|
||
re.IGNORECASE,
|
||
)
|
||
)
|
||
if controls and not real_ambiguities and not unresolved and not fuzzy_goal:
|
||
return "complete"
|
||
if controls:
|
||
return "partial"
|
||
return "unparsed"
|
||
|
||
@classmethod
|
||
def _merge_parsed_results(
|
||
cls, results: list[dict[str, Any]]
|
||
) -> dict[str, Any]:
|
||
"""Merge clause-level parses while retaining their source order."""
|
||
|
||
merged: dict[str, Any] = {
|
||
"actions": [],
|
||
"domain_actions": [],
|
||
"native_can_actions": [],
|
||
"preset_actions": [],
|
||
"preset_events": [],
|
||
"deferred_requests": [],
|
||
"model_requests": [],
|
||
"ambiguities": [],
|
||
"decision_summary": [],
|
||
"semantic_constraints": [],
|
||
"unresolved_spans": [],
|
||
"detected_domains": [],
|
||
"confidence": 0.0,
|
||
}
|
||
list_fields = tuple(
|
||
key
|
||
for key in merged
|
||
if key not in {"detected_domains", "confidence"}
|
||
)
|
||
for parsed in results:
|
||
for key in list_fields:
|
||
merged[key] = cls._merge_unique(
|
||
merged[key], list(parsed.get(key, []))
|
||
)
|
||
merged["detected_domains"] = cls._merge_domains(
|
||
merged["detected_domains"],
|
||
list(parsed.get("detected_domains", [])),
|
||
)
|
||
merged["confidence"] = max(
|
||
float(merged["confidence"]), float(parsed.get("confidence", 0))
|
||
)
|
||
return merged
|
||
|
||
@classmethod
|
||
def _merge_local_and_model_intent(
|
||
cls,
|
||
local: dict[str, Any],
|
||
model: dict[str, Any],
|
||
) -> dict[str, Any]:
|
||
"""Merge a fallback model plan without overriding local controls."""
|
||
|
||
merged = deepcopy(model)
|
||
for key in ("actions", "domain_actions", "native_can_actions"):
|
||
local_items = list(local.get(key, []))
|
||
local_signatures = {
|
||
cls._control_signature(item) for item in local_items
|
||
}
|
||
model_items = [
|
||
item
|
||
for item in model.get(key, [])
|
||
if cls._control_signature(item) not in local_signatures
|
||
]
|
||
merged[key] = cls._merge_unique(local_items, model_items)
|
||
for key in (
|
||
"preset_actions",
|
||
"preset_events",
|
||
"deferred_requests",
|
||
"model_requests",
|
||
"ambiguities",
|
||
"semantic_constraints",
|
||
"unresolved_spans",
|
||
):
|
||
merged[key] = cls._merge_unique(
|
||
list(local.get(key, [])), list(model.get(key, []))
|
||
)
|
||
merged["decision_summary"] = cls._merge_unique(
|
||
list(local.get("decision_summary", [])),
|
||
list(model.get("decision_summary", [])),
|
||
)
|
||
merged["detected_domains"] = cls._merge_domains(
|
||
list(local.get("detected_domains", [])),
|
||
list(model.get("detected_domains", [])),
|
||
)
|
||
merged["confidence"] = max(
|
||
float(local.get("confidence", 0)), float(model.get("confidence", 0))
|
||
)
|
||
return merged
|
||
|
||
@staticmethod
|
||
def _mark_control_parse_source(
|
||
parsed: dict[str, Any],
|
||
source: str,
|
||
*,
|
||
overwrite: bool = False,
|
||
) -> dict[str, Any]:
|
||
"""Attach per-control provenance without changing semantic content."""
|
||
|
||
marked = deepcopy(parsed)
|
||
for key in ("actions", "domain_actions", "native_can_actions"):
|
||
for action in marked.get(key, []):
|
||
if overwrite or not action.get("parse_source"):
|
||
action["parse_source"] = source
|
||
return marked
|
||
|
||
def _local_clause_results(
|
||
self,
|
||
utterance: str,
|
||
context: dict[str, Any],
|
||
) -> list[tuple[str, dict[str, Any], str]]:
|
||
"""Parse each clause deterministically and preserve inherited seat scope."""
|
||
|
||
clause_results: list[tuple[str, dict[str, Any], str]] = []
|
||
|
||
def parse_clause(clause: str) -> dict[str, Any]:
|
||
local = parse_local_intent(clause, context)
|
||
relative = (
|
||
self.native_can_relative_resolver.resolve(clause, context)
|
||
if self.native_can_relative_resolver.matches(clause)
|
||
else None
|
||
)
|
||
local = merge_native_can_intent(
|
||
local,
|
||
clause,
|
||
relative_actions=relative.actions if relative else None,
|
||
relative_ambiguities=relative.ambiguities if relative else None,
|
||
relative_claimed_controls=(
|
||
relative.claimed_controls if relative else None
|
||
),
|
||
)
|
||
return self._mark_control_parse_source(local, "local_rule")
|
||
|
||
def has_seat_domain(parsed: dict[str, Any]) -> bool:
|
||
if parsed.get("actions") or "seat" in parsed.get("detected_domains", []):
|
||
return True
|
||
for key in (
|
||
"domain_actions",
|
||
"native_can_actions",
|
||
"deferred_requests",
|
||
"model_requests",
|
||
):
|
||
if any(item.get("domain") == "seat" for item in parsed.get(key, [])):
|
||
return True
|
||
return any(
|
||
str(item.get("field", "")).startswith("seat.")
|
||
for item in parsed.get("ambiguities", [])
|
||
)
|
||
|
||
inherited_seat_scope: str | None = None
|
||
for raw_clause in self._split_intent_clauses(utterance):
|
||
clause = raw_clause
|
||
local = parse_clause(clause)
|
||
clause_has_seat = has_seat_domain(local)
|
||
explicit_driver = bool(DRIVER_SEAT_SCOPE_PATTERN.search(raw_clause))
|
||
explicit_passenger = bool(PASSENGER_SEAT_SCOPE_PATTERN.search(raw_clause))
|
||
if clause_has_seat and explicit_driver != explicit_passenger:
|
||
inherited_seat_scope = "driver" if explicit_driver else "passenger"
|
||
elif clause_has_seat and inherited_seat_scope:
|
||
scope_label = (
|
||
"主驾座椅"
|
||
if inherited_seat_scope == "driver"
|
||
else "副驾座椅"
|
||
)
|
||
clause = f"{scope_label},{raw_clause}"
|
||
local = parse_clause(clause)
|
||
clause_results.append(
|
||
(clause, local, self._local_clause_status(local, clause))
|
||
)
|
||
return clause_results
|
||
|
||
@classmethod
|
||
def _trusted_local_preview(
|
||
cls,
|
||
clause_results: list[tuple[str, dict[str, Any], str]],
|
||
) -> dict[str, Any]:
|
||
"""Keep deterministic controls while deferring incomplete text to GPT."""
|
||
|
||
trusted = cls._merge_parsed_results(
|
||
[parsed for _, parsed, status in clause_results if status != "unparsed"]
|
||
)
|
||
# Ambiguities and unresolved spans belong to the supplement phase. A
|
||
# deterministic action still goes through the normal schema, compiler,
|
||
# whitelist and physical execution gates before it can be sent.
|
||
trusted["ambiguities"] = []
|
||
trusted["unresolved_spans"] = []
|
||
trusted["model_requests"] = []
|
||
return trusted
|
||
|
||
@classmethod
|
||
def _filter_excluded_controls(
|
||
cls,
|
||
parsed: dict[str, Any],
|
||
excluded: set[tuple[Any, ...]],
|
||
) -> dict[str, Any]:
|
||
filtered = deepcopy(parsed)
|
||
for key in ("actions", "domain_actions", "native_can_actions"):
|
||
filtered[key] = [
|
||
action
|
||
for action in filtered.get(key, [])
|
||
if cls._control_signature(action) not in excluded
|
||
]
|
||
return filtered
|
||
|
||
def _interpret(
|
||
self,
|
||
utterance: str,
|
||
provider: str,
|
||
context: dict[str, Any],
|
||
vehicle_profile: VehicleProfile | None = None,
|
||
*,
|
||
reasoning_effort: str | None = None,
|
||
local_preview: bool = False,
|
||
model_parse_source: str = "cloud_model",
|
||
assistant_plan: dict[str, Any] | None = None,
|
||
) -> tuple[dict[str, Any], str, str | None]:
|
||
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
||
clause_results = self._local_clause_results(utterance, context)
|
||
|
||
trusted_local = self._merge_parsed_results(
|
||
[parsed for _, parsed, status in clause_results if status != "unparsed"]
|
||
)
|
||
preview_local = self._trusted_local_preview(clause_results)
|
||
all_local = self._merge_parsed_results(
|
||
[parsed for _, parsed, _ in clause_results]
|
||
)
|
||
fallback_clauses = [
|
||
clause for clause, _, status in clause_results if status != "complete"
|
||
]
|
||
trusted_controls = any(
|
||
trusted_local.get(key)
|
||
for key in ("actions", "domain_actions", "native_can_actions")
|
||
)
|
||
safe_fallback = trusted_local if trusted_controls else all_local
|
||
|
||
# A Codex-authored plan is an untrusted normalization hint for only
|
||
# the clauses the deterministic parser could not finish. The guard
|
||
# binds every proposed capability back to that server-owned text and
|
||
# prevents it from replacing a locally resolved capability/target.
|
||
# ModelPlanAdapter then applies the same target, parameter, range and
|
||
# runtime-availability checks as the established hosted-model path.
|
||
if assistant_plan is not None and fallback_clauses:
|
||
fallback_utterance = ",".join(fallback_clauses)
|
||
guarded = guard_assistant_plan(
|
||
assistant_plan,
|
||
fallback_text=fallback_utterance,
|
||
capability_context=self.capability_context,
|
||
executable_capabilities={
|
||
str(capability.get("name") or "")
|
||
for domain in self.capability_context.get("domains", [])
|
||
for capability in domain.get("capabilities", [])
|
||
if domain.get("availability") == "executable"
|
||
or str(capability.get("name") or "")
|
||
in self.native_can_runtime.supported_capabilities
|
||
},
|
||
fallback_intents=[
|
||
parsed
|
||
for _, parsed, status in clause_results
|
||
if status != "complete"
|
||
],
|
||
trusted_local=trusted_local,
|
||
)
|
||
model = self.model_plan_adapter.adapt(
|
||
guarded.plan,
|
||
context,
|
||
vehicle_profile,
|
||
utterance=fallback_utterance,
|
||
)
|
||
model = self._mark_control_parse_source(
|
||
model,
|
||
model_parse_source,
|
||
overwrite=True,
|
||
)
|
||
return (
|
||
self._merge_local_and_model_intent(trusted_local, model),
|
||
"local_rules+codex_app_server",
|
||
"本地规则结果优先;Codex 结构化计划仅补足未解析子句,并已重新通过能力注册表校验。",
|
||
)
|
||
if provider == "local" or not fallback_clauses:
|
||
return (
|
||
(
|
||
preview_local
|
||
if provider == "local" and local_preview
|
||
else all_local
|
||
if provider == "local"
|
||
else trusted_local
|
||
),
|
||
"local_rules",
|
||
None,
|
||
)
|
||
|
||
if not self.openai.available:
|
||
if provider == "openai":
|
||
raise ValueError("存在本地规则无法完整解析的子句;GPT 模式需要设置 OPENAI_API_KEY")
|
||
return (
|
||
safe_fallback,
|
||
"local_rules",
|
||
"未配置 OPENAI_API_KEY;已保留本地规则结果,未解析子句没有下发控制。",
|
||
)
|
||
|
||
fallback_utterance = ",".join(fallback_clauses)
|
||
try:
|
||
interpret_options = {}
|
||
if reasoning_effort is not None:
|
||
interpret_options["reasoning_effort"] = reasoning_effort
|
||
plan = self.openai.interpret(
|
||
fallback_utterance,
|
||
context,
|
||
self.capability_context,
|
||
**interpret_options,
|
||
)
|
||
model = self.model_plan_adapter.adapt(
|
||
plan,
|
||
context,
|
||
vehicle_profile,
|
||
utterance=fallback_utterance,
|
||
)
|
||
model = self._mark_control_parse_source(
|
||
model,
|
||
model_parse_source,
|
||
overwrite=True,
|
||
)
|
||
return (
|
||
self._merge_local_and_model_intent(trusted_local, model),
|
||
f"local_rules+openai:{self.openai.model}",
|
||
"本地规则先完成确定性解析;仅未覆盖或有歧义的子句交由 GPT 补充。",
|
||
)
|
||
except OpenAIIntentError:
|
||
return (
|
||
safe_fallback,
|
||
"local_rules_fallback",
|
||
"GPT 调用失败,已降级到本地规则解析(逐子句);已保留确定性结果,CAN 编译和执行路径不变。",
|
||
)
|
||
|
||
@staticmethod
|
||
def _context_for_vehicle(
|
||
context: dict[str, Any],
|
||
vehicle_profile: VehicleProfile,
|
||
) -> dict[str, Any]:
|
||
projected = deepcopy(context)
|
||
projected["vehicle"] = vehicle_profile.public_dict()
|
||
for target_state in projected.get("devices", {}).values():
|
||
positions = target_state.get("position", {})
|
||
for axis, state in positions.items():
|
||
if not isinstance(state, dict):
|
||
continue
|
||
motor = vehicle_profile.seat_motors.get(str(axis))
|
||
if motor is None or motor.display_angle is None:
|
||
continue
|
||
position_pct = state.get("position_pct")
|
||
if (
|
||
not isinstance(position_pct, (int, float))
|
||
or isinstance(position_pct, bool)
|
||
):
|
||
continue
|
||
current_angle = state.get("angle_deg")
|
||
legacy_angle = position_pct_to_angle(str(axis), float(position_pct))
|
||
should_project = current_angle is None or (
|
||
legacy_angle is not None
|
||
and isinstance(current_angle, (int, float))
|
||
and not isinstance(current_angle, bool)
|
||
and abs(float(current_angle) - float(legacy_angle)) < 0.0001
|
||
)
|
||
if should_project:
|
||
state["angle_deg"] = _clean_vehicle_number(
|
||
motor.display_angle_for_pct(int(position_pct))
|
||
)
|
||
return projected
|
||
|
||
@staticmethod
|
||
def _trace(
|
||
traces: list[dict[str, Any]],
|
||
request_id: str,
|
||
stage: str,
|
||
status: str,
|
||
summary: str,
|
||
started: float,
|
||
details: dict[str, Any],
|
||
domain: str = "seat",
|
||
) -> None:
|
||
traces.append(
|
||
{
|
||
"apiVersion": "cognitive-agent/v1alpha1",
|
||
"kind": "TraceEvent",
|
||
"event_id": f"evt-{len(traces) + 1:02d}-{request_id}",
|
||
"request_id": request_id,
|
||
"timestamp": _now(),
|
||
"stage": stage,
|
||
"status": status,
|
||
"domain": domain,
|
||
"summary": summary,
|
||
"details": details,
|
||
"duration_ms": max(0, int((time.perf_counter() - started) * 1000)),
|
||
}
|
||
)
|
||
|
||
def persist_composed_result(self, result: dict[str, Any]) -> None:
|
||
"""Persist a validated result composed with a non-CAN control domain."""
|
||
|
||
request_id = str(result.get("request_id") or "")
|
||
if not MODE_EDITOR_REQUEST_ID_PATTERN.fullmatch(request_id):
|
||
raise ValueError("invalid composed request id")
|
||
result.setdefault("trace_events", [])
|
||
self._persist(result)
|
||
|
||
def _persist(self, result: dict[str, Any]) -> None:
|
||
request_dir = self.artifact_root / result["request_id"]
|
||
(request_dir / "functions").mkdir(parents=True, exist_ok=True)
|
||
(request_dir / "presets").mkdir(parents=True, exist_ok=True)
|
||
(request_dir / "command-plans").mkdir(parents=True, exist_ok=True)
|
||
try:
|
||
artifact_path = request_dir.relative_to(self.root)
|
||
except ValueError:
|
||
artifact_path = request_dir
|
||
result["artifact_path"] = str(artifact_path)
|
||
|
||
self._write_yaml(request_dir / "intent.yaml", result.get("intent_ir"))
|
||
function_configs = result.get("function_configs") or {}
|
||
if function_configs:
|
||
for domain, document in function_configs.items():
|
||
self._write_yaml(
|
||
request_dir / "functions" / f"{domain}.yaml", document
|
||
)
|
||
else:
|
||
self._write_yaml(
|
||
request_dir / "functions" / "seat.yaml",
|
||
result.get("function_config"),
|
||
)
|
||
self._write_yaml(request_dir / "scene.yaml", result.get("scene_config"))
|
||
self._write_yaml(
|
||
request_dir / "presets" / "seat.yaml", result.get("preset_config")
|
||
)
|
||
command_plans = result.get("command_plans") or {}
|
||
if command_plans:
|
||
for domain, document in command_plans.items():
|
||
self._write_yaml(
|
||
request_dir / "command-plans" / f"{domain}.yaml", document
|
||
)
|
||
else:
|
||
self._write_yaml(
|
||
request_dir / "command-plans" / "seat.yaml",
|
||
result.get("command_plan"),
|
||
)
|
||
self._write_yaml(
|
||
request_dir / "execution-report.yaml", result.get("execution_report")
|
||
)
|
||
trace_text = "".join(
|
||
json.dumps(event, ensure_ascii=False) + "\n" for event in result["trace_events"]
|
||
)
|
||
self._atomic_write(request_dir / "trace.jsonl", trace_text)
|
||
self._atomic_write(
|
||
request_dir / "result.json",
|
||
json.dumps(result, ensure_ascii=False, indent=2) + "\n",
|
||
)
|
||
|
||
def _write_yaml(self, path: Path, document: dict[str, Any] | None) -> None:
|
||
if document is None:
|
||
return
|
||
text = yaml.safe_dump(document, allow_unicode=True, sort_keys=False)
|
||
self._atomic_write(path, text)
|
||
|
||
@staticmethod
|
||
def _atomic_write(path: Path, content: str) -> None:
|
||
temporary = path.with_suffix(path.suffix + ".tmp")
|
||
temporary.write_text(content, encoding="utf-8")
|
||
temporary.replace(path)
|