mqtt切换为远程服务器部署,优化简单指令解析时长
This commit is contained in:
@@ -287,6 +287,59 @@ test("a completed NDJSON answer remains the authoritative visible response", asy
|
||||
assert.equal(harness.completed[0].error, null);
|
||||
});
|
||||
|
||||
test("assistant bridges request start and validated local controls to the console", async () => {
|
||||
const payload = {
|
||||
request_id: "req-local-mixed",
|
||||
utterance: "空调温度设置到23度,打开闪烁灯",
|
||||
execution_mode: "dry_run",
|
||||
provider_used: "local_rules+infrared_local_rules",
|
||||
validated_control_commands: [
|
||||
{
|
||||
operation: {
|
||||
id: "climate-temperature-1",
|
||||
capability: "climate.set_temperature",
|
||||
target: { zone: "driver" },
|
||||
parameters: { temperature_c: 23 },
|
||||
parse_source: "local_rule",
|
||||
},
|
||||
channel: "CAN1",
|
||||
actual_command: "ac temp driver 23",
|
||||
frames: [{
|
||||
label: "CAN1 帧",
|
||||
value: "0x1DE · 00 00 00 00 17 00 00 00",
|
||||
}],
|
||||
},
|
||||
],
|
||||
trace_events: [{ stage: "compile", duration_ms: 2 }],
|
||||
};
|
||||
const harness = createHarness([
|
||||
{ type: "validated_control_commands", payload },
|
||||
{ type: "answer.completed", route: "vehicle_control", answer: "模拟编排已完成。" },
|
||||
]);
|
||||
const starts = [];
|
||||
const validated = [];
|
||||
harness.sandbox.addEventListener("cognitive-agent:control-request-start", (event) => {
|
||||
starts.push(event.detail);
|
||||
});
|
||||
harness.sandbox.addEventListener("cognitive-agent:validated-can-commands", (event) => {
|
||||
validated.push(event.detail);
|
||||
});
|
||||
harness.input.value = payload.utterance;
|
||||
harness.input.dataset.speechUtteranceId = "speech-local-mixed";
|
||||
harness.input.dataset.speechValue = payload.utterance;
|
||||
|
||||
await harness.sandbox.CognitiveAgentAssistant.submit();
|
||||
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(starts)), [{
|
||||
utterance: payload.utterance,
|
||||
speech_utterance_id: "speech-local-mixed",
|
||||
speech_value: payload.utterance,
|
||||
}]);
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(validated)), [payload]);
|
||||
assert.equal(harness.requestCalls.length, 1);
|
||||
assert.equal(JSON.parse(harness.requestCalls[0].options.body).execution_mode, "dry_run");
|
||||
});
|
||||
|
||||
test("unsupported-control suggestions render once and click through as a new command", async () => {
|
||||
const suggestions = [
|
||||
{ id: "climate-on", label: "打开空调", command: "打开空调", domain: "climate" },
|
||||
|
||||
@@ -14,14 +14,14 @@ def test_console_contains_ai_assistant_surface():
|
||||
assert 'placeholder="向 AI助手提问或交代工作"' in html
|
||||
assert 'aria-label="发送消息"' in html
|
||||
assert 'href="/assistant-chat.css?v=codex-assistant-26"' in html
|
||||
assert 'src="/assistant-chat.js?v=codex-assistant-26"' in html
|
||||
assert 'src="/assistant-chat.js?v=codex-assistant-27"' in html
|
||||
|
||||
|
||||
def test_codex_assistant_assets_are_versioned_together_in_the_service_worker():
|
||||
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v277"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v282"' in service_worker
|
||||
assert '"/assistant-chat.css?v=codex-assistant-26"' in service_worker
|
||||
assert '"/assistant-chat.js?v=codex-assistant-26"' in service_worker
|
||||
assert '"/assistant-chat.js?v=codex-assistant-27"' in service_worker
|
||||
|
||||
|
||||
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
|
||||
@@ -33,6 +33,11 @@ def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
|
||||
assert 'event.type === "answer.delta"' in script
|
||||
assert '"work.started", "work.progress"' in script
|
||||
assert 'event.type === "work.completed"' in script
|
||||
assert 'event.type === "validated_control_commands"' in script
|
||||
assert '"cognitive-agent:validated-can-commands"' in script
|
||||
assert '"cognitive-agent:control-request-start"' in script
|
||||
assert "speech_utterance_id:" in script
|
||||
assert "speech_value:" in script
|
||||
assert 'Object.prototype.hasOwnProperty.call(event, "answer")' in script
|
||||
assert 'pendingController.abort("user_stop")' in script
|
||||
assert 'signal.addEventListener("abort", cancelReader, { once: true })' in script
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
import unittest
|
||||
from copy import deepcopy
|
||||
|
||||
from cognitive_agent.application.codex_assistant import (
|
||||
CodexAssistantService,
|
||||
_VehicleAuthorization,
|
||||
_VehicleTurnContext,
|
||||
)
|
||||
|
||||
|
||||
UTTERANCE = "空调温度设置到23度,打开闪烁灯"
|
||||
|
||||
|
||||
def compiled_mixed_result() -> dict[str, object]:
|
||||
return {
|
||||
"request_id": "req-assistant-local-mixed",
|
||||
"utterance": UTTERANCE,
|
||||
"status": "processing",
|
||||
"phase": "commands_validated",
|
||||
"provider_used": "local_rules+infrared_local_rules",
|
||||
"execution_mode": "dry_run",
|
||||
"control_transport": "wired_can",
|
||||
"function_configs": {
|
||||
"native_climate": {
|
||||
"spec": {
|
||||
"domain": "climate",
|
||||
"operations": [
|
||||
{
|
||||
"id": "climate-temperature-1",
|
||||
"capability": "climate.set_temperature",
|
||||
"target": {"zone": "driver"},
|
||||
"parameters": {"temperature_c": 23},
|
||||
"parse_source": "local_rule",
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
"self_extension": {
|
||||
"spec": {
|
||||
"domain": "self_extension",
|
||||
"operations": [
|
||||
{
|
||||
"id": "infrared-1",
|
||||
"capability": "infrared.emit",
|
||||
"target": {"device": "toy-001", "name": "闪灯玩具"},
|
||||
"parameters": {"action": "开启", "slot": 1},
|
||||
"parse_source": "local_rule",
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
"command_plans": {
|
||||
"self_extension": {
|
||||
"spec": {
|
||||
"domain": "self_extension",
|
||||
"commands": [
|
||||
{
|
||||
"operation_ref": "unrelated-operation",
|
||||
"message": "Native_0xBAD",
|
||||
"signals": {
|
||||
"debug_command": "must-not-cross-tool-boundary",
|
||||
"frame_id": "0xBAD",
|
||||
"data_hex": "DE AD BE EF",
|
||||
},
|
||||
},
|
||||
{
|
||||
"operation_ref": "infrared-1",
|
||||
"bus": "TCP",
|
||||
"message": "infrared.emit",
|
||||
"signals": {"slot": 1},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
"native_climate": {
|
||||
"spec": {
|
||||
"domain": "climate",
|
||||
"commands": [
|
||||
{
|
||||
"operation_ref": "climate-temperature-1",
|
||||
"bus": "body_can",
|
||||
"message": "Native_0x1DE",
|
||||
"signals": {
|
||||
"debug_command": "ac temp driver 23",
|
||||
"frame_id": "0x1DE",
|
||||
"data_hex": "00 00 00 00 17 00 00 00",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
"trace_events": [
|
||||
{"stage": "intent_parse", "duration_ms": 2},
|
||||
{"stage": "context_resolve", "duration_ms": 1},
|
||||
{"stage": "domain_route", "duration_ms": 1},
|
||||
{"stage": "function_generate", "duration_ms": 2},
|
||||
{"stage": "schema_validate", "duration_ms": 1},
|
||||
{"stage": "compile", "duration_ms": 2},
|
||||
{"stage": "execute", "duration_ms": 999},
|
||||
],
|
||||
"raw_can": "must-not-cross-tool-boundary",
|
||||
}
|
||||
|
||||
|
||||
class ValidatedControlProjectionTest(unittest.TestCase):
|
||||
def test_projection_exposes_only_exact_display_commands(self) -> None:
|
||||
projection = CodexAssistantService._validated_control_payload(
|
||||
compiled_mixed_result()
|
||||
)
|
||||
|
||||
self.assertIsNotNone(projection)
|
||||
assert projection is not None
|
||||
commands = projection["validated_control_commands"]
|
||||
assert isinstance(commands, list)
|
||||
self.assertEqual(len(commands), 2)
|
||||
by_capability = {
|
||||
command["operation"]["capability"]: command for command in commands
|
||||
}
|
||||
climate = by_capability["climate.set_temperature"]
|
||||
infrared = by_capability["infrared.emit"]
|
||||
self.assertEqual(climate["actual_command"], "ac temp driver 23")
|
||||
self.assertEqual(
|
||||
climate["frames"],
|
||||
[
|
||||
{
|
||||
"label": "CAN1 帧",
|
||||
"value": "0x1DE · 00 00 00 00 17 00 00 00",
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertEqual(infrared["actual_command"], "infrared emit 1")
|
||||
self.assertEqual(
|
||||
infrared["frames"],
|
||||
[
|
||||
{
|
||||
"label": "GSCU1B TCP",
|
||||
"value": '{"action":"emit","data":{"no":1},"type":"infrared"}',
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
[event["stage"] for event in projection["trace_events"]],
|
||||
[
|
||||
"intent_parse",
|
||||
"context_resolve",
|
||||
"domain_route",
|
||||
"function_generate",
|
||||
"schema_validate",
|
||||
"compile",
|
||||
],
|
||||
)
|
||||
serialized = json.dumps(projection, ensure_ascii=False)
|
||||
self.assertNotIn("command_plans", serialized)
|
||||
self.assertNotIn("data_hex", serialized)
|
||||
self.assertNotIn("raw_can", serialized)
|
||||
self.assertNotIn("must-not-cross-tool-boundary", serialized)
|
||||
|
||||
def test_projection_drops_secrets_and_sensitive_nested_keys(self) -> None:
|
||||
result = compiled_mixed_result()
|
||||
secret = "projection-secret-token"
|
||||
climate_config = result["function_configs"]["native_climate"]
|
||||
climate_config["spec"]["domain"] = f"climate-{secret}"
|
||||
operation = climate_config["spec"]["operations"][0]
|
||||
operation["target"].update(
|
||||
{
|
||||
"api_key": "API-LEAK",
|
||||
"cookie": "COOKIE-LEAK",
|
||||
}
|
||||
)
|
||||
operation["parameters"].update(
|
||||
{
|
||||
"credential": "CRED-LEAK",
|
||||
"access_key": "ACCESS-LEAK",
|
||||
}
|
||||
)
|
||||
operation["parse_source"] = f"local-{secret}"
|
||||
result["command_plans"]["native_climate"]["spec"]["commands"][0][
|
||||
"signals"
|
||||
]["debug_command"] = f"ac temp driver 23 {secret}"
|
||||
|
||||
projection = CodexAssistantService._validated_control_payload(
|
||||
result,
|
||||
secret=secret,
|
||||
)
|
||||
|
||||
self.assertIsNotNone(projection)
|
||||
assert projection is not None
|
||||
serialized = json.dumps(projection, ensure_ascii=False)
|
||||
for forbidden in (
|
||||
secret,
|
||||
"API-LEAK",
|
||||
"COOKIE-LEAK",
|
||||
"CRED-LEAK",
|
||||
"ACCESS-LEAK",
|
||||
"api_key",
|
||||
"credential",
|
||||
"access_key",
|
||||
):
|
||||
self.assertNotIn(forbidden, serialized)
|
||||
climate = next(
|
||||
item
|
||||
for item in projection["validated_control_commands"]
|
||||
if item["operation"]["capability"] == "climate.set_temperature"
|
||||
)
|
||||
self.assertEqual(climate["domain"], "")
|
||||
self.assertNotIn("parse_source", climate["operation"])
|
||||
self.assertEqual(climate["operation"]["target"], {"zone": "driver"})
|
||||
self.assertEqual(
|
||||
climate["operation"]["parameters"],
|
||||
{"temperature_c": 23},
|
||||
)
|
||||
self.assertEqual(climate["actual_command"], "")
|
||||
|
||||
def test_projection_drops_operation_when_identity_contains_secret(self) -> None:
|
||||
result = compiled_mixed_result()
|
||||
secret = "projection-secret-token"
|
||||
operation = result["function_configs"]["native_climate"]["spec"][
|
||||
"operations"
|
||||
][0]
|
||||
operation["id"] = f"climate-{secret}"
|
||||
result["command_plans"]["native_climate"]["spec"]["commands"][0][
|
||||
"operation_ref"
|
||||
] = operation["id"]
|
||||
|
||||
projection = CodexAssistantService._validated_control_payload(
|
||||
result,
|
||||
secret=secret,
|
||||
)
|
||||
|
||||
self.assertIsNotNone(projection)
|
||||
assert projection is not None
|
||||
serialized = json.dumps(projection, ensure_ascii=False)
|
||||
self.assertNotIn(secret, serialized)
|
||||
self.assertEqual(
|
||||
[
|
||||
item["operation"]["capability"]
|
||||
for item in projection["validated_control_commands"]
|
||||
],
|
||||
["infrared.emit"],
|
||||
)
|
||||
|
||||
def test_duplicate_operation_refs_never_cross_configuration_boundaries(self) -> None:
|
||||
result = {
|
||||
"function_configs": {
|
||||
"native_climate": {
|
||||
"spec": {
|
||||
"domain": "climate",
|
||||
"operations": [
|
||||
{
|
||||
"id": "op-1",
|
||||
"capability": "climate.set_temperature",
|
||||
"target": {"zone": "driver"},
|
||||
"parameters": {"temperature_c": 23},
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
"native_seat": {
|
||||
"spec": {
|
||||
"domain": "seat",
|
||||
"operations": [
|
||||
{
|
||||
"id": "op-1",
|
||||
"capability": "seat.set_heating",
|
||||
"target": {"zone": "passenger"},
|
||||
"parameters": {"level": 2},
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
"command_plans": {
|
||||
"native_seat": {
|
||||
"spec": {
|
||||
"domain": "seat",
|
||||
"commands": [
|
||||
{
|
||||
"operation_ref": "op-1",
|
||||
"bus": "seat_can0",
|
||||
"signals": {
|
||||
"debug_command": "seat passenger heat 2"
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
projection = CodexAssistantService._validated_control_payload(result)
|
||||
|
||||
self.assertIsNotNone(projection)
|
||||
assert projection is not None
|
||||
commands = projection["validated_control_commands"]
|
||||
self.assertEqual(len(commands), 1)
|
||||
self.assertEqual(
|
||||
commands[0]["operation"]["capability"],
|
||||
"seat.set_heating",
|
||||
)
|
||||
self.assertEqual(commands[0]["actual_command"], "seat passenger heat 2")
|
||||
|
||||
def test_compiled_semantic_operation_is_kept_without_debug_command(self) -> None:
|
||||
result = deepcopy(compiled_mixed_result())
|
||||
climate_command = result["command_plans"]["native_climate"]["spec"][
|
||||
"commands"
|
||||
][0]
|
||||
climate_command["signals"].pop("debug_command")
|
||||
|
||||
projection = CodexAssistantService._validated_control_payload(result)
|
||||
|
||||
self.assertIsNotNone(projection)
|
||||
assert projection is not None
|
||||
climate = next(
|
||||
item
|
||||
for item in projection["validated_control_commands"]
|
||||
if item["operation"]["capability"] == "climate.set_temperature"
|
||||
)
|
||||
self.assertEqual(climate["actual_command"], "")
|
||||
self.assertEqual(
|
||||
climate["frames"][0]["value"],
|
||||
"0x1DE · 00 00 00 00 17 00 00 00",
|
||||
)
|
||||
|
||||
def test_direct_stream_emits_projection_before_progress_and_completion(self) -> None:
|
||||
client = type("UnavailableClient", (), {"available": False})()
|
||||
assistant = CodexAssistantService(client)
|
||||
events: list[dict[str, object]] = []
|
||||
|
||||
def execute_control(instruction, progress):
|
||||
self.assertEqual(instruction, UTTERANCE)
|
||||
result = compiled_mixed_result()
|
||||
progress(result)
|
||||
return {**result, "status": "succeeded"}
|
||||
|
||||
result = assistant.stream(
|
||||
UTTERANCE,
|
||||
conversation_id="conv-validated-projection",
|
||||
principal="local-test",
|
||||
history=[],
|
||||
emit=events.append,
|
||||
execute_control=execute_control,
|
||||
control_terms=("空调", "闪烁灯"),
|
||||
quick_control_commands=[UTTERANCE],
|
||||
)
|
||||
|
||||
event_types = [event["type"] for event in events]
|
||||
validated_index = event_types.index("validated_control_commands")
|
||||
self.assertLess(validated_index, event_types.index("action.progress"))
|
||||
self.assertLess(validated_index, event_types.index("action.completed"))
|
||||
self.assertLess(validated_index, event_types.index("answer.completed"))
|
||||
self.assertEqual(result["route"], "vehicle_control")
|
||||
self.assertEqual(result["success"], True)
|
||||
|
||||
safe_result = CodexAssistantService._safe_vehicle_result(
|
||||
compiled_mixed_result()
|
||||
)
|
||||
self.assertNotIn("command_plans", safe_result)
|
||||
self.assertNotIn("validated_control_commands", safe_result)
|
||||
|
||||
def test_agent_tool_path_queues_projection_for_the_browser_thread(self) -> None:
|
||||
client = type("UnavailableClient", (), {"available": False})()
|
||||
assistant = CodexAssistantService(client)
|
||||
request_token = "projection-secret-token"
|
||||
|
||||
def execute_control(instruction, progress):
|
||||
self.assertEqual(instruction, UTTERANCE)
|
||||
result = compiled_mixed_result()
|
||||
progress(result)
|
||||
return {**result, "status": "succeeded"}
|
||||
|
||||
context = _VehicleTurnContext(
|
||||
original_message=UTTERANCE,
|
||||
execute_control=execute_control,
|
||||
authorization=_VehicleAuthorization(
|
||||
instruction=UTTERANCE,
|
||||
reason="explicit_registered_control",
|
||||
),
|
||||
capability_snapshot={},
|
||||
query_capabilities=lambda _snapshot, _query: {},
|
||||
lock=threading.Lock(),
|
||||
)
|
||||
assistant._tool_contexts[request_token] = context
|
||||
|
||||
safe_result = assistant.execute_vehicle_control(request_token, UTTERANCE)
|
||||
|
||||
queued_events = []
|
||||
while not context.events.empty():
|
||||
queued_events.append(context.events.get_nowait())
|
||||
event_types = [event["type"] for event in queued_events]
|
||||
self.assertEqual(event_types[0], "action.started")
|
||||
self.assertLess(
|
||||
event_types.index("validated_control_commands"),
|
||||
event_types.index("action.progress"),
|
||||
)
|
||||
self.assertLess(
|
||||
event_types.index("validated_control_commands"),
|
||||
event_types.index("action.completed"),
|
||||
)
|
||||
self.assertNotIn("command_plans", safe_result)
|
||||
self.assertNotIn(request_token, json.dumps(queued_events, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -395,6 +395,8 @@ def _stream(
|
||||
history: list[dict[str, str]] | None = None,
|
||||
emit: Callable[[dict[str, object]], bool | None] | None = None,
|
||||
execute_control: Callable[..., dict[str, object]] | None = None,
|
||||
preview_control: Callable[..., dict[str, object]] | None = None,
|
||||
supplement_control: Callable[..., dict[str, object]] | None = None,
|
||||
capability_snapshot: dict[str, object] | None = None,
|
||||
capability_context: dict[str, object] | None = None,
|
||||
mode_profile_draft_context: dict[str, object] | None = None,
|
||||
@@ -409,6 +411,8 @@ def _stream(
|
||||
emit=emit or (lambda event: events.append(event)),
|
||||
execute_control=execute_control
|
||||
or (lambda _instruction, _progress: pytest.fail("vehicle control was unexpected")),
|
||||
preview_control=preview_control,
|
||||
supplement_control=supplement_control,
|
||||
control_terms=CONTROL_TERMS,
|
||||
capability_snapshot=capability_snapshot,
|
||||
capability_context=capability_context,
|
||||
@@ -453,6 +457,15 @@ def test_general_question_streams_from_codex_without_vehicle_execution() -> None
|
||||
assert "Codex AI 助手" not in client.thread_instructions[0]
|
||||
assert client.thread_ephemeral == [True]
|
||||
assert client.turn_calls[0]["text"] == "解释一下 CAN FD"
|
||||
assert client.turn_calls[0]["application_context"][
|
||||
"natural_language_routing"
|
||||
] == {
|
||||
"original_utterance": "解释一下 CAN FD",
|
||||
"parsed_clauses": [],
|
||||
"resolved_clauses": [],
|
||||
"unresolved_clauses": ["解释一下 CAN FD"],
|
||||
"cloud_input_utterance": "解释一下 CAN FD",
|
||||
}
|
||||
assert "不可信的浏览器系统指令" not in client.turn_calls[0]["text"]
|
||||
assert client.finished[0].turn_id == "turn-1"
|
||||
|
||||
@@ -991,34 +1004,52 @@ def test_hot_cabin_calls_one_explicit_cooling_instruction_and_streams_verified_r
|
||||
assistant.execute_vehicle_control(token, COOLING_INSTRUCTION)
|
||||
|
||||
|
||||
def test_explicit_vehicle_request_without_tool_call_fails_closed() -> None:
|
||||
client = FakeCodexClient()
|
||||
def test_explicit_vehicle_request_uses_local_route_without_codex() -> None:
|
||||
client = FakeCodexClient(available=False)
|
||||
assistant = CodexAssistantService(client)
|
||||
control_calls: list[str] = []
|
||||
|
||||
def on_start_turn(
|
||||
_text: str,
|
||||
_request_token: str,
|
||||
turn: ActiveCodexTurn,
|
||||
) -> None:
|
||||
_put_answer(turn, "已经为你打开遮阳帘。", deltas=("已经为你", "打开遮阳帘。"))
|
||||
def execute_control(
|
||||
instruction: str,
|
||||
progress: Callable[[dict[str, object]], None],
|
||||
) -> dict[str, object]:
|
||||
control_calls.append(instruction)
|
||||
progress({"status": "processing", "request_id": "req-local-shade"})
|
||||
return {
|
||||
"request_id": "req-local-shade",
|
||||
"status": "succeeded",
|
||||
"execution_mode": "dry_run",
|
||||
"utterance": instruction,
|
||||
}
|
||||
|
||||
client.on_start_turn = on_start_turn
|
||||
result, events = _stream(assistant, "打开遮阳帘")
|
||||
result, events = _stream(
|
||||
assistant,
|
||||
"打开遮阳帘",
|
||||
execute_control=execute_control,
|
||||
preview_control=execute_control,
|
||||
supplement_control=lambda *_args: pytest.fail(
|
||||
"fully local control must not invoke cloud supplement"
|
||||
),
|
||||
)
|
||||
|
||||
assert result["answer"].startswith("车辆控制未完成")
|
||||
assert "未向车辆下发任何操作" in result["answer"]
|
||||
assert "已经为你" not in str(events)
|
||||
assert control_calls == ["打开遮阳帘"]
|
||||
assert result["route"] == "vehicle_control"
|
||||
assert result["success"] is True
|
||||
assert "打开遮阳帘" in result["answer"]
|
||||
assert [event["type"] for event in events] == [
|
||||
"message.accepted",
|
||||
"work.started",
|
||||
"action.started",
|
||||
"action.progress",
|
||||
"action.completed",
|
||||
"answer.delta",
|
||||
"work.completed",
|
||||
"answer.completed",
|
||||
]
|
||||
assert events[2]["success"] is False
|
||||
assert events[4]["success"] is False
|
||||
assert events[-1]["answer"] == result["answer"]
|
||||
assert events[0]["engine"] == "n7web-control-router"
|
||||
assert events[1]["display_text"] == "正在本地解析并下发明确控制指令……"
|
||||
assert not client.thread_instructions
|
||||
assert not client.turn_calls
|
||||
|
||||
|
||||
def test_exact_server_owned_quick_control_executes_without_codex() -> None:
|
||||
@@ -1057,22 +1088,253 @@ def test_exact_server_owned_quick_control_executes_without_codex() -> None:
|
||||
assert not client.turn_calls
|
||||
|
||||
|
||||
def test_quick_control_requires_exact_normalized_command_match() -> None:
|
||||
client = FakeCodexClient()
|
||||
def test_explicit_control_does_not_require_exact_quick_card_match() -> None:
|
||||
client = FakeCodexClient(available=False)
|
||||
assistant = CodexAssistantService(client)
|
||||
control_calls: list[str] = []
|
||||
|
||||
def execute_control(
|
||||
instruction: str,
|
||||
_progress: Callable[[dict[str, object]], None],
|
||||
) -> dict[str, object]:
|
||||
control_calls.append(instruction)
|
||||
return {
|
||||
"request_id": "req-explicit-local",
|
||||
"status": "succeeded",
|
||||
"execution_mode": "dry_run",
|
||||
"utterance": instruction,
|
||||
}
|
||||
|
||||
result, events = _stream(
|
||||
assistant,
|
||||
"请关闭所有车窗",
|
||||
execute_control=lambda instruction, _progress: control_calls.append(instruction),
|
||||
execute_control=execute_control,
|
||||
preview_control=execute_control,
|
||||
supplement_control=lambda *_args: pytest.fail(
|
||||
"fully local control must not invoke cloud supplement"
|
||||
),
|
||||
quick_control_commands=["关闭所有车窗"],
|
||||
)
|
||||
|
||||
assert events[0]["route"] == "agent"
|
||||
assert result["answer"].startswith("车辆控制未完成")
|
||||
assert not control_calls
|
||||
assert client.turn_calls
|
||||
assert events[0]["route"] == "vehicle_control"
|
||||
assert result["success"] is True
|
||||
assert control_calls == ["请关闭所有车窗"]
|
||||
assert not client.turn_calls
|
||||
|
||||
|
||||
def test_mixed_control_streams_local_result_before_unresolved_codex_turn() -> None:
|
||||
client = FakeCodexClient()
|
||||
assistant = CodexAssistantService(client)
|
||||
order: list[str] = []
|
||||
original = "空调温度设置为23度并把遮阳帘调整到适合观影"
|
||||
local_clause = "空调温度设置为23度"
|
||||
unresolved_clause = "把遮阳帘调整到适合观影"
|
||||
preview_result = {
|
||||
"request_id": "req-local-preview",
|
||||
"status": "partially_succeeded",
|
||||
"execution_mode": "dry_run",
|
||||
"utterance": original,
|
||||
"provider_used": "local_rules",
|
||||
"local_control_budget_ms": 1000,
|
||||
"local_control_elapsed_ms": 18.0,
|
||||
"local_control_within_budget": True,
|
||||
"natural_language_routing": {
|
||||
"original_utterance": original,
|
||||
"parsed_clauses": [local_clause],
|
||||
"resolved_clauses": [local_clause],
|
||||
"unresolved_clauses": [unresolved_clause],
|
||||
},
|
||||
"function_configs": {
|
||||
"climate": {
|
||||
"spec": {
|
||||
"domain": "climate",
|
||||
"operations": [
|
||||
{
|
||||
"id": "local-temperature",
|
||||
"capability": "climate.set_temperature",
|
||||
"target": {"zone": "driver"},
|
||||
"parameters": {"temperature_c": 23},
|
||||
"parse_source": "local_rule",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"command_plans": {
|
||||
"climate": {
|
||||
"spec": {
|
||||
"domain": "climate",
|
||||
"commands": [
|
||||
{
|
||||
"operation_ref": "local-temperature",
|
||||
"bus": "body-can1",
|
||||
"message": "climate.set_temperature",
|
||||
"signals": {"debug_command": "ac temp driver 23"},
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
def preview_control(
|
||||
instruction: str,
|
||||
progress: Callable[[dict[str, object]], None],
|
||||
) -> dict[str, object]:
|
||||
assert instruction == original
|
||||
order.append("local_preview")
|
||||
progress(preview_result)
|
||||
return preview_result
|
||||
|
||||
def supplement_control(
|
||||
instruction: str,
|
||||
progress: Callable[[dict[str, object]], None],
|
||||
plan: dict[str, object],
|
||||
) -> dict[str, object]:
|
||||
assert instruction == unresolved_clause
|
||||
assert plan["requests"][0]["capability"] == "roof.set_shade_position"
|
||||
order.append("cloud_supplement")
|
||||
result = {
|
||||
"request_id": "req-cloud-supplement",
|
||||
"status": "succeeded",
|
||||
"execution_mode": "dry_run",
|
||||
"utterance": original,
|
||||
}
|
||||
progress(result)
|
||||
return result
|
||||
|
||||
cloud_plan = {
|
||||
"detected_domains": ["roof"],
|
||||
"requests": [
|
||||
{
|
||||
"domain": "roof",
|
||||
"capability": "roof.set_shade_position",
|
||||
"target_scope": "vehicle",
|
||||
"operation": "set",
|
||||
"duration_ms": None,
|
||||
"parameters": [{"name": "position_pct", "value": 100}],
|
||||
"reason": "补足未解析的观影遮光需求。",
|
||||
}
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["只处理未解析的遮阳帘子句。"],
|
||||
"confidence": 0.9,
|
||||
}
|
||||
|
||||
def on_start_turn(
|
||||
text: str,
|
||||
request_token: str,
|
||||
turn: ActiveCodexTurn,
|
||||
) -> None:
|
||||
order.append("cloud_turn")
|
||||
assert text == unresolved_clause
|
||||
assistant.execute_cockpit_request(request_token, cloud_plan)
|
||||
_put_answer(turn, "本地温度已先行处理,遮阳帘补充控制已完成。")
|
||||
|
||||
client.on_start_turn = on_start_turn
|
||||
result, events = _stream(
|
||||
assistant,
|
||||
original,
|
||||
preview_control=preview_control,
|
||||
supplement_control=supplement_control,
|
||||
)
|
||||
|
||||
assert order == ["local_preview", "cloud_turn", "cloud_supplement"]
|
||||
assert client.turn_calls[0]["text"] == unresolved_clause
|
||||
assert client.turn_calls[0]["application_context"][
|
||||
"natural_language_routing"
|
||||
] == {
|
||||
"original_utterance": original,
|
||||
"parsed_clauses": [local_clause],
|
||||
"resolved_clauses": [local_clause],
|
||||
"unresolved_clauses": [unresolved_clause],
|
||||
"cloud_input_utterance": unresolved_clause,
|
||||
}
|
||||
validated_index = next(
|
||||
index
|
||||
for index, event in enumerate(events)
|
||||
if event["type"] == "validated_control_commands"
|
||||
)
|
||||
cloud_progress_index = next(
|
||||
index
|
||||
for index, event in enumerate(events)
|
||||
if event.get("display_text")
|
||||
== "本地明确子句已处理,正在补充未解析子句……"
|
||||
)
|
||||
assert validated_index < cloud_progress_index
|
||||
assert result["route"] == "agent"
|
||||
|
||||
|
||||
def test_fully_unresolved_control_starts_bounded_cloud_turn_and_fails_closed() -> None:
|
||||
client = FakeCodexClient()
|
||||
assistant = CodexAssistantService(client)
|
||||
instruction = "把遮阳帘调整到适合观影"
|
||||
preview_result = {
|
||||
"request_id": "req-unresolved-preview",
|
||||
"preview_request_id": "req-unresolved-preview",
|
||||
"status": "no_action",
|
||||
"execution_mode": "dry_run",
|
||||
"utterance": instruction,
|
||||
"provider_used": "local_rules",
|
||||
"function_configs": {},
|
||||
"command_plans": {},
|
||||
"local_control_budget_ms": 1000,
|
||||
"local_control_elapsed_ms": 4.0,
|
||||
"local_control_within_budget": True,
|
||||
"supplement_required": True,
|
||||
"natural_language_routing": {
|
||||
"original_utterance": instruction,
|
||||
"parsed_clauses": [],
|
||||
"resolved_clauses": [],
|
||||
"unresolved_clauses": [instruction],
|
||||
},
|
||||
}
|
||||
|
||||
def preview_control(
|
||||
text: str,
|
||||
_progress: Callable[[dict[str, object]], None],
|
||||
) -> dict[str, object]:
|
||||
assert text == instruction
|
||||
return preview_result
|
||||
|
||||
def on_start_turn(
|
||||
text: str,
|
||||
_request_token: str,
|
||||
turn: ActiveCodexTurn,
|
||||
) -> None:
|
||||
assert text == instruction
|
||||
# The browser must never receive this unsupported execution claim
|
||||
# because no guarded vehicle tool call follows it.
|
||||
_put_answer(turn, "遮阳帘已经调整完成。")
|
||||
|
||||
client.on_start_turn = on_start_turn
|
||||
result, events = _stream(
|
||||
assistant,
|
||||
instruction,
|
||||
preview_control=preview_control,
|
||||
supplement_control=lambda *_args: pytest.fail(
|
||||
"a model answer without a tool call must not execute control"
|
||||
),
|
||||
)
|
||||
|
||||
assert client.turn_calls[0]["text"] == instruction
|
||||
assert client.turn_calls[0]["application_context"][
|
||||
"natural_language_routing"
|
||||
] == {
|
||||
"original_utterance": instruction,
|
||||
"parsed_clauses": [],
|
||||
"resolved_clauses": [],
|
||||
"unresolved_clauses": [instruction],
|
||||
"cloud_input_utterance": instruction,
|
||||
}
|
||||
assert not any(
|
||||
event["type"] == "validated_control_commands" for event in events
|
||||
)
|
||||
assert result == {
|
||||
"route": "agent",
|
||||
"answer": "车辆控制未完成:AI 助手没有调用受控执行链,未向车辆下发任何操作。",
|
||||
}
|
||||
assert events[-1]["answer"] == result["answer"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -35,10 +35,10 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
|
||||
):
|
||||
assert asset in html
|
||||
assert f'"{asset}"' in service_worker
|
||||
assert html.index("/assistant-chat.js?v=codex-assistant-26") < html.index(
|
||||
assert html.index("/assistant-chat.js?v=codex-assistant-27") < html.index(
|
||||
"/energy-monitor.js?v=energy-monitor-4"
|
||||
)
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v277"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v282"' in service_worker
|
||||
|
||||
|
||||
def test_energy_monitor_uses_the_fifth_shared_dock_icon_and_starts_closed() -> None:
|
||||
@@ -160,7 +160,7 @@ def test_energy_monitor_tracks_ai_and_direct_control_activity() -> None:
|
||||
assert "completeControlActivity" in script
|
||||
assert 'event.type === "action.progress" && event.payload' in assistant
|
||||
assert 'new CustomEvent("cognitive-agent:vehicle-control-activity"' in assistant
|
||||
assert 'new CustomEvent("cognitive-agent:validated-can-commands"' not in assistant
|
||||
assert '"cognitive-agent:validated-can-commands"' in assistant
|
||||
|
||||
|
||||
def test_energy_monitor_reads_persistent_seat_state_for_both_seats() -> None:
|
||||
|
||||
@@ -41,13 +41,13 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
|
||||
|
||||
for asset in (
|
||||
"/styles.css?v=dual-seat-23",
|
||||
"/console-v2.css?v=delivery-state-66",
|
||||
"/console-v2.css?v=delivery-state-68",
|
||||
"/self-extension.css?v=gscu-tcp-43",
|
||||
"/speech-input.css?v=sensevoice-7",
|
||||
):
|
||||
assert asset in html
|
||||
assert f'"{asset}"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v277"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v282"' in service_worker
|
||||
|
||||
|
||||
def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None:
|
||||
|
||||
@@ -42,10 +42,22 @@ class _FakeMqttClient:
|
||||
self.on_message = None
|
||||
self.subscriptions: list[tuple[str, int]] = []
|
||||
self.published: list[tuple[str, str, int, bool]] = []
|
||||
self.websocket_options: list[dict[str, object]] = []
|
||||
self.credentials: tuple[str, str | None] | None = None
|
||||
self.tls_configured = False
|
||||
|
||||
def reconnect_delay_set(self, **_kwargs) -> None:
|
||||
return None
|
||||
|
||||
def ws_set_options(self, **kwargs: object) -> None:
|
||||
self.websocket_options.append(kwargs)
|
||||
|
||||
def username_pw_set(self, username: str, password: str | None) -> None:
|
||||
self.credentials = (username, password)
|
||||
|
||||
def tls_set(self) -> None:
|
||||
self.tls_configured = True
|
||||
|
||||
def connect_async(self, host: str, port: int, keepalive: int) -> None:
|
||||
self.connection = (host, port, keepalive)
|
||||
|
||||
@@ -89,6 +101,8 @@ def _settings():
|
||||
"password": None,
|
||||
"tls_enabled": False,
|
||||
"feedback_timeout_ms": 100,
|
||||
"transport": "tcp",
|
||||
"websocket_path": "/mqtt",
|
||||
}
|
||||
values["public_info"] = lambda: {
|
||||
"enabled": True,
|
||||
@@ -109,9 +123,31 @@ class MqttCanPayloadTests(unittest.TestCase):
|
||||
self.assertEqual(settings.condition_topic, "n7/n7-headunit/can/state")
|
||||
self.assertEqual(settings.control_topic, "n7/n7-headunit/can/command")
|
||||
self.assertFalse(settings.approval_required)
|
||||
self.assertEqual(settings.transport, "tcp")
|
||||
self.assertEqual(settings.websocket_path, "/mqtt")
|
||||
self.assertEqual(info["state_contract"], "n7-can-signals/v1")
|
||||
self.assertEqual(info["control_contract"], "n7-headunit-can-command/v1")
|
||||
|
||||
def test_websocket_transport_settings_are_loaded_from_environment(self) -> None:
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"COGNITIVE_AGENT_VEHICLE_MQTT_HOST": "mqtt.example.test",
|
||||
"COGNITIVE_AGENT_VEHICLE_MQTT_PORT": "443",
|
||||
"COGNITIVE_AGENT_VEHICLE_MQTT_TRANSPORT": "websocket",
|
||||
"COGNITIVE_AGENT_VEHICLE_MQTT_WEBSOCKET_PATH": "/mqtt",
|
||||
"COGNITIVE_AGENT_VEHICLE_MQTT_TLS": "true",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
settings = VehicleMqttSettings.from_env()
|
||||
|
||||
self.assertTrue(settings.adapter_configured)
|
||||
self.assertEqual(settings.transport, "websockets")
|
||||
self.assertEqual(settings.websocket_path, "/mqtt")
|
||||
self.assertTrue(settings.tls_enabled)
|
||||
self.assertEqual(settings.public_info()["transport"], "websockets")
|
||||
|
||||
def test_explicit_shared_approval_policy_still_locks_mqtt_control(self) -> None:
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
@@ -461,6 +497,29 @@ class MqttCanTransportTests(unittest.TestCase):
|
||||
def tearDown(self) -> None:
|
||||
self.transport.close()
|
||||
|
||||
def test_websocket_client_applies_transport_path_authentication_and_tls(self) -> None:
|
||||
client = _FakeMqttClient()
|
||||
settings = _settings()
|
||||
settings.transport = "websockets"
|
||||
settings.websocket_path = "/mqtt"
|
||||
settings.username = "n7web"
|
||||
settings.password = "secret"
|
||||
settings.tls_enabled = True
|
||||
transport = MqttCanTransport(settings)
|
||||
|
||||
with patch("paho.mqtt.client.Client", return_value=client) as constructor:
|
||||
transport.start()
|
||||
try:
|
||||
self.assertEqual(
|
||||
constructor.call_args.kwargs["transport"], "websockets"
|
||||
)
|
||||
self.assertEqual(client.websocket_options, [{"path": "/mqtt"}])
|
||||
self.assertEqual(client.credentials, ("n7web", "secret"))
|
||||
self.assertTrue(client.tls_configured)
|
||||
self.assertEqual(client.connection, ("127.0.0.1", 1883, 30))
|
||||
finally:
|
||||
transport.close()
|
||||
|
||||
def test_condition_frames_use_existing_decoder_and_snapshot_contract(self) -> None:
|
||||
self.client.emit(
|
||||
"n7/n7-headunit/can/state",
|
||||
|
||||
@@ -137,7 +137,8 @@ def test_main_parsed_control_panel_renders_validated_infrared_operations() -> No
|
||||
assert 'capability === "infrared.emit"' in console
|
||||
assert 'channel: "IR"' in console
|
||||
assert 'frameLabel: "GSCU1B TCP"' in console
|
||||
assert "已解析控制指令" in console
|
||||
assert "下发控制指令" in console
|
||||
assert "已解析控制指令" not in console
|
||||
assert 'li[data-can-channel="IR"]' in styles
|
||||
assert "console-v2.js?v=delivery-state-62" in html
|
||||
assert "console-v2.js?v=local-first-66" in html
|
||||
assert 'speech_semantic: "语音语义"' in console
|
||||
|
||||
@@ -195,9 +195,15 @@ def test_left_sidebar_exposes_collapsed_parsed_can_command_panel() -> None:
|
||||
assert 'canCommandPanel.id = "v2CanCommandPanel"' in console
|
||||
assert 'id="v2CanCommandCount">0</strong>' in console
|
||||
assert 'id="v2CanCommandList"' in console
|
||||
assert "canCommandPanel.open" not in console
|
||||
assert 'id="v2CommandTiming"' in console
|
||||
assert "panel.open = true;" in console
|
||||
assert "if (ui.sidebarCollapsed) setSidebarCollapsed(false);" in console
|
||||
assert "function parsedCanCommandForOperation(operation = {})" in console
|
||||
assert "function parsedCanCommands(result = {})" in console
|
||||
assert "function semanticControlInstruction(operation = {})" in console
|
||||
assert "function compiledCommandsForOperation(result, configKey, operation = {})" in console
|
||||
assert "function parsedCommandTimingRows(result = {}, timingState = {})" in console
|
||||
assert "result.validated_control_commands" in console
|
||||
assert "getParsedCanCommands: (result) => parsedCanCommands(result)" in console
|
||||
assert "const configurations = result.function_configs || {};" in console
|
||||
assert "if (!configurationEntries.length && result.function_config)" in console
|
||||
@@ -206,8 +212,18 @@ def test_left_sidebar_exposes_collapsed_parsed_can_command_panel() -> None:
|
||||
assert 'const command = (category, text, channel = "CAN1", frame = "")' in console
|
||||
assert 'data-can-channel="${escapeHtml(item.channel)}"' in console
|
||||
assert 'class="v2-can-command-channel"' in console
|
||||
assert 'class="v2-can-command-layer v2-can-command-semantic"' in console
|
||||
assert 'class="v2-can-command-layer v2-can-command-actual"' in console
|
||||
assert console.index("语义控制指令") < console.index("实际控制指令")
|
||||
assert 'recordTiming: true' in console
|
||||
assert 'reveal: true' in console
|
||||
assert 'window.addEventListener("cognitive-agent:speech-state"' in console
|
||||
assert 'window.addEventListener("cognitive-agent:speech-result"' in console
|
||||
assert 'window.addEventListener("cognitive-agent:control-request-start"' in console
|
||||
assert 'window.addEventListener("cognitive-agent:result"' in console
|
||||
assert '$("#requestForm")?.addEventListener("submit"' in console
|
||||
assert "function commandResultMatchesTimedRequest(result = {})" in console
|
||||
assert "matchesTimedRequest && !validatedCommandsVisible" in console
|
||||
|
||||
for command_contract in (
|
||||
"door rear ${action}",
|
||||
@@ -246,6 +262,9 @@ def test_left_sidebar_exposes_collapsed_parsed_can_command_panel() -> None:
|
||||
assert ".v2-can-command-list li code" in styles
|
||||
assert ".v2-can-command-channel" in styles
|
||||
assert ".v2-can-command-delivery" in styles
|
||||
assert ".v2-command-timing" in styles
|
||||
assert ".v2-can-command-semantic" in styles
|
||||
assert ".v2-can-command-actual" in styles
|
||||
assert '.v2-can-command-delivery[data-delivery-state="submitted"]' in styles
|
||||
assert 'li[data-can-channel="CAN0"]' in styles
|
||||
assert ".v2-sidebar-collapsed .v2-can-command-panel" in styles
|
||||
@@ -297,10 +316,12 @@ def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands()
|
||||
"process.stdout.write(JSON.stringify(parsedCanCommands(sample)));"
|
||||
)
|
||||
completed = subprocess.run(
|
||||
[node, "-e", script],
|
||||
[node, "-"],
|
||||
input=script,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert json.loads(completed.stdout) == [
|
||||
@@ -310,6 +331,8 @@ def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands()
|
||||
"command": "seat passenger backrest position 36",
|
||||
"frame": "0x300 · 01 12 <SEQ_LO> <SEQ_HI> 10 0E 30 75",
|
||||
"parseSource": "local_rule",
|
||||
"semantic": "seat.set_backrest_recline(target=front-right, angle_deg=120, position_pct=36)",
|
||||
"operationRef": "",
|
||||
"deliveryState": "not_submitted",
|
||||
"deliveryLabel": "未下发",
|
||||
"deliveryTransport": "有线 CAN",
|
||||
@@ -319,6 +342,8 @@ def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands()
|
||||
"category": "空调温度",
|
||||
"command": "ac temp driver 24",
|
||||
"parseSource": "cloud_supplement",
|
||||
"semantic": "climate.set_temperature(target=driver, temperature_c=24)",
|
||||
"operationRef": "",
|
||||
"deliveryState": "not_submitted",
|
||||
"deliveryLabel": "未下发",
|
||||
"deliveryTransport": "有线 CAN",
|
||||
@@ -326,6 +351,335 @@ def test_parsed_can_command_panel_combines_can0_seat_and_can1_vehicle_commands()
|
||||
]
|
||||
|
||||
|
||||
def test_local_mixed_command_uses_compiled_can_and_ir_details_with_stage_timings() -> None:
|
||||
node = shutil.which("node")
|
||||
if node is None:
|
||||
return
|
||||
console = source("console-v2.js")
|
||||
start = console.index(" const normalizedCanTarget")
|
||||
end = console.index("\n\n function renderParsedCanCommands", start)
|
||||
helpers = console[start:end]
|
||||
sample = {
|
||||
"provider_used": "local_rules",
|
||||
"function_configs": {
|
||||
"native_climate": {
|
||||
"spec": {
|
||||
"domain": "climate",
|
||||
"operations": [
|
||||
{
|
||||
"id": "climate-temperature-1",
|
||||
"capability": "climate.set_temperature",
|
||||
"target": {"zone": "driver"},
|
||||
"parameters": {"temperature_c": 23},
|
||||
"parse_source": "local_rule",
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
"self_extension": {
|
||||
"spec": {
|
||||
"domain": "self_extension",
|
||||
"operations": [
|
||||
{
|
||||
"id": "infrared-1",
|
||||
"capability": "infrared.emit",
|
||||
"target": {"device": "toy-001", "name": "闪灯玩具"},
|
||||
"parameters": {"slot": 1, "action": "开启"},
|
||||
"parse_source": "local_rule",
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
# Deliberately reverse the plan order and include a distracting command;
|
||||
# the renderer must join only on the exact operation_ref.
|
||||
"command_plans": {
|
||||
"self_extension": {
|
||||
"spec": {
|
||||
"domain": "self_extension",
|
||||
"commands": [
|
||||
{
|
||||
"operation_ref": "unrelated-ir",
|
||||
"message": "infrared.emit",
|
||||
"signals": {"slot": 9},
|
||||
},
|
||||
{
|
||||
"operation_ref": "infrared-1",
|
||||
"bus": "TCP",
|
||||
"message": "infrared.emit",
|
||||
"signals": {"slot": 1},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
"native_climate": {
|
||||
"spec": {
|
||||
"domain": "climate",
|
||||
"commands": [
|
||||
{
|
||||
"operation_ref": "climate-temperature-1",
|
||||
"bus": "body_can",
|
||||
"message": "Native_0x1DE",
|
||||
"signals": {
|
||||
"debug_command": "ac temp driver 23",
|
||||
"frame_id": "0x1DE",
|
||||
"data_hex": "00 00 00 00 17 00 00 00",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
"trace_events": [
|
||||
{"stage": "local_peripheral_plan", "duration_ms": 1},
|
||||
{"stage": "nfc_peripheral_sequence", "duration_ms": 0},
|
||||
{"stage": "intent_parse", "duration_ms": 12},
|
||||
{"stage": "context_resolve", "duration_ms": 1},
|
||||
{"stage": "compile", "duration_ms": 2},
|
||||
{"stage": "execute", "duration_ms": 999},
|
||||
],
|
||||
}
|
||||
timing_state = {
|
||||
"inputMode": "speech",
|
||||
"speechStartedAt": 0,
|
||||
"speechResultAt": 80,
|
||||
"speechTimings": {
|
||||
"audio_read_ms": 1,
|
||||
"vad_ms": 2,
|
||||
"asr_ms": 30,
|
||||
"assembly_ms": 1,
|
||||
"semantic_ms": 3,
|
||||
"command_resolution_ms": 1,
|
||||
"recovery_ms": 0,
|
||||
"total_ms": 37,
|
||||
},
|
||||
"requestStartedAt": 100,
|
||||
"validatedAt": 125,
|
||||
"displayedAt": 126,
|
||||
"renderDurationMs": 1,
|
||||
}
|
||||
browser_projection = {
|
||||
"provider_used": "local_rules+infrared_local_rules",
|
||||
"execution_mode": "dry_run",
|
||||
"validated_control_commands": [
|
||||
{
|
||||
"operation": sample["function_configs"]["native_climate"]["spec"][
|
||||
"operations"
|
||||
][0],
|
||||
"domain": "climate",
|
||||
"channel": "CAN1",
|
||||
"actual_command": "ac temp driver 23",
|
||||
"frames": [
|
||||
{
|
||||
"label": "CAN1 帧",
|
||||
"value": "0x1DE · 00 00 00 00 17 00 00 00",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"operation": sample["function_configs"]["self_extension"]["spec"][
|
||||
"operations"
|
||||
][0],
|
||||
"domain": "self_extension",
|
||||
"channel": "IR",
|
||||
"actual_command": "infrared emit 1",
|
||||
"frames": [
|
||||
{
|
||||
"label": "GSCU1B TCP",
|
||||
"value": '{"action":"emit","data":{"no":1},"type":"infrared"}',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
"trace_events": sample["trace_events"][:3],
|
||||
}
|
||||
script = (
|
||||
f"{helpers}\n"
|
||||
f"const sample = {json.dumps(sample, ensure_ascii=False)};\n"
|
||||
f"const browserProjection = {json.dumps(browser_projection, ensure_ascii=False)};\n"
|
||||
f"const timingState = {json.dumps(timing_state)};\n"
|
||||
"process.stdout.write(JSON.stringify({"
|
||||
"commands: parsedCanCommands(sample), "
|
||||
"projectedCommands: parsedCanCommands(browserProjection), "
|
||||
"timings: parsedCommandTimingRows(sample, timingState)"
|
||||
"}));"
|
||||
)
|
||||
completed = subprocess.run(
|
||||
[node, "-"],
|
||||
input=script,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
parsed = json.loads(completed.stdout)
|
||||
climate, infrared = parsed["commands"]
|
||||
projected_climate, projected_infrared = parsed["projectedCommands"]
|
||||
assert climate["semantic"] == (
|
||||
"climate.set_temperature(target=driver, temperature_c=23)"
|
||||
)
|
||||
assert climate["command"] == "ac temp driver 23"
|
||||
assert climate["frame"] == "0x1DE · 00 00 00 00 17 00 00 00"
|
||||
assert climate["frameLabel"] == "CAN1 帧"
|
||||
assert climate["parseSource"] == "local_rule"
|
||||
assert infrared["semantic"] == (
|
||||
"infrared.emit(target=toy-001, action=开启, slot=1)"
|
||||
)
|
||||
assert infrared["command"] == "infrared emit 1"
|
||||
assert infrared["frameLabel"] == "GSCU1B TCP"
|
||||
assert json.loads(infrared["frame"])["data"]["no"] == 1
|
||||
assert infrared["parseSource"] == "local_rule"
|
||||
assert projected_climate["semantic"] == climate["semantic"]
|
||||
assert projected_climate["command"] == climate["command"]
|
||||
assert projected_climate["frames"] == climate["frames"]
|
||||
assert projected_infrared["semantic"] == infrared["semantic"]
|
||||
assert projected_infrared["command"] == infrared["command"]
|
||||
assert projected_infrared["frames"] == infrared["frames"]
|
||||
|
||||
timing_by_key = {item["key"]: item for item in parsed["timings"]}
|
||||
assert timing_by_key["local_peripheral_plan-0"]["label"] == "红外本地规则编译"
|
||||
assert timing_by_key["nfc_peripheral_sequence-1"]["label"] == "红外指令序列生成"
|
||||
assert timing_by_key["intent_parse-2"]["label"] == "本地规则意图解析"
|
||||
assert timing_by_key["request_to_validated"]["durationMs"] == 25
|
||||
assert timing_by_key["dom_render"]["durationMs"] == 1
|
||||
assert timing_by_key["request_to_display"]["durationMs"] == 26
|
||||
assert timing_by_key["speech_to_display"]["durationMs"] == 126
|
||||
assert timing_by_key["total_ms"]["durationMs"] == 37
|
||||
assert all(item["durationMs"] >= 0 for item in parsed["timings"])
|
||||
assert not any(item["key"].startswith("execute") for item in parsed["timings"])
|
||||
|
||||
|
||||
def test_duplicate_operation_refs_do_not_borrow_another_configs_plan() -> None:
|
||||
node = shutil.which("node")
|
||||
if node is None:
|
||||
return
|
||||
console = source("console-v2.js")
|
||||
start = console.index(" const normalizedCanTarget")
|
||||
end = console.index("\n\n function renderParsedCanCommands", start)
|
||||
helpers = console[start:end]
|
||||
sample = {
|
||||
"function_configs": {
|
||||
"native_climate": {
|
||||
"spec": {
|
||||
"domain": "climate",
|
||||
"operations": [
|
||||
{
|
||||
"id": "op-1",
|
||||
"capability": "climate.set_temperature",
|
||||
"target": {"zone": "driver"},
|
||||
"parameters": {"temperature_c": 23},
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
"native_seat": {
|
||||
"spec": {
|
||||
"domain": "seat",
|
||||
"operations": [
|
||||
{
|
||||
"id": "op-1",
|
||||
"capability": "seat.set_heating",
|
||||
"target": {"zone": "passenger"},
|
||||
"parameters": {"level": 2},
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
"command_plans": {
|
||||
"native_seat": {
|
||||
"spec": {
|
||||
"domain": "seat",
|
||||
"commands": [
|
||||
{
|
||||
"operation_ref": "op-1",
|
||||
"bus": "seat_can0",
|
||||
"signals": {
|
||||
"debug_command": "seat passenger heat 2",
|
||||
"frame_id": "0x300",
|
||||
"data_hex": "01 02 03 04 05 06 07 08",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
script = (
|
||||
f"{helpers}\n"
|
||||
f"const sample = {json.dumps(sample)};\n"
|
||||
"process.stdout.write(JSON.stringify(parsedCanCommands(sample)));"
|
||||
)
|
||||
|
||||
completed = subprocess.run(
|
||||
[node, "-"],
|
||||
input=script,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
parsed = json.loads(completed.stdout)
|
||||
by_semantic = {item["semantic"]: item for item in parsed}
|
||||
climate = by_semantic[
|
||||
"climate.set_temperature(target=driver, temperature_c=23)"
|
||||
]
|
||||
seat = by_semantic["seat.set_heating(target=passenger, level=2)"]
|
||||
assert climate["command"] == "ac temp driver 23"
|
||||
assert "frame" not in climate
|
||||
assert climate["channel"] == "CAN1"
|
||||
assert seat["command"] == "seat passenger heat 2"
|
||||
assert seat["channel"] == "CAN0"
|
||||
|
||||
|
||||
def test_command_timing_rejects_stale_request_events() -> None:
|
||||
node = shutil.which("node")
|
||||
if node is None:
|
||||
return
|
||||
console = source("console-v2.js")
|
||||
correlation_start = console.index(" function commandCorrelationIds")
|
||||
correlation_end = console.index(
|
||||
"\n\n function rememberValidatedCommandContext",
|
||||
correlation_start,
|
||||
)
|
||||
correlation_helper = console[correlation_start:correlation_end]
|
||||
start = console.index(" const normalizedCanTarget")
|
||||
end = console.index("\n\n function renderParsedCanCommands", start)
|
||||
helpers = console[start:end]
|
||||
script = (
|
||||
"let commandTimingState = {requestStartedAt: 1, requestId: '', "
|
||||
"utterance: '空调温度设置到23度,打开闪烁灯'};\n"
|
||||
f"{correlation_helper}\n"
|
||||
f"{helpers}\n"
|
||||
"const first = commandResultMatchesTimedRequest({request_id: 'req-current', "
|
||||
"utterance: '空调温度设置到23度,打开闪烁灯'});\n"
|
||||
"const staleId = commandResultMatchesTimedRequest({request_id: 'req-old', "
|
||||
"utterance: '空调温度设置到23度,打开闪烁灯'});\n"
|
||||
"const staleText = commandResultMatchesTimedRequest({request_id: 'req-current', "
|
||||
"utterance: '打开车窗'});\n"
|
||||
"process.stdout.write(JSON.stringify({first, staleId, staleText, "
|
||||
"boundRequestId: commandTimingState.requestId}));"
|
||||
)
|
||||
|
||||
completed = subprocess.run(
|
||||
[node, "-"],
|
||||
input=script,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert json.loads(completed.stdout) == {
|
||||
"first": True,
|
||||
"staleId": False,
|
||||
"staleText": False,
|
||||
"boundRequestId": "req-current",
|
||||
}
|
||||
|
||||
|
||||
def test_parsed_command_delivery_is_independent_of_transport_and_target() -> None:
|
||||
node = shutil.which("node")
|
||||
if node is None:
|
||||
@@ -398,10 +752,12 @@ def test_parsed_command_delivery_is_independent_of_transport_and_target() -> Non
|
||||
"parsedCanCommands(sample))));"
|
||||
)
|
||||
completed = subprocess.run(
|
||||
[node, "-e", script],
|
||||
[node, "-"],
|
||||
input=script,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
parsed = json.loads(completed.stdout)
|
||||
@@ -423,6 +779,7 @@ def test_parsed_command_delivery_is_independent_of_transport_and_target() -> Non
|
||||
|
||||
def test_mode_editor_renders_readable_commands_with_expandable_mqtt_sequences() -> None:
|
||||
app = source("app.js")
|
||||
console = source("console-v2.js")
|
||||
styles = source("styles.css")
|
||||
html = source("index.html")
|
||||
|
||||
@@ -442,6 +799,10 @@ def test_mode_editor_renders_readable_commands_with_expandable_mqtt_sequences()
|
||||
assert 'executionMode: "dry_run"' in app
|
||||
assert 'clientType: "mode_editor_compile"' in app
|
||||
assert "previewOnly: true" in app
|
||||
assert 'const nonDispatchTaskSources = new Set(["mode_editor_compile"])' in console
|
||||
assert "if (isDispatchTask(task))" in console
|
||||
assert "下发控制指令" in console
|
||||
assert "已解析控制指令" not in console
|
||||
assert "function executeSavedModeProfile(mode)" in app
|
||||
assert "/api/mode-profiles/${encodeURIComponent(mode.id)}/execute" in app
|
||||
assert '$("#utterance").value = mode.name || mode.id;' in app
|
||||
@@ -450,6 +811,9 @@ def test_mode_editor_renders_readable_commands_with_expandable_mqtt_sequences()
|
||||
|
||||
assert "生成指令" in html
|
||||
assert ">执行</span></button>" not in html
|
||||
assert 'id="hardwareNote"' not in html
|
||||
assert 'id="hardwareAliasNote"' not in html
|
||||
assert 'id="vehicleTransportNote"' not in html
|
||||
|
||||
assert ".mode-editor-command-card" in styles
|
||||
assert ".mode-editor-command-meta" in styles
|
||||
@@ -544,10 +908,12 @@ def test_mode_editor_prepares_one_to_many_mqtt_message_sequences() -> None:
|
||||
"process.stdout.write(JSON.stringify(modeEditorReadableCommands(flow)));"
|
||||
)
|
||||
completed = subprocess.run(
|
||||
[node, "-e", script],
|
||||
[node, "-"],
|
||||
input=script,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
commands = json.loads(completed.stdout)
|
||||
|
||||
@@ -619,10 +985,12 @@ def test_left_sidebar_extracts_speech_and_peripheral_control_commands() -> None:
|
||||
"}));"
|
||||
)
|
||||
completed = subprocess.run(
|
||||
[node, "-e", script],
|
||||
[node, "-"],
|
||||
input=script,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
output = json.loads(completed.stdout)
|
||||
|
||||
@@ -924,7 +1292,8 @@ def test_assistant_mode_preview_groups_validated_actions_for_the_card() -> None:
|
||||
"process.stdout.write(JSON.stringify(modeDraftRows(draft, actions)));"
|
||||
)
|
||||
completed = subprocess.run(
|
||||
[node, "-e", script],
|
||||
[node, "-"],
|
||||
input=script,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -792,7 +792,15 @@ def test_unknown_mode_uses_one_structured_turn_then_local_dry_run() -> None:
|
||||
"roof.set_shade_position",
|
||||
]
|
||||
call = client.structured_calls[0]
|
||||
assert call["application_context"] == _scene_context()
|
||||
expected_context = _scene_context()
|
||||
expected_context["natural_language_routing"] = {
|
||||
"original_utterance": "构建一个星夜模式",
|
||||
"parsed_clauses": [],
|
||||
"resolved_clauses": [],
|
||||
"unresolved_clauses": ["构建一个星夜模式"],
|
||||
"cloud_input_utterance": "构建一个星夜模式",
|
||||
}
|
||||
assert call["application_context"] == expected_context
|
||||
assert call["output_schema"]["type"] == "object"
|
||||
assert call["output_schema"]["additionalProperties"] is False
|
||||
assert "requests" in call["output_schema"]["required"]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from cognitive_agent.application.seat_service import SeatDemoService
|
||||
@@ -47,6 +48,19 @@ def test_preview_validates_local_controls_without_calling_cloud(tmp_path: Path)
|
||||
assert result["status"] == "succeeded"
|
||||
assert result["supplement_required"] is True
|
||||
assert result["unresolved_clauses"] == ["播放音乐"]
|
||||
assert result["parsed_clauses"] == [
|
||||
"空调温度设置为24度",
|
||||
"风量设置为2档",
|
||||
]
|
||||
assert result["natural_language_routing"] == {
|
||||
"original_utterance": "空调温度设置为24度,风量设置为2档,播放音乐",
|
||||
"parsed_clauses": ["空调温度设置为24度", "风量设置为2档"],
|
||||
"resolved_clauses": ["空调温度设置为24度", "风量设置为2档"],
|
||||
"unresolved_clauses": ["播放音乐"],
|
||||
}
|
||||
assert result["local_control_budget_ms"] == 1000
|
||||
assert result["local_control_elapsed_ms"] < 1000
|
||||
assert result["local_control_within_budget"] is True
|
||||
assert [item["capability"] for item in _operations(result)] == [
|
||||
"climate.set_temperature",
|
||||
"climate.set_fan_level",
|
||||
@@ -58,6 +72,8 @@ def test_preview_validates_local_controls_without_calling_cloud(tmp_path: Path)
|
||||
assert validated_events[0]["phase"] == "deterministic_commands_validated"
|
||||
assert validated_events[0]["status"] == "processing"
|
||||
assert validated_events[0]["execution_report"] is None
|
||||
assert validated_events[0]["local_control_elapsed_ms"] < 1000
|
||||
assert validated_events[0]["local_control_within_budget"] is True
|
||||
assert _operations(validated_events[0]) == _operations(result)
|
||||
assert validated_events[0]["command_plans"]
|
||||
|
||||
@@ -119,12 +135,90 @@ def test_fully_local_cabin_request_streams_all_commands_without_supplement(
|
||||
assert _operations(validated_events[0]) == operations
|
||||
|
||||
|
||||
def test_qualitative_window_position_keeps_adjacent_reading_light_clause(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service = _service(tmp_path)
|
||||
cases = (
|
||||
("空调 29℃、打开阅读灯、副驾车窗开到20%", 20),
|
||||
("空调 29℃、打开阅读灯、副驾车窗开到一半", 50),
|
||||
)
|
||||
|
||||
for index, (utterance, expected_position) in enumerate(cases):
|
||||
result = service.preview_native_can(
|
||||
utterance,
|
||||
provider="local",
|
||||
conversation_id=f"conv-reading-window-{index:02d}",
|
||||
execution_mode="dry_run",
|
||||
)
|
||||
operations = _operations(result)
|
||||
by_capability = {item["capability"]: item for item in operations}
|
||||
|
||||
assert result["unresolved_clauses"] == []
|
||||
assert result["local_control_elapsed_ms"] < 1000
|
||||
assert result["local_control_within_budget"] is True
|
||||
assert result["parsed_clauses"] == [
|
||||
"空调 29℃",
|
||||
"打开阅读灯",
|
||||
utterance.rsplit("、", 1)[-1],
|
||||
]
|
||||
assert set(by_capability) == {
|
||||
"climate.set_temperature",
|
||||
"interior_light.set_reading_power",
|
||||
"window.set_position",
|
||||
}
|
||||
assert (
|
||||
by_capability["interior_light.set_reading_power"]["target"]["zone"]
|
||||
== "all"
|
||||
)
|
||||
assert by_capability["interior_light.set_reading_power"]["parameters"] == {
|
||||
"state": "on"
|
||||
}
|
||||
assert (
|
||||
by_capability["window.set_position"]["target"]["zone"]
|
||||
== "front_right"
|
||||
)
|
||||
assert by_capability["window.set_position"]["parameters"] == {
|
||||
"position_pct": expected_position
|
||||
}
|
||||
|
||||
|
||||
def test_local_window_parser_accepts_front_and_rear_row_side_aliases(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service = _service(tmp_path)
|
||||
cases = (
|
||||
("左前排车窗开到20%", "front_left", 20),
|
||||
("右前排窗开到一半", "front_right", 50),
|
||||
("左后排车窗开到35%", "rear_left", 35),
|
||||
("右后排窗开到一半", "rear_right", 50),
|
||||
)
|
||||
|
||||
for index, (utterance, expected_target, expected_position) in enumerate(cases):
|
||||
result = service.preview_native_can(
|
||||
utterance,
|
||||
provider="local",
|
||||
conversation_id=f"conv-window-alias-{index:02d}",
|
||||
execution_mode="dry_run",
|
||||
)
|
||||
operations = _operations(result)
|
||||
|
||||
assert result["status"] == "succeeded"
|
||||
assert result["unresolved_clauses"] == []
|
||||
assert len(operations) == 1
|
||||
assert operations[0]["capability"] == "window.set_position"
|
||||
assert operations[0]["target"]["zone"] == expected_target
|
||||
assert operations[0]["parameters"] == {
|
||||
"position_pct": expected_position
|
||||
}
|
||||
|
||||
|
||||
def test_supplement_receives_only_unresolved_text_and_deduplicates_local_control(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service = _service(tmp_path)
|
||||
preview = service.preview_native_can(
|
||||
"观影时空调温度设置为24度并播放音乐",
|
||||
"空调温度设置为24度并播放音乐",
|
||||
provider="openai",
|
||||
conversation_id="conv-two-stage-deduplicate",
|
||||
execution_mode="dry_run",
|
||||
@@ -169,7 +263,15 @@ def test_supplement_receives_only_unresolved_text_and_deduplicates_local_control
|
||||
provider="openai",
|
||||
)
|
||||
|
||||
assert interpret.call_args.args[0] == "观影时空调温度设置为24度并播放音乐"
|
||||
assert interpret.call_args.args[0] == "播放音乐"
|
||||
routing = interpret.call_args.args[1]["natural_language_routing"]
|
||||
assert routing == {
|
||||
"original_utterance": "空调温度设置为24度并播放音乐",
|
||||
"parsed_clauses": ["空调温度设置为24度"],
|
||||
"resolved_clauses": ["空调温度设置为24度"],
|
||||
"unresolved_clauses": ["播放音乐"],
|
||||
"cloud_input_utterance": "播放音乐",
|
||||
}
|
||||
operations = _operations(result)
|
||||
assert [item["capability"] for item in operations].count(
|
||||
"climate.set_temperature"
|
||||
@@ -217,6 +319,11 @@ def test_frontend_two_stage_contract_and_source_badges() -> None:
|
||||
assert 'item.deliveryLabel' in console
|
||||
assert 'data-delivery-state="${escapeHtml(item.deliveryState)}"' in console
|
||||
assert '"cognitive-agent:validated-can-commands"' in console
|
||||
assert "function commandCorrelationIds(result = {})" in console
|
||||
assert "result.preview_request_id" in console
|
||||
assert "result.supplement_of" in console
|
||||
assert '"local_control_elapsed"' in console
|
||||
assert '"本地规则解析与下发"' in console
|
||||
assert '.v2-can-command-source[data-parse-source="local_rule"]' in styles
|
||||
assert '.v2-can-command-delivery[data-delivery-state="submitted"]' in styles
|
||||
assert '"/api/can/commands"' in server
|
||||
@@ -287,6 +394,7 @@ def test_unified_result_preserves_cloud_infrared_attribution() -> None:
|
||||
"command_plans": {},
|
||||
"operation_results": [],
|
||||
"decision_summary": [],
|
||||
"trace_events": [{"stage": "compile", "duration_ms": 3}],
|
||||
}
|
||||
infrared_result = {
|
||||
"status": "succeeded",
|
||||
@@ -296,6 +404,10 @@ def test_unified_result_preserves_cloud_infrared_attribution() -> None:
|
||||
"command_plans": {},
|
||||
"operation_results": [],
|
||||
"decision_summary": [],
|
||||
"trace_events": [
|
||||
{"stage": "local_peripheral_plan", "duration_ms": 1},
|
||||
{"stage": "nfc_peripheral_sequence", "duration_ms": 0},
|
||||
],
|
||||
}
|
||||
entries = [
|
||||
{
|
||||
@@ -321,6 +433,368 @@ def test_unified_result_preserves_cloud_infrared_attribution() -> None:
|
||||
|
||||
assert merged["provider_used"] == "local_rules+gpt:gpt-5.6-luna"
|
||||
assert merged["infrared_actions"][0]["parse_source"] == "cloud_model"
|
||||
assert [event["stage"] for event in merged["trace_events"]] == [
|
||||
"local_peripheral_plan",
|
||||
"nfc_peripheral_sequence",
|
||||
"compile",
|
||||
]
|
||||
|
||||
|
||||
def test_pure_infrared_request_publishes_compiled_commands() -> None:
|
||||
handler = SeatDemoHandler.__new__(SeatDemoHandler)
|
||||
handler.service = SimpleNamespace(
|
||||
is_mode_profile_configuration_request=lambda _utterance: False,
|
||||
conversations=SimpleNamespace(snapshot=lambda _conversation_id: {}),
|
||||
persist_composed_result=lambda _result: None,
|
||||
)
|
||||
handler.extension = SimpleNamespace(list_peripheral_capabilities=lambda: [])
|
||||
handler.nfc = SimpleNamespace(snapshot=lambda: {})
|
||||
infrared_result = {
|
||||
"request_id": "req-ir-only",
|
||||
"utterance": "打开闪烁灯",
|
||||
"status": "succeeded",
|
||||
"function_configs": {
|
||||
"self_extension": {
|
||||
"spec": {
|
||||
"domain": "self_extension",
|
||||
"operations": [
|
||||
{
|
||||
"id": "infrared-1",
|
||||
"capability": "infrared.emit",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"command_plans": {
|
||||
"self_extension": {
|
||||
"spec": {
|
||||
"domain": "self_extension",
|
||||
"commands": [
|
||||
{
|
||||
"operation_ref": "infrared-1",
|
||||
"message": "infrared.emit",
|
||||
"signals": {"slot": 1},
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"trace_events": [
|
||||
{"stage": "nfc_peripheral_route", "duration_ms": 0}
|
||||
],
|
||||
}
|
||||
handler._execute_local_peripheral_actions = (
|
||||
lambda _utterance, _entries, **_kwargs: infrared_result
|
||||
)
|
||||
validated_events: list[dict[str, object]] = []
|
||||
partition = {
|
||||
"peripheral_actions": [
|
||||
{
|
||||
"index": 0,
|
||||
"clause": "打开闪烁灯",
|
||||
"capability": {
|
||||
"id": "toy-001-light-on",
|
||||
"slot": 1,
|
||||
},
|
||||
}
|
||||
],
|
||||
"residual_utterance": "",
|
||||
"unresolved_peripheral_clauses": [],
|
||||
"denied_peripheral_actions": [],
|
||||
}
|
||||
|
||||
with patch(
|
||||
"cognitive_agent.server.partition_active_peripheral_intent",
|
||||
return_value=partition,
|
||||
):
|
||||
result = handler._execute_request_with_peripherals(
|
||||
"/api/requests/preview",
|
||||
{
|
||||
"utterance": "打开闪烁灯",
|
||||
"provider": "local",
|
||||
"execution_mode": "dry_run",
|
||||
},
|
||||
on_commands_validated=validated_events.append,
|
||||
)
|
||||
|
||||
assert result is infrared_result
|
||||
assert len(validated_events) == 2
|
||||
assert validated_events[0]["status"] == "processing"
|
||||
assert validated_events[0]["phase"] == "commands_validated"
|
||||
assert validated_events[0]["command_plans"] == infrared_result["command_plans"]
|
||||
assert validated_events[0]["trace_events"] == infrared_result["trace_events"]
|
||||
assert validated_events[1] is infrared_result
|
||||
|
||||
|
||||
def test_pure_unresolved_peripheral_returns_cloud_ready_local_preview(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
handler = SeatDemoHandler.__new__(SeatDemoHandler)
|
||||
handler.service = _service(tmp_path)
|
||||
handler.extension = SimpleNamespace(list_peripheral_capabilities=lambda: [])
|
||||
handler.nfc = SimpleNamespace(snapshot=lambda: {})
|
||||
validated_events: list[dict[str, object]] = []
|
||||
utterance = "让闪烁灯喜庆一点"
|
||||
partition = {
|
||||
"peripheral_actions": [],
|
||||
"residual_utterance": "",
|
||||
"unresolved_peripheral_clauses": [utterance],
|
||||
"unresolved_candidates": [
|
||||
{
|
||||
"id": "light-festive",
|
||||
"status": "learned",
|
||||
"device_id": "toy-001",
|
||||
"device_name": "闪烁灯",
|
||||
"action": "喜庆灯效",
|
||||
"slot": 2,
|
||||
}
|
||||
],
|
||||
"denied_peripheral_actions": [],
|
||||
}
|
||||
|
||||
with patch(
|
||||
"cognitive_agent.server.partition_active_peripheral_intent",
|
||||
return_value=partition,
|
||||
):
|
||||
result = handler._execute_request_with_peripherals(
|
||||
"/api/requests/preview",
|
||||
{
|
||||
"utterance": utterance,
|
||||
"provider": "local",
|
||||
"conversation_id": "conv-unresolved-peripheral-preview",
|
||||
"execution_mode": "dry_run",
|
||||
},
|
||||
on_commands_validated=validated_events.append,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result["status"] == "no_action"
|
||||
assert result["supplement_required"] is True
|
||||
assert result["function_configs"] == {}
|
||||
assert result["command_plans"] == {}
|
||||
assert result["preview_request_id"] == result["request_id"]
|
||||
assert result["local_control_budget_ms"] == 1000
|
||||
assert result["local_control_elapsed_ms"] < 1000
|
||||
assert result["local_control_within_budget"] is True
|
||||
assert result["natural_language_routing"] == {
|
||||
"original_utterance": utterance,
|
||||
"parsed_clauses": [],
|
||||
"resolved_clauses": [],
|
||||
"unresolved_clauses": [utterance],
|
||||
}
|
||||
assert validated_events == [result]
|
||||
assert (
|
||||
handler.service.artifact_root
|
||||
/ str(result["request_id"])
|
||||
/ "result.json"
|
||||
).is_file()
|
||||
|
||||
|
||||
def test_mixed_request_finishes_all_local_phases_before_cloud_ir() -> None:
|
||||
handler = SeatDemoHandler.__new__(SeatDemoHandler)
|
||||
order: list[str] = []
|
||||
validated_events: list[dict[str, object]] = []
|
||||
local_capability = {
|
||||
"id": "light-on",
|
||||
"status": "learned",
|
||||
"device_id": "toy-001",
|
||||
"device_name": "闪烁灯",
|
||||
"card_uid": "A1B2",
|
||||
"action": "打开",
|
||||
"slot": 1,
|
||||
}
|
||||
cloud_capability = {
|
||||
**local_capability,
|
||||
"id": "light-festive",
|
||||
"action": "喜庆灯效",
|
||||
"slot": 2,
|
||||
}
|
||||
|
||||
def peripheral_result(
|
||||
request_id: str,
|
||||
capability: dict[str, object],
|
||||
parse_source: str,
|
||||
) -> dict[str, object]:
|
||||
operation_id = str(capability["id"])
|
||||
return {
|
||||
"request_id": request_id,
|
||||
"utterance": "空调温度设置到23度,打开闪烁灯,让闪烁灯喜庆一点",
|
||||
"status": "succeeded",
|
||||
"success": True,
|
||||
"provider_used": (
|
||||
"infrared_local_rules"
|
||||
if parse_source == "local_rule"
|
||||
else "gpt:test"
|
||||
),
|
||||
"execution_mode": "dry_run",
|
||||
"control_transport": "gscu_dry_run",
|
||||
"function_configs": {
|
||||
"self_extension": {
|
||||
"spec": {
|
||||
"domain": "self_extension",
|
||||
"operations": [
|
||||
{
|
||||
"id": operation_id,
|
||||
"capability": "infrared.emit",
|
||||
"parse_source": parse_source,
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"command_plans": {
|
||||
"self_extension": {
|
||||
"spec": {
|
||||
"domain": "self_extension",
|
||||
"commands": [
|
||||
{
|
||||
"operation_ref": operation_id,
|
||||
"message": "infrared.emit",
|
||||
"signals": {"slot": capability["slot"]},
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"trace_events": [],
|
||||
"validation": [],
|
||||
"operation_results": [],
|
||||
"decision_summary": [],
|
||||
"execution_report": None,
|
||||
}
|
||||
|
||||
local_result = peripheral_result(
|
||||
"req-local-ir",
|
||||
local_capability,
|
||||
"local_rule",
|
||||
)
|
||||
cloud_result = peripheral_result(
|
||||
"req-cloud-ir",
|
||||
cloud_capability,
|
||||
"cloud_model",
|
||||
)
|
||||
can_result = {
|
||||
"request_id": "req-local-can",
|
||||
"utterance": "空调温度设置到23度",
|
||||
"status": "succeeded",
|
||||
"success": True,
|
||||
"provider_used": "local_rules",
|
||||
"execution_mode": "dry_run",
|
||||
"control_transport": "wired_can",
|
||||
"function_configs": {},
|
||||
"command_plans": {},
|
||||
"trace_events": [],
|
||||
"validation": [],
|
||||
"operation_results": [],
|
||||
"decision_summary": [],
|
||||
"execution_report": None,
|
||||
"natural_language_routing": {
|
||||
"original_utterance": "空调温度设置到23度,打开闪烁灯,让闪烁灯喜庆一点",
|
||||
"parsed_clauses": ["打开闪烁灯", "空调温度设置到23度"],
|
||||
"resolved_clauses": ["打开闪烁灯", "空调温度设置到23度"],
|
||||
"unresolved_clauses": ["让闪烁灯喜庆一点"],
|
||||
},
|
||||
}
|
||||
|
||||
def preview_native_can(**kwargs: object) -> dict[str, object]:
|
||||
order.append("local_can")
|
||||
callback = kwargs.get("on_commands_validated")
|
||||
if callable(callback):
|
||||
callback(dict(can_result))
|
||||
return dict(can_result)
|
||||
|
||||
handler.service = SimpleNamespace(
|
||||
is_mode_profile_configuration_request=lambda _utterance: False,
|
||||
conversations=SimpleNamespace(snapshot=lambda _conversation_id: {}),
|
||||
preview_native_can=preview_native_can,
|
||||
persist_composed_result=lambda _result: None,
|
||||
)
|
||||
|
||||
def plan(payload: dict[str, object]) -> dict[str, object]:
|
||||
order.append("cloud_plan")
|
||||
assert payload["utterance"] == "让闪烁灯喜庆一点"
|
||||
routing = payload["natural_language_routing"]
|
||||
assert isinstance(routing, dict)
|
||||
assert routing["original_utterance"] == (
|
||||
"空调温度设置到23度,打开闪烁灯,让闪烁灯喜庆一点"
|
||||
)
|
||||
assert routing["parsed_clauses"] == [
|
||||
"打开闪烁灯",
|
||||
"空调温度设置到23度",
|
||||
]
|
||||
return {
|
||||
"steps": [
|
||||
{"kind": "peripheral", "capability_id": "light-festive"}
|
||||
]
|
||||
}
|
||||
|
||||
def execute_cloud(
|
||||
_utterance: str,
|
||||
_plan: dict[str, object],
|
||||
catalog: list[dict[str, object]],
|
||||
**_kwargs: object,
|
||||
) -> dict[str, object]:
|
||||
order.append("cloud_ir")
|
||||
assert [item["id"] for item in catalog] == ["light-festive"]
|
||||
return cloud_result
|
||||
|
||||
handler.extension = SimpleNamespace(
|
||||
list_peripheral_capabilities=lambda: [],
|
||||
plan=plan,
|
||||
execute_peripheral_plan_request=execute_cloud,
|
||||
)
|
||||
handler.nfc = SimpleNamespace(snapshot=lambda: {})
|
||||
|
||||
def execute_local(
|
||||
_utterance: str,
|
||||
_entries: list[dict[str, object]],
|
||||
**kwargs: object,
|
||||
) -> dict[str, object]:
|
||||
order.append("local_ir")
|
||||
callback = kwargs.get("on_commands_validated")
|
||||
if callable(callback):
|
||||
callback(local_result)
|
||||
return local_result
|
||||
|
||||
handler._execute_local_peripheral_actions = execute_local
|
||||
partition = {
|
||||
"peripheral_actions": [
|
||||
{"index": 1, "clause": "打开闪烁灯", "capability": local_capability}
|
||||
],
|
||||
"residual_utterance": "空调温度设置到23度",
|
||||
"unresolved_peripheral_clauses": ["让闪烁灯喜庆一点"],
|
||||
"unresolved_candidates": [cloud_capability],
|
||||
"denied_peripheral_actions": [],
|
||||
}
|
||||
|
||||
with patch(
|
||||
"cognitive_agent.server.partition_active_peripheral_intent",
|
||||
return_value=partition,
|
||||
):
|
||||
result = handler._execute_request_with_peripherals(
|
||||
"/api/requests/preview",
|
||||
{
|
||||
"utterance": (
|
||||
"空调温度设置到23度,打开闪烁灯,让闪烁灯喜庆一点"
|
||||
),
|
||||
"provider": "auto",
|
||||
"conversation_id": "conv-mixed-order",
|
||||
"execution_mode": "dry_run",
|
||||
},
|
||||
on_commands_validated=validated_events.append,
|
||||
)
|
||||
|
||||
assert order == ["local_ir", "local_can", "cloud_plan", "cloud_ir"]
|
||||
assert result is not None
|
||||
assert result["status"] == "succeeded"
|
||||
assert any(event.get("local_control_within_budget") for event in validated_events)
|
||||
operation_sources = {
|
||||
operation["parse_source"]
|
||||
for config in result["function_configs"].values()
|
||||
for operation in config["spec"].get("operations", [])
|
||||
}
|
||||
assert operation_sources == {"local_rule", "cloud_model"}
|
||||
|
||||
|
||||
def test_nfc_command_denial_keeps_allowed_result_and_reports_partial() -> None:
|
||||
|
||||
Reference in New Issue
Block a user