feat: integrate production cockpit AI assistant

This commit is contained in:
vee
2026-09-08 17:46:07 +08:00
parent 5e228e12ae
commit 93103c2579
66 changed files with 11046 additions and 374 deletions
+514 -4
View File
@@ -9,6 +9,9 @@ from collections.abc import Callable
from http.server import ThreadingHTTPServer
from unittest.mock import patch
from cognitive_agent.application.assistant_capabilities import (
AssistantCapabilityCatalog,
)
from cognitive_agent.application.codex_assistant import (
ActiveCodexTurn,
CodexAssistantService,
@@ -41,6 +44,101 @@ ROOF_TRANSMITTANCE_PLAN: dict[str, object] = {
}
SAVED_MODE_CAPABILITY_SNAPSHOT: dict[str, object] = {
"schema_version": "n7web-assistant-capabilities/v1",
"catalog_revision": "saved-mode-http-r1",
"authority": "test runtime registry",
"vehicle": {"id": "denza_n7", "name": "腾势 N7"},
"device_groups": [],
"capabilities": [
{
"id": "climate.set_temperature",
"domain": "climate",
"assistant_callable": True,
"target_scopes": ["vehicle", "passenger"],
},
{
"id": "seat.set_backrest_recline",
"domain": "seat",
"assistant_callable": True,
"target_scopes": ["passenger"],
},
],
"modes": [
{
"id": "office",
"name": "办公模式",
"aliases": ["办公"],
"description": "用户保存的办公场景。",
"domains": ["seat", "climate"],
"operation_count": 2,
"operation_ids": [
"seat.set_backrest_recline",
"climate.set_temperature",
],
"operations": [
{
"domain": "seat",
"capability": "seat.set_backrest_recline",
"target_scope": "passenger",
"operation": "set",
"parameters": [
{"name": "position_pct", "value": 22},
{"name": "angle_deg", "value": 110},
],
},
{
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "passenger",
"operation": "set",
"parameters": [{"name": "temperature_c", "value": 25}],
},
],
"assistant_callable": True,
}
],
"scene_templates": [],
"learned_peripherals": [],
"service_surfaces": [],
"source_status": {
"core_capabilities": "ready",
"modes": "ready",
"scene_templates": "ready",
"learned_peripherals": "ready",
},
"semantics": {},
}
class StaticSavedModeCatalog:
@staticmethod
def snapshot(vehicle_id: str | None = None) -> dict[str, object]:
snapshot = json.loads(json.dumps(SAVED_MODE_CAPABILITY_SNAPSHOT))
snapshot["vehicle"]["id"] = vehicle_id or "denza_n7"
return snapshot
@staticmethod
def compact_context(snapshot: dict[str, object]) -> dict[str, object]:
return snapshot
@staticmethod
def scene_composition_context(snapshot: dict[str, object]) -> dict[str, object]:
return {"vehicle": snapshot["vehicle"], "capabilities": []}
@staticmethod
def fast_answer(message: str, snapshot: dict[str, object]) -> object:
return AssistantCapabilityCatalog.fast_answer(None, message, snapshot)
@staticmethod
def control_terms(_snapshot: dict[str, object]) -> tuple[str, ...]:
return ("办公模式", "办公", "空调", "温度", "座椅", "靠背")
@staticmethod
def query(snapshot: dict[str, object], _query: str) -> dict[str, object]:
return snapshot
def _answer_events(turn: ActiveCodexTurn, answer: str) -> None:
item_id = f"answer-{turn.turn_id}"
turn.events.put(
@@ -548,6 +646,355 @@ class TestCodexAssistantWebApi:
assert not AssistantApiHandler.service.calls
assert not FAKE_CODEX.bridge_results
def test_assistant_capability_snapshot_uses_requested_vehicle_id(self) -> None:
requested_vehicle_ids: list[str | None] = []
class RecordingCatalog:
def snapshot(self, vehicle_id: str | None = None) -> dict[str, object]:
requested_vehicle_ids.append(vehicle_id)
return {
"vehicle": {
"id": vehicle_id or "denza_n7",
"name": "测试车型",
},
"capabilities": [],
"modes": [],
"source_status": {"modes": "ready"},
}
@staticmethod
def compact_context(snapshot: dict[str, object]) -> dict[str, object]:
return {"vehicle": snapshot["vehicle"]}
@staticmethod
def scene_composition_context(snapshot: dict[str, object]) -> dict[str, object]:
return {"vehicle": snapshot["vehicle"]}
@staticmethod
def fast_answer(
_message: str,
_snapshot: dict[str, object],
) -> None:
return None
@staticmethod
def control_terms(_snapshot: dict[str, object]) -> tuple[str, ...]:
return ()
@staticmethod
def query(
snapshot: dict[str, object],
_query: str,
) -> dict[str, object]:
return snapshot
with patch.object(
AssistantApiHandler,
"assistant_capability_catalog",
RecordingCatalog(),
):
events = self.post_assistant(
{
"message": "解释一下 CAN FD",
"conversation_id": "conv-api-selected-vehicle-catalog",
"vehicle_id": "alternate_vehicle",
"history": [],
}
)
assert requested_vehicle_ids == ["alternate_vehicle"]
assert events[-1]["type"] == "answer.completed"
def test_saved_mode_detail_question_is_answered_from_catalog_over_http(
self,
) -> None:
message = "办公模式下会控制哪些车端设备,执行哪些动作?"
with patch.object(
AssistantApiHandler,
"assistant_capability_catalog",
StaticSavedModeCatalog(),
):
events = self.post_assistant(
{
"message": message,
"conversation_id": "conv-api-saved-mode-detail",
"execution_mode": "hardware",
}
)
assert events[0]["route"] == "capability_catalog"
assert events[0]["engine"] == "n7web-capability-registry"
assert events[-1]["route"] == "capability_catalog"
assert "办公模式" in events[-1]["answer"]
assert "副驾" in events[-1]["answer"]
assert "25" in events[-1]["answer"]
assert "空调" in events[-1]["answer"]
assert not AssistantApiHandler.service.calls
assert not FAKE_CODEX.turn_calls
assert not FAKE_CODEX.bridge_results
def test_unknown_saved_mode_detail_question_returns_a_bounded_answer_over_http(
self,
) -> None:
with patch.object(
AssistantApiHandler,
"assistant_capability_catalog",
StaticSavedModeCatalog(),
):
events = self.post_assistant(
{
"message": "火星模式中包含哪些设备和动作?",
"conversation_id": "conv-api-missing-mode-detail",
"execution_mode": "hardware",
}
)
assert events[0]["route"] == "capability_catalog"
assert events[-1]["route"] == "capability_catalog"
assert "火星模式" in events[-1]["answer"]
assert any(word in events[-1]["answer"] for word in ("未找到", "不存在"))
assert not AssistantApiHandler.service.calls
assert not FAKE_CODEX.turn_calls
assert not FAKE_CODEX.bridge_results
def test_saved_mode_update_uses_deterministic_management_route_over_http(
self,
) -> None:
message = "调整办公模式,把办公模式的副驾空调温度调整为26度。"
def update_mode(**kwargs: object) -> dict[str, object]:
AssistantApiHandler.service.calls.append(dict(kwargs))
callback = kwargs.get("on_commands_validated")
if callable(callback):
callback({"status": "processing", "request_id": "req-api-mode-update"})
return {
"request_id": "req-api-mode-update",
"utterance": message,
"conversation_id": kwargs["conversation_id"],
"status": "succeeded",
"execution_mode": "dry_run",
"hardware_execution_allowed": False,
"supplement_required": False,
"orchestration_route": "mode_profile_update",
"decision_summary": ["已更新长期模式“办公模式”。"],
"mode_profile": {
"id": "office",
"name": "办公模式",
"origin": "customized",
"domains": ["seat", "climate"],
"operation_count": 2,
},
"intent_ir": None,
"command_plans": {},
"can_commands": [],
}
with (
patch.object(
AssistantApiHandler,
"assistant_capability_catalog",
StaticSavedModeCatalog(),
),
patch.object(
AssistantApiHandler.service,
"preview_native_can",
side_effect=update_mode,
),
):
events = self.post_assistant(
{
"message": message,
"conversation_id": "conv-api-saved-mode-update",
"execution_mode": "hardware",
}
)
assert events[0]["route"] == "mode_profile_update"
assert events[0]["engine"] == "n7web-mode-profile-manager"
action = next(event for event in events if event["type"] == "action.completed")
assert action["success"] is True
assert action["payload"]["orchestration_route"] == "mode_profile_update"
assert action["payload"]["execution_mode"] == "dry_run"
assert action["payload"]["hardware_execution_allowed"] is False
assert events[-1]["route"] == "mode_profile_update"
assert "办公模式" in events[-1]["answer"]
assert "车辆控制未完成" not in events[-1]["answer"]
assert len(AssistantApiHandler.service.calls) == 1
assert AssistantApiHandler.service.calls[0]["utterance"] == message
assert not FAKE_CODEX.turn_calls
assert not FAKE_CODEX.bridge_results
def test_saved_mode_delete_uses_deterministic_management_route_over_http(
self,
) -> None:
message = "删除办公模式"
def delete_mode(**kwargs: object) -> dict[str, object]:
AssistantApiHandler.service.calls.append(dict(kwargs))
return {
"request_id": "req-api-mode-delete",
"utterance": message,
"conversation_id": kwargs["conversation_id"],
"status": "succeeded",
"execution_mode": "dry_run",
"hardware_execution_allowed": False,
"supplement_required": False,
"orchestration_route": "mode_profile_delete",
"decision_summary": ["已删除自定义模式“办公模式”。"],
"deleted_mode_profile": {
"id": "office",
"name": "办公模式",
"origin": "user",
"operation_count": 2,
},
"intent_ir": None,
"command_plans": {},
"can_commands": [],
}
with (
patch.object(
AssistantApiHandler,
"assistant_capability_catalog",
StaticSavedModeCatalog(),
),
patch.object(
AssistantApiHandler.service,
"preview_native_can",
side_effect=delete_mode,
),
):
events = self.post_assistant(
{
"message": message,
"conversation_id": "conv-api-saved-mode-delete",
"execution_mode": "hardware",
}
)
assert events[0]["route"] == "mode_profile_delete"
assert events[0]["engine"] == "n7web-mode-profile-manager"
action = next(event for event in events if event["type"] == "action.completed")
assert action["success"] is True
assert action["payload"]["orchestration_route"] == "mode_profile_delete"
assert action["payload"]["execution_mode"] == "dry_run"
assert action["payload"]["hardware_execution_allowed"] is False
assert events[-1]["route"] == "mode_profile_delete"
assert "办公模式" in events[-1]["answer"]
assert "删除" in events[-1]["answer"]
assert "仍保留" not in events[-1]["answer"]
assert len(AssistantApiHandler.service.calls) == 1
assert not FAKE_CODEX.turn_calls
assert not FAKE_CODEX.bridge_results
def test_oral_template_preview_request_is_zero_codex_over_http(self) -> None:
template_plan = {
"detected_domains": ["climate"],
"requests": [
{
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "vehicle",
"operation": "set",
"duration_ms": None,
"parameters": [{"name": "temperature_c", "value": 24}],
"reason": "观影模式模板温度。",
}
],
"ambiguities": [],
"decision_summary": ["命中测试观影模板。"],
"confidence": 1.0,
"metadata": {
"source": "scene_template",
"template_id": "cinema",
"template_revision": "http-template-r1",
},
}
class TemplateCatalog:
@staticmethod
def snapshot(vehicle_id: str | None = None) -> dict[str, object]:
return {
"vehicle": {"id": vehicle_id or "denza_n7", "name": "腾势 N7"},
"capabilities": [],
"modes": [],
"scene_templates": [
{
"id": "cinema",
"name": "观影模式",
"aliases": ["影院模式"],
"assistant_callable": True,
"template_revision": "http-template-r1",
"plan": template_plan,
}
],
"source_status": {
"modes": "ready",
"scene_templates": "ready",
},
}
@staticmethod
def compact_context(snapshot: dict[str, object]) -> dict[str, object]:
return {"vehicle": snapshot["vehicle"]}
@staticmethod
def scene_composition_context(snapshot: dict[str, object]) -> dict[str, object]:
return {
"composition_revision": "http-scene-r1",
"vehicle": snapshot["vehicle"],
"capabilities": [],
}
@staticmethod
def fast_answer(
_message: str,
_snapshot: dict[str, object],
) -> None:
return None
@staticmethod
def control_terms(_snapshot: dict[str, object]) -> tuple[str, ...]:
return ("空调", "车窗", "遮阳帘")
@staticmethod
def query(
snapshot: dict[str, object],
_query: str,
) -> dict[str, object]:
return snapshot
message = "请帮我创建一个观影模式,先给我看看吧?"
thread_count = len(FAKE_CODEX.thread_instructions)
with patch.object(
AssistantApiHandler,
"assistant_capability_catalog",
TemplateCatalog(),
):
events = self.post_assistant(
{
"message": message,
"conversation_id": "conv-api-oral-scene-template",
"execution_mode": "hardware",
}
)
assert events[0]["route"] == "scene_template"
assert events[0]["engine"] == "n7web-scene-template"
assert events[-1]["route"] == "scene_template"
assert not FAKE_CODEX.turn_calls
assert not FAKE_CODEX.bridge_results
assert len(FAKE_CODEX.thread_instructions) == thread_count
assert len(AssistantApiHandler.service.calls) == 1
call = AssistantApiHandler.service.calls[0]
assert call["utterance"] == message
assert call["provider"] == "local"
assert call["assistant_plan"]["metadata"] == template_plan["metadata"]
action = next(event for event in events if event["type"] == "action.completed")
assert action["success"] is True
assert action["payload"]["execution_mode"] == "dry_run"
assert action["payload"]["hardware_execution_allowed"] is False
def test_model_auth_failure_is_a_safe_terminal_ndjson_event(self) -> None:
secret = "deployment-secret-must-never-reach-the-browser"
provider_error = CodexAppServerError(
@@ -571,7 +1018,7 @@ class TestCodexAssistantWebApi:
assert events[-1] == {
"type": "error",
"error": "assistant_model_failed",
"message": "Codex AI 助手暂时没有响应,请稍后再试。",
"message": "小迪暂时没有响应,请稍后再试。",
}
serialized = json.dumps(events, ensure_ascii=False)
assert secret not in serialized
@@ -836,9 +1283,12 @@ class TestCodexAssistantWebApi:
}
)
assert events[0]["route"] == "agent"
assert events[-1]["answer"] == "这是 Codex 的测试回答。"
assert events[0]["route"] == "capability_catalog"
assert events[-1]["route"] == "capability_catalog"
assert "小憩模式" in events[-1]["answer"]
assert "未下发车辆控制" in events[-1]["answer"]
assert not AssistantApiHandler.service.calls
assert not FAKE_CODEX.turn_calls
def test_exact_movie_mode_request_streams_a_nine_action_preview(self) -> None:
events = self.post_assistant(
@@ -999,7 +1449,9 @@ class TestCodexAssistantWebApi:
assert capabilities["roof.set_shade_position"]["native_can_verified"] is True
assert capabilities["door.set_vehicle_lock"]["assistant_callable"] is False
assert capabilities["interior_light.set_wall_power"]["assistant_callable"] is False
assert len(payload["modes"]) == 13
# The endpoint is backed by the live registry and may legitimately
# include user-created profiles in addition to the 13 bundled modes.
assert len(payload["modes"]) >= 13
serialized = json.dumps(payload, ensure_ascii=False)
assert "approval_token" not in serialized
@@ -1036,6 +1488,64 @@ class TestCodexAssistantWebApi:
assert "车窗" in events[-1]["answer"]
assert "车门锁" in events[-1]["answer"]
def test_unsupported_vehicle_control_returns_clickable_suggestions_without_codex(
self,
) -> None:
FAKE_CODEX.available = False
events = self.post_assistant(
{
"message": "打开后备箱",
"conversation_id": "conv-api-fast-unsupported-control",
"history": [],
}
)
assert [event["type"] for event in events] == [
"message.accepted",
"work.started",
"answer.delta",
"control.suggestions",
"work.completed",
"answer.completed",
]
assert events[0]["route"] == "unsupported_control"
assert events[0]["engine"] == "n7web-capability-registry"
assert events[-1]["route"] == "unsupported_control"
assert events[-1]["topic"] == "unsupported_vehicle_control"
assert events[-1]["answer"].startswith(
"当前车端暂不支持该功能控制哦,您可以尝试以下控制:"
)
suggestion_event = events[3]["suggestions"]
assert suggestion_event == events[-1]["suggestions"]
assert {item["command"] for item in suggestion_event} == {
"关闭所有车窗",
"关闭遮阳帘",
"打开空调,将全车温度设置为24度",
}
assert all(item["capability_ids"] for item in suggestion_event)
assert not AssistantApiHandler.service.calls
assert not FAKE_CODEX.turn_calls
assert not FAKE_CODEX.bridge_results
clicked = self.post_assistant(
{
"message": "关闭所有车窗",
"conversation_id": "conv-api-click-quick-control",
"history": [],
}
)
assert clicked[0]["route"] == "vehicle_control"
assert clicked[0]["engine"] == "n7web-control-router"
assert clicked[-1]["route"] == "vehicle_control"
assert "关闭所有车窗" in clicked[-1]["answer"]
assert "升温" not in clicked[-1]["answer"]
assert len(AssistantApiHandler.service.calls) == 1
assert AssistantApiHandler.service.calls[0]["utterance"] == "关闭所有车窗"
assert not FAKE_CODEX.turn_calls
assert not FAKE_CODEX.bridge_results
def test_internal_capability_query_rejects_missing_turn_context(self) -> None:
status, content_type, body = self._request_json(
"/api/internal/codex/vehicle-control",