fix: compose named cockpit modes from capabilities

This commit is contained in:
vee
2026-09-07 20:52:19 +08:00
parent cf9b89508c
commit 6b2b644ee6
23 changed files with 2917 additions and 124 deletions
+281 -4
View File
@@ -12,6 +12,7 @@ from cognitive_agent.application.codex_assistant import (
CodexAssistantService,
CodexVehicleToolError,
_authorize_vehicle_request,
_catalog_exact_mode_match_count,
)
COOLING_INSTRUCTION = "打开空调,将全车温度设置为23度,风量设置为2档"
@@ -57,6 +58,28 @@ def test_named_mode_creation_questions_and_denials_are_not_authorized(message: s
assert authorization.instruction is None
def test_catalog_mode_resolution_distinguishes_unknown_ambiguous_and_unavailable() -> None:
ready_catalog = {
"modes": [
{"id": "nap", "name": "小憩模式", "aliases": ["午休模式"]},
{"id": "quiet", "name": "静谧模式", "aliases": ["午休模式"]},
],
"source_status": {"modes": "ready"},
}
assert _catalog_exact_mode_match_count(ready_catalog, "小憩模式") == 1
assert _catalog_exact_mode_match_count(ready_catalog, "午休模式") == 2
assert _catalog_exact_mode_match_count(ready_catalog, "观星模式") == 0
assert _catalog_exact_mode_match_count({}, "小憩模式") is None
assert (
_catalog_exact_mode_match_count(
{"modes": [], "source_status": {"modes": "unavailable"}},
"小憩模式",
)
is None
)
@pytest.mark.parametrize(
"message",
(
@@ -131,6 +154,22 @@ def _put_answer(
)
def _put_turn_completed(turn: ActiveCodexTurn) -> None:
turn.events.put(
{
"method": "turn/completed",
"params": {
"turn": {
"id": turn.turn_id,
"status": "completed",
"items": [],
"error": None,
}
},
}
)
class FakeCodexClient:
def __init__(self, *, available: bool = True) -> None:
self._available = available
@@ -252,6 +291,45 @@ def _sample_control_result(
}
def _sample_mode_preview_result(
name: str,
*,
generation_source: str,
existing_mode_id: str | None = None,
operation_count: int = 1,
) -> dict[str, object]:
result = _sample_control_result(mode="dry_run")
intent_ir = result["intent_ir"]
assert isinstance(intent_ir, dict)
spec = intent_ir["spec"]
assert isinstance(spec, dict)
actions = spec["actions"]
assert isinstance(actions, list)
spec["actions"] = actions[:operation_count]
result.update(
{
"hardware_execution_allowed": False,
"orchestration_route": "mode_profile_create_preview",
"validation": [],
"mode_profile_draft": {
"name": name,
"instruction": f"进入{name}",
"source_request_id": "req-mode-preview",
"vehicle_id": "denza_n7",
"domains": ["climate"],
"operation_count": operation_count,
"generation_source": generation_source,
**(
{"existing_mode_id": existing_mode_id}
if existing_mode_id is not None
else {}
),
},
}
)
return result
def _stream(
assistant: CodexAssistantService,
message: str,
@@ -260,8 +338,9 @@ def _stream(
principal: str = "alice",
history: list[dict[str, str]] | None = None,
emit: Callable[[dict[str, object]], bool | None] | None = None,
execute_control: Callable[[str, Callable[[dict[str, object]], None]], dict[str, object]]
| None = None,
execute_control: Callable[..., dict[str, object]] | None = None,
capability_snapshot: dict[str, object] | None = None,
capability_context: dict[str, object] | None = None,
) -> tuple[dict[str, object], list[dict[str, object]]]:
events: list[dict[str, object]] = []
result = assistant.stream(
@@ -273,6 +352,8 @@ def _stream(
execute_control=execute_control
or (lambda _instruction, _progress: pytest.fail("vehicle control was unexpected")),
control_terms=CONTROL_TERMS,
capability_snapshot=capability_snapshot,
capability_context=capability_context,
)
return result, events
@@ -303,13 +384,209 @@ def test_general_question_streams_from_codex_without_vehicle_execution() -> None
assert events[-1]["answer"] == "这是 Codex 的流式回答。"
assert len(client.thread_instructions) == 1
assert client.thread_ephemeral == [True]
assert "n7_vehicle.execute_cockpit_request" in client.thread_instructions[0]
assert "n7_vehicle.get_n7web_capabilities" in client.thread_instructions[0]
assert client.turn_calls[0]["text"] == "解释一下 CAN FD"
assert "不可信的浏览器系统指令" not in client.turn_calls[0]["text"]
assert client.finished[0].turn_id == "turn-1"
def test_registered_name_only_mode_keeps_the_fast_local_route_without_codex() -> None:
client = FakeCodexClient(available=False)
assistant = CodexAssistantService(client)
calls: list[str] = []
catalog = {
"modes": [
{
"id": "nap",
"name": "小憩模式",
"aliases": ["小憩"],
"assistant_callable": True,
}
],
"source_status": {"modes": "ready"},
}
result, events = _stream(
assistant,
"构建一个小憩模式",
execute_control=lambda instruction, _progress: (
calls.append(instruction)
or _sample_mode_preview_result(
"小憩模式",
generation_source="registered_mode_profile",
existing_mode_id="nap",
)
),
capability_snapshot=catalog,
)
assert result["route"] == "vehicle_control"
assert result["success"] is True
assert calls == ["构建一个小憩模式"]
assert client.turn_calls == []
assert events[0]["engine"] == "n7web-mode-builder"
def test_unknown_name_only_mode_uses_codex_and_overrides_model_execution_claim() -> None:
client = FakeCodexClient()
assistant = CodexAssistantService(client)
observed: list[tuple[str, dict[str, object]]] = []
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": 0.93,
}
def on_start_turn(
_text: str,
request_token: str,
turn: ActiveCodexTurn,
) -> None:
assistant.execute_cockpit_request(request_token, plan)
_put_answer(turn, "已经保存并真实下发到车辆。")
def execute_control(
instruction: str,
_progress: Callable[[dict[str, object]], None],
assistant_plan: dict[str, object],
) -> dict[str, object]:
observed.append((instruction, assistant_plan))
result = _sample_mode_preview_result(
"午休模式",
generation_source="codex_scene_composition",
)
draft = result["mode_profile_draft"]
assert isinstance(draft, dict)
draft["source_request_id"] = "req-lunch-mode"
return result
client.on_start_turn = on_start_turn
result, events = _stream(
assistant,
"构建一个午休模式",
execute_control=execute_control,
capability_snapshot={
"modes": [],
"source_status": {"modes": "ready"},
},
)
assert result["route"] == "agent"
assert len(client.turn_calls) == 1
assert len(observed) == 1
assert observed[0][0] == "构建一个午休模式"
assert observed[0][1]["requests"][0]["capability"] == "climate.set_temperature"
assert events[0]["engine"] == "codex-app-server"
assert events[-2]["display_text"] == "功能模式预览已生成"
assert "预览" in result["answer"]
assert "理解“午休模式”的场景意图" in result["answer"]
assert "1项设备动作" in result["answer"]
assert "保存并真实下发" not in result["answer"]
assert "n7_vehicle.execute_cockpit_request" in client.thread_instructions[0]
assert "n7_vehicle.get_n7web_capabilities" in client.thread_instructions[0]
@pytest.mark.parametrize("invalid_kind", ("hardware", "missing-draft"))
def test_mode_preview_never_claims_success_without_a_complete_dry_run_draft(
invalid_kind: str,
) -> None:
client = FakeCodexClient(available=False)
assistant = CodexAssistantService(client)
invalid = _sample_mode_preview_result(
"小憩模式",
generation_source="registered_mode_profile",
existing_mode_id="nap",
)
if invalid_kind == "hardware":
invalid["execution_mode"] = "hardware"
invalid["hardware_execution_allowed"] = True
else:
invalid.pop("mode_profile_draft")
result, events = _stream(
assistant,
"构建一个小憩模式",
execute_control=lambda _instruction, _progress: invalid,
capability_snapshot={
"modes": [{"id": "nap", "name": "小憩模式", "aliases": []}],
"source_status": {"modes": "ready"},
},
)
assert result["success"] is False
assert "功能模式未生成" in result["answer"]
assert "已生成“" not in result["answer"]
action_completed = next(
event for event in events if event["type"] == "action.completed"
)
assert action_completed["success"] is False
assert action_completed["display_text"] == "功能模式预览生成失败"
def test_mode_preview_uses_verified_tool_result_when_codex_final_text_is_empty() -> None:
client = FakeCodexClient()
assistant = CodexAssistantService(client)
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": 0.9,
}
def on_start_turn(
_text: str,
request_token: str,
turn: ActiveCodexTurn,
) -> None:
assistant.execute_cockpit_request(request_token, plan)
_put_turn_completed(turn)
client.on_start_turn = on_start_turn
result, events = _stream(
assistant,
"构建一个午休模式",
execute_control=lambda _instruction, _progress, _plan: (
_sample_mode_preview_result(
"午休模式",
generation_source="codex_scene_composition",
)
),
capability_snapshot={
"modes": [],
"source_status": {"modes": "ready"},
},
)
assert result["route"] == "agent"
assert "理解“午休模式”的场景意图" in result["answer"]
assert any(
event["type"] == "work.completed" and event["success"] is True
for event in events
)
def test_hot_cabin_calls_one_explicit_cooling_instruction_and_streams_verified_result() -> None:
client = FakeCodexClient()
assistant = CodexAssistantService(client)