feat: integrate production cockpit AI assistant
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user