优化前端,优化前端指令,优化自定义控制

This commit is contained in:
minghanchen
2026-08-24 09:40:16 +08:00
parent fff7657fea
commit 8d05815773
130 changed files with 5543 additions and 2206 deletions
+604 -120
View File
@@ -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(