优化前端,优化前端指令,优化自定义控制
This commit is contained in:
@@ -6,7 +6,6 @@ import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
@@ -121,6 +120,7 @@ def _clean_vehicle_number(value: float | None) -> int | float | None:
|
||||
|
||||
|
||||
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|主驾|驾驶位|司机位|左前",
|
||||
@@ -137,6 +137,18 @@ VEHICLE_CONTROL_TRANSPORTS = frozenset(
|
||||
)
|
||||
|
||||
|
||||
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": [],
|
||||
}
|
||||
|
||||
|
||||
def _has_mixed_driver_passenger_seat_scope(utterance: str) -> bool:
|
||||
"""Detect one request that crosses the CH1-driver and CH0-passenger boundary."""
|
||||
|
||||
@@ -267,13 +279,19 @@ class SeatDemoService:
|
||||
}
|
||||
self.model_plan_adapter = ModelPlanAdapter(self.capability_context)
|
||||
self.vehicle_profiles = VehicleProfileRegistry(repository_root / "config" / "vehicles")
|
||||
self._mode_session_directory = tempfile.TemporaryDirectory(
|
||||
prefix="cognitive-agent-mode-session-",
|
||||
ignore_cleanup_errors=True,
|
||||
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=Path(self._mode_session_directory.name),
|
||||
user_root=self.mode_profile_user_root,
|
||||
)
|
||||
self._artifact_root = repository_root / "artifacts" / "runs"
|
||||
self.conversations = ConversationStateStore(
|
||||
@@ -425,7 +443,8 @@ class SeatDemoService:
|
||||
"vehicle": default_vehicle.public_dict(),
|
||||
"vehicles": self.vehicle_profiles.public_profiles(),
|
||||
"modes": self.mode_profiles.public_modes(default_vehicle.id),
|
||||
"mode_profile_storage": "runtime_session_volatile",
|
||||
"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],
|
||||
@@ -476,6 +495,204 @@ class SeatDemoService:
|
||||
"vehicle_id": resolved_vehicle_id,
|
||||
}
|
||||
|
||||
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 CAN command flow."""
|
||||
|
||||
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
|
||||
clean_mode_id = str(mode_id).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("CAN 指令流来源请求编号无效。")
|
||||
|
||||
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 profile is None:
|
||||
raise KeyError(clean_mode_id)
|
||||
normalized_name = re.sub(r"\s+", "", clean_name).casefold()
|
||||
if any(
|
||||
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("未找到执行按钮生成的 CAN 指令流,请重新执行。")
|
||||
try:
|
||||
source_result = json.loads(result_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise ValueError("CAN 指令流来源结果无法读取,请重新执行。") from exc
|
||||
if not isinstance(source_result, dict) or source_result.get(
|
||||
"request_id"
|
||||
) != clean_request_id:
|
||||
raise ValueError("CAN 指令流来源结果不完整,请重新执行。")
|
||||
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("自然语言指令已修改,请重新点击执行生成 CAN 指令流。")
|
||||
|
||||
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("执行结果中没有可永久化的 CAN 指令流。")
|
||||
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("请先点击执行并成功生成 CAN 控制指令流。")
|
||||
|
||||
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()
|
||||
target = str((operation.get("target") or {}).get("zone", "")).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("CAN 指令流没有关联到可复用的模式操作。")
|
||||
|
||||
execution_steps = []
|
||||
for item in (
|
||||
(source_result.get("execution_report") or {}).get("spec", {}).get(
|
||||
"results", []
|
||||
)
|
||||
):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
telemetry = item.get("telemetry") or {}
|
||||
command_evidence = {
|
||||
key: deepcopy(telemetry[key])
|
||||
for key in (
|
||||
"command_frame",
|
||||
"sent_frames",
|
||||
"mqtt_messages",
|
||||
"response_frame",
|
||||
"requested_parameters",
|
||||
)
|
||||
if key in telemetry
|
||||
}
|
||||
if command_evidence:
|
||||
execution_steps.append(
|
||||
{
|
||||
"ref": item.get("ref"),
|
||||
"domain": item.get("domain"),
|
||||
"status": item.get("status"),
|
||||
**command_evidence,
|
||||
}
|
||||
)
|
||||
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},
|
||||
"execution_steps": execution_steps,
|
||||
}
|
||||
saved = self.mode_profiles.save_mode(
|
||||
clean_name,
|
||||
operations,
|
||||
mode_id=profile.id,
|
||||
aliases=list(profile.aliases),
|
||||
vehicle_id=resolved_vehicle_id,
|
||||
description=profile.description,
|
||||
icon=profile.icon,
|
||||
instruction=clean_instruction,
|
||||
command_flow=command_flow,
|
||||
timeline=profile.timeline,
|
||||
)
|
||||
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,
|
||||
@@ -999,6 +1216,7 @@ class SeatDemoService:
|
||||
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",
|
||||
@@ -1118,6 +1336,12 @@ class SeatDemoService:
|
||||
),
|
||||
)
|
||||
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(
|
||||
{
|
||||
@@ -1180,7 +1404,7 @@ class SeatDemoService:
|
||||
if provider_notice:
|
||||
parsed["decision_summary"].append(provider_notice)
|
||||
result["provider_used"] = provider_used
|
||||
if provider_used.startswith("openai:"):
|
||||
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", [])
|
||||
@@ -1598,21 +1822,7 @@ class SeatDemoService:
|
||||
mqtt_selected = control_transport == WIRELESS_MQTT_TRANSPORT
|
||||
vehicle_safety_preflight = None
|
||||
if mqtt_selected:
|
||||
try:
|
||||
vehicle_safety_preflight = observe_vehicle_safety(
|
||||
self._vehicle_mqtt_transport.bus(channel=1),
|
||||
timeout_ms=min(
|
||||
5_000,
|
||||
max(1_000, self.vehicle_mqtt_settings.feedback_timeout_ms),
|
||||
),
|
||||
required_channel=1,
|
||||
)
|
||||
require_stationary_vehicle(vehicle_safety_preflight)
|
||||
except VehicleSafetyPreflightError as exc:
|
||||
raise HardwareSafetyError(
|
||||
"CAN1 vehicle safety preflight refused wireless passenger-seat execution: "
|
||||
f"{exc}"
|
||||
) from exc
|
||||
vehicle_safety_preflight = _wireless_mqtt_publish_preflight()
|
||||
hardware_executor = self._new_hardware_executor(
|
||||
lambda: self._vehicle_mqtt_transport.bus(channel=0),
|
||||
remote_transport=True,
|
||||
@@ -3122,19 +3332,69 @@ class SeatDemoService:
|
||||
)
|
||||
]
|
||||
for item in deferred_requests:
|
||||
if item.get("capability") in {
|
||||
"door.set_vehicle_lock",
|
||||
"door.set_rear_lock",
|
||||
}:
|
||||
if item.get("capability") == "door.set_vehicle_lock":
|
||||
item["execution_backend"] = "unsupported"
|
||||
item["rejection_code"] = "CH1_DOOR_LOCK_CONTROL_UNSUPPORTED"
|
||||
item["reason"] = (
|
||||
"Current CH1 door-lock control is unsupported; lock feedback "
|
||||
"is observational only and no verified physical command will be built."
|
||||
"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
|
||||
|
||||
def _process_orchestrated(
|
||||
self,
|
||||
utterance: str,
|
||||
@@ -3415,7 +3675,12 @@ class SeatDemoService:
|
||||
"safety_check",
|
||||
"succeeded",
|
||||
(
|
||||
"所有动作已通过 CH1 车身 CAN 白名单和物理反馈门禁。"
|
||||
"所有动作已通过无线 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
|
||||
@@ -3526,7 +3791,14 @@ class SeatDemoService:
|
||||
"labels": {
|
||||
"simulation": "mixed" if hardware_request else "true",
|
||||
"seat_feedback": (
|
||||
"physical_can_ch1"
|
||||
"wireless_mqtt_ch1"
|
||||
if (
|
||||
hardware_request
|
||||
and control_transport == WIRELESS_MQTT_TRANSPORT
|
||||
and native_config_keys
|
||||
and "seat" not in configs
|
||||
)
|
||||
else "physical_can_ch1"
|
||||
if hardware_request and native_config_keys and "seat" not in configs
|
||||
else "physical_can"
|
||||
if hardware_request
|
||||
@@ -3570,7 +3842,14 @@ class SeatDemoService:
|
||||
"execute",
|
||||
"succeeded" if execution_succeeded and not report_issues else "failed",
|
||||
(
|
||||
"CH1 车身 CAN 动作已按目标字段物理回读闭环执行。"
|
||||
"无线 MQTT 白名单控制帧已发布;车辆实际状态以状态话题回读为准。"
|
||||
if (
|
||||
hardware_request
|
||||
and control_transport == WIRELESS_MQTT_TRANSPORT
|
||||
and native_config_keys
|
||||
and execution_succeeded
|
||||
)
|
||||
else "CH1 车身 CAN 动作已按目标字段物理回读闭环执行。"
|
||||
if hardware_request and native_config_keys and execution_succeeded
|
||||
else "副驾座椅已按独立 T2G 物理 CAN 回读闭环执行。"
|
||||
if hardware_request and execution_succeeded
|
||||
@@ -3800,35 +4079,39 @@ class SeatDemoService:
|
||||
seat_channel = int(self.hardware_settings.channel, 0)
|
||||
seat_bus_factory = lambda: bridge.channel_bus(seat_channel)
|
||||
|
||||
# The 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_mqtt_settings.feedback_timeout_ms
|
||||
if mqtt_selected
|
||||
else self.vehicle_can_settings.feedback_timeout_ms,
|
||||
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
|
||||
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]]:
|
||||
@@ -3846,6 +4129,7 @@ class SeatDemoService:
|
||||
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,
|
||||
@@ -3959,6 +4243,148 @@ class SeatDemoService:
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _split_intent_clauses(utterance: str) -> list[str]:
|
||||
fragments = [
|
||||
item.strip()
|
||||
for item in re.split(r"[,,。;;::!!??]+", utterance)
|
||||
if item.strip()
|
||||
]
|
||||
clauses: list[str] = []
|
||||
for fragment in fragments:
|
||||
# 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.
|
||||
if clauses and re.search(r"除外|除去|不包括|不含", fragment):
|
||||
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
|
||||
|
||||
def _interpret(
|
||||
self,
|
||||
utterance: str,
|
||||
@@ -3969,69 +4395,127 @@ class SeatDemoService:
|
||||
reasoning_effort: str | None = None,
|
||||
) -> tuple[dict[str, Any], str, str | None]:
|
||||
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
||||
if provider == "local":
|
||||
return parse_local_intent(utterance, context), "local_rules", None
|
||||
if provider == "openai":
|
||||
if not self.openai.available:
|
||||
raise ValueError("GPT 模式需要设置 OPENAI_API_KEY")
|
||||
try:
|
||||
interpret_options = {}
|
||||
if reasoning_effort is not None:
|
||||
interpret_options["reasoning_effort"] = reasoning_effort
|
||||
plan = self.openai.interpret(
|
||||
utterance,
|
||||
context,
|
||||
self.capability_context,
|
||||
**interpret_options,
|
||||
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 local
|
||||
|
||||
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:
|
||||
# Chinese compound controls commonly state the seat once and
|
||||
# then list axes or thermal functions after commas. Carry the
|
||||
# last explicit seat scope only into later seat clauses; this
|
||||
# prevents a passenger request from silently falling through
|
||||
# to the CH1 driver default while leaving climate/door/window
|
||||
# clauses untouched.
|
||||
scope_label = (
|
||||
"主驾座椅"
|
||||
if inherited_seat_scope == "driver"
|
||||
else "副驾座椅"
|
||||
)
|
||||
return (
|
||||
self.model_plan_adapter.adapt(
|
||||
plan,
|
||||
context,
|
||||
vehicle_profile,
|
||||
utterance=utterance,
|
||||
),
|
||||
f"openai:{self.openai.model}",
|
||||
None,
|
||||
)
|
||||
except OpenAIIntentError:
|
||||
return (
|
||||
parse_local_intent(utterance, context),
|
||||
"local_rules_fallback",
|
||||
"GPT 调用失败,已降级到本地规则解析;CAN 编译和执行路径不变。",
|
||||
)
|
||||
if self.openai.available:
|
||||
try:
|
||||
interpret_options = {}
|
||||
if reasoning_effort is not None:
|
||||
interpret_options["reasoning_effort"] = reasoning_effort
|
||||
plan = self.openai.interpret(
|
||||
utterance,
|
||||
context,
|
||||
self.capability_context,
|
||||
**interpret_options,
|
||||
)
|
||||
return (
|
||||
self.model_plan_adapter.adapt(
|
||||
plan,
|
||||
context,
|
||||
vehicle_profile,
|
||||
utterance=utterance,
|
||||
),
|
||||
f"openai:{self.openai.model}",
|
||||
None,
|
||||
)
|
||||
except OpenAIIntentError:
|
||||
return (
|
||||
parse_local_intent(utterance, context),
|
||||
"local_rules_fallback",
|
||||
"GPT 调用失败,已降级到本地规则解析;CAN 编译和执行路径不变。",
|
||||
)
|
||||
return (
|
||||
parse_local_intent(utterance, context),
|
||||
"local_rules",
|
||||
"未配置 OPENAI_API_KEY,本次使用可重复的本地规则解析。",
|
||||
clause = f"{scope_label},{raw_clause}"
|
||||
local = parse_clause(clause)
|
||||
clause_results.append(
|
||||
(clause, local, self._local_clause_status(local, clause))
|
||||
)
|
||||
|
||||
trusted_local = self._merge_parsed_results(
|
||||
[parsed for _, parsed, status in clause_results if status != "unparsed"]
|
||||
)
|
||||
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
|
||||
if provider == "local" or not fallback_clauses:
|
||||
return (
|
||||
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,
|
||||
)
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user