merge: integrate remote MQTT routing with seat-scoped dynamic modes

This commit is contained in:
vee
2026-09-12 15:10:16 +08:00
43 changed files with 5726 additions and 492 deletions
+11
View File
@@ -3,25 +3,33 @@ pyproject.toml
src/cognitive_agent/__init__.py
src/cognitive_agent/auth.py
src/cognitive_agent/config_loader.py
src/cognitive_agent/lan_scene_server.py
src/cognitive_agent/server.py
src/cognitive_agent.egg-info/PKG-INFO
src/cognitive_agent.egg-info/SOURCES.txt
src/cognitive_agent.egg-info/dependency_links.txt
src/cognitive_agent.egg-info/requires.txt
src/cognitive_agent.egg-info/top_level.txt
src/cognitive_agent/application/assistant_capabilities.py
src/cognitive_agent/application/assistant_plan_guard.py
src/cognitive_agent/application/capability_registry.py
src/cognitive_agent/application/chinese_values.py
src/cognitive_agent/application/codex_assistant.py
src/cognitive_agent/application/conversation_state.py
src/cognitive_agent/application/direct_can_control.py
src/cognitive_agent/application/domain_runtime.py
src/cognitive_agent/application/gesture_bridge.py
src/cognitive_agent/application/mode_profile.py
src/cognitive_agent/application/mode_profile_intent.py
src/cognitive_agent/application/mode_scene_catalog.py
src/cognitive_agent/application/model_plan_adapter.py
src/cognitive_agent/application/mqtt_ota_seat_executor.py
src/cognitive_agent/application/native_can_feedback.py
src/cognitive_agent/application/native_can_intent.py
src/cognitive_agent/application/native_can_relative.py
src/cognitive_agent/application/native_can_runtime.py
src/cognitive_agent/application/scene_composer.py
src/cognitive_agent/application/scene_templates.py
src/cognitive_agent/application/seat_calibration.py
src/cognitive_agent/application/seat_service.py
src/cognitive_agent/application/self_extension.py
@@ -51,6 +59,8 @@ src/cognitive_agent/infrastructure/can/matrix.py
src/cognitive_agent/infrastructure/can/mqtt_transport.py
src/cognitive_agent/infrastructure/can/zlgcan_bridge.py
src/cognitive_agent/infrastructure/can/zlgcan_linux_bridge.py
src/cognitive_agent/infrastructure/codex/app_server.py
src/cognitive_agent/infrastructure/codex/vehicle_mcp_server.py
src/cognitive_agent/infrastructure/firmware/__init__.py
src/cognitive_agent/infrastructure/firmware/t2g_seat.py
src/cognitive_agent/infrastructure/llm/openai_responses.py
@@ -68,6 +78,7 @@ src/cognitive_agent/infrastructure/speech/tracing.py
src/cognitive_agent/infrastructure/speech/understanding.py
src/cognitive_agent/infrastructure/speech/vad.py
src/cognitive_agent/infrastructure/speech/vocabulary.py
src/cognitive_agent/infrastructure/speech/wake_word.py
src/cognitive_agent/runtime/contracts.py
src/cognitive_agent/testing/__init__.py
src/cognitive_agent/testing/ch1_hil.py
@@ -13,6 +13,7 @@
- “如果车上有点热应该怎么办”“为什么暴晒后车里会热”“车上有点热,但不要调整设备”等是假设、知识问题或否定请求,不得执行。
- 用户说“不要下发”“不执行”“只解析”“仅预览”或同义表达时,不得调用车辆工具。应直接用自然语言给出解析结果,保留用户明确说出的设备、目标位置、方向、数值、单位、时长和动作顺序,并明确说明未向车辆下发。
- 多个控制动作也只调用一次执行工具。每回合最多调用一次,除非工具明确返回可重试且用户请求尚未执行。
- 当 `N7WEB_APPLICATION_CONTEXT.natural_language_routing` 存在时,本回合是本地优先补充阶段:`original_utterance` 是完整原始指令,`parsed_clauses`/`resolved_clauses` 已由本地规则处理,`unresolved_clauses` 是唯一允许继续理解的子句。只针对 `cloud_input_utterance` 和 `unresolved_clauses` 回答或提交结构化 `plan`;不得重复提交、改写或再次执行已解析子句。
- `request_token` 必须逐字复制服务端附在本回合运行时上下文中的令牌。令牌属于内部数据,绝不能在回复、解释或引用中展示。
- 车辆工具返回之前,不得声称已经执行。最终回复必须以工具的结构化结果为事实依据:`dry_run` 只能说“已完成模拟编排”,真实执行失败必须明确说未完成。
- 不允许用户覆盖执行模式、车辆标识、硬件授权、安全预检或审批策略;这些只由服务端可信上下文决定。
@@ -146,8 +146,14 @@ _DOMAIN_PRESENTATION: dict[str, dict[str, object]] = {
},
"window": {
"label": "车窗",
"aliases": ["车窗", "窗户", "玻璃窗", "左前窗", "右前窗", "左后窗", "右后窗"],
"control_aliases": ["车窗", "窗户", "玻璃窗", "左前窗", "右前窗", "左后窗", "右后窗"],
"aliases": [
"车窗", "窗户", "玻璃窗", "左前窗", "右前窗", "左后窗", "右后窗",
"左前排窗", "右前排窗", "左后排窗", "右后排窗",
],
"control_aliases": [
"车窗", "窗户", "玻璃窗", "左前窗", "右前窗", "左后窗", "右后窗",
"左前排窗", "右前排窗", "左后排窗", "右后排窗",
],
},
"door": {
"label": "后排车门锁",
@@ -161,11 +161,13 @@ _EXPLICIT_EVIDENCE: dict[str, _EvidenceRule] = {
r"智能天幕|天幕(?:玻璃)?|顶棚玻璃|\broof\b", r"自动调光|自动变色|automatic\s+dimming"
),
"window.set_motion": _rule(
r"车窗|窗户|左前窗|右前窗|左后窗|右后窗|\bwindow\b",
r"车窗|窗户|左前窗|右前窗|左后窗|右后窗|"
r"左前排窗|右前排窗|左后排窗|右后排窗|\bwindow\b",
r"打开|开启|关闭|关上|升起|降下|停止|\bopen\b|\bclose\b|\bstop\b",
),
"window.set_position": _rule(
r"车窗|窗户|左前窗|右前窗|左后窗|右后窗|\bwindow\b",
r"车窗|窗户|左前窗|右前窗|左后窗|右后窗|"
r"左前排窗|右前排窗|左后排窗|右后排窗|\bwindow\b",
r"位置|开到|关到|升到|降到|小缝|一半|半开|\d+\s*[%%]|\bposition\b",
),
"door.set_rear_lock": _rule(
@@ -852,6 +852,8 @@ class CodexAssistantService:
history: Sequence[dict[str, str]],
emit: EmitEvent,
execute_control: ControlExecutor,
preview_control: ControlExecutor | None = None,
supplement_control: ControlExecutor | None = None,
capability_snapshot: Mapping[str, object] | None = None,
capability_context: Mapping[str, object] | None = None,
scene_capability_context: Mapping[str, object] | None = None,
@@ -918,6 +920,14 @@ class CodexAssistantService:
authorization.reason == "explicit_registered_control"
and _normalize_instruction(message) in normalized_quick_controls
)
explicit_local_control = (
authorization.reason == "explicit_registered_control"
)
local_first_control = bool(
explicit_local_control
and preview_control is not None
and supplement_control is not None
)
mode_profile_preview = authorization.reason in {
"explicit_mode_profile_create",
"explicit_mode_profile_draft_update",
@@ -971,6 +981,7 @@ class CodexAssistantService:
fast_comfort_control
or authorization.reason == "explicit_schedule_stop"
or exact_quick_control
or local_first_control
or saved_mode_execution_id is not None
or direct_mode_profile_preview
or authorization.reason == "explicit_mode_profile_draft_update"
@@ -1007,7 +1018,7 @@ class CodexAssistantService:
else "codex-app-server-scene-composer"
if scene_composition_preview
else "n7web-control-router"
if fast_comfort_control or exact_quick_control
if fast_comfort_control or exact_quick_control or local_first_control
else "codex-app-server"
)
@@ -1053,6 +1064,8 @@ class CodexAssistantService:
if fast_comfort_control
else "正在执行快捷车控……"
if exact_quick_control
else "正在本地解析并下发明确控制指令……"
if local_first_control
else "小迪正在理解并处理……"
),
}
@@ -1121,6 +1134,25 @@ class CodexAssistantService:
),
}
if (
local_first_control
and preview_control is not None
and supplement_control is not None
):
return self._stream_local_first_vehicle_control(
authorization,
message_id=message_id,
emit=emit,
preview_control=preview_control,
supplement_control=supplement_control,
conversation_id=conversation_id,
principal=principal,
capability_snapshot=catalog,
capability_context=model_context,
control_terms=control_terms,
query_capabilities=catalog_query,
)
if direct_vehicle_control:
return self._stream_direct_vehicle_control(
(
@@ -1571,7 +1603,16 @@ class CodexAssistantService:
active = start_structured_turn(
thread_id,
message,
application_context=scene_context,
application_context={
**dict(scene_context),
"natural_language_routing": {
"original_utterance": message,
"parsed_clauses": [],
"resolved_clauses": [],
"unresolved_clauses": [message],
"cloud_input_utterance": message,
},
},
output_schema=output_schema,
timeout=self._remaining_scene_time(deadline),
)
@@ -1766,6 +1807,285 @@ class CodexAssistantService:
except (RuntimeError, TypeError) as exc:
LOGGER.debug("Scene Composer thread cleanup failed", exc_info=exc)
def _stream_local_first_vehicle_control(
self,
authorization: _VehicleAuthorization,
*,
message_id: str,
emit: EmitEvent,
preview_control: ControlExecutor,
supplement_control: ControlExecutor,
conversation_id: str,
principal: str,
capability_snapshot: Mapping[str, object],
capability_context: Mapping[str, object],
control_terms: Sequence[str],
query_capabilities: CapabilityQuery,
) -> dict[str, object]:
"""Publish deterministic controls, then delegate only unresolved text."""
instruction = authorization.instruction
if instruction is None:
raise CodexAssistantError("local-first vehicle control was not authorized")
if (
emit(
{
"type": "action.started",
"message_id": message_id,
"display_text": "正在通过本地规则解析明确控制指令……",
}
)
is False
):
return {"cancelled": True}
stream_open = True
def on_progress(payload: dict[str, object]) -> None:
nonlocal stream_open
if not stream_open:
return
validated_payload = self._validated_control_payload(payload)
if validated_payload is not None:
stream_open = (
emit(
{
"type": "validated_control_commands",
"message_id": message_id,
"payload": validated_payload,
}
)
is not False
)
if not stream_open:
return
stream_open = (
emit(
{
"type": "action.progress",
"message_id": message_id,
"display_text": "本地控制指令已解析并进入下发链路……",
"payload": self._safe_vehicle_result(payload),
}
)
is not False
)
try:
preview = preview_control(instruction, on_progress)
if not isinstance(preview, dict):
raise TypeError("vehicle preview returned an invalid result")
except Exception: # noqa: BLE001 - stable browser-facing failure
answer = "车辆控制未完成,本地安全执行链没有返回有效结果。"
for event in (
{
"type": "action.completed",
"message_id": message_id,
"success": False,
"state": "failed",
"display_text": "本地控制未完成",
},
{
"type": "answer.delta",
"message_id": message_id,
"delta": answer,
},
{
"type": "work.completed",
"message_id": message_id,
"success": False,
"display_text": "车辆控制未完成",
},
{
"type": "answer.completed",
"message_id": message_id,
"route": "vehicle_control",
"answer": answer,
},
):
if emit(event) is False:
return {"cancelled": True, "answer": answer}
return {
"route": "vehicle_control",
"answer": answer,
"success": False,
}
if not stream_open:
return {"cancelled": True}
routing_value = preview.get("natural_language_routing")
routing = dict(routing_value) if isinstance(routing_value, Mapping) else {}
raw_unresolved = routing.get("unresolved_clauses")
if not isinstance(raw_unresolved, Sequence) or isinstance(
raw_unresolved,
(str, bytes, bytearray),
):
raw_unresolved = preview.get("unresolved_clauses")
unresolved_clauses = list(
dict.fromkeys(
text
for item in (
list(raw_unresolved)[:64]
if isinstance(raw_unresolved, Sequence)
and not isinstance(raw_unresolved, (str, bytes, bytearray))
else []
)
for text in [str(item).strip()[:1000]]
if text
)
)
safe_preview = self._safe_vehicle_result(preview)
if not unresolved_clauses:
action_state, successful, completion_text = _vehicle_action_state(preview)
answer = self._fast_comfort_answer(
"explicit_registered_control",
preview,
successful,
)
if not stream_open:
return {"cancelled": True, "answer": answer}
for event in (
{
"type": "action.completed",
"message_id": message_id,
"success": successful,
"state": action_state,
"display_text": completion_text,
"payload": safe_preview,
},
{
"type": "answer.delta",
"message_id": message_id,
"delta": answer,
},
{
"type": "work.completed",
"message_id": message_id,
"success": successful,
"state": action_state,
"display_text": completion_text,
},
{
"type": "answer.completed",
"message_id": message_id,
"route": "vehicle_control",
"answer": answer,
},
):
if emit(event) is False:
return {"cancelled": True, "answer": answer}
return {
"route": "vehicle_control",
"answer": answer,
"success": successful,
"result": safe_preview,
}
local_has_commands = bool(
preview.get("function_configs") and preview.get("command_plans")
)
local_status = str(preview.get("status") or "")
local_succeeded = local_has_commands and local_status in {
"succeeded",
"partially_succeeded",
"no_action",
}
if stream_open and emit(
{
"type": "action.completed",
"message_id": message_id,
"success": local_succeeded,
"state": "submitted" if local_succeeded else "failed",
"display_text": (
"本地明确控制已先行处理"
if local_succeeded
else "本地阶段未生成控制指令"
),
"payload": safe_preview,
}
) is False:
return {"cancelled": True}
if emit(
{
"type": "work.progress",
"message_id": message_id,
"display_text": "本地明确子句已处理,正在补充未解析子句……",
}
) is False:
return {"cancelled": True}
cloud_message = ",".join(unresolved_clauses)
routing.update(
{
"original_utterance": str(
routing.get("original_utterance") or instruction
)[:1000],
"parsed_clauses": list(routing.get("parsed_clauses") or []),
"resolved_clauses": list(routing.get("resolved_clauses") or []),
"unresolved_clauses": unresolved_clauses,
"cloud_input_utterance": cloud_message,
}
)
if not self.client.available:
answer = (
"已先行处理本地可解析的控制;其余子句暂无法连接云端模型,"
"未生成额外控制。"
)
emit(
{
"type": "answer.delta",
"message_id": message_id,
"delta": answer,
}
)
emit(
{
"type": "work.completed",
"message_id": message_id,
"success": local_succeeded,
"display_text": "本地控制已处理,云端补充不可用",
}
)
emit(
{
"type": "answer.completed",
"message_id": message_id,
"route": "vehicle_control",
"answer": answer,
}
)
return {
"route": "vehicle_control",
"answer": answer,
"success": local_succeeded,
"result": safe_preview,
}
conversation, conversation_key = self._conversation(
principal,
conversation_id,
)
try:
with conversation.lock:
return self._stream_locked(
conversation,
cloud_message,
message_id=message_id,
emit=emit,
execute_control=supplement_control,
capability_snapshot=capability_snapshot,
capability_context={
**dict(capability_context),
"natural_language_routing": routing,
},
control_terms=control_terms,
query_capabilities=query_capabilities,
)
finally:
self._release_conversation(conversation_key, conversation)
def _stream_direct_vehicle_control(
self,
authorization: _VehicleAuthorization,
@@ -1789,6 +2109,10 @@ class CodexAssistantService:
authorization.reason == "explicit_mode_profile_draft_update"
)
exact_quick_control = authorization.reason == "exact_quick_control"
explicit_local_control = authorization.reason in {
"exact_quick_control",
"explicit_registered_control",
}
saved_mode_execution = authorization.reason == "explicit_saved_mode_execute"
mode_profile_management_route = {
"explicit_mode_profile_update": "mode_profile_update",
@@ -1812,6 +2136,8 @@ class CodexAssistantService:
if saved_mode_execution
else "正在通过车辆安全链执行快捷控制……"
if exact_quick_control
else "正在通过本地规则解析并下发控制……"
if explicit_local_control
else "正在通过车辆安全链执行舒适性调节……"
),
}
@@ -1826,6 +2152,20 @@ class CodexAssistantService:
nonlocal stream_open
if not stream_open:
return
validated_payload = self._validated_control_payload(payload)
if validated_payload is not None:
stream_open = (
emit(
{
"type": "validated_control_commands",
"message_id": message_id,
"payload": validated_payload,
}
)
is not False
)
if not stream_open:
return
stream_open = (
emit(
{
@@ -1838,6 +2178,8 @@ class CodexAssistantService:
if saved_mode_execution
else "功能模式存储请求已解析,正在校验……"
if mode_profile_management_route is not None
else "本地控制指令已通过校验并进入下发链路……"
if explicit_local_control
else "控制指令已通过校验,正在执行……"
),
"payload": self._safe_vehicle_result(payload),
@@ -1947,6 +2289,14 @@ class CodexAssistantService:
if exact_quick_control and successful
else "快捷控制未完成"
if exact_quick_control
else "明确控制已提交,等待车辆状态确认"
if explicit_local_control and action_state == "submitted"
else "明确控制模拟编排已完成"
if explicit_local_control and action_state == "simulated"
else "明确控制已完成"
if explicit_local_control and successful
else "明确控制未完成"
if explicit_local_control
else "舒适性调节已提交,等待车辆状态确认"
if action_state == "submitted"
else "舒适性调节模拟编排已完成"
@@ -2308,7 +2658,11 @@ class CodexAssistantService:
return ("动态模式时间轴已启动,将按卡片中的时长逐阶段调节。尚未完成所有阶段;输入“停止动态模式”可取消后续阶段。"
if result.get("status") == "running"
else "已按虚拟时钟模拟验证动态模式的全部阶段,未向实际车辆下发操作。")
if reason in {"exact_quick_control", "explicit_saved_mode_execute"}:
if reason in {
"exact_quick_control",
"explicit_registered_control",
"explicit_saved_mode_execute",
}:
instruction = re.sub(
r"\s+",
" ",
@@ -2317,7 +2671,7 @@ class CodexAssistantService:
or (
"功能模式"
if reason == "explicit_saved_mode_execute"
else "快捷控制指令"
else "控制指令"
)
),
).strip()[:160]
@@ -2389,12 +2743,24 @@ class CodexAssistantService:
answer_redactor = _StreamingSecretRedactor(request_token)
cancelled = False
deadline = time.monotonic() + self._turn_timeout
application_context = dict(capability_context)
if not isinstance(
application_context.get("natural_language_routing"),
Mapping,
):
application_context["natural_language_routing"] = {
"original_utterance": message,
"parsed_clauses": [],
"resolved_clauses": [],
"unresolved_clauses": [message],
"cloud_input_utterance": message,
}
try:
active = self.client.start_turn(
conversation.thread_id,
message,
request_token,
application_context=capability_context,
application_context=application_context,
)
terminal: dict[str, object] | None = None
while terminal is None:
@@ -2699,6 +3065,17 @@ class CodexAssistantService:
)
def on_progress(payload: dict[str, object]) -> None:
validated_payload = self._validated_control_payload(
payload,
secret=request_token,
)
if validated_payload is not None:
context.events.put(
{
"type": "validated_control_commands",
"payload": validated_payload,
}
)
context.events.put(
{
"type": "action.progress",
@@ -3017,8 +3394,9 @@ class CodexAssistantService:
for raw_key, raw_value in list(value.items())[:24]:
key = str(raw_key)[:80]
if re.search(
r"token|secret|password|authorization|approval|raw.?can|"
r"can.?frame|data.?hex|payload.?hex",
r"token|secret|password|authorization|approval|api.?key|"
r"credential|cookie|bearer|private.?key|access.?key|"
r"raw.?can|can.?frame|data.?hex|payload.?hex",
key,
re.IGNORECASE,
):
@@ -3041,7 +3419,290 @@ class CodexAssistantService:
)
for item in list(value)[:24]
]
return str(value).replace(secret, "[内部令牌已隐藏]")[:240]
text = str(value)
if secret:
text = text.replace(secret, "[内部令牌已隐藏]")
return text[:240]
@staticmethod
def _validated_control_payload(
result: Mapping[str, object],
*,
secret: str = "",
) -> dict[str, object] | None:
"""Build a browser-only allowlist projection of compiled controls.
MCP tool results intentionally exclude command plans and raw CAN
material. The web console still needs a deterministic view after
validation, so copy only semantic fields, canonical debug commands,
reconstructed IR payloads, display-safe CAN bytes, and stage timings.
"""
raw_configurations = result.get("function_configs")
configuration_entries: list[tuple[str, Mapping[str, object]]] = []
if isinstance(raw_configurations, Mapping):
configuration_entries.extend(
(str(key), value)
for key, value in raw_configurations.items()
if isinstance(value, Mapping)
)
if not configuration_entries and isinstance(
result.get("function_config"), Mapping
):
configuration_entries.append(("default", result["function_config"]))
raw_plans = result.get("command_plans")
plan_entries: list[tuple[str, Mapping[str, object]]] = []
if isinstance(raw_plans, Mapping):
plan_entries.extend(
(str(key), value)
for key, value in raw_plans.items()
if isinstance(value, Mapping)
)
if not plan_entries and isinstance(result.get("command_plan"), Mapping):
plan_entries.append(("default", result["command_plan"]))
if not configuration_entries or not plan_entries:
return None
def safe_text(value: object, limit: int) -> str:
"""Return bounded display text without copying the request secret."""
text = str(value or "")
if secret and secret in text:
return ""
return text[:limit]
def command_refs(command: Mapping[str, object]) -> tuple[str, ...]:
raw_refs = command.get("operation_ref")
if isinstance(raw_refs, Sequence) and not isinstance(
raw_refs, (str, bytes, bytearray)
):
values = raw_refs
else:
values = str(raw_refs or "").split(",")
return tuple(str(value).strip() for value in values if str(value).strip())
def matching_commands(
entries: Sequence[tuple[str, Mapping[str, object]]],
operation_id: str,
) -> list[tuple[Mapping[str, object], Mapping[str, object]]]:
matches: list[tuple[Mapping[str, object], Mapping[str, object]]] = []
for _plan_key, plan in entries:
spec = plan.get("spec")
if not isinstance(spec, Mapping):
continue
commands = spec.get("commands")
if not isinstance(commands, Sequence) or isinstance(
commands, (str, bytes, bytearray)
):
continue
matches.extend(
(command, plan)
for command in commands
if isinstance(command, Mapping)
and operation_id in command_refs(command)
)
return matches
presentations: list[dict[str, object]] = []
for config_key, configuration in configuration_entries[:16]:
spec = configuration.get("spec")
if not isinstance(spec, Mapping):
continue
operations = spec.get("operations")
if not isinstance(operations, Sequence) or isinstance(
operations, (str, bytes, bytearray)
):
continue
raw_domain = str(spec.get("domain") or "")
domain = safe_text(raw_domain, 80)
preferred_plans = [
entry for entry in plan_entries if entry[0] == config_key
]
for operation in operations[:24]:
if not isinstance(operation, Mapping):
continue
raw_operation_id = str(operation.get("id") or "").strip()[:160]
operation_id = safe_text(raw_operation_id, 160).strip()
capability = safe_text(operation.get("capability"), 160).strip()
if not raw_operation_id or not operation_id or not capability:
continue
compiled = matching_commands(preferred_plans, raw_operation_id)
if (
not preferred_plans
and len(configuration_entries) == 1
and len(plan_entries) == 1
):
# Keep the legacy function_config/command_plan pair working,
# but never search globally across multi-config results. A
# repeated operation id must fail closed instead of borrowing
# a compiled command from another domain.
compiled = matching_commands(plan_entries, raw_operation_id)
if not compiled:
continue
channel = "IR" if raw_domain == "self_extension" else "CAN1"
actual_commands: list[str] = []
frames: list[dict[str, str]] = []
for command, plan in compiled[:16]:
signals = command.get("signals")
signals = signals if isinstance(signals, Mapping) else {}
plan_spec = plan.get("spec")
plan_domain = (
str(plan_spec.get("domain") or "")
if isinstance(plan_spec, Mapping)
else ""
)
message = str(command.get("message") or "")
bus = str(command.get("bus") or "").lower()
if message == "infrared.emit" or plan_domain == "self_extension":
command_channel = "IR"
elif "seat" in bus or "can0" in bus:
command_channel = "CAN0"
else:
command_channel = "CAN1"
channel = command_channel
debug_command = " ".join(
safe_text(signals.get("debug_command"), 256).split()
)
if re.fullmatch(r"[A-Za-z0-9_.:+% -]{1,256}", debug_command):
if debug_command not in actual_commands:
actual_commands.append(debug_command)
slot_value = signals.get("slot")
slot = (
int(slot_value)
if isinstance(slot_value, int)
and not isinstance(slot_value, bool)
and 1 <= slot_value <= 248
else None
)
if message == "infrared.emit" and slot is not None:
actual = f"infrared emit {slot}"
if actual not in actual_commands:
actual_commands.append(actual)
frame = {
"label": "GSCU1B TCP",
"value": json.dumps(
{
"action": "emit",
"data": {"no": slot},
"type": "infrared",
},
ensure_ascii=False,
separators=(",", ":"),
),
}
if frame not in frames:
frames.append(frame)
continue
frame_id = str(signals.get("frame_id") or "").strip()
data_bytes = " ".join(
str(signals.get("data_hex") or "").strip().split()
).upper()
if re.fullmatch(
r"0[xX][0-9A-Fa-f]{1,8}", frame_id
) and re.fullmatch(
r"[0-9A-F]{2}(?: [0-9A-F]{2}){0,63}", data_bytes
):
frame = {
"label": f"{command_channel} 帧",
"value": f"0x{frame_id[2:].upper()} · {data_bytes}",
}
if frame not in frames:
frames.append(frame)
safe_target = CodexAssistantService._safe_result_value(
operation.get("target") or {}, secret=secret
)
safe_parameters = CodexAssistantService._safe_result_value(
operation.get("parameters") or {}, secret=secret
)
presentations.append(
{
"operation": {
"id": operation_id,
"capability": capability,
"target": safe_target if isinstance(safe_target, dict) else {},
"parameters": (
safe_parameters
if isinstance(safe_parameters, dict)
else {}
),
**(
{
"parse_source": safe_text(
operation.get("parse_source"), 80
)
}
if safe_text(operation.get("parse_source"), 80)
else {}
),
},
"domain": domain,
"channel": channel,
"actual_command": ";".join(actual_commands),
"frames": frames,
}
)
if not presentations:
return None
allowed_trace_stages = {
"intent_parse",
"context_resolve",
"domain_route",
"function_generate",
"schema_validate",
"domain_validate",
"compile",
"nfc_peripheral_route",
"local_peripheral_plan",
"nfc_peripheral_sequence",
}
safe_trace: list[dict[str, object]] = []
raw_trace = result.get("trace_events")
if isinstance(raw_trace, Sequence) and not isinstance(
raw_trace, (str, bytes, bytearray)
):
for item in raw_trace[:32]:
if not isinstance(item, Mapping):
continue
stage = str(item.get("stage") or "")
duration = item.get("duration_ms")
if (
stage in allowed_trace_stages
and isinstance(duration, (int, float))
and not isinstance(duration, bool)
and duration >= 0
):
safe_trace.append({"stage": stage, "duration_ms": duration})
payload: dict[str, object] = {
"validated_control_commands": presentations,
"trace_events": safe_trace,
}
for key in (
"request_id",
"preview_request_id",
"supplement_of",
"utterance",
"provider_used",
"execution_mode",
"control_transport",
"local_control_budget_ms",
"local_control_elapsed_ms",
"local_control_within_budget",
"natural_language_routing",
):
if result.get(key) is not None:
payload[key] = CodexAssistantService._safe_result_value(
result[key], secret=secret
)
return payload
@staticmethod
def _safe_vehicle_result(
@@ -5,6 +5,7 @@ from __future__ import annotations
import hashlib
import json
import re
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from threading import RLock
@@ -533,6 +534,7 @@ class ModeProfileRegistry:
self.user_root = Path(user_root) if user_root is not None else self.profile_root
self.session_user_profiles = user_root is not None
self._profiles: list[ModeProfile] = []
self._change_listeners: list[Callable[["ModeProfileRegistry"], None]] = []
self._write_lock = RLock()
self.reload()
@@ -555,6 +557,30 @@ class ModeProfileRegistry:
if path.is_file()
)
self._profiles = profiles
self._notify_change_listeners()
def add_change_listener(
self,
listener: Callable[["ModeProfileRegistry"], None],
*,
notify: bool = True,
) -> None:
"""Run ``listener`` after every successful registry reload.
Mode profiles remain YAML-backed. Listeners are intended for derived,
read-only projections such as the LAN scene catalog; they do not replace
or alter the existing persistence path.
"""
with self._write_lock:
if listener not in self._change_listeners:
self._change_listeners.append(listener)
if notify:
listener(self)
def _notify_change_listeners(self) -> None:
for listener in tuple(self._change_listeners):
listener(self)
def profiles(self) -> list[ModeProfile]:
return list(self._profiles)
@@ -0,0 +1,283 @@
"""Project persisted mode cards into the read-only N7 scene catalog contract."""
from __future__ import annotations
import hashlib
import json
import os
import threading
from copy import deepcopy
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from cognitive_agent.application.mode_profile import (
ModeOperation,
ModeProfile,
ModeProfileRegistry,
)
N7_SCENE_SERVICE = "n7-scenes"
N7_SCENE_SCHEMA_VERSION = 1
DEFAULT_N7_SCENE_SERVER_ID = "n7-cognitive-agent"
DEFAULT_N7_SCENE_SERVER_NAME = "N7 模式卡片目录"
def _utc_now() -> str:
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
def _background_time(profile: ModeProfile) -> str:
"""Choose a compatible visual hint when the mode schema has no time field."""
text = f"{profile.name} {profile.description}"
if any(keyword in text for keyword in ("晨", "早间", "清晨")):
return "morning"
if any(keyword in text for keyword in ("小憩", "午睡", "睡眠", "夜", "冥想")):
return "evening"
return "noon"
def _action_type(capability: str) -> str:
legacy_types = {
"climate.set_power": "climate_power",
"climate.set_temperature": "climate_temperature",
"climate.set_fan_level": "climate_fan",
}
if capability in legacy_types:
return legacy_types[capability]
domain, separator, action = capability.partition(".")
if not separator:
return capability.replace(".", "_")
if action.startswith("set_"):
action = action.removeprefix("set_")
return f"{domain}_{action}".replace(".", "_")
def _legacy_zone(target_scope: str) -> str:
return {
"vehicle": "both",
"all": "both",
"passenger": "passenger",
"front_right": "passenger",
"driver": "driver",
"front_left": "driver",
}.get(target_scope, target_scope)
def _scene_action(operation: ModeOperation) -> dict[str, Any]:
"""Preserve the complete operation and add fields understood by the v1 demo."""
parameters = deepcopy(operation.parameters)
action: dict[str, Any] = {
"type": _action_type(operation.capability),
"at_ms": 0,
"domain": operation.domain,
"capability": operation.capability,
"target_scope": operation.target_scope,
"operation": operation.operation,
"parameters": parameters,
}
# The original demo catalog flattened these three climate actions. Keep
# those aliases so an existing v1 client can continue reading them, while
# the generic fields above retain every seat/roof/light/custom operation.
if operation.capability == "climate.set_power" and "state" in parameters:
state = parameters["state"]
action["enabled"] = (
state
if isinstance(state, bool)
else str(state).strip().lower() in {"1", "true", "on", "enabled"}
)
elif (
operation.capability == "climate.set_temperature"
and "temperature_c" in parameters
):
action["zone"] = _legacy_zone(operation.target_scope)
action["celsius"] = parameters["temperature_c"]
elif operation.capability == "climate.set_fan_level" and "level" in parameters:
action["level"] = parameters["level"]
return action
def _scene_from_profile(
registry: ModeProfileRegistry,
profile: ModeProfile,
public_profile: dict[str, Any],
) -> dict[str, Any]:
scene = {
"id": profile.id,
"label": profile.name,
"name": profile.name,
"background_time": _background_time(profile),
"description": profile.description,
"icon": profile.icon,
"aliases": list(profile.aliases),
"origin": public_profile.get("origin", "preset"),
"deletable": bool(public_profile.get("deletable", False)),
"behavior": public_profile.get("behavior", "static"),
"domains": list(public_profile.get("domains") or []),
"operation_count": len(profile.operations),
"scope": deepcopy(public_profile.get("scope") or {}),
"profile_revision": registry.profile_revision(profile),
"actions": [_scene_action(operation) for operation in profile.operations],
}
if public_profile.get("preset_override") is True:
scene["preset_override"] = True
if profile.timeline is not None:
scene["timeline"] = profile.timeline.public_dict()
if profile.schedule is not None:
schedule = profile.schedule
preview_actions = []
elapsed_ms = 0
for phase in schedule.phases:
for raw_operation in phase.operations:
action = _scene_action(ModeOperation.from_dict(raw_operation))
action.update(
at_ms=elapsed_ms,
phase_id=phase.id,
phase_name=phase.name,
phase_duration_ms=phase.duration_ms,
)
preview_actions.append(action)
elapsed_ms += phase.duration_ms
# A v1 client may ignore timestamps and immediately send every action.
# Keep its executable list empty rather than flattening a new timeline.
# Schedule-aware clients can render the complete contract and ask the
# authenticated application to execute the saved mode by its identity.
scene.update(
behavior="cyclic",
operation_count=schedule.operation_count,
domains=sorted({op["domain"] for phase in schedule.phases for op in phase.operations}),
actions=[],
schedule=schedule.to_dict(),
preview_actions=preview_actions,
preview_actions_scope="single_cycle",
execution={
"kind": "server_schedule",
"mode_id": profile.id,
"client_actions_supported": False,
"requires_authentication": True,
},
execution_notice="分阶段动态模式需通过工程服务端时间轴执行,不支持按静态动作列表下发。",
)
return scene
def build_mode_scene_catalog(
registry: ModeProfileRegistry,
vehicle_id: str,
*,
server_id: str = DEFAULT_N7_SCENE_SERVER_ID,
name: str = DEFAULT_N7_SCENE_SERVER_NAME,
generated_at: str | None = None,
) -> dict[str, Any]:
"""Build a lossless, client-safe projection of effective mode profiles."""
clean_vehicle_id = str(vehicle_id).strip()
if not clean_vehicle_id:
raise ValueError("scene catalog vehicle_id must not be empty")
clean_server_id = str(server_id).strip()
if not clean_server_id:
raise ValueError("scene catalog server_id must not be empty")
public_profiles = registry.public_modes(clean_vehicle_id)
scenes = [
_scene_from_profile(
registry,
registry.get_profile(str(public_profile["id"]), clean_vehicle_id),
public_profile,
)
for public_profile in public_profiles
]
revision_source = {
"service": N7_SCENE_SERVICE,
"schema_version": N7_SCENE_SCHEMA_VERSION,
"server_id": clean_server_id,
"name": str(name).strip() or DEFAULT_N7_SCENE_SERVER_NAME,
"vehicle_id": clean_vehicle_id,
"scenes": scenes,
}
canonical = json.dumps(
revision_source,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
return {
**revision_source,
"catalog_revision": hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16],
"generated_at": generated_at or _utc_now(),
"scene_count": len(scenes),
"read_only": True,
}
class ModeSceneCatalogPublisher:
"""Atomically maintain the JSON projection consumed by LAN clients."""
def __init__(
self,
path: Path,
vehicle_id: str,
*,
server_id: str = DEFAULT_N7_SCENE_SERVER_ID,
name: str = DEFAULT_N7_SCENE_SERVER_NAME,
) -> None:
self.path = Path(path)
self.vehicle_id = str(vehicle_id).strip()
self.server_id = str(server_id).strip() or DEFAULT_N7_SCENE_SERVER_ID
self.name = str(name).strip() or DEFAULT_N7_SCENE_SERVER_NAME
self._lock = threading.RLock()
self._catalog: dict[str, Any] | None = None
def publish(self, registry: ModeProfileRegistry) -> dict[str, Any]:
catalog = build_mode_scene_catalog(
registry,
self.vehicle_id,
server_id=self.server_id,
name=self.name,
)
with self._lock:
on_disk = self._read_catalog()
if (
on_disk is not None
and on_disk.get("catalog_revision") == catalog["catalog_revision"]
and {
key: value for key, value in on_disk.items() if key != "generated_at"
}
== {
key: value for key, value in catalog.items() if key != "generated_at"
}
):
self._catalog = on_disk
return deepcopy(on_disk)
self.path.parent.mkdir(parents=True, exist_ok=True)
temporary = self.path.with_name(
f".{self.path.name}.{os.getpid()}.{threading.get_ident()}.tmp"
)
try:
temporary.write_text(
json.dumps(catalog, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
temporary.replace(self.path)
finally:
if temporary.exists():
temporary.unlink()
self._catalog = catalog
return deepcopy(catalog)
def snapshot(self) -> dict[str, Any] | None:
with self._lock:
return deepcopy(self._catalog)
def _read_catalog(self) -> dict[str, Any] | None:
if not self.path.is_file():
return None
try:
document = json.loads(self.path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None
return document if isinstance(document, dict) else None
@@ -20,6 +20,10 @@ WINDOW_TARGETS = {
"所有": "all",
"四个": "all",
"全车": "all",
"左前排": "front_left",
"右前排": "front_right",
"左后排": "rear_left",
"右后排": "rear_right",
"前排": "front",
"前座": "front",
"后排": "rear",
@@ -88,7 +92,8 @@ LIGHT_TARGETS = {
_EXPLICIT_LOCATION_HINT = re.compile(
r"全部|所有|四个|全车|整车|前排|前座|后排|后座|二排|第二排|"
r"中排|第三排|主驾|主驾驶|驾驶位|驾驶员|司机位|副驾|副驾驶|"
r"乘客位|乘员|左前|前左|右前|前右|左后|后左|右后|后右|"
r"乘客位|乘员|左前排|右前排|左后排|右后排|"
r"左前|前左|右前|前右|左后|后左|右后|后右|"
r"左侧|右侧|左边|右边"
)
_WHOLE_VEHICLE_HINT = re.compile(r"全部|所有|四个|全车|整车")
@@ -339,6 +344,7 @@ CH1_DEFAULT_TARGETS = {
_EXPLICIT_TARGET_PATTERNS = {
"window": re.compile(
r"(?:全部|所有|四个|全车|前排|前座|后排|后座|二排|第二排|"
r"左前排|右前排|左后排|右后排|"
r"左前|前左|右前|前右|左后|后左|右后|后右|左侧|右侧|"
r"主驾驶|驾驶位|驾驶员|司机位|主驾|副驾驶|乘客位|乘员|副驾)"
r"(?:的)?(?:车窗|窗)|window\s+(?:all|front|rear|front-left|front-right|rear-left|rear-right)",
@@ -1094,11 +1100,16 @@ def _parse_chinese_window_commands(compact: str) -> list[dict[str, Any]]:
clauses = [clause for clause in CLAUSE_SEPARATOR.split(compact) if clause]
exclusion_pattern = re.compile(
r"^(?:主驾|主驾驶|驾驶位|司机位|副驾|副驾驶|乘客位|"
r"左前排|右前排|左后排|右后排|"
r"左前|前左|右前|前右|左后|后左|右后|后右)"
r"(?:车窗)?(?:除外|除去|不包括|不含)$"
r"(?:车窗|窗)?(?:除外|除去|不包括|不含)$"
)
for index, clause in enumerate(clauses):
if "车窗" not in clause:
if (
"车窗" not in clause
and "窗户" not in clause
and not _EXPLICIT_TARGET_PATTERNS["window"].search(clause)
):
continue
window_text = clause
if index + 1 < len(clauses) and exclusion_pattern.search(
@@ -2007,7 +2018,8 @@ def merge_native_can_intent(
]
constraints = list(result.get("semantic_constraints", []))
for match in re.finditer(
r"(?P<target>左前|前左|右前|前右|左后|后左|右后|后右)(?:车窗)?"
r"(?P<target>左前排|右前排|左后排|右后排|"
r"左前|前左|右前|前右|左后|后左|右后|后右)(?:车窗|窗)?"
r"(?:除外|除去|不包括|不含)",
utterance,
):
@@ -34,6 +34,8 @@ CHINESE_DIGITS = {
"六": 6, "七": 7, "八": 8, "九": 9, "十": 10,
}
WINDOW_TARGET_LABELS = {
"左前排": ("front_left",), "右前排": ("front_right",),
"左后排": ("rear_left",), "右后排": ("rear_right",),
"后排左侧": ("rear_left",), "后座左侧": ("rear_left",),
"二排左侧": ("rear_left",), "第二排左侧": ("rear_left",),
"后排右侧": ("rear_right",), "后座右侧": ("rear_right",),
@@ -57,6 +59,7 @@ WINDOW_TARGET_LABELS = {
NARROW_LOCATION = re.compile(
r"主驾|主驾驶|驾驶位|驾驶员|司机位|副驾|副驾驶|乘客位|乘员|"
r"前排|前座|后排|后座|二排|第二排|中排|第三排|三排|前舱|后舱|"
r"左前排|右前排|左后排|右后排|"
r"左前|前左|右前|前右|左后|后左|右后|后右|左侧|右侧|左边|右边"
)
UNSUPPORTED_SINGLE_HVAC_ZONE = re.compile(
+276 -7
View File
@@ -66,6 +66,12 @@ from cognitive_agent.application.mode_profile_intent import ( # noqa: E402
parse_mode_profile_delete,
parse_mode_profile_update,
)
from cognitive_agent.application.mode_scene_catalog import ( # noqa: E402
DEFAULT_N7_SCENE_SERVER_ID,
DEFAULT_N7_SCENE_SERVER_NAME,
ModeSceneCatalogPublisher,
build_mode_scene_catalog,
)
from cognitive_agent.application.mode_schedule import ModeSchedule, ModeScheduleRunner # noqa: E402
from cognitive_agent.application.mode_schedule_service import ModeScheduleServiceMixin # noqa: E402
from cognitive_agent.application.model_plan_adapter import ( # noqa: E402
@@ -179,6 +185,7 @@ MODE_PROFILE_UPDATE_EXPLICIT_TARGET_PATTERN = re.compile(
)
WIRED_CAN_TRANSPORT = "wired_can"
WIRELESS_MQTT_TRANSPORT = "wireless_mqtt"
LOCAL_CONTROL_BUDGET_MS = 1000
VEHICLE_CONTROL_TRANSPORTS = frozenset(
{WIRED_CAN_TRANSPORT, WIRELESS_MQTT_TRANSPORT}
)
@@ -300,6 +307,7 @@ CONTROL_CLAUSE_SUBJECT_PATTERN = re.compile(
r"座椅|座位|靠背|椅背|坐盆|坐垫|腿托|脚托|滑轨|"
r"通风|加热|按摩|零重力|半躺|女王副驾|休闲躺椅|"
r"遮阳帘|天幕帘|天幕|车窗|窗户|"
r"(?:左前|前左|右前|前右|左后|后左|右后|后右)(?:排)?窗|"
r"灯光|室内灯|车内灯|阅读灯|氛围灯|"
r"空调|压缩机|温度|风量|风速|内循环|外循环|"
r"车门|门锁|后备箱|前大灯|近光灯|远光灯|雾灯|转向灯|双闪|"
@@ -310,10 +318,17 @@ CONTROL_CLAUSE_PREDICATE_PATTERN = re.compile(
r"调整|调节|调到|调成|调为|调至|设置|设为|设到|切换|"
r"移动|移到|移至|前移|后移|上移|下移|升高|降低|抬起|放倒|"
r"调直|立直|竖直|归零|复位|锁上|上锁|落锁|解锁|"
r"零重力|半躺|最大|最小|最高|最低|最前|最后|到底|"
r"零重力|半躺|一半|半开|半关|最大|最小|最高|最低|最前|最后|到底|"
r"(?:\d+(?:\.\d+)?|[零一二三四五六七八九十百两半]+)"
r"(?:%|%|度|℃|档|级|秒|毫秒)"
)
# These verbs identify an independent request that may be outside the local
# vehicle registry. They are used only to split mixed sentences so an
# already deterministic clause never has to be repeated to a hosted model.
CLOUD_REQUEST_CLAUSE_PATTERN = re.compile(
r"播放|暂停播放|停止播放|导航|搜索|查找|查询|拨打|呼叫|发送|"
r"提醒|推荐|讲解|介绍|告诉|规划"
)
RESTORE_MODE_PROFILE_PREFIX = re.compile(
r"^(?:请(?:帮我)?|帮我)?(?:恢复|还原|重置|初始化)"
r"(?P<name>[^,。,.!!\s]{1,64}?模式)"
@@ -429,6 +444,17 @@ class SeatDemoService(ModeScheduleServiceMixin):
repository_root / "config" / "modes",
user_root=self.mode_profile_user_root,
)
self._mode_scene_catalog_publisher: ModeSceneCatalogPublisher | None = None
self._mode_scene_catalog_status: dict[str, Any] = {
"enabled": False,
"state": "disabled",
"path": None,
"vehicle_id": self.vehicle_profiles.default().id,
"server_id": DEFAULT_N7_SCENE_SERVER_ID,
"catalog_revision": None,
"scene_count": 0,
"last_error": None,
}
self.scene_templates = SceneTemplateRegistry(
repository_root / "config" / "scene_templates"
)
@@ -447,6 +473,88 @@ class SeatDemoService(ModeScheduleServiceMixin):
if hasattr(self, "conversations"):
self.conversations.preset_root = self._artifact_root / "user-presets"
def configure_mode_scene_catalog(
self,
path: Path,
*,
server_id: str = DEFAULT_N7_SCENE_SERVER_ID,
name: str = DEFAULT_N7_SCENE_SERVER_NAME,
) -> dict[str, Any]:
"""Enable the derived JSON catalog without changing YAML persistence."""
catalog_path = Path(path).expanduser()
if not catalog_path.is_absolute():
catalog_path = self.root / catalog_path
vehicle_id = self.vehicle_profiles.default().id
self._mode_scene_catalog_publisher = ModeSceneCatalogPublisher(
catalog_path,
vehicle_id,
server_id=server_id,
name=name,
)
self._mode_scene_catalog_status = {
"enabled": True,
"state": "initializing",
"path": str(catalog_path),
"vehicle_id": vehicle_id,
"server_id": self._mode_scene_catalog_publisher.server_id,
"catalog_revision": None,
"scene_count": 0,
"last_error": None,
}
# The registry notifies after create, update, delete, restore, and any
# explicit reload. The callback only updates a derived projection; YAML
# remains the authoritative store used by every existing execution path.
self.mode_profiles.add_change_listener(self._synchronize_mode_scene_catalog)
return self.mode_scene_catalog_status()
def _synchronize_mode_scene_catalog(self, registry: ModeProfileRegistry) -> None:
publisher = self._mode_scene_catalog_publisher
if publisher is None:
return
try:
catalog = publisher.publish(registry)
except Exception as exc: # noqa: BLE001 - derived export must not block controls
self._mode_scene_catalog_status.update(
{
"state": "error",
"last_error": str(exc),
}
)
return
self._mode_scene_catalog_status.update(
{
"state": "ready",
"catalog_revision": catalog["catalog_revision"],
"scene_count": catalog["scene_count"],
"last_error": None,
}
)
def mode_scene_catalog(self, vehicle_id: str | None = None) -> dict[str, Any]:
"""Return the current read-only mode-card catalog for an N7 client."""
resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id
publisher = self._mode_scene_catalog_publisher
if publisher is not None and resolved_vehicle_id == publisher.vehicle_id:
self._synchronize_mode_scene_catalog(self.mode_profiles)
snapshot = publisher.snapshot()
if snapshot is not None and self._mode_scene_catalog_status["state"] == "ready":
return snapshot
return build_mode_scene_catalog(
self.mode_profiles,
resolved_vehicle_id,
server_id=(
publisher.server_id
if publisher is not None
else DEFAULT_N7_SCENE_SERVER_ID
),
name=(publisher.name if publisher is not None else DEFAULT_N7_SCENE_SERVER_NAME),
)
def mode_scene_catalog_status(self) -> dict[str, Any]:
return deepcopy(self._mode_scene_catalog_status)
@property
def driver_only_ch1_execution_active(self) -> bool:
"""Return whether deployment policy authorizes only the CH1 vehicle path."""
@@ -603,6 +711,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
"scene_templates": self.scene_templates.public_templates(),
"mode_profile_storage": "persistent_yaml",
"mode_profile_user_root": str(self.mode_profile_user_root),
"mode_scene_catalog": self.mode_scene_catalog_status(),
"preset_storage": "conversation_durable",
"inferred_level_range": [1, 3],
"motion_position_range_pct": [0, 100],
@@ -1545,9 +1654,11 @@ class SeatDemoService(ModeScheduleServiceMixin):
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,
_cloud_instruction_context: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Validate and execute deterministic controls before hosted parsing."""
local_stage_started = time.perf_counter()
utterance = utterance.strip()
if not utterance:
raise ValueError("utterance must not be empty")
@@ -1576,6 +1687,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
_on_commands_validated=on_commands_validated,
_assistant_plan=assistant_plan,
_mode_profile_draft_context=mode_profile_draft_context,
_cloud_instruction_context=_cloud_instruction_context,
)
result.update(
{
@@ -1594,14 +1706,71 @@ class SeatDemoService(ModeScheduleServiceMixin):
vehicle_profile,
)
clause_results = self._local_clause_results(utterance, context)
unresolved_clauses = [
locally_parsed_clauses = [
clause
for clause, _, status in clause_results
if status != "unparsed"
]
locally_resolved_clauses = [
clause
for clause, _, status in clause_results
if status == "complete"
]
locally_unresolved_clauses = [
clause
for clause, _, status in clause_results
if status != "complete"
]
started = time.perf_counter()
supplied_routing = (
_cloud_instruction_context
if isinstance(_cloud_instruction_context, dict)
else {}
)
def routing_clauses(key: str) -> list[str]:
raw = supplied_routing.get(key)
if not isinstance(raw, list):
return []
return [
text
for item in raw[:64]
for text in [str(item).strip()[:1000]]
if text
]
def unique_clauses(*groups: list[str]) -> list[str]:
return list(dict.fromkeys(item for group in groups for item in group))
parsed_clauses = unique_clauses(
routing_clauses("parsed_clauses"),
locally_parsed_clauses,
)
resolved_clauses = unique_clauses(
routing_clauses("resolved_clauses"),
locally_resolved_clauses,
)
unresolved_clauses = locally_unresolved_clauses
routing_unresolved_clauses = unique_clauses(
routing_clauses("unresolved_clauses"),
locally_unresolved_clauses,
)
original_utterance = str(
supplied_routing.get("original_utterance") or utterance
).strip()[:1000]
natural_language_routing = {
"original_utterance": original_utterance,
"parsed_clauses": parsed_clauses,
"resolved_clauses": resolved_clauses,
"unresolved_clauses": routing_unresolved_clauses,
}
local_dispatch_elapsed_ms: float | None = None
def publish_local_commands(validated: dict[str, Any]) -> None:
nonlocal local_dispatch_elapsed_ms
local_dispatch_elapsed_ms = round(
(time.perf_counter() - local_stage_started) * 1000,
3,
)
if on_commands_validated is None:
return
event = deepcopy(validated)
@@ -1613,6 +1782,14 @@ class SeatDemoService(ModeScheduleServiceMixin):
"supplement_required": bool(
provider != "local" and unresolved_clauses
),
"natural_language_routing": deepcopy(
natural_language_routing
),
"local_control_budget_ms": LOCAL_CONTROL_BUDGET_MS,
"local_control_elapsed_ms": local_dispatch_elapsed_ms,
"local_control_within_budget": (
local_dispatch_elapsed_ms < LOCAL_CONTROL_BUDGET_MS
),
}
)
on_commands_validated(event)
@@ -1634,8 +1811,17 @@ class SeatDemoService(ModeScheduleServiceMixin):
_on_commands_validated=publish_local_commands,
_assistant_plan=assistant_plan,
_mode_profile_draft_context=mode_profile_draft_context,
_cloud_instruction_context=natural_language_routing,
)
elapsed_ms = round(
(time.perf_counter() - local_stage_started) * 1000,
3,
)
local_control_elapsed_ms = (
local_dispatch_elapsed_ms
if local_dispatch_elapsed_ms is not None
else elapsed_ms
)
elapsed_ms = round((time.perf_counter() - started) * 1000, 3)
supplement_required = bool(
provider != "local" and unresolved_clauses
)
@@ -1645,7 +1831,17 @@ class SeatDemoService(ModeScheduleServiceMixin):
"provider_requested": provider,
"preview_request_id": result["request_id"],
"preview_elapsed_ms": elapsed_ms,
"local_control_budget_ms": LOCAL_CONTROL_BUDGET_MS,
"local_control_elapsed_ms": local_control_elapsed_ms,
"local_control_within_budget": (
local_control_elapsed_ms < LOCAL_CONTROL_BUDGET_MS
),
"parsed_clauses": parsed_clauses,
"resolved_clauses": resolved_clauses,
"unresolved_clauses": unresolved_clauses,
"natural_language_routing": deepcopy(
natural_language_routing
),
"supplement_required": supplement_required,
}
)
@@ -1669,7 +1865,10 @@ class SeatDemoService(ModeScheduleServiceMixin):
"hardware_client_id": hardware_client_id,
"reasoning_effort": reasoning_effort,
"control_transport": control_transport,
"parsed_clauses": parsed_clauses,
"resolved_clauses": resolved_clauses,
"unresolved_clauses": unresolved_clauses,
"natural_language_routing": natural_language_routing,
"preview_control_signatures": result.get(
"parsed_control_signatures", []
),
@@ -1734,6 +1933,34 @@ class SeatDemoService(ModeScheduleServiceMixin):
for item in manifest.get("preview_control_signatures", [])
if isinstance(item, list)
}
manifest_routing = manifest.get("natural_language_routing")
natural_language_routing = (
deepcopy(manifest_routing)
if isinstance(manifest_routing, dict)
else {
"original_utterance": str(manifest.get("utterance") or ""),
"parsed_clauses": list(manifest.get("parsed_clauses") or []),
"resolved_clauses": list(manifest.get("resolved_clauses") or []),
"unresolved_clauses": unresolved_clauses,
}
)
routing_unresolved = natural_language_routing.get("unresolved_clauses")
natural_language_routing["unresolved_clauses"] = list(
dict.fromkeys(
[
str(item).strip()
for item in (
routing_unresolved
if isinstance(routing_unresolved, list)
else []
)
if str(item).strip()
]
+ unresolved_clauses
)
)
cloud_utterance = ",".join(unresolved_clauses)
natural_language_routing["cloud_input_utterance"] = cloud_utterance
def publish_cloud_commands(validated: dict[str, Any]) -> None:
if on_commands_validated is None:
@@ -1750,7 +1977,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
on_commands_validated(event)
supplement = self.process(
",".join(unresolved_clauses),
cloud_utterance,
provider=selected_provider,
conversation_id=str(manifest["conversation_id"]),
execution_mode=str(manifest.get("execution_mode") or "dry_run"),
@@ -1769,6 +1996,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
_model_parse_source="cloud_supplement",
_allow_empty_control_result=True,
_on_commands_validated=publish_cloud_commands,
_cloud_instruction_context=natural_language_routing,
)
merged = self._merge_native_can_phases(preview, supplement)
merged.update(
@@ -1785,6 +2013,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
"supplement_request_id": supplement["request_id"],
"supplement_required": False,
"unresolved_clauses": unresolved_clauses,
"natural_language_routing": natural_language_routing,
}
)
self._persist(merged)
@@ -2081,6 +2310,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
_speech_understanding: dict[str, Any] | None = None,
_assistant_plan: dict[str, Any] | None = None,
_mode_profile_draft_context: dict[str, Any] | None = None,
_cloud_instruction_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
@@ -2155,6 +2385,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
_speech_understanding,
assistant_plan=_assistant_plan,
mode_profile_draft_context=_mode_profile_draft_context,
cloud_instruction_context=_cloud_instruction_context,
)
if control_transport == WIRELESS_MQTT_TRANSPORT:
# The head unit owns both physical CAN channels in wireless mode.
@@ -2180,6 +2411,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
_speech_understanding,
assistant_plan=_assistant_plan,
mode_profile_draft_context=_mode_profile_draft_context,
cloud_instruction_context=_cloud_instruction_context,
)
if hardware_access_bypass or self._driver_only_vehicle_execution_active(
control_transport
@@ -2205,6 +2437,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
_speech_understanding,
assistant_plan=_assistant_plan,
mode_profile_draft_context=_mode_profile_draft_context,
cloud_instruction_context=_cloud_instruction_context,
)
authorized_client_id = self.hardware_access.ensure_authorized(hardware_client_id)
with self.hardware_execution.reserve():
@@ -2229,6 +2462,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
_speech_understanding,
assistant_plan=_assistant_plan,
mode_profile_draft_context=_mode_profile_draft_context,
cloud_instruction_context=_cloud_instruction_context,
)
def _process_once(
@@ -2256,6 +2490,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
scene_scope_utterance: str | None = None,
mode_creation_scopes: frozenset[str] | None = None,
mode_profile_draft_context: dict[str, Any] | None = None,
cloud_instruction_context: dict[str, Any] | None = None,
) -> dict[str, Any]:
control_transport = self._validate_control_transport(control_transport)
utterance = utterance.strip()
@@ -2589,6 +2824,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
assistant_plan=assistant_plan,
allow_scene_composition=allow_scene_composition,
scene_scope_utterance=scene_scope_utterance,
cloud_instruction_context=cloud_instruction_context,
)
scene_label_only = assistant_plan is not None and allow_scene_composition
dynamic_mode_stop = (
@@ -7450,6 +7686,11 @@ class SeatDemoService(ModeScheduleServiceMixin):
and CONTROL_CLAUSE_PREDICATE_PATTERN.search(value)
)
def looks_like_independent_request(value: str) -> bool:
return looks_like_control(value) or bool(
CLOUD_REQUEST_CLAUSE_PATTERN.search(value)
)
def split_independent_controls(
value: str,
separator_pattern: str,
@@ -7460,7 +7701,14 @@ class SeatDemoService(ModeScheduleServiceMixin):
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 (
looks_like_independent_request(current)
and looks_like_independent_request(next_item)
and (
looks_like_control(current)
or looks_like_control(next_item)
)
):
if current:
separated.append(current)
current = next_item
@@ -7771,6 +8019,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
assistant_plan: dict[str, Any] | None = None,
allow_scene_composition: bool = False,
scene_scope_utterance: str | None = None,
cloud_instruction_context: dict[str, Any] | None = None,
) -> tuple[dict[str, Any], str, str | None]:
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
if assistant_plan is not None and allow_scene_composition:
@@ -7936,9 +8185,29 @@ class SeatDemoService(ModeScheduleServiceMixin):
interpret_options = {}
if reasoning_effort is not None:
interpret_options["reasoning_effort"] = reasoning_effort
hosted_context = deepcopy(context)
if isinstance(cloud_instruction_context, dict):
routing_context = deepcopy(cloud_instruction_context)
else:
routing_context = {
"original_utterance": utterance,
"parsed_clauses": [
clause
for clause, _, status in clause_results
if status != "unparsed"
],
"resolved_clauses": [
clause
for clause, _, status in clause_results
if status == "complete"
],
"unresolved_clauses": fallback_clauses,
}
routing_context["cloud_input_utterance"] = fallback_utterance
hosted_context["natural_language_routing"] = routing_context
plan = self.openai.interpret(
fallback_utterance,
context,
hosted_context,
self.capability_context,
**interpret_options,
)
+179 -64
View File
@@ -15,7 +15,7 @@ import uuid
from contextlib import closing
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from typing import Any, Callable
from urllib.error import HTTPError, URLError
from urllib.parse import urlparse
from urllib.request import Request, urlopen
@@ -2387,6 +2387,7 @@ class SelfExtensionService:
conversation_turn: int = 0,
execution_mode: str = "dry_run",
provider_requested: str = "auto",
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
) -> dict[str, Any]:
"""Execute a live-NFC peripheral match through the main request API."""
@@ -2398,41 +2399,8 @@ class SelfExtensionService:
request_id = f"req-extension-{uuid.uuid4().hex}"
conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}"
hardware_request = execution_mode == "hardware"
if hardware_request:
gateway_result = self.execute_gateway("emit", {"slot": slot})
transport = str(gateway_result.get("transport") or self.gateway.transport)
gateway_acknowledged = gateway_result.get("gateway_acknowledged") is True
command = gateway_result.get("request")
operation_message = (
f"{capability.get('device_name')} · {capability.get('action')} 已发送至 "
"GSCU1B;网关已确认接收,外设物理效果待独立确认。"
)
delivery_status = "published"
else:
command = build_gateway_command("emit", {"slot": slot})
transport = "dry_run"
gateway_acknowledged = False
operation_message = (
f"{capability.get('device_name')} · {capability.get('action')} 已完成红外路由校验;"
"当前为 dry-run,未向 GSCU1B 发射。"
)
delivery_status = "planned"
transport = self.gateway.transport if hardware_request else "dry_run"
operation_ref = str(capability.get("id") or f"ir-slot-{slot}")
operation = {
"ref": operation_ref,
"domain": "self_extension",
"status": "succeeded",
"message": operation_message,
"telemetry": {
"transport": transport,
"slot": slot,
"delivery_status": delivery_status,
"gateway_acknowledged": gateway_acknowledged,
"physical_effect_confirmed": False,
"command": command,
},
}
function_config = {
"apiVersion": "cognitive-agent.io/v1alpha1",
"kind": "FunctionConfig",
@@ -2455,7 +2423,9 @@ class SelfExtensionService:
"action": capability.get("action"),
"slot": slot,
},
"parse_source": capability.get("parse_source", "local_rule"),
"parse_source": capability.get(
"parse_source", "local_rule"
),
"timeout_ms": int(self.command_timeout * 1000),
"failure_policy": "abort",
}
@@ -2479,6 +2449,78 @@ class SelfExtensionService:
],
},
}
if on_commands_validated is not None:
on_commands_validated(
{
"request_id": request_id,
"utterance": utterance,
"conversation_id": conversation_id,
"conversation_turn": conversation_turn,
"status": "processing",
"phase": "commands_validated",
"orchestration_route": "nfc_peripheral",
"provider_requested": provider_requested,
"provider_used": "nfc_peripheral_router",
"execution_mode": execution_mode,
"control_transport": (
"wireless_mqtt"
if transport == "mqtt"
else f"gscu_{transport}"
),
"trace_events": [
{
"stage": "nfc_peripheral_route",
"status": "succeeded",
"summary": "本地规则已完成红外能力路由与指令编译",
"duration_ms": 0,
}
],
"validation": [],
"function_config": function_config,
"function_configs": {"self_extension": function_config},
"command_plan": command_plan,
"command_plans": {"self_extension": command_plan},
"execution_report": None,
"operation_results": [],
"deferred_requests": [],
"domain_state": {},
"actuator_state": {},
}
)
if hardware_request:
gateway_result = self.execute_gateway("emit", {"slot": slot})
transport = str(gateway_result.get("transport") or self.gateway.transport)
gateway_acknowledged = gateway_result.get("gateway_acknowledged") is True
command = gateway_result.get("request")
operation_message = (
f"{capability.get('device_name')} · {capability.get('action')} 已发送至 "
"GSCU1B;网关已确认接收,外设物理效果待独立确认。"
)
delivery_status = "published"
else:
command = build_gateway_command("emit", {"slot": slot})
transport = "dry_run"
gateway_acknowledged = False
operation_message = (
f"{capability.get('device_name')} · {capability.get('action')} 已完成红外路由校验;"
"当前为 dry-run,未向 GSCU1B 发射。"
)
delivery_status = "planned"
operation = {
"ref": operation_ref,
"domain": "self_extension",
"status": "succeeded",
"message": operation_message,
"telemetry": {
"transport": transport,
"slot": slot,
"delivery_status": delivery_status,
"gateway_acknowledged": gateway_acknowledged,
"physical_effect_confirmed": False,
"command": command,
},
}
control_transport = (
"wireless_mqtt" if transport == "mqtt" else f"gscu_{transport}"
)
@@ -2548,6 +2590,7 @@ class SelfExtensionService:
conversation_turn: int = 0,
execution_mode: str = "dry_run",
provider_requested: str = "auto",
on_commands_validated: Callable[[dict[str, Any]], None] | None = None,
) -> dict[str, Any]:
"""Execute a validated ordered peripheral/delay plan for the live NFC."""
@@ -2642,6 +2685,76 @@ class SelfExtensionService:
}
)
function_config = {
"apiVersion": "cognitive-agent.io/v1alpha1",
"kind": "FunctionConfig",
"metadata": {
"name": f"{request_id}-sequence",
"labels": {"execution_scope": "nfc_bound_ir_peripheral"},
},
"spec": {
"domain": "self_extension",
"operations": function_operations,
},
}
command_plan = {
"apiVersion": "cognitive-agent.io/v1alpha1",
"kind": "CommandPlan",
"metadata": {"name": f"{request_id}-gscu-sequence"},
"spec": {
"domain": "self_extension",
"platform": "gscu1b",
"commands": commands,
},
}
if on_commands_validated is not None:
on_commands_validated(
{
"request_id": request_id,
"utterance": utterance,
"conversation_id": conversation_id,
"conversation_turn": conversation_turn,
"status": "processing",
"phase": "commands_validated",
"orchestration_route": "nfc_peripheral_sequence",
"provider_requested": provider_requested,
"provider_used": str(
plan_result.get("provider_used")
or f"gpt:{plan_result.get('model') or self.openai_model}"
),
"execution_mode": execution_mode,
"control_transport": (
"wireless_mqtt"
if transport == "mqtt"
else f"gscu_{transport}"
),
"trace_events": [
{
"stage": (
"local_peripheral_plan"
if str(plan_result.get("provider_used") or "").startswith(
"infrared_local"
)
else "gpt_peripheral_plan"
),
"status": "succeeded",
"summary": f"已编译并校验 {len(steps)} 个有序步骤",
"duration_ms": 0,
}
],
"validation": [],
"function_config": function_config,
"function_configs": {"self_extension": function_config},
"command_plan": command_plan,
"command_plans": {"self_extension": command_plan},
"execution_report": None,
"operation_results": [],
"deferred_requests": [],
"domain_state": {},
"actuator_state": {},
}
)
for index, step in enumerate(steps):
if step.get("kind") == "delay":
delay_ms = int(step.get("delay_ms", 0))
@@ -2781,28 +2894,6 @@ class SelfExtensionService:
)
report_status = "succeeded" if succeeded else "failed"
first_capability = next(iter(catalog_by_id.values()))
function_config = {
"apiVersion": "cognitive-agent.io/v1alpha1",
"kind": "FunctionConfig",
"metadata": {
"name": f"{request_id}-sequence",
"labels": {"execution_scope": "nfc_bound_ir_peripheral"},
},
"spec": {
"domain": "self_extension",
"operations": function_operations,
},
}
command_plan = {
"apiVersion": "cognitive-agent.io/v1alpha1",
"kind": "CommandPlan",
"metadata": {"name": f"{request_id}-gscu-sequence"},
"spec": {
"domain": "self_extension",
"platform": "gscu1b",
"commands": commands,
},
}
control_transport = (
"wireless_mqtt" if transport == "mqtt" else f"gscu_{transport}"
)
@@ -2988,12 +3079,22 @@ class SelfExtensionService:
item for item in vehicle_devices[:32] if isinstance(item, dict)
]
allowed_vehicle_commands = set(_vehicle_catalog_commands(vehicle_devices))
response_text = self._request_gpt_plan(
utterance,
planner_peripherals,
vehicle_devices,
api_key,
)
raw_routing = payload.get("natural_language_routing")
if isinstance(raw_routing, dict):
response_text = self._request_gpt_plan(
utterance,
planner_peripherals,
vehicle_devices,
api_key,
natural_language_routing=dict(raw_routing),
)
else:
response_text = self._request_gpt_plan(
utterance,
planner_peripherals,
vehicle_devices,
api_key,
)
try:
proposed = json.loads(response_text)
except json.JSONDecodeError as exc:
@@ -3120,6 +3221,8 @@ class SelfExtensionService:
peripherals: list[dict[str, Any]],
vehicle_devices: list[dict[str, Any]],
api_key: str,
*,
natural_language_routing: dict[str, Any] | None = None,
) -> str:
peripheral_ids = [
str(item.get("id"))
@@ -3172,6 +3275,9 @@ class SelfExtensionService:
"required": ["summary", "needs_confirmation", "question", "steps"],
}
system_prompt = f"""你是车载外设自扩展工作台的意图规划器。只输出 JSON 对象。
输入中的 natural_language_routing 是可信路由数据:original_utterance 为完整原句,
parsed_clauses/resolved_clauses 已由本地规则处理,只能为 unresolved_clauses 和当前 utterance
补充计划,禁止重复、替换或重新执行已解析子句。
peripheral 只能逐字引用目录中 status=learned 的短 capability_id,绝不能编造或改写 ID。
仅当同类存在多台可匹配的外设玩具且用户未说明目标时,设置 needs_confirmation=true 并追问;
此追问规则不适用于车窗、空调、座椅、电动风口等车端能力。
@@ -3218,6 +3324,15 @@ delay 步骤的 capability_id 和 command 必须为空字符串,不能位于
"text": json.dumps(
{
"utterance": utterance,
"natural_language_routing": (
natural_language_routing or {
"original_utterance": utterance,
"parsed_clauses": [],
"resolved_clauses": [],
"unresolved_clauses": [utterance],
"cloud_input_utterance": utterance,
}
),
"learned_peripheral_capabilities": peripherals,
"can_dev_vehicle_capabilities": vehicle_devices,
},
@@ -880,7 +880,7 @@ class VehicleCanSettings:
@dataclass(frozen=True)
class VehicleMqttSettings:
"""Wireless CAN0/CAN1 relay settings for the local head-unit broker."""
"""Wireless CAN0/CAN1 relay settings for the head-unit broker."""
enabled: bool
approval_required: bool
@@ -895,10 +895,19 @@ class VehicleMqttSettings:
password: str | None
tls_enabled: bool
feedback_timeout_ms: int
transport: str = "tcp"
websocket_path: str = "/mqtt"
@classmethod
def from_env(cls) -> "VehicleMqttSettings":
username = os.getenv("COGNITIVE_AGENT_VEHICLE_MQTT_USERNAME")
raw_transport = os.getenv(
"COGNITIVE_AGENT_VEHICLE_MQTT_TRANSPORT", "tcp"
).strip().lower()
transport = {
"ws": "websockets",
"websocket": "websockets",
}.get(raw_transport, raw_transport)
return cls(
enabled=_env_bool("COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED", default=True),
approval_required=_env_bool(
@@ -927,6 +936,10 @@ class VehicleMqttSettings:
feedback_timeout_ms=_env_int(
"COGNITIVE_AGENT_VEHICLE_MQTT_FEEDBACK_TIMEOUT_MS", 1_500
),
transport=transport,
websocket_path=os.getenv(
"COGNITIVE_AGENT_VEHICLE_MQTT_WEBSOCKET_PATH", "/mqtt"
).strip(),
)
@property
@@ -940,6 +953,11 @@ class VehicleMqttSettings:
and self.qos in {0, 1, 2}
and 5 <= self.keepalive_s <= 3_600
and 100 <= self.feedback_timeout_ms <= 10_000
and self.transport in {"tcp", "websockets"}
and (
self.transport != "websockets"
or self.websocket_path.startswith("/")
)
and _module_available("paho.mqtt.client")
)
@@ -984,6 +1002,10 @@ class VehicleMqttSettings:
"control_topic": self.control_topic,
"qos": self.qos,
"tls_enabled": self.tls_enabled,
"transport": self.transport,
"websocket_path": (
self.websocket_path if self.transport == "websockets" else None
),
"authentication_configured": bool(self.username),
"channels": {"seat": 0, "vehicle": 1},
"target_policy": "dual_channel_mqtt_can0_passenger_can1_vehicle",
@@ -627,14 +627,20 @@ class MqttCanTransport:
except ImportError as exc:
raise CanGatewayError("paho-mqtt is not installed") from exc
client_id = f"n7-cognitive-agent-{uuid.uuid4().hex[:12]}"
transport = getattr(self.settings, "transport", "tcp")
try:
return mqtt.Client(
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
client_id=client_id,
protocol=mqtt.MQTTv311,
transport=transport,
)
except (AttributeError, TypeError):
return mqtt.Client(client_id=client_id, protocol=mqtt.MQTTv311)
return mqtt.Client(
client_id=client_id,
protocol=mqtt.MQTTv311,
transport=transport,
)
def start(self) -> None:
with self._condition:
@@ -647,6 +653,14 @@ class MqttCanTransport:
client.on_connect = self._on_connect
client.on_disconnect = self._on_disconnect
client.on_message = self._on_message
if getattr(self.settings, "transport", "tcp") == "websockets":
if not hasattr(client, "ws_set_options"):
raise CanGatewayError(
"the installed MQTT client does not support WebSockets"
)
client.ws_set_options(
path=getattr(self.settings, "websocket_path", "/mqtt")
)
if self.settings.username:
client.username_pw_set(self.settings.username, self.settings.password)
if self.settings.tls_enabled:
@@ -135,6 +135,11 @@ The capability registry is the source of truth for available domains, capabiliti
target scopes, and execution availability. The runtime conversation context is the source of truth
for current actuator state and prior successful actions.
When runtime context contains natural_language_routing, original_utterance is the complete user
instruction, parsed_clauses/resolved_clauses have already been handled by deterministic local rules,
and unresolved_clauses are the only text you may supplement. Never repeat, replace, or reinterpret
controls represented by parsed_clauses; the current user message is the bounded cloud input.
Interpret the user's goal semantically. Consider causal language, explicit scope, current state,
follow-up references, and simultaneous compatible goals. Do not classify by isolated keywords.
Prefer the smallest reversible action that addresses the goal when the user leaves intensity open.
+208
View File
@@ -0,0 +1,208 @@
"""Serve the synchronized N7 mode-card catalog over HTTP and UDP discovery."""
from __future__ import annotations
import argparse
import json
import os
import threading
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from socketserver import BaseRequestHandler, ThreadingUDPServer
from typing import Any
from urllib.parse import unquote, urlparse
DISCOVERY_REQUEST = b"N7_SCENES_DISCOVER_V1"
DISCOVERY_RESPONSE = b"N7_SCENES_V1"
DEFAULT_CATALOG_PATH = (
Path(__file__).resolve().parents[2].parent / "scenses" / "demo-scenes.json"
)
class SceneCatalogError(ValueError):
"""Raised when the synchronized catalog cannot satisfy the public contract."""
class SceneCatalogSource:
def __init__(self, path: Path, server_id: str | None = None) -> None:
self.path = Path(path)
self.server_id = str(server_id or "").strip() or None
def load(self) -> dict[str, Any]:
try:
document = json.loads(self.path.read_text(encoding="utf-8"))
except OSError as exc:
raise SceneCatalogError(f"scene catalog is unavailable: {self.path}") from exc
except json.JSONDecodeError as exc:
raise SceneCatalogError(f"scene catalog is invalid JSON: {self.path}") from exc
if not isinstance(document, dict):
raise SceneCatalogError("scene catalog root must be an object")
if document.get("service") != "n7-scenes":
raise SceneCatalogError("scene catalog service must be n7-scenes")
if document.get("schema_version") != 1:
raise SceneCatalogError("scene catalog schema_version must be 1")
scenes = document.get("scenes")
if not isinstance(scenes, list):
raise SceneCatalogError("scene catalog scenes must be an array")
scene_ids = [
str(scene.get("id") or "").strip()
for scene in scenes
if isinstance(scene, dict)
]
if len(scene_ids) != len(scenes) or any(not scene_id for scene_id in scene_ids):
raise SceneCatalogError("every scene must be an object with a non-empty id")
if len(set(scene_ids)) != len(scene_ids):
raise SceneCatalogError("scene ids must be unique")
payload = dict(document)
if self.server_id is not None:
payload["server_id"] = self.server_id
payload["scene_count"] = len(scenes)
return payload
class N7SceneHttpServer(ThreadingHTTPServer):
daemon_threads = True
allow_reuse_address = True
def __init__(self, address: tuple[str, int], source: SceneCatalogSource) -> None:
self.catalog_source = source
super().__init__(address, N7SceneRequestHandler)
class N7SceneRequestHandler(BaseHTTPRequestHandler):
server_version = "N7SceneCatalog/1.0"
def do_GET(self) -> None: # noqa: N802 - stdlib handler API
path = unquote(urlparse(self.path).path)
if path == "/n7/scenes":
try:
self._json(self._catalog_source().load())
except SceneCatalogError as exc:
self._json(
{"error": "catalog_unavailable", "message": str(exc)},
HTTPStatus.SERVICE_UNAVAILABLE,
)
return
if path == "/n7/health":
try:
catalog = self._catalog_source().load()
except SceneCatalogError as exc:
self._json(
{"status": "unavailable", "message": str(exc)},
HTTPStatus.SERVICE_UNAVAILABLE,
)
return
self._json(
{
"status": "ok",
"service": catalog["service"],
"server_id": catalog.get("server_id"),
"catalog_revision": catalog.get("catalog_revision"),
"scene_count": catalog["scene_count"],
}
)
return
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
def do_OPTIONS(self) -> None: # noqa: N802 - stdlib handler API
path = unquote(urlparse(self.path).path)
if path not in {"/n7/scenes", "/n7/health"}:
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
self.send_response(HTTPStatus.NO_CONTENT)
self._common_headers()
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Accept, Content-Type")
self.send_header("Content-Length", "0")
self.end_headers()
def _catalog_source(self) -> SceneCatalogSource:
source = getattr(self.server, "catalog_source", None)
if not isinstance(source, SceneCatalogSource):
raise SceneCatalogError("scene catalog source is not configured")
return source
def _json(
self,
payload: object,
status: HTTPStatus = HTTPStatus.OK,
) -> None:
content = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self._common_headers()
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(content)))
self.end_headers()
self.wfile.write(content)
def _common_headers(self) -> None:
self.send_header("Cache-Control", "no-store")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("X-Content-Type-Options", "nosniff")
class N7SceneUdpServer(ThreadingUDPServer):
daemon_threads = True
allow_reuse_address = True
class N7SceneDiscoveryHandler(BaseRequestHandler):
def handle(self) -> None:
data, udp_socket = self.request
if bytes(data).strip() == DISCOVERY_REQUEST:
udp_socket.sendto(DISCOVERY_RESPONSE, self.client_address)
def main() -> None:
parser = argparse.ArgumentParser(description="Serve the N7 LAN mode scene catalog")
parser.add_argument("--bind", default=os.getenv("N7_SCENE_BIND", "0.0.0.0"))
parser.add_argument(
"--port",
type=int,
default=int(os.getenv("N7_SCENE_PORT", "8090")),
)
parser.add_argument(
"--server-id",
default=os.getenv("N7_SCENE_SERVER_ID", "n7-cognitive-agent"),
)
parser.add_argument(
"--catalog",
type=Path,
default=Path(
os.getenv("N7_SCENE_CATALOG_PATH", "").strip() or DEFAULT_CATALOG_PATH
),
)
args = parser.parse_args()
source = SceneCatalogSource(args.catalog, args.server_id)
source.load() # Fail before binding when the shared file is missing or malformed.
http_server = N7SceneHttpServer((args.bind, args.port), source)
udp_server = N7SceneUdpServer(
(args.bind, http_server.server_port),
N7SceneDiscoveryHandler,
)
udp_thread = threading.Thread(
target=udp_server.serve_forever,
name="n7-scene-discovery",
daemon=True,
)
udp_thread.start()
print(
f"N7 scene catalog: http://{args.bind}:{http_server.server_port}/n7/scenes",
flush=True,
)
try:
http_server.serve_forever()
except KeyboardInterrupt:
pass
finally:
http_server.server_close()
udp_server.shutdown()
udp_server.server_close()
udp_thread.join(timeout=2)
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff