fix: support conversational mode draft updates
This commit is contained in:
@@ -18,6 +18,12 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Protocol
|
||||
|
||||
from cognitive_agent.application.mode_profile_intent import (
|
||||
is_mode_profile_query,
|
||||
normalize_mode_profile_name,
|
||||
parse_mode_profile_update,
|
||||
)
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
INTERNAL_SCENE_BUILDER_MARKER = "根据该场景名称使用当前可控原子能力"
|
||||
|
||||
@@ -145,6 +151,7 @@ _MODE_PROFILE_CREATE_PATTERN = re.compile(
|
||||
r"(?:配置)?(?:并(?:添加|加入)(?:到)?功能模式(?:栏)?)?"
|
||||
r"(?:[,,::](?P<instruction>.{1,800}))?[。.!!]?$"
|
||||
)
|
||||
_MODE_EDITOR_REQUEST_ID_PATTERN = re.compile(r"^req-[a-z0-9-]{6,100}$")
|
||||
_HOT_DISCOMFORT_PATTERN = re.compile(r"热|闷热|好热|太晒")
|
||||
_COLD_DISCOMFORT_PATTERN = re.compile(r"冷|好冷")
|
||||
_IMPLICIT_HOT_REQUEST_PATTERN = re.compile(
|
||||
@@ -185,6 +192,59 @@ def is_mode_profile_create_request(message: str) -> bool:
|
||||
return _mode_profile_create_parts(message) is not None
|
||||
|
||||
|
||||
def _mode_profile_update_parts(
|
||||
message: str,
|
||||
*,
|
||||
expected_mode_name: object | None = None,
|
||||
) -> tuple[str, str] | None:
|
||||
"""Return a bounded named-mode update without interpreting its action."""
|
||||
|
||||
return parse_mode_profile_update(
|
||||
message,
|
||||
expected_mode_name=expected_mode_name,
|
||||
)
|
||||
|
||||
|
||||
def _validated_mode_profile_draft_context(
|
||||
value: Mapping[str, object] | None,
|
||||
*,
|
||||
message: str,
|
||||
) -> dict[str, str] | None:
|
||||
"""Validate the browser's opaque draft reference, never client actions."""
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
update = _mode_profile_update_parts(
|
||||
message,
|
||||
expected_mode_name=(value.get("name") if isinstance(value, Mapping) else None),
|
||||
)
|
||||
if update is None:
|
||||
return None
|
||||
if not isinstance(value, Mapping):
|
||||
raise ValueError("mode_profile_draft_context must be an object")
|
||||
referenced_name = " ".join(str(value.get("name") or "").strip().split())
|
||||
if normalize_mode_profile_name(update[0]) != normalize_mode_profile_name(
|
||||
referenced_name
|
||||
):
|
||||
return None
|
||||
required = {"name", "source_request_id", "vehicle_id"}
|
||||
if set(value) != required:
|
||||
raise ValueError("mode_profile_draft_context fields are invalid")
|
||||
clean = {key: " ".join(str(value.get(key) or "").strip().split()) for key in required}
|
||||
if (
|
||||
not clean["name"]
|
||||
or len(clean["name"]) > 64
|
||||
or not clean["vehicle_id"]
|
||||
or len(clean["vehicle_id"]) > 80
|
||||
or not _MODE_EDITOR_REQUEST_ID_PATTERN.fullmatch(clean["source_request_id"])
|
||||
):
|
||||
raise ValueError("mode_profile_draft_context values are invalid")
|
||||
# The UI keeps the latest card selected while ordinary questions are
|
||||
# asked. A reference is only a routing hint when this exact sentence edits
|
||||
# that exact draft; all non-matching references returned above are inert.
|
||||
return clean
|
||||
|
||||
|
||||
def _catalog_exact_mode_match_count(
|
||||
catalog: Mapping[str, object],
|
||||
requested_name: str,
|
||||
@@ -314,13 +374,32 @@ def _vehicle_action_state(result: Mapping[str, object]) -> tuple[str, bool, str]
|
||||
def _authorize_vehicle_request(
|
||||
message: str,
|
||||
control_terms: Sequence[str] = (),
|
||||
mode_profile_draft_context: Mapping[str, object] | None = None,
|
||||
) -> _VehicleAuthorization:
|
||||
"""Create a server-owned capability envelope from the original user text."""
|
||||
|
||||
expected_draft_name = (
|
||||
mode_profile_draft_context.get("name")
|
||||
if isinstance(mode_profile_draft_context, Mapping)
|
||||
else None
|
||||
)
|
||||
mode_profile_draft_context = _validated_mode_profile_draft_context(
|
||||
mode_profile_draft_context,
|
||||
message=message,
|
||||
)
|
||||
normalized = "".join(str(message).lower().split())
|
||||
if _DENIAL_PATTERN.search(normalized):
|
||||
return _VehicleAuthorization(None, "denied_by_user")
|
||||
|
||||
if is_mode_profile_query(
|
||||
normalized,
|
||||
expected_mode_name=expected_draft_name,
|
||||
):
|
||||
# A question about a named mode may contain device names, control
|
||||
# verbs and numeric targets. It remains a question and must never
|
||||
# fall through to the registered-control executor.
|
||||
return _VehicleAuthorization(None, "mode_profile_query")
|
||||
|
||||
implicit_request = re.sub(r"[,。!?!?、]", "", normalized)
|
||||
if not _KNOWLEDGE_PATTERN.search(normalized):
|
||||
if _IMPLICIT_HOT_REQUEST_PATTERN.fullmatch(implicit_request):
|
||||
@@ -342,6 +421,12 @@ def _authorize_vehicle_request(
|
||||
# user as a saveable draft.
|
||||
return _VehicleAuthorization(message, "explicit_mode_profile_create")
|
||||
|
||||
if mode_profile_draft_context is not None:
|
||||
# A validated opaque reference means this sentence edits a pending
|
||||
# server-generated preview. It is a configuration operation, never a
|
||||
# request to execute the mentioned device against the live vehicle.
|
||||
return _VehicleAuthorization(message, "explicit_mode_profile_draft_update")
|
||||
|
||||
normalized_terms = {
|
||||
"".join(str(term).lower().split()) for term in control_terms if str(term).strip()
|
||||
}
|
||||
@@ -453,6 +538,7 @@ class CodexAssistantService:
|
||||
control_terms: Sequence[str] = (),
|
||||
query_capabilities: CapabilityQuery | None = None,
|
||||
authoritative_answer: str | None = None,
|
||||
mode_profile_draft_context: Mapping[str, object] | None = None,
|
||||
) -> dict[str, object]:
|
||||
del history # Codex owns conversation history after the thread is created.
|
||||
message = " ".join(str(message).strip().split())
|
||||
@@ -472,11 +558,22 @@ class CodexAssistantService:
|
||||
authoritative_answer = " ".join(str(authoritative_answer or "").strip().split())
|
||||
if len(authoritative_answer) > 16_000:
|
||||
raise ValueError("authoritative_answer must not exceed 16000 characters")
|
||||
authorization = _authorize_vehicle_request(message, control_terms)
|
||||
draft_context = _validated_mode_profile_draft_context(
|
||||
mode_profile_draft_context,
|
||||
message=message,
|
||||
)
|
||||
authorization = _authorize_vehicle_request(
|
||||
message,
|
||||
control_terms,
|
||||
draft_context,
|
||||
)
|
||||
fast_comfort_control = (
|
||||
self._fast_implicit_controls and authorization.reason in _FAST_COMFORT_REASONS
|
||||
)
|
||||
mode_profile_preview = authorization.reason == "explicit_mode_profile_create"
|
||||
mode_profile_preview = authorization.reason in {
|
||||
"explicit_mode_profile_create",
|
||||
"explicit_mode_profile_draft_update",
|
||||
}
|
||||
mode_parts = _mode_profile_create_parts(message) if mode_profile_preview else None
|
||||
exact_mode_matches = (
|
||||
_catalog_exact_mode_match_count(catalog, mode_parts[0])
|
||||
@@ -493,11 +590,18 @@ class CodexAssistantService:
|
||||
or exact_mode_matches != 0
|
||||
)
|
||||
)
|
||||
direct_mode_profile_update = (
|
||||
authorization.reason == "explicit_mode_profile_draft_update"
|
||||
)
|
||||
# Explicit device settings and repository-backed scenes remain on the
|
||||
# zero-model-latency path. A genuinely new name-only scene needs Codex
|
||||
# to compose a candidate plan from the live capability catalog; the
|
||||
# vehicle service still limits it to dry-run and requires confirmation.
|
||||
direct_vehicle_control = fast_comfort_control or direct_mode_profile_preview
|
||||
direct_vehicle_control = (
|
||||
fast_comfort_control
|
||||
or direct_mode_profile_preview
|
||||
or authorization.reason == "explicit_mode_profile_draft_update"
|
||||
)
|
||||
route = (
|
||||
"capability_catalog"
|
||||
if authoritative_answer
|
||||
@@ -509,7 +613,7 @@ class CodexAssistantService:
|
||||
"n7web-capability-registry"
|
||||
if authoritative_answer
|
||||
else "n7web-mode-builder"
|
||||
if direct_mode_profile_preview
|
||||
if direct_mode_profile_preview or direct_mode_profile_update
|
||||
else "n7web-control-router"
|
||||
if fast_comfort_control
|
||||
else "codex-app-server"
|
||||
@@ -538,7 +642,7 @@ class CodexAssistantService:
|
||||
"正在读取工程能力目录……"
|
||||
if authoritative_answer
|
||||
else "正在构建并校验功能模式……"
|
||||
if direct_mode_profile_preview
|
||||
if direct_mode_profile_preview or direct_mode_profile_update
|
||||
else "Codex 正在理解场景并组合工程能力……"
|
||||
if mode_profile_preview
|
||||
else "正在快速处理座舱舒适需求……"
|
||||
@@ -622,7 +726,13 @@ class CodexAssistantService:
|
||||
instruction = authorization.instruction
|
||||
if instruction is None:
|
||||
raise CodexAssistantError("direct vehicle control was not authorized")
|
||||
mode_profile_preview = authorization.reason == "explicit_mode_profile_create"
|
||||
mode_profile_preview = authorization.reason in {
|
||||
"explicit_mode_profile_create",
|
||||
"explicit_mode_profile_draft_update",
|
||||
}
|
||||
mode_profile_draft_update = (
|
||||
authorization.reason == "explicit_mode_profile_draft_update"
|
||||
)
|
||||
if (
|
||||
emit(
|
||||
{
|
||||
@@ -630,7 +740,9 @@ class CodexAssistantService:
|
||||
"message_id": message_id,
|
||||
"display_text": (
|
||||
"正在生成模式动作预览……"
|
||||
if mode_profile_preview
|
||||
if mode_profile_preview and not mode_profile_draft_update
|
||||
else "正在调整待保存模式动作……"
|
||||
if mode_profile_draft_update
|
||||
else "正在通过车辆安全链执行舒适性调节……"
|
||||
),
|
||||
}
|
||||
@@ -854,6 +966,28 @@ class CodexAssistantService:
|
||||
if isinstance(draft, Mapping)
|
||||
else "新功能模式"
|
||||
)
|
||||
if (
|
||||
isinstance(draft, Mapping)
|
||||
and draft.get("generation_source") == "draft_revision"
|
||||
):
|
||||
raw_operation_count = draft.get("operation_count")
|
||||
operation_count = (
|
||||
raw_operation_count
|
||||
if isinstance(raw_operation_count, int)
|
||||
and not isinstance(raw_operation_count, bool)
|
||||
and raw_operation_count > 0
|
||||
else 0
|
||||
)
|
||||
persistence_hint = (
|
||||
"确认后可更新现有模式。"
|
||||
if draft.get("existing_mode_id")
|
||||
else "确认后可保存为功能模式。"
|
||||
)
|
||||
return (
|
||||
f"已调整待保存的“{name}”模式,并重新校验全部"
|
||||
f"{operation_count}项组合动作。原草稿未被保存或执行,"
|
||||
f"可以继续调整;{persistence_hint}"
|
||||
)
|
||||
if isinstance(draft, Mapping) and draft.get("existing_mode_id"):
|
||||
raw_operation_count = draft.get("operation_count")
|
||||
operation_count = (
|
||||
@@ -1614,6 +1748,10 @@ class CodexAssistantService:
|
||||
"status": str(result.get("status") or "unknown"),
|
||||
"execution_mode": str(result.get("execution_mode") or "dry_run"),
|
||||
}
|
||||
if isinstance(result.get("hardware_execution_allowed"), bool):
|
||||
safe["hardware_execution_allowed"] = result[
|
||||
"hardware_execution_allowed"
|
||||
]
|
||||
for key in ("request_id", "utterance", "decision_summary"):
|
||||
if result.get(key) is not None:
|
||||
safe[key] = CodexAssistantService._safe_result_value(
|
||||
@@ -1699,6 +1837,8 @@ class CodexAssistantService:
|
||||
"existing_mode_id",
|
||||
"existing_mode_revision",
|
||||
"generation_source",
|
||||
"draft_revision",
|
||||
"revises_source_request_id",
|
||||
)
|
||||
if key in mode_profile_draft
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Canonical, side-effect-free parsing for named-mode configuration turns."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
|
||||
_LEADING_POLITE = r"(?:(?:请|麻烦)(?:帮我|帮忙|给我)?|(?:帮我|帮忙|给我))?"
|
||||
_MODE_NAME = r"(?P<name>[^,。,.!!::\s“”\"'「」『』]{1,64}?模式)"
|
||||
_MODE_POSSESSIVE_BOUNDARY = r"(?:中的|里的|内的|之中的|的|中|里|内)"
|
||||
_MODE_MEMBER_BOUNDARY = rf"(?:{_MODE_POSSESSIVE_BOUNDARY}|[,,::])"
|
||||
_PREFIX_UPDATE = re.compile(
|
||||
rf"^{_LEADING_POLITE}"
|
||||
rf"(?:修改一下|修改|调整|更改|更新|改一下|变更){_MODE_NAME}"
|
||||
rf"{_MODE_MEMBER_BOUNDARY}[,,::]?(?P<change>.*?)[。.!!]?$"
|
||||
)
|
||||
_BA_UPDATE = re.compile(
|
||||
rf"^{_LEADING_POLITE}(?:把|将){_MODE_NAME}"
|
||||
rf"{_MODE_MEMBER_BOUNDARY}[,,::]?"
|
||||
r"(?P<change>.+?)[。.!!]?$"
|
||||
)
|
||||
_NAME_FIRST_UPDATE = re.compile(
|
||||
rf"^{_LEADING_POLITE}{_MODE_NAME}"
|
||||
rf"{_MODE_POSSESSIVE_BOUNDARY}[,,::]?"
|
||||
r"(?P<change>.+?)[。.!!]?$"
|
||||
)
|
||||
_CONTEXTUAL_NAME_FIRST_UPDATE = re.compile(
|
||||
rf"^{_LEADING_POLITE}{_MODE_NAME}[,,::]"
|
||||
r"(?P<change>.+?)[。.!!]?$"
|
||||
)
|
||||
MODE_PROFILE_CHANGE_PREDICATE = re.compile(
|
||||
r"修改|改成|改为|更改|更新|变更|变为|换成|"
|
||||
r"调整|调节|调高|调低|调到|调成|调为|调至|"
|
||||
r"设置|设为|设成|设到|设至|"
|
||||
r"打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|"
|
||||
r"移动|前移|后退|前进|升高|降低|抬高|放低|降下|升起|调直|放倒|"
|
||||
r"复位|半开|开到|关到|拉开|拉上|上锁|解锁"
|
||||
)
|
||||
MODE_PROFILE_QUERY_CUE = re.compile(
|
||||
r"什么|哪些|多少|几(?:度|档|级|个|项)?|是否|是不是|"
|
||||
r"合适(?:吗)?|合理(?:吗)?|会(?:怎么样|怎样|如何)|"
|
||||
r"怎么|如何|列(?:出|一下)|介绍(?:一下)?|说明(?:一下)?|"
|
||||
r"告诉我|查询|查看|有什么作用|是何作用|还是|[??]"
|
||||
)
|
||||
|
||||
|
||||
def compact_mode_profile_text(value: object) -> str:
|
||||
"""Remove presentation whitespace without changing CJK command meaning."""
|
||||
|
||||
return re.sub(r"\s+", "", str(value or "").strip())
|
||||
|
||||
|
||||
def normalize_mode_profile_name(value: object) -> str:
|
||||
return compact_mode_profile_text(value).strip("“”\"'「」『』").casefold()
|
||||
|
||||
|
||||
def _candidate_matches(
|
||||
text: str,
|
||||
expected_mode_name: object | None,
|
||||
) -> list[tuple[re.Match[str], bool]]:
|
||||
matches: list[tuple[re.Match[str], bool]] = []
|
||||
for pattern, predicate_required in (
|
||||
(_PREFIX_UPDATE, False),
|
||||
(_BA_UPDATE, True),
|
||||
(_NAME_FIRST_UPDATE, True),
|
||||
):
|
||||
match = pattern.fullmatch(text)
|
||||
if match is not None:
|
||||
matches.append((match, predicate_required))
|
||||
if expected_mode_name is not None:
|
||||
contextual = _CONTEXTUAL_NAME_FIRST_UPDATE.fullmatch(text)
|
||||
if (
|
||||
contextual is not None
|
||||
and normalize_mode_profile_name(contextual.group("name"))
|
||||
== normalize_mode_profile_name(expected_mode_name)
|
||||
):
|
||||
matches.append((contextual, True))
|
||||
return matches
|
||||
|
||||
|
||||
def is_mode_profile_query(
|
||||
value: object,
|
||||
*,
|
||||
expected_mode_name: object | None = None,
|
||||
) -> bool:
|
||||
"""Return whether a named-mode-shaped sentence asks instead of edits."""
|
||||
|
||||
text = compact_mode_profile_text(value)
|
||||
if not text:
|
||||
return False
|
||||
for match, _predicate_required in _candidate_matches(
|
||||
text,
|
||||
expected_mode_name,
|
||||
):
|
||||
change = str(match.group("change") or "").strip("的中里内,,::。.!!")
|
||||
if MODE_PROFILE_QUERY_CUE.search(change):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def parse_mode_profile_update(
|
||||
value: object,
|
||||
*,
|
||||
expected_mode_name: object | None = None,
|
||||
) -> tuple[str, str] | None:
|
||||
"""Parse one explicit update and reject nouns such as ``空调`` as verbs."""
|
||||
|
||||
text = compact_mode_profile_text(value)
|
||||
if not text:
|
||||
return None
|
||||
for match, predicate_required in _candidate_matches(text, expected_mode_name):
|
||||
change = str(match.group("change") or "").strip("的中里内,,::。.!!")
|
||||
if (
|
||||
(not predicate_required or MODE_PROFILE_CHANGE_PREDICATE.search(change))
|
||||
and not is_mode_profile_query(
|
||||
text,
|
||||
expected_mode_name=expected_mode_name,
|
||||
)
|
||||
):
|
||||
return str(match.group("name") or "").strip(), change
|
||||
return None
|
||||
@@ -74,6 +74,11 @@ from cognitive_agent.application.semantic_ir import ( # noqa: E402
|
||||
apply_language_safety_guards,
|
||||
resolve_explicit_seat_scopes,
|
||||
)
|
||||
from cognitive_agent.application.mode_profile_intent import ( # noqa: E402
|
||||
is_mode_profile_query,
|
||||
normalize_mode_profile_name,
|
||||
parse_mode_profile_update,
|
||||
)
|
||||
from cognitive_agent.application.native_can_feedback import ( # noqa: E402
|
||||
VehicleSafetyPreflightError,
|
||||
authoritative_physical_ch1_feedback_state,
|
||||
@@ -82,6 +87,7 @@ from cognitive_agent.application.native_can_feedback import ( # noqa: E402
|
||||
require_stationary_vehicle,
|
||||
)
|
||||
from cognitive_agent.application.native_can_runtime import ( # noqa: E402
|
||||
NativeCanCompileError,
|
||||
NativeCanExecutor,
|
||||
NativeCanRuntime,
|
||||
UNVERIFIED_NATIVE_CAPABILITIES,
|
||||
@@ -297,16 +303,6 @@ CONTROL_CLAUSE_PREDICATE_PATTERN = re.compile(
|
||||
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}?模式)"
|
||||
@@ -1515,6 +1511,7 @@ class SeatDemoService:
|
||||
control_transport: str = WIRED_CAN_TRANSPORT,
|
||||
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
|
||||
assistant_plan: dict[str, Any] | None = None,
|
||||
mode_profile_draft_context: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Validate and execute deterministic controls before hosted parsing."""
|
||||
|
||||
@@ -1545,6 +1542,7 @@ class SeatDemoService:
|
||||
control_transport=control_transport,
|
||||
_on_commands_validated=on_commands_validated,
|
||||
_assistant_plan=assistant_plan,
|
||||
_mode_profile_draft_context=mode_profile_draft_context,
|
||||
)
|
||||
result.update(
|
||||
{
|
||||
@@ -1602,6 +1600,7 @@ class SeatDemoService:
|
||||
_allow_empty_control_result=True,
|
||||
_on_commands_validated=publish_local_commands,
|
||||
_assistant_plan=assistant_plan,
|
||||
_mode_profile_draft_context=mode_profile_draft_context,
|
||||
)
|
||||
elapsed_ms = round((time.perf_counter() - started) * 1000, 3)
|
||||
supplement_required = bool(
|
||||
@@ -2048,13 +2047,37 @@ class SeatDemoService:
|
||||
_direct_mode_profile_id: str | None = None,
|
||||
_speech_understanding: dict[str, Any] | None = None,
|
||||
_assistant_plan: dict[str, Any] | None = None,
|
||||
_mode_profile_draft_context: 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:
|
||||
draft_reference_name = (
|
||||
str(_mode_profile_draft_context.get("name") or "").strip()
|
||||
if isinstance(_mode_profile_draft_context, dict)
|
||||
else ""
|
||||
)
|
||||
draft_update = parse_mode_profile_update(
|
||||
utterance.strip(),
|
||||
expected_mode_name=draft_reference_name or None,
|
||||
)
|
||||
mode_profile_query = is_mode_profile_query(
|
||||
utterance,
|
||||
expected_mode_name=draft_reference_name or None,
|
||||
)
|
||||
draft_update_requested = bool(
|
||||
draft_update is not None
|
||||
and draft_reference_name
|
||||
and normalize_mode_profile_name(draft_update[0])
|
||||
== normalize_mode_profile_name(draft_reference_name)
|
||||
)
|
||||
if (
|
||||
self._parse_mode_profile_create_name(utterance.strip()) is not None
|
||||
or draft_update_requested
|
||||
or mode_profile_query
|
||||
):
|
||||
execution_mode = "dry_run"
|
||||
if execution_mode != "hardware":
|
||||
return self._process_once(
|
||||
@@ -2076,6 +2099,7 @@ class SeatDemoService:
|
||||
_direct_mode_profile_id,
|
||||
_speech_understanding,
|
||||
assistant_plan=_assistant_plan,
|
||||
mode_profile_draft_context=_mode_profile_draft_context,
|
||||
)
|
||||
if control_transport == WIRELESS_MQTT_TRANSPORT:
|
||||
# The head unit owns both physical CAN channels in wireless mode.
|
||||
@@ -2100,6 +2124,7 @@ class SeatDemoService:
|
||||
_direct_mode_profile_id,
|
||||
_speech_understanding,
|
||||
assistant_plan=_assistant_plan,
|
||||
mode_profile_draft_context=_mode_profile_draft_context,
|
||||
)
|
||||
if hardware_access_bypass or self._driver_only_vehicle_execution_active(
|
||||
control_transport
|
||||
@@ -2124,6 +2149,7 @@ class SeatDemoService:
|
||||
_direct_mode_profile_id,
|
||||
_speech_understanding,
|
||||
assistant_plan=_assistant_plan,
|
||||
mode_profile_draft_context=_mode_profile_draft_context,
|
||||
)
|
||||
authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id)
|
||||
with self.hardware_execution.reserve():
|
||||
@@ -2147,6 +2173,7 @@ class SeatDemoService:
|
||||
_direct_mode_profile_id,
|
||||
_speech_understanding,
|
||||
assistant_plan=_assistant_plan,
|
||||
mode_profile_draft_context=_mode_profile_draft_context,
|
||||
)
|
||||
|
||||
def _process_once(
|
||||
@@ -2171,6 +2198,7 @@ class SeatDemoService:
|
||||
assistant_plan: dict[str, Any] | None = None,
|
||||
allow_fuzzy_mode_profile_match: bool = True,
|
||||
allow_scene_composition: bool = False,
|
||||
mode_profile_draft_context: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
control_transport = self._validate_control_transport(control_transport)
|
||||
utterance = utterance.strip()
|
||||
@@ -2203,11 +2231,24 @@ class SeatDemoService:
|
||||
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)
|
||||
draft_reference_name = (
|
||||
str(mode_profile_draft_context.get("name") or "").strip()
|
||||
if isinstance(mode_profile_draft_context, dict)
|
||||
else ""
|
||||
)
|
||||
mode_update = parse_mode_profile_update(
|
||||
utterance,
|
||||
expected_mode_name=draft_reference_name or None,
|
||||
)
|
||||
mode_profile_query = is_mode_profile_query(
|
||||
utterance,
|
||||
expected_mode_name=draft_reference_name or None,
|
||||
)
|
||||
control_plane_request = (
|
||||
mode_save_name is not None
|
||||
or mode_restore_name is not None
|
||||
or mode_update is not None
|
||||
or mode_profile_query
|
||||
)
|
||||
hardware_request = execution_mode == "hardware"
|
||||
if hardware_request and _has_mixed_driver_passenger_seat_scope(utterance):
|
||||
@@ -2242,7 +2283,12 @@ class SeatDemoService:
|
||||
assistant_plan=assistant_plan,
|
||||
)
|
||||
|
||||
request_id = f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}"
|
||||
# A request id is also the opaque capability used to reference a
|
||||
# server-owned draft artifact. Keep the full 128 bits of randomness;
|
||||
# the previous six-hex suffix was guessable under multi-user traffic.
|
||||
request_id = (
|
||||
f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex}"
|
||||
)
|
||||
context = self._context_for_vehicle(
|
||||
self.conversations.snapshot(conversation_id),
|
||||
vehicle_profile,
|
||||
@@ -2319,6 +2365,24 @@ class SeatDemoService:
|
||||
}
|
||||
|
||||
started = time.perf_counter()
|
||||
if mode_profile_query:
|
||||
result["status"] = "needs_confirmation"
|
||||
result["execution_mode"] = "dry_run"
|
||||
result["hardware_execution_allowed"] = False
|
||||
result["orchestration_route"] = "mode_profile_query"
|
||||
result["validation"] = [
|
||||
{
|
||||
"severity": "warning",
|
||||
"code": "MODE_PROFILE_QUERY_NOT_CONTROL",
|
||||
"path": "utterance",
|
||||
"message": "该语句是在询问功能模式,不是模式修改或车辆执行指令。",
|
||||
}
|
||||
]
|
||||
result["decision_summary"] = [
|
||||
"识别为功能模式问答;未生成或执行任何车辆控制动作。"
|
||||
]
|
||||
self._persist(result)
|
||||
return result
|
||||
if mode_save_name is not None:
|
||||
return self._process_mode_profile_save(
|
||||
utterance,
|
||||
@@ -2342,6 +2406,28 @@ class SeatDemoService:
|
||||
)
|
||||
if mode_update is not None:
|
||||
mode_name, change_text = mode_update
|
||||
if (
|
||||
draft_reference_name
|
||||
and normalize_mode_profile_name(draft_reference_name)
|
||||
== normalize_mode_profile_name(mode_name)
|
||||
):
|
||||
return self._process_mode_profile_draft_update(
|
||||
utterance,
|
||||
request_id,
|
||||
provider,
|
||||
conversation_id,
|
||||
context,
|
||||
mode_name,
|
||||
change_text,
|
||||
vehicle_profile,
|
||||
result,
|
||||
traces,
|
||||
mode_profile_draft_context,
|
||||
approval_token=approval_token,
|
||||
simulation_fault=simulation_fault,
|
||||
control_transport=control_transport,
|
||||
assistant_plan=assistant_plan,
|
||||
)
|
||||
return self._process_mode_profile_update(
|
||||
utterance,
|
||||
request_id,
|
||||
@@ -3550,18 +3636,7 @@ class SeatDemoService:
|
||||
|
||||
@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
|
||||
return parse_mode_profile_update(utterance)
|
||||
|
||||
@staticmethod
|
||||
def _parse_mode_profile_restore_name(utterance: str) -> str | None:
|
||||
@@ -3953,6 +4028,414 @@ class SeatDemoService:
|
||||
domains.append(domain)
|
||||
return domains or ["seat"]
|
||||
|
||||
@staticmethod
|
||||
def _preview_mode_operations(
|
||||
source_result: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Recover every reusable operation from server-compiled preview data."""
|
||||
|
||||
plans = source_result.get("command_plans")
|
||||
configs = source_result.get("function_configs")
|
||||
if not isinstance(plans, dict) or not isinstance(configs, dict):
|
||||
return []
|
||||
operations: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, str, str]] = set()
|
||||
for key, plan in plans.items():
|
||||
if (
|
||||
not isinstance(plan, dict)
|
||||
or not isinstance(plan.get("spec"), dict)
|
||||
or not isinstance(plan["spec"].get("commands"), list)
|
||||
or not plan["spec"]["commands"]
|
||||
):
|
||||
continue
|
||||
config = configs.get(key)
|
||||
spec = config.get("spec") if isinstance(config, dict) else None
|
||||
if not isinstance(spec, dict):
|
||||
continue
|
||||
domain = str(spec.get("domain") or "").strip()
|
||||
raw_operations = spec.get("operations")
|
||||
if not domain or not isinstance(raw_operations, list):
|
||||
continue
|
||||
for operation in raw_operations:
|
||||
if not isinstance(operation, dict):
|
||||
continue
|
||||
capability = str(operation.get("capability") or "").strip()
|
||||
raw_target = operation.get("target")
|
||||
target = (
|
||||
str(
|
||||
raw_target.get("instance")
|
||||
or raw_target.get("zone")
|
||||
or raw_target.get("scope")
|
||||
or ""
|
||||
).strip()
|
||||
if isinstance(raw_target, dict)
|
||||
else ""
|
||||
)
|
||||
parameters = operation.get("parameters")
|
||||
signature = (domain, capability, target)
|
||||
if (
|
||||
not capability
|
||||
or not target
|
||||
or not isinstance(parameters, dict)
|
||||
or signature in seen
|
||||
):
|
||||
continue
|
||||
reusable_parameters = deepcopy(parameters)
|
||||
if (
|
||||
capability.startswith("seat.set_")
|
||||
and reusable_parameters.get("position_pct") is not None
|
||||
and reusable_parameters.get("angle_deg") is not None
|
||||
):
|
||||
# Seat compilation adds the calibrated angle alongside the
|
||||
# requested percentage. The scene contract intentionally
|
||||
# permits one absolute representation only; retain the
|
||||
# portable percentage and let the new compile derive angle.
|
||||
reusable_parameters.pop("angle_deg", None)
|
||||
seen.add(signature)
|
||||
operations.append(
|
||||
{
|
||||
"domain": domain,
|
||||
"capability": capability,
|
||||
"target_scope": target,
|
||||
"operation": "set",
|
||||
"parameters": reusable_parameters,
|
||||
"reason": "由服务端已校验功能模式草稿保留。",
|
||||
}
|
||||
)
|
||||
return operations
|
||||
|
||||
def _load_mode_profile_draft_reference(
|
||||
self,
|
||||
reference: dict[str, Any],
|
||||
*,
|
||||
conversation_id: str,
|
||||
mode_name: str,
|
||||
vehicle_id: str,
|
||||
) -> tuple[dict[str, Any], dict[str, Any], list[dict[str, Any]]]:
|
||||
"""Resolve an opaque client reference back to one trusted local artifact."""
|
||||
|
||||
if not isinstance(reference, dict) or set(reference) != {
|
||||
"name",
|
||||
"source_request_id",
|
||||
"vehicle_id",
|
||||
}:
|
||||
raise ValueError("待保存功能模式草稿引用格式无效。")
|
||||
clean_name = str(reference.get("name") or "").strip()
|
||||
clean_request_id = str(reference.get("source_request_id") or "").strip()
|
||||
clean_vehicle_id = str(reference.get("vehicle_id") or "").strip()
|
||||
if not MODE_EDITOR_REQUEST_ID_PATTERN.fullmatch(clean_request_id):
|
||||
raise ValueError("待保存功能模式草稿来源请求编号无效。")
|
||||
if (
|
||||
normalize_mode_profile_name(clean_name)
|
||||
!= normalize_mode_profile_name(mode_name)
|
||||
):
|
||||
raise ValueError("修改请求与待保存功能模式草稿名称不匹配。")
|
||||
if clean_vehicle_id != vehicle_id:
|
||||
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):
|
||||
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_result.get("request_id") != clean_request_id
|
||||
or source_result.get("conversation_id") != conversation_id
|
||||
or source_vehicle_id != vehicle_id
|
||||
or source_result.get("status") != "succeeded"
|
||||
or source_result.get("execution_mode") != "dry_run"
|
||||
or source_result.get("hardware_execution_allowed") is not False
|
||||
or source_result.get("orchestration_route")
|
||||
!= "mode_profile_create_preview"
|
||||
):
|
||||
raise ValueError("待保存功能模式草稿与当前会话不匹配,请重新构建。")
|
||||
draft = source_result.get("mode_profile_draft")
|
||||
if not isinstance(draft, dict):
|
||||
raise ValueError("来源请求不是可修改的功能模式草稿。")
|
||||
if (
|
||||
str(draft.get("name") or "").strip() != clean_name
|
||||
or str(draft.get("source_request_id") or "").strip()
|
||||
!= clean_request_id
|
||||
or str(draft.get("vehicle_id") or "").strip() != vehicle_id
|
||||
):
|
||||
raise ValueError("待保存功能模式草稿引用与来源内容不匹配。")
|
||||
validation = source_result.get("validation")
|
||||
if isinstance(validation, list) and any(
|
||||
isinstance(issue, dict) and issue.get("severity") == "error"
|
||||
for issue in validation
|
||||
):
|
||||
raise ValueError("待保存功能模式草稿没有通过完整安全校验。")
|
||||
operations = self._preview_mode_operations(source_result)
|
||||
operation_count = draft.get("operation_count")
|
||||
if (
|
||||
not isinstance(operation_count, int)
|
||||
or isinstance(operation_count, bool)
|
||||
or operation_count <= 0
|
||||
or len(operations) != operation_count
|
||||
):
|
||||
raise ValueError("待保存功能模式草稿动作证据不完整,请重新构建。")
|
||||
return source_result, draft, operations
|
||||
|
||||
@staticmethod
|
||||
def _absolute_mode_operations(
|
||||
operations: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Convert bounded motion commands into replayable absolute mode targets."""
|
||||
|
||||
normalized: list[dict[str, Any]] = []
|
||||
window_targets = {
|
||||
"all": ("front_left", "front_right", "rear_left", "rear_right"),
|
||||
"front": ("front_left", "front_right"),
|
||||
"rear": ("rear_left", "rear_right"),
|
||||
}
|
||||
for operation in operations:
|
||||
item = deepcopy(operation)
|
||||
capability = str(item.get("capability") or "")
|
||||
parameters = dict(item.get("parameters") or {})
|
||||
if capability == "roof.set_shade_motion":
|
||||
positions = {"close": 0, "half": 50, "open": 100}
|
||||
motion = str(parameters.get("motion") or "")
|
||||
if motion not in positions:
|
||||
raise ValueError("遮阳帘停止动作没有可保存的最终位置,请改为明确开度。")
|
||||
item["capability"] = "roof.set_shade_position"
|
||||
item["parameters"] = {"position_pct": positions[motion]}
|
||||
normalized.append(item)
|
||||
continue
|
||||
if capability == "window.set_motion":
|
||||
positions = {"close": 0, "open": 100}
|
||||
motion = str(parameters.get("motion") or "")
|
||||
if motion not in positions:
|
||||
raise ValueError("车窗停止动作没有可保存的最终位置,请改为明确开度。")
|
||||
target = str(item.get("target_scope") or "")
|
||||
targets = window_targets.get(target, (target,))
|
||||
for resolved_target in targets:
|
||||
absolute = deepcopy(item)
|
||||
absolute["capability"] = "window.set_position"
|
||||
absolute["target_scope"] = resolved_target
|
||||
absolute["parameters"] = {"position_pct": positions[motion]}
|
||||
normalized.append(absolute)
|
||||
continue
|
||||
normalized.append(item)
|
||||
return normalized
|
||||
|
||||
@staticmethod
|
||||
def _mode_profile_draft_plan(
|
||||
operations: list[dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"detected_domains": list(
|
||||
dict.fromkeys(str(item["domain"]) for item in operations)
|
||||
),
|
||||
"requests": [
|
||||
{
|
||||
"domain": str(item["domain"]),
|
||||
"capability": str(item["capability"]),
|
||||
"target_scope": str(item["target_scope"]),
|
||||
"operation": "set",
|
||||
"duration_ms": None,
|
||||
"parameters": [
|
||||
{"name": str(name), "value": deepcopy(value)}
|
||||
for name, value in dict(item.get("parameters") or {}).items()
|
||||
],
|
||||
"reason": "按用户要求修改待保存功能模式草稿。",
|
||||
}
|
||||
for item in operations
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["已在服务端合并并重新校验待保存模式动作。"],
|
||||
"confidence": 1.0,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _mode_update_target_is_explicit(change_text: str, target: str) -> bool:
|
||||
patterns = {
|
||||
"driver": r"主驾|驾驶位|驾驶员|左前",
|
||||
"front_left": r"主驾|驾驶位|驾驶员|左前",
|
||||
"passenger": r"副驾|乘客位|右前",
|
||||
"front_right": r"副驾|乘客位|右前",
|
||||
"front_row": r"前排|前舱",
|
||||
"rear": r"后排|后座|后舱|所有后排",
|
||||
"rear_row": r"后排|后座|后舱|所有后排",
|
||||
"rear_left": r"左后|后排左|左侧后排",
|
||||
"rear_right": r"右后|后排右|右侧后排",
|
||||
"vehicle": r"全车|整车|所有|全部",
|
||||
"all": r"全车|整车|所有|全部",
|
||||
}
|
||||
pattern = patterns.get(str(target))
|
||||
return bool(pattern and re.search(pattern, change_text))
|
||||
|
||||
def _process_mode_profile_draft_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]],
|
||||
draft_reference: dict[str, Any],
|
||||
*,
|
||||
approval_token: str | None,
|
||||
simulation_fault: str,
|
||||
control_transport: str,
|
||||
assistant_plan: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Patch an unsaved preview and produce a new atomic dry-run artifact."""
|
||||
|
||||
del request_id, traces
|
||||
try:
|
||||
_source_result, source_draft, source_operations = (
|
||||
self._load_mode_profile_draft_reference(
|
||||
draft_reference,
|
||||
conversation_id=conversation_id,
|
||||
mode_name=mode_name,
|
||||
vehicle_id=vehicle_profile.id,
|
||||
)
|
||||
)
|
||||
base_operations = self._absolute_mode_operations(source_operations)
|
||||
if not change_text:
|
||||
raise ValueError("请说明要调整的设备和明确目标值。")
|
||||
parseable_change_text = re.sub(
|
||||
r"改为|改成|变更为|换成|变为",
|
||||
"调整为",
|
||||
change_text,
|
||||
)
|
||||
unresolved_change_clauses = [
|
||||
clause
|
||||
for clause, _parsed, status in self._local_clause_results(
|
||||
parseable_change_text,
|
||||
context,
|
||||
)
|
||||
if status != "complete"
|
||||
]
|
||||
if unresolved_change_clauses and assistant_plan is None:
|
||||
raise ValueError(
|
||||
"调整说明包含未完整识别的内容:"
|
||||
+ ";".join(unresolved_change_clauses)
|
||||
)
|
||||
change_result = self._process_once(
|
||||
parseable_change_text,
|
||||
provider,
|
||||
conversation_id,
|
||||
"dry_run",
|
||||
approval_token,
|
||||
vehicle_profile.id,
|
||||
simulation_fault,
|
||||
control_transport=control_transport,
|
||||
assistant_plan=assistant_plan,
|
||||
allow_fuzzy_mode_profile_match=False,
|
||||
)
|
||||
change_operations = self._absolute_mode_operations(
|
||||
self._preview_mode_operations(change_result)
|
||||
)
|
||||
if (
|
||||
change_result.get("status") != "succeeded"
|
||||
or not change_operations
|
||||
or change_result.get("supplement_required") is True
|
||||
or bool(change_result.get("unresolved_clauses"))
|
||||
or bool(change_result.get("deferred_requests"))
|
||||
):
|
||||
raise ValueError("没有从调整说明中得到完整、可复用的设备目标。")
|
||||
validation = change_result.get("validation")
|
||||
if isinstance(validation, list) and any(
|
||||
isinstance(issue, dict) and issue.get("severity") == "error"
|
||||
for issue in validation
|
||||
):
|
||||
raise ValueError("调整动作没有通过车辆能力或安全校验。")
|
||||
|
||||
normalized_changes: list[dict[str, Any]] = []
|
||||
for change in change_operations:
|
||||
same_capability = [
|
||||
item
|
||||
for item in base_operations
|
||||
if item["domain"] == change["domain"]
|
||||
and item["capability"] == change["capability"]
|
||||
]
|
||||
normalized = deepcopy(change)
|
||||
if len(same_capability) == 1 and not self._mode_update_target_is_explicit(
|
||||
change_text,
|
||||
str(change["target_scope"]),
|
||||
):
|
||||
normalized["target_scope"] = same_capability[0]["target_scope"]
|
||||
elif len(same_capability) > 1 and not self._mode_update_target_is_explicit(
|
||||
change_text,
|
||||
str(change["target_scope"]),
|
||||
):
|
||||
raise ValueError(
|
||||
f"“{mode_name}”中该设备能力包含多个目标,请明确主驾、副驾或具体区域。"
|
||||
)
|
||||
normalized_changes.append(normalized)
|
||||
|
||||
change_by_key = {
|
||||
(
|
||||
str(item["domain"]),
|
||||
str(item["capability"]),
|
||||
str(item["target_scope"]),
|
||||
): item
|
||||
for item in normalized_changes
|
||||
}
|
||||
merged: list[dict[str, Any]] = []
|
||||
for operation in base_operations:
|
||||
key = (
|
||||
str(operation["domain"]),
|
||||
str(operation["capability"]),
|
||||
str(operation["target_scope"]),
|
||||
)
|
||||
merged.append(deepcopy(change_by_key.pop(key, operation)))
|
||||
merged.extend(deepcopy(list(change_by_key.values())))
|
||||
|
||||
return self._process_mode_profile_create(
|
||||
utterance,
|
||||
"local",
|
||||
conversation_id,
|
||||
"dry_run",
|
||||
approval_token,
|
||||
vehicle_profile,
|
||||
simulation_fault,
|
||||
mode_name,
|
||||
control_transport,
|
||||
assistant_plan=self._mode_profile_draft_plan(merged),
|
||||
source_draft=source_draft,
|
||||
)
|
||||
except ValueError as exc:
|
||||
result["status"] = "needs_confirmation"
|
||||
result["execution_mode"] = "dry_run"
|
||||
result["hardware_execution_allowed"] = False
|
||||
result["orchestration_route"] = "mode_profile_create_preview"
|
||||
result["validation"] = [
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "MODE_PROFILE_DRAFT_UPDATE_REJECTED",
|
||||
"path": "mode_profile_draft_context",
|
||||
"message": str(exc),
|
||||
}
|
||||
]
|
||||
result["mode_creation_failure"] = {
|
||||
"code": "MODE_PROFILE_DRAFT_UPDATE_REJECTED",
|
||||
"reason": str(exc),
|
||||
"next_step": "请保留当前模式卡片,并明确要修改的设备、区域和目标值后重试",
|
||||
"unresolved_clauses": [change_text] if change_text else [],
|
||||
"issues": [],
|
||||
}
|
||||
result["decision_summary"] = [
|
||||
f"未修改待保存的“{mode_name}”草稿;原草稿保持不变。"
|
||||
]
|
||||
self._persist(result)
|
||||
return result
|
||||
|
||||
def _process_mode_profile_update(
|
||||
self,
|
||||
utterance: str,
|
||||
@@ -4303,10 +4786,11 @@ class SeatDemoService:
|
||||
traces: list[dict[str, Any]],
|
||||
request_id: str,
|
||||
) -> ModeProfile | None:
|
||||
matches = self.mode_profiles.match_text(
|
||||
exact = self.mode_profiles.resolve_exact_name(
|
||||
mode_name,
|
||||
vehicle_id=vehicle_profile.id,
|
||||
)
|
||||
matches = [exact] if exact is not None else []
|
||||
result["matched_modes"] = [profile.public_dict() for profile in matches]
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
@@ -4614,6 +5098,7 @@ class SeatDemoService:
|
||||
mode_name: str,
|
||||
control_transport: str,
|
||||
assistant_plan: dict[str, Any] | None = None,
|
||||
source_draft: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build and verify a named-mode draft for explicit user confirmation.
|
||||
|
||||
@@ -4629,14 +5114,42 @@ class SeatDemoService:
|
||||
if create_match is not None
|
||||
else ""
|
||||
)
|
||||
existing_profile = self.mode_profiles.resolve_exact_name(
|
||||
mode_name,
|
||||
vehicle_id=vehicle_profile.id,
|
||||
source_existing_mode_id = (
|
||||
str(source_draft.get("existing_mode_id") or "").strip()
|
||||
if isinstance(source_draft, dict)
|
||||
else ""
|
||||
)
|
||||
if source_draft is not None and source_existing_mode_id:
|
||||
try:
|
||||
existing_profile = self.mode_profiles.get_profile(
|
||||
source_existing_mode_id,
|
||||
vehicle_profile.id,
|
||||
)
|
||||
except KeyError:
|
||||
existing_profile = None
|
||||
elif source_draft is not None:
|
||||
existing_profile = None
|
||||
else:
|
||||
existing_profile = self.mode_profiles.resolve_exact_name(
|
||||
mode_name,
|
||||
vehicle_id=vehicle_profile.id,
|
||||
)
|
||||
source_profile = (
|
||||
existing_profile
|
||||
if not requested_instruction and source_draft is None
|
||||
else None
|
||||
)
|
||||
draft_mode_name = (
|
||||
str(source_draft.get("name") or mode_name).strip()
|
||||
if isinstance(source_draft, dict)
|
||||
else existing_profile.name
|
||||
if existing_profile is not None
|
||||
else mode_name
|
||||
)
|
||||
source_profile = existing_profile if not requested_instruction else None
|
||||
draft_mode_name = existing_profile.name if existing_profile is not None else mode_name
|
||||
generation_source = (
|
||||
"registered_mode_profile"
|
||||
"draft_revision"
|
||||
if source_draft is not None
|
||||
else "registered_mode_profile"
|
||||
if source_profile is not None
|
||||
else "codex_scene_composition"
|
||||
if assistant_plan is not None and not requested_instruction
|
||||
@@ -4647,31 +5160,99 @@ class SeatDemoService:
|
||||
# of appending a model prompt to the user's utterance. The latter was
|
||||
# being parsed as a second control clause on the assistant's local fast
|
||||
# path, turning a valid nine-action mode into an incomplete draft.
|
||||
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,
|
||||
direct_mode_profile_id=(
|
||||
source_profile.id if source_profile is not None else None
|
||||
),
|
||||
assistant_plan=assistant_plan,
|
||||
# ``source_utterance`` is synthesized for a name-only, unknown
|
||||
# scene. It must not fall back to substring matching (for example
|
||||
# ``我的小憩模式`` -> ``小憩模式``), otherwise the exact resolver's
|
||||
# ambiguity and collision checks can be bypassed.
|
||||
allow_fuzzy_mode_profile_match=bool(requested_instruction),
|
||||
allow_scene_composition=(
|
||||
assistant_plan is not None
|
||||
and source_profile is None
|
||||
and not requested_instruction
|
||||
),
|
||||
)
|
||||
source_utterance = requested_instruction or f"进入{draft_mode_name}"
|
||||
try:
|
||||
source_result = self._process_once(
|
||||
source_utterance,
|
||||
provider,
|
||||
conversation_id,
|
||||
"dry_run",
|
||||
approval_token,
|
||||
vehicle_profile.id,
|
||||
simulation_fault,
|
||||
control_transport=control_transport,
|
||||
direct_mode_profile_id=(
|
||||
source_profile.id if source_profile is not None else None
|
||||
),
|
||||
assistant_plan=assistant_plan,
|
||||
# ``source_utterance`` is synthesized for a name-only, unknown
|
||||
# scene. It must not fall back to substring matching (for example
|
||||
# ``我的小憩模式`` -> ``小憩模式``), otherwise the exact resolver's
|
||||
# ambiguity and collision checks can be bypassed.
|
||||
allow_fuzzy_mode_profile_match=bool(requested_instruction),
|
||||
allow_scene_composition=(
|
||||
assistant_plan is not None
|
||||
and source_profile is None
|
||||
and not requested_instruction
|
||||
),
|
||||
)
|
||||
except NativeCanCompileError:
|
||||
# Repository modes and model-composed scenes share the same native
|
||||
# compiler. A stale mode parameter must become a bounded failed
|
||||
# preview, never an uncaught HTTP 500 or a partially usable card.
|
||||
failure_context = self._context_for_vehicle(
|
||||
self.conversations.snapshot(conversation_id),
|
||||
vehicle_profile,
|
||||
)
|
||||
source_result = {
|
||||
"request_id": (
|
||||
f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-"
|
||||
f"{uuid.uuid4().hex}"
|
||||
),
|
||||
"utterance": source_utterance,
|
||||
"conversation_id": conversation_id,
|
||||
"conversation_turn": failure_context["turn_count"],
|
||||
"status": "validation_failed",
|
||||
"provider_requested": provider,
|
||||
"provider_used": "mode_profile_native_can_guard",
|
||||
"execution_mode": "dry_run",
|
||||
"control_transport": control_transport,
|
||||
"hardware_execution_allowed": False,
|
||||
"vehicle": vehicle_profile.public_dict(),
|
||||
"matched_modes": (
|
||||
[source_profile.public_dict()]
|
||||
if source_profile is not None
|
||||
else []
|
||||
),
|
||||
"mode_profile": None,
|
||||
"decision_summary": [
|
||||
f"“{draft_mode_name}”包含当前车型协议无法编译的动作,未生成草稿。"
|
||||
],
|
||||
"deferred_requests": [],
|
||||
"model_requests": [],
|
||||
"unresolved_clauses": [],
|
||||
"intent_ir": None,
|
||||
"function_config": None,
|
||||
"function_configs": {},
|
||||
"scene_config": None,
|
||||
"preset_config": None,
|
||||
"validation": [
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "MODE_PROFILE_NATIVE_COMPILE_FAILED",
|
||||
"path": "mode_profile_draft",
|
||||
"message": "模式动作无法编译为当前车型支持的控制指令。",
|
||||
}
|
||||
],
|
||||
"command_plan": None,
|
||||
"command_plans": {},
|
||||
"execution_report": None,
|
||||
"actuator_state": failure_context["devices"],
|
||||
"domain_state": failure_context["domain_devices"],
|
||||
"saved_presets": [],
|
||||
"trace_events": [],
|
||||
"can_commands": [],
|
||||
"orchestration_route": "mode_profile_create_preview",
|
||||
"mode_creation_failure": {
|
||||
"code": "MODE_PROFILE_NATIVE_COMPILE_FAILED",
|
||||
"reason": "模式动作未通过当前车型 CAN 协议编译校验",
|
||||
"next_step": "请检查模式中的设备参数是否符合当前车型能力范围",
|
||||
"unresolved_clauses": [],
|
||||
"issues": [],
|
||||
},
|
||||
}
|
||||
self._persist(source_result)
|
||||
return source_result
|
||||
draft_context = self._context_for_vehicle(
|
||||
self.conversations.snapshot(conversation_id),
|
||||
vehicle_profile,
|
||||
@@ -4736,7 +5317,7 @@ class SeatDemoService:
|
||||
] if isinstance(source_actions, list) else []
|
||||
if (
|
||||
source_result.get("status") == "succeeded"
|
||||
and generation_source == "codex_scene_composition"
|
||||
and generation_source in {"codex_scene_composition", "draft_revision"}
|
||||
and assistant_plan is not None
|
||||
and (
|
||||
self._scene_plan_signatures(assistant_plan)
|
||||
@@ -4762,7 +5343,7 @@ class SeatDemoService:
|
||||
]
|
||||
|
||||
if source_result.get("status") != "succeeded":
|
||||
if generation_source == "codex_scene_composition":
|
||||
if generation_source in {"codex_scene_composition", "draft_revision"}:
|
||||
# Final fail-closed barrier for errors discovered after the
|
||||
# plan guard (adapter, schema, compiler, or safety validation).
|
||||
# A failed reusable scene must not expose a trial-able subset.
|
||||
@@ -4825,6 +5406,40 @@ class SeatDemoService:
|
||||
)
|
||||
)
|
||||
source_request_id = str(source_result.get("request_id") or "")
|
||||
source_existing_revision = (
|
||||
str(source_draft.get("existing_mode_revision") or "").strip()
|
||||
if isinstance(source_draft, dict)
|
||||
else ""
|
||||
)
|
||||
existing_binding = (
|
||||
{
|
||||
"existing_mode_id": source_existing_mode_id,
|
||||
"existing_mode_revision": source_existing_revision,
|
||||
}
|
||||
if source_draft is not None and source_existing_mode_id
|
||||
else {
|
||||
"existing_mode_id": existing_profile.id,
|
||||
"existing_mode_revision": (
|
||||
self.mode_profiles.profile_revision(existing_profile)
|
||||
),
|
||||
}
|
||||
if existing_profile is not None
|
||||
else {}
|
||||
)
|
||||
raw_source_revision = (
|
||||
source_draft.get("draft_revision")
|
||||
if isinstance(source_draft, dict)
|
||||
else None
|
||||
)
|
||||
source_revision = (
|
||||
raw_source_revision
|
||||
if isinstance(raw_source_revision, int)
|
||||
and not isinstance(raw_source_revision, bool)
|
||||
and raw_source_revision >= 1
|
||||
else 1
|
||||
if source_draft is not None
|
||||
else 0
|
||||
)
|
||||
source_result["mode_profile_draft"] = {
|
||||
"name": draft_mode_name,
|
||||
"instruction": source_utterance,
|
||||
@@ -4832,17 +5447,18 @@ class SeatDemoService:
|
||||
"vehicle_id": vehicle_profile.id,
|
||||
"domains": domains,
|
||||
"operation_count": len(reusable_actions),
|
||||
**existing_binding,
|
||||
"generation_source": generation_source,
|
||||
"draft_revision": source_revision + 1,
|
||||
**(
|
||||
{
|
||||
"existing_mode_id": existing_profile.id,
|
||||
"existing_mode_revision": (
|
||||
self.mode_profiles.profile_revision(existing_profile)
|
||||
),
|
||||
"revises_source_request_id": str(
|
||||
source_draft.get("source_request_id") or ""
|
||||
).strip()
|
||||
}
|
||||
if existing_profile is not None
|
||||
if isinstance(source_draft, dict)
|
||||
else {}
|
||||
),
|
||||
"generation_source": generation_source,
|
||||
}
|
||||
source_result["mode_creation"] = {
|
||||
"requested_utterance": utterance,
|
||||
@@ -4851,17 +5467,9 @@ class SeatDemoService:
|
||||
"source_status": source_result.get("status"),
|
||||
"source_provider": source_result.get("provider_used"),
|
||||
"operation_count": len(reusable_actions),
|
||||
**(
|
||||
{
|
||||
"existing_mode_id": existing_profile.id,
|
||||
"existing_mode_revision": (
|
||||
self.mode_profiles.profile_revision(existing_profile)
|
||||
),
|
||||
}
|
||||
if existing_profile is not None
|
||||
else {}
|
||||
),
|
||||
**existing_binding,
|
||||
"generation_source": generation_source,
|
||||
"draft_revision": source_revision + 1,
|
||||
}
|
||||
source_result["orchestration_route"] = "mode_profile_create_preview"
|
||||
source_result["decision_summary"] = [
|
||||
@@ -4871,6 +5479,8 @@ class SeatDemoService:
|
||||
if source_profile is not None
|
||||
else f"Codex 已根据场景语义组合并验证“{draft_mode_name}”的工程能力。"
|
||||
if generation_source == "codex_scene_composition"
|
||||
else f"已调整并重新校验待保存的“{draft_mode_name}”模式草稿。"
|
||||
if generation_source == "draft_revision"
|
||||
else f"已构建并验证“{mode_name}”场景。"
|
||||
),
|
||||
*list(source_result.get("decision_summary", [])),
|
||||
|
||||
@@ -43,6 +43,10 @@ from cognitive_agent.application.codex_assistant import ( # noqa: E402
|
||||
CodexVehicleToolError,
|
||||
)
|
||||
from cognitive_agent.application.gesture_bridge import GestureCapabilityBridge # noqa: E402
|
||||
from cognitive_agent.application.mode_profile_intent import ( # noqa: E402
|
||||
normalize_mode_profile_name,
|
||||
parse_mode_profile_update,
|
||||
)
|
||||
from cognitive_agent.application.seat_service import SeatDemoService # noqa: E402
|
||||
from cognitive_agent.application.self_extension import ( # noqa: E402
|
||||
ExtensionIntegrationError,
|
||||
@@ -1657,6 +1661,25 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
if payload.get("reasoning_effort") is not None
|
||||
else "low"
|
||||
)
|
||||
mode_profile_draft_context = payload.get("mode_profile_draft_context")
|
||||
parsed_mode_update = parse_mode_profile_update(
|
||||
message,
|
||||
expected_mode_name=(
|
||||
mode_profile_draft_context.get("name")
|
||||
if isinstance(mode_profile_draft_context, Mapping)
|
||||
else None
|
||||
),
|
||||
)
|
||||
matching_mode_profile_draft_context = (
|
||||
dict(mode_profile_draft_context)
|
||||
if isinstance(mode_profile_draft_context, Mapping)
|
||||
and parsed_mode_update is not None
|
||||
and normalize_mode_profile_name(parsed_mode_update[0])
|
||||
== normalize_mode_profile_name(
|
||||
mode_profile_draft_context.get("name")
|
||||
)
|
||||
else None
|
||||
)
|
||||
common_arguments: dict[str, object] = {
|
||||
"utterance": message,
|
||||
# Codex has already converted an implicit request into one explicit
|
||||
@@ -1672,6 +1695,15 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
"hardware_access_bypass": self._hardware_access_bypass_allowed(),
|
||||
"reasoning_effort": reasoning_effort,
|
||||
"control_transport": str(payload.get("control_transport") or "wired_can"),
|
||||
**(
|
||||
{
|
||||
"mode_profile_draft_context": (
|
||||
matching_mode_profile_draft_context
|
||||
)
|
||||
}
|
||||
if matching_mode_profile_draft_context is not None
|
||||
else {}
|
||||
),
|
||||
}
|
||||
capability_catalog = self._assistant_capability_catalog()
|
||||
capability_snapshot = capability_catalog.snapshot()
|
||||
@@ -1783,6 +1815,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
control_terms=capability_catalog.control_terms(capability_snapshot),
|
||||
query_capabilities=capability_catalog.query,
|
||||
authoritative_answer=fast_answer.answer if fast_answer else None,
|
||||
mode_profile_draft_context=mode_profile_draft_context,
|
||||
)
|
||||
except HardwareAccessRequiredError as exc:
|
||||
self.vehicle_display_sync.update_task(
|
||||
|
||||
Reference in New Issue
Block a user