feat: 完善 N7 智能座舱交互与控制链路
This commit is contained in:
@@ -38,6 +38,9 @@ from seat_domain import ( # noqa: E402
|
||||
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
|
||||
@@ -68,6 +71,7 @@ from cognitive_agent.application.native_can_relative import ( # noqa: E402
|
||||
)
|
||||
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,
|
||||
@@ -79,6 +83,7 @@ from cognitive_agent.application.native_can_feedback import ( # noqa: E402
|
||||
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
|
||||
@@ -134,12 +139,12 @@ 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"主驾|主驾驶(?:座|座椅|席)?|驾驶位|驾驶员|驾驶席|驾驶座(?:椅)?|"
|
||||
r"司机位|司机座(?:椅)?|左前",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
PASSENGER_SEAT_SCOPE_PATTERN = re.compile(
|
||||
r"\bpassenger\b|\bfront[-_ ]?right\b|\bright[-_ ]?front\b|副驾|乘客|右前",
|
||||
r"\bpassenger\b|\bfront[-_ ]?right\b|\bright[-_ ]?front\b|副驾|乘客|乘员|右前",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
WIRED_CAN_TRANSPORT = "wired_can"
|
||||
@@ -258,10 +263,38 @@ SAVE_MODE_PROFILE_NAME = re.compile(
|
||||
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
|
||||
)
|
||||
CREATE_MODE_PROFILE_NAME = re.compile(
|
||||
r"^(?:请(?:帮我)?|帮我)?(?:构建|创建|新建|新增|生成|添加)"
|
||||
r"(?:一个|个|一套|套)?"
|
||||
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
|
||||
r"(?:配置)?(?:并(?:添加|加入)(?:到)?功能模式(?:栏)?)?[。.!!]?$"
|
||||
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"^(?:请(?:帮我)?|帮我)?(?:修改一下|修改|调整|更改|更新|改一下)"
|
||||
@@ -430,9 +463,6 @@ class SeatDemoService:
|
||||
return normalized
|
||||
|
||||
def runtime_info(self) -> dict[str, Any]:
|
||||
catalog = yaml.safe_load(
|
||||
(self.domain_root / "config" / "capabilities.yaml").read_text(encoding="utf-8")
|
||||
)
|
||||
domain_runtime_info = [
|
||||
runtime.runtime_info() for runtime in self.domain_runtimes.values()
|
||||
]
|
||||
@@ -756,6 +786,30 @@ class SeatDemoService:
|
||||
"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 = {
|
||||
@@ -1353,6 +1407,7 @@ class SeatDemoService:
|
||||
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."""
|
||||
|
||||
@@ -1390,12 +1445,15 @@ class SeatDemoService:
|
||||
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": [],
|
||||
"unresolved_clauses": list(
|
||||
result.get("unresolved_clauses") or []
|
||||
),
|
||||
"supplement_required": False,
|
||||
}
|
||||
)
|
||||
@@ -1444,6 +1502,7 @@ class SeatDemoService:
|
||||
_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(
|
||||
@@ -1889,6 +1948,7 @@ class SeatDemoService:
|
||||
_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
|
||||
@@ -1916,6 +1976,7 @@ class SeatDemoService:
|
||||
_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.
|
||||
@@ -1939,6 +2000,7 @@ class SeatDemoService:
|
||||
_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
|
||||
@@ -1962,6 +2024,7 @@ class SeatDemoService:
|
||||
_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():
|
||||
@@ -1984,6 +2047,7 @@ class SeatDemoService:
|
||||
_direct_can_batch,
|
||||
_direct_mode_profile_id,
|
||||
_speech_understanding,
|
||||
assistant_plan=_assistant_plan,
|
||||
)
|
||||
|
||||
def _process_once(
|
||||
@@ -2005,6 +2069,7 @@ class SeatDemoService:
|
||||
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()
|
||||
@@ -2073,6 +2138,7 @@ class SeatDemoService:
|
||||
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]}"
|
||||
@@ -2258,18 +2324,44 @@ class SeatDemoService:
|
||||
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(utterance, context)
|
||||
if self.native_can_relative_resolver.matches(utterance)
|
||||
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,
|
||||
utterance,
|
||||
native_merge_utterance,
|
||||
relative_actions=(
|
||||
relative_resolution.actions if relative_resolution else None
|
||||
),
|
||||
@@ -2299,8 +2391,25 @@ class SeatDemoService:
|
||||
"saved_mode_command_flow",
|
||||
)
|
||||
if local_preview:
|
||||
parsed["ambiguities"] = []
|
||||
parsed["unresolved_spans"] = []
|
||||
# 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(
|
||||
@@ -2332,9 +2441,18 @@ class SeatDemoService:
|
||||
]
|
||||
has_control = any(
|
||||
parsed.get(key)
|
||||
for key in ("actions", "domain_actions", "native_can_actions")
|
||||
for key in (
|
||||
"actions",
|
||||
"domain_actions",
|
||||
"native_can_actions",
|
||||
"preset_actions",
|
||||
)
|
||||
)
|
||||
if allow_empty_control_result and not has_control:
|
||||
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", []),
|
||||
@@ -3367,6 +3485,47 @@ class SeatDemoService:
|
||||
]
|
||||
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,
|
||||
@@ -3449,6 +3608,46 @@ class SeatDemoService:
|
||||
)
|
||||
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],
|
||||
@@ -4071,6 +4270,114 @@ class SeatDemoService:
|
||||
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,
|
||||
@@ -4082,17 +4389,23 @@ class SeatDemoService:
|
||||
simulation_fault: str,
|
||||
mode_name: str,
|
||||
control_transport: str,
|
||||
assistant_plan: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build, verify, and persist a new named mode in one user turn.
|
||||
"""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. Although that scene could execute
|
||||
successfully, it never reached the mode registry. Run the generated
|
||||
scene first so only validated successful actions are persisted, then
|
||||
use the existing control-plane save path to create the YAML profile.
|
||||
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.
|
||||
"""
|
||||
|
||||
source_utterance = (
|
||||
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(
|
||||
@@ -4104,8 +4417,67 @@ class SeatDemoService:
|
||||
vehicle_profile.id,
|
||||
simulation_fault,
|
||||
control_transport=control_transport,
|
||||
assistant_plan=assistant_plan,
|
||||
)
|
||||
if source_result.get("status") not in {"succeeded", "partially_succeeded"}:
|
||||
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", [])),
|
||||
@@ -4113,40 +4485,75 @@ class SeatDemoService:
|
||||
self._persist(source_result)
|
||||
return source_result
|
||||
|
||||
saved_result = self._process_once(
|
||||
f"这个模式不错,固化为{mode_name}",
|
||||
"local",
|
||||
conversation_id,
|
||||
"dry_run",
|
||||
None,
|
||||
vehicle_profile.id,
|
||||
"normal",
|
||||
control_transport=control_transport,
|
||||
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 []
|
||||
)
|
||||
if saved_result.get("status") != "succeeded" or not saved_result.get(
|
||||
"mode_profile"
|
||||
):
|
||||
return saved_result
|
||||
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
|
||||
|
||||
saved_result["mode_creation"] = {
|
||||
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_result.get("request_id"),
|
||||
"source_request_id": source_request_id,
|
||||
"source_status": source_result.get("status"),
|
||||
"source_provider": source_result.get("provider_used"),
|
||||
"operation_count": saved_result["mode_profile"].get(
|
||||
"operation_count", 0
|
||||
),
|
||||
"operation_count": len(reusable_actions),
|
||||
}
|
||||
source_summary = list(source_result.get("decision_summary", []))
|
||||
saved_result["decision_summary"] = [
|
||||
source_result["orchestration_route"] = "mode_profile_create_preview"
|
||||
source_result["decision_summary"] = [
|
||||
f"已构建并验证“{mode_name}”场景。",
|
||||
*source_summary,
|
||||
*list(saved_result.get("decision_summary", [])),
|
||||
f"“{mode_name}”已加入功能模式,可直接点击执行。",
|
||||
*list(source_result.get("decision_summary", [])),
|
||||
"请确认动作后保存为功能模式,或先进行一次模拟试运行。",
|
||||
]
|
||||
self._persist(saved_result)
|
||||
return saved_result
|
||||
# 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,
|
||||
@@ -4415,6 +4822,21 @@ class SeatDemoService:
|
||||
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")),
|
||||
@@ -4458,7 +4880,16 @@ class SeatDemoService:
|
||||
for item in result.get("deferred_requests", [])
|
||||
if not (
|
||||
item.get("execution_backend") == "native_can"
|
||||
and self.native_can_runtime.supports(item)
|
||||
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:
|
||||
@@ -5480,13 +5911,62 @@ class SeatDemoService:
|
||||
|
||||
@staticmethod
|
||||
def _split_intent_clauses(utterance: str) -> list[str]:
|
||||
fragments = [
|
||||
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"[,,。;;::!!??]+", utterance)
|
||||
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.
|
||||
@@ -5755,6 +6235,7 @@ class SeatDemoService:
|
||||
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)
|
||||
@@ -5774,6 +6255,50 @@ class SeatDemoService:
|
||||
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 (
|
||||
(
|
||||
|
||||
Reference in New Issue
Block a user