fix: compose named cockpit modes from capabilities

This commit is contained in:
vee
2026-09-07 20:52:19 +08:00
parent cf9b89508c
commit 6b2b644ee6
23 changed files with 2917 additions and 124 deletions
@@ -19,6 +19,7 @@ from pathlib import Path
from typing import Protocol
LOGGER = logging.getLogger(__name__)
INTERNAL_SCENE_BUILDER_MARKER = "根据该场景名称使用当前可控原子能力"
class CodexAssistantError(RuntimeError):
@@ -99,6 +100,7 @@ class _VehicleTurnContext:
capability_queries: int = 0
action_succeeded: bool | None = None
action_state: str | None = None
action_result: Mapping[str, object] | None = None
@dataclass(frozen=True)
@@ -139,9 +141,9 @@ _MODE_PROFILE_CREATE_PATTERN = re.compile(
r"(?:构建|创建|新建|新增|生成|添加)(?:一下)?"
r"(?:一个|个|一套|套)?"
r"(?:名为|叫做|叫)?[“\"'「『]?"
r"[^,。,.!!::\s“”\"'「」『』]{1,64}?模式[”\"'」』]?"
r"(?P<name>[^,。,.!!::\s“”\"'「」『』]{1,64}?模式)[”\"'」』]?"
r"(?:配置)?(?:并(?:添加|加入)(?:到)?功能模式(?:栏)?)?"
r"(?:[,,::].{1,800})?[。.!!]?$"
r"(?:[,,::](?P<instruction>.{1,800}))?[。.!!]?$"
)
_HOT_DISCOMFORT_PATTERN = re.compile(r"热|闷热|好热|太晒")
_COLD_DISCOMFORT_PATTERN = re.compile(r"冷|好冷")
@@ -164,12 +166,114 @@ def _normalize_instruction(value: object) -> str:
return re.sub(r"[\s,。!?!?、;;::]", "", str(value).lower())
def _mode_profile_create_parts(message: str) -> tuple[str, str] | None:
"""Return the requested scene name and optional explicit settings."""
normalized = "".join(str(message).strip().split())
match = _MODE_PROFILE_CREATE_PATTERN.fullmatch(normalized)
if match is None:
return None
return (
str(match.group("name") or "").strip(),
str(match.group("instruction") or "").strip(",,::。.!!"),
)
def is_mode_profile_create_request(message: str) -> bool:
"""Return whether the original user text is a bounded mode-create request."""
return _mode_profile_create_parts(message) is not None
def _catalog_exact_mode_match_count(
catalog: Mapping[str, object],
requested_name: str,
) -> int | None:
"""Count exact visible mode matches; ``None`` means catalog unavailable."""
needle = "".join(str(requested_name).casefold().split())
source_status = catalog.get("source_status")
if "modes" not in catalog or (
isinstance(source_status, Mapping)
and source_status.get("modes") != "ready"
):
return None
matched_ids: set[str] = set()
raw_modes = catalog.get("modes", [])
if not isinstance(raw_modes, Sequence) or isinstance(
raw_modes,
(str, bytes, bytearray),
):
return None
for raw_mode in raw_modes:
if not isinstance(raw_mode, Mapping):
continue
candidates = [raw_mode.get("id"), raw_mode.get("name")]
aliases = raw_mode.get("aliases", [])
if isinstance(aliases, Sequence) and not isinstance(
aliases,
(str, bytes, bytearray),
):
candidates.extend(aliases)
if needle in {
"".join(str(candidate or "").casefold().split())
for candidate in candidates
if str(candidate or "").strip()
}:
matched_ids.add(str(raw_mode.get("id") or ""))
matched_ids.discard("")
return len(matched_ids)
def _vehicle_result_succeeded(result: Mapping[str, object]) -> bool:
"""Only a terminal, fully successful service result completes a tool call."""
return str(result.get("status") or "").lower() == "succeeded"
def _mode_profile_result_succeeded(result: Mapping[str, object]) -> bool:
"""Require a complete, non-physical mode preview before claiming success."""
if (
not _vehicle_result_succeeded(result)
or str(result.get("execution_mode") or "").lower() != "dry_run"
or result.get("hardware_execution_allowed") is not False
or result.get("orchestration_route") != "mode_profile_create_preview"
):
return False
draft = result.get("mode_profile_draft")
if not isinstance(draft, Mapping):
return False
operation_count = draft.get("operation_count")
if (
not isinstance(operation_count, int)
or isinstance(operation_count, bool)
or operation_count <= 0
):
return False
intent_ir = result.get("intent_ir")
spec = intent_ir.get("spec") if isinstance(intent_ir, Mapping) else None
actions = spec.get("actions") if isinstance(spec, Mapping) else None
if not isinstance(actions, Sequence) or isinstance(
actions,
(str, bytes, bytearray),
):
return False
if len(actions) != operation_count or not all(
isinstance(action, Mapping) for action in actions
):
return False
validation = result.get("validation", [])
return not (
isinstance(validation, Sequence)
and not isinstance(validation, (str, bytes, bytearray))
and any(
isinstance(issue, Mapping) and issue.get("severity") == "error"
for issue in validation
)
)
def _vehicle_verification_status(result: Mapping[str, object]) -> str:
"""Normalize the service's execution evidence without inventing feedback."""
@@ -231,7 +335,7 @@ def _authorize_vehicle_request(
):
return _VehicleAuthorization(None, "knowledge_or_non_vehicle_request")
if _MODE_PROFILE_CREATE_PATTERN.fullmatch(normalized):
if is_mode_profile_create_request(normalized):
# Named-mode creation is a bounded configuration operation. The
# vehicle service parses the same original sentence again and always
# validates the generated scene in dry-run before offering it to the
@@ -373,7 +477,27 @@ class CodexAssistantService:
self._fast_implicit_controls and authorization.reason in _FAST_COMFORT_REASONS
)
mode_profile_preview = authorization.reason == "explicit_mode_profile_create"
direct_vehicle_control = fast_comfort_control or mode_profile_preview
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])
if mode_parts
else None
)
direct_mode_profile_preview = bool(
mode_parts
and (
mode_parts[1]
# Ambiguous or unavailable mode catalogs fail closed in the
# local builder. Only a known-zero match is safe to hand to
# semantic scene composition.
or exact_mode_matches != 0
)
)
# 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
route = (
"capability_catalog"
if authoritative_answer
@@ -385,7 +509,7 @@ class CodexAssistantService:
"n7web-capability-registry"
if authoritative_answer
else "n7web-mode-builder"
if mode_profile_preview
if direct_mode_profile_preview
else "n7web-control-router"
if fast_comfort_control
else "codex-app-server"
@@ -414,6 +538,8 @@ class CodexAssistantService:
"正在读取工程能力目录……"
if authoritative_answer
else "正在构建并校验功能模式……"
if direct_mode_profile_preview
else "Codex 正在理解场景并组合工程能力……"
if mode_profile_preview
else "正在快速处理座舱舒适需求……"
if fast_comfort_control
@@ -542,6 +668,8 @@ class CodexAssistantService:
safe_result = self._safe_vehicle_result(result)
action_state, successful, completion_text = _vehicle_action_state(result)
if mode_profile_preview:
successful = _mode_profile_result_succeeded(result)
action_state = "simulated" if successful else "failed"
completion_text = (
"功能模式预览已生成" if successful else "功能模式预览生成失败"
)
@@ -632,6 +760,14 @@ class CodexAssistantService:
reason = clean(failure.get("reason"))
next_step = clean(failure.get("next_step"))
if reason and next_step:
if (
INTERNAL_SCENE_BUILDER_MARKER in reason
or INTERNAL_SCENE_BUILDER_MARKER in next_step
):
return (
"未能根据场景名称生成通过工程能力校验的座舱动作",
"请稍后重试;若该场景尚未登记,可补充希望联动的设备状态",
)
return reason, next_step
unresolved = result.get("unresolved_clauses")
@@ -644,6 +780,8 @@ class CodexAssistantService:
clean(item, limit=120)
for item in unresolved[:3]
if clean(item, limit=120)
and INTERNAL_SCENE_BUILDER_MARKER
not in clean(item, limit=120)
)
)
if clauses:
@@ -716,6 +854,40 @@ class CodexAssistantService:
if isinstance(draft, Mapping)
else "新功能模式"
)
if isinstance(draft, Mapping) and draft.get("existing_mode_id"):
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
)
if draft.get("generation_source") == "registered_mode_profile":
return (
f"工程中已有“{name}”,已按当前车型载入预览并验证"
f"{operation_count}项设备动作。可以先试运行,确认后更新现有模式。"
)
return (
f"已按你提供的设置为现有“{name}”生成预览并验证"
f"{operation_count}项设备动作。可以先试运行,确认后更新现有模式。"
)
if (
isinstance(draft, Mapping)
and draft.get("generation_source") == "codex_scene_composition"
):
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
)
return (
f"已理解“{name}”的场景意图,并从当前工程能力中组合、验证了"
f"{operation_count}项设备动作。确认预览后可保存为功能模式,也可以先试运行。"
)
return f"已为你生成“{name}”预览。确认动作后可保存为功能模式,也可以先试运行。"
@staticmethod
@@ -765,6 +937,9 @@ class CodexAssistantService:
query_capabilities,
)
vehicle_control_expected = tool_context.authorization.instruction is not None
mode_profile_expected = (
tool_context.authorization.reason == "explicit_mode_profile_create"
)
with self._tool_contexts_lock:
self._tool_contexts[request_token] = tool_context
@@ -938,8 +1113,6 @@ class CodexAssistantService:
raise CodexAssistantError(str(detail or f"Codex turn {status}"))
if not final_answer:
final_answer = streamed_answer.strip()
if not final_answer:
raise CodexAssistantError("Codex produced an empty response")
if vehicle_control_expected:
if not tool_context.called:
tool_context.action_succeeded = False
@@ -955,9 +1128,25 @@ class CodexAssistantService:
}
)
elif tool_context.action_succeeded is not True:
final_answer = "车辆控制未完成,受控执行链未返回成功结果。"
final_answer = (
self._mode_profile_preview_answer(
tool_context.action_result or {},
False,
)
if mode_profile_expected
else "车辆控制未完成,受控执行链未返回成功结果。"
)
elif mode_profile_expected:
final_answer = self._mode_profile_preview_answer(
tool_context.action_result or {},
True,
)
elif tool_context.action_state == "submitted":
final_answer = "控制指令已提交,正在等待车辆状态确认。"
elif not final_answer and tool_context.action_state == "simulated":
final_answer = "车辆控制模拟编排已完成。"
elif not final_answer and tool_context.action_state == "verified":
final_answer = "车辆控制已完成并通过状态确认。"
if (
emit(
{
@@ -969,6 +1158,8 @@ class CodexAssistantService:
is False
):
return {"cancelled": True, "answer": final_answer}
if not final_answer:
raise CodexAssistantError("Codex produced an empty response")
work_succeeded = (
tool_context.called and tool_context.action_succeeded is True
if vehicle_control_expected
@@ -980,7 +1171,11 @@ class CodexAssistantService:
"message_id": message_id,
"success": work_succeeded,
"display_text": (
"车辆控制模拟编排已完成"
"功能模式预览已生成"
if mode_profile_expected and work_succeeded
else "功能模式预览生成失败"
if mode_profile_expected
else "车辆控制模拟编排已完成"
if vehicle_control_expected
and work_succeeded
and tool_context.action_state == "simulated"
@@ -1106,6 +1301,15 @@ class CodexAssistantService:
secret=request_token,
)
action_state, successful, completion_text = _vehicle_action_state(result)
if context.authorization.reason == "explicit_mode_profile_create":
successful = _mode_profile_result_succeeded(result)
action_state = "simulated" if successful else "failed"
completion_text = (
"功能模式预览已生成"
if successful
else "功能模式预览生成失败"
)
context.action_result = result
context.action_succeeded = successful
context.action_state = action_state
context.events.put(
@@ -1158,7 +1362,17 @@ class CodexAssistantService:
if "decision_summary" in normalized_plan:
normalized_plan["decision_summary"] = ["Codex 结构化计划已提交应用安全链复核。"]
if "ambiguities" in normalized_plan:
normalized_plan["ambiguities"] = []
normalized_plan["ambiguities"] = (
[
{
"field": "assistant_plan",
"question": "Codex 候选计划包含未解决歧义。",
}
]
if isinstance(normalized_plan["ambiguities"], list)
and normalized_plan["ambiguities"]
else []
)
requests = normalized_plan.get("requests")
if isinstance(requests, list):
for request in requests:
@@ -1482,6 +1696,9 @@ class CodexAssistantService:
"vehicle_id",
"domains",
"operation_count",
"existing_mode_id",
"existing_mode_revision",
"generation_source",
)
if key in mode_profile_draft
}
@@ -1507,6 +1724,7 @@ class CodexAssistantService:
)
for item in unresolved_items
if isinstance(item, str)
and INTERNAL_SCENE_BUILDER_MARKER not in item
],
}
peripheral = result.get("peripheral")