From a6d974cb297db650683b75158d0d65a50eb9787d Mon Sep 17 00:00:00 2001 From: vee Date: Sun, 2 Aug 2026 06:12:36 +0800 Subject: [PATCH] feat: tolerate recoverable seat intent inputs --- ...0260802_experience_tolerance_targeted.json | 823 ++++++++++++++++++ contracts/schemas/function-config.schema.json | 2 +- docs/testing/t2g-seat-frontend-test-suite.md | 4 +- domains/seat/src/seat_domain/intent.py | 75 +- domains/seat/tests/unit/test_intent.py | 23 + .../infrastructure/llm/openai_responses.py | 11 +- tests/fixtures/seat_frontend_hil_suite.json | 30 +- .../test_seat_frontend_input_coverage.py | 2 +- tests/unit/test_openai_responses.py | 3 + 9 files changed, 942 insertions(+), 31 deletions(-) create mode 100644 artifacts/hil/seat_frontend_hil_20260802_experience_tolerance_targeted.json diff --git a/artifacts/hil/seat_frontend_hil_20260802_experience_tolerance_targeted.json b/artifacts/hil/seat_frontend_hil_20260802_experience_tolerance_targeted.json new file mode 100644 index 0000000..5cc043f --- /dev/null +++ b/artifacts/hil/seat_frontend_hil_20260802_experience_tolerance_targeted.json @@ -0,0 +1,823 @@ +{ + "suite": "t2g-seat-frontend-comprehensive-hil", + "started_at": "2026-08-01T22:06:10.662623Z", + "base_url": "http://127.0.0.1:8000", + "vehicle_id": "denza_n7", + "conversation_id": "conv-hil-full-20260801220610", + "runtime": { + "hardware_execution_allowed": true, + "firmware_profiles": [ + "combined" + ] + }, + "results": [ + { + "id": "MOTION-M0-ABS-DECIMAL-ROUND", + "group": "motion", + "utterance": "副驾座椅水平位置调到50.5%", + "started_at": "2026-08-01T22:06:33.819391Z", + "status": "passed", + "errors": [], + "http_status": 201, + "request_id": "req-20260801220633-35a9ee", + "provider_used": "local_rules", + "matched_modes": [], + "state_http_status": 200, + "physical_state": { + "position": { + "backrest_recline": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 0, + "motor_result": 3, + "motor_selector": 2, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "02 30 18 0B 00 00 00 00", + "timestamp": 52437.943305 + } + }, + "legrest_height": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 0, + "motor_result": 3, + "motor_selector": 3, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "03 30 18 0B 00 00 00 00", + "timestamp": 52437.968795 + } + }, + "longitudinal": { + "position_pct": 51, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 0, + "value_x100": 5110, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "00 26 08 0C 00 F6 13 00", + "timestamp": 52437.995098 + } + }, + "cushion_tilt": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 0, + "motor_result": 3, + "motor_selector": 1, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "01 30 18 0B 00 00 00 00", + "timestamp": 52438.021395 + } + } + }, + "ventilation": { + "state": "off", + "level": 0, + "feedback_value": 1, + "feedback_frame": { + "frame_id": "0x4FA", + "payload_size": 8, + "data_hex": "09 00 00 00 00 00 00 00", + "timestamp": 52438.031724 + } + }, + "heating": { + "state": "off", + "level": 0, + "feedback_value": 1, + "feedback_frame": { + "frame_id": "0x4FA", + "payload_size": 8, + "data_hex": "09 00 00 00 00 00 00 00", + "timestamp": 52438.031724 + } + }, + "captured_at": "2026-08-01T22:06:42.683267Z", + "source": "physical_can", + "target": { + "device": "seat", + "zone": "front_right", + "instance": "passenger" + }, + "complete": true, + "missing": [] + }, + "finished_at": "2026-08-01T22:06:42.768028Z", + "duration_ms": 8949 + }, + { + "id": "MOTION-M0-ABS-NEGATIVE-CLAMP", + "group": "motion", + "utterance": "副驾座椅水平位置调到-10%", + "started_at": "2026-08-01T22:06:42.768083Z", + "status": "passed", + "errors": [], + "http_status": 201, + "request_id": "req-20260801220642-ffc18b", + "provider_used": "local_rules", + "matched_modes": [], + "state_http_status": 200, + "physical_state": { + "position": { + "cushion_tilt": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 0, + "motor_result": 3, + "motor_selector": 1, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "01 30 18 0D 00 00 00 00", + "timestamp": 52448.169066 + } + }, + "backrest_recline": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 0, + "motor_result": 3, + "motor_selector": 2, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "02 30 18 0D 00 00 00 00", + "timestamp": 52448.196 + } + }, + "legrest_height": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 0, + "motor_result": 3, + "motor_selector": 3, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "03 30 18 0D 00 00 00 00", + "timestamp": 52448.221821 + } + }, + "longitudinal": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 0, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "00 26 18 0E 00 00 00 00", + "timestamp": 52448.247656 + } + } + }, + "ventilation": { + "state": "off", + "level": 0, + "feedback_value": 1, + "feedback_frame": { + "frame_id": "0x4FA", + "payload_size": 8, + "data_hex": "09 00 00 00 00 00 00 00", + "timestamp": 52448.255615 + } + }, + "heating": { + "state": "off", + "level": 0, + "feedback_value": 1, + "feedback_frame": { + "frame_id": "0x4FA", + "payload_size": 8, + "data_hex": "09 00 00 00 00 00 00 00", + "timestamp": 52448.255615 + } + }, + "captured_at": "2026-08-01T22:06:52.906777Z", + "source": "physical_can", + "target": { + "device": "seat", + "zone": "front_right", + "instance": "passenger" + }, + "complete": true, + "missing": [] + }, + "finished_at": "2026-08-01T22:06:52.966157Z", + "duration_ms": 10198 + }, + { + "id": "MOTION-M2-TIMED-ZERO-MIN-CLAMP", + "group": "motion", + "utterance": "副驾靠背向后移动0秒", + "started_at": "2026-08-01T22:06:52.966239Z", + "status": "passed", + "errors": [], + "http_status": 201, + "request_id": "req-20260801220652-ebe458", + "provider_used": "local_rules", + "matched_modes": [], + "state_http_status": 200, + "physical_state": { + "position": { + "longitudinal": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 0, + "motor_result": 3, + "motor_selector": 0, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "00 30 18 0F 00 00 00 00", + "timestamp": 52449.295516 + } + }, + "cushion_tilt": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 0, + "motor_result": 3, + "motor_selector": 1, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "01 30 18 0F 00 00 00 00", + "timestamp": 52449.321809 + } + }, + "backrest_recline": { + "position_pct": 1, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 2, + "value_x100": 120, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "02 26 08 10 00 78 00 00", + "timestamp": 52449.347017 + } + }, + "legrest_height": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 0, + "motor_result": 3, + "motor_selector": 3, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "03 30 18 0F 00 00 00 00", + "timestamp": 52449.37364 + } + } + }, + "ventilation": { + "state": "off", + "level": 0, + "feedback_value": 1, + "feedback_frame": { + "frame_id": "0x4FA", + "payload_size": 8, + "data_hex": "09 00 00 00 00 00 00 00", + "timestamp": 52449.382777 + } + }, + "heating": { + "state": "off", + "level": 0, + "feedback_value": 1, + "feedback_frame": { + "frame_id": "0x4FA", + "payload_size": 8, + "data_hex": "09 00 00 00 00 00 00 00", + "timestamp": 52449.382777 + } + }, + "captured_at": "2026-08-01T22:06:54.033862Z", + "source": "physical_can", + "target": { + "device": "seat", + "zone": "front_right", + "instance": "passenger" + }, + "complete": true, + "missing": [] + }, + "finished_at": "2026-08-01T22:06:54.110745Z", + "duration_ms": 1145 + }, + { + "id": "MODEL-FUZZY-COMFORT-ONLY", + "group": "model", + "utterance": "把座椅调舒服一点", + "started_at": "2026-08-01T22:06:54.110804Z", + "status": "passed", + "errors": [], + "http_status": 201, + "request_id": "req-20260801220654-5f0af4", + "provider_used": "openai:gpt-5.6-sol", + "matched_modes": [], + "state_http_status": 200, + "physical_state": { + "position": { + "legrest_height": { + "position_pct": 10, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 3, + "value_x100": 1005, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "03 26 08 15 00 ED 03 00", + "timestamp": 52491.95849 + } + }, + "longitudinal": { + "position_pct": 80, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 0, + "value_x100": 8016, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "00 26 08 12 00 50 1F 00", + "timestamp": 52491.984306 + } + }, + "cushion_tilt": { + "position_pct": 30, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 1, + "value_x100": 2995, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "01 26 08 13 00 B3 0B 00", + "timestamp": 52492.010881 + } + }, + "backrest_recline": { + "position_pct": 35, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 2, + "value_x100": 3501, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "02 26 08 14 00 AD 0D 00", + "timestamp": 52492.036223 + } + } + }, + "ventilation": { + "state": "off", + "level": 0, + "feedback_value": 1, + "feedback_frame": { + "frame_id": "0x4FA", + "payload_size": 8, + "data_hex": "09 00 00 00 00 00 00 00", + "timestamp": 52492.046648 + } + }, + "heating": { + "state": "off", + "level": 0, + "feedback_value": 1, + "feedback_frame": { + "frame_id": "0x4FA", + "payload_size": 8, + "data_hex": "09 00 00 00 00 00 00 00", + "timestamp": 52492.046648 + } + }, + "captured_at": "2026-08-01T22:07:36.697054Z", + "source": "physical_can", + "target": { + "device": "seat", + "zone": "front_right", + "instance": "passenger" + }, + "complete": true, + "missing": [] + }, + "finished_at": "2026-08-01T22:07:36.773665Z", + "duration_ms": 42663 + }, + { + "id": "MODEL-UNKNOWN-BOSS-MODE", + "group": "model", + "utterance": "打开老板模式", + "started_at": "2026-08-01T22:07:36.773730Z", + "status": "passed", + "errors": [], + "http_status": 201, + "request_id": "req-20260801220736-be8392", + "provider_used": "openai:gpt-5.6-sol", + "matched_modes": [], + "state_http_status": 200, + "physical_state": { + "position": { + "legrest_height": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 3, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "03 26 18 1A 00 00 00 00", + "timestamp": 52526.819003 + } + }, + "longitudinal": { + "position_pct": 20, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 0, + "value_x100": 2004, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "00 26 08 17 00 D4 07 00", + "timestamp": 52526.845094 + } + }, + "cushion_tilt": { + "position_pct": 30, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 1, + "value_x100": 2995, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "01 26 08 18 00 B3 0B 00", + "timestamp": 52526.87121 + } + }, + "backrest_recline": { + "position_pct": 10, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 2, + "value_x100": 1003, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "02 26 08 19 00 EB 03 00", + "timestamp": 52526.897575 + } + } + }, + "ventilation": { + "state": "off", + "level": 0, + "feedback_value": 1, + "feedback_frame": { + "frame_id": "0x4FA", + "payload_size": 8, + "data_hex": "09 00 00 00 00 00 00 00", + "timestamp": 52526.90671 + } + }, + "heating": { + "state": "off", + "level": 0, + "feedback_value": 1, + "feedback_frame": { + "frame_id": "0x4FA", + "payload_size": 8, + "data_hex": "09 00 00 00 00 00 00 00", + "timestamp": 52526.90671 + } + }, + "captured_at": "2026-08-01T22:08:11.556208Z", + "source": "physical_can", + "target": { + "device": "seat", + "zone": "front_right", + "instance": "passenger" + }, + "complete": true, + "missing": [] + }, + "finished_at": "2026-08-01T22:08:11.634298Z", + "duration_ms": 34861 + } + ], + "position_memory_sync": { + "id": "PREFLIGHT-POSITION-MEMORY-SYNC", + "group": "preflight", + "utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零", + "started_at": "2026-08-01T22:06:10.662651Z", + "status": "passed", + "errors": [], + "http_status": 201, + "request_id": "req-20260801220610-fe1cde", + "provider_used": "local_rules", + "matched_modes": [], + "state_http_status": 200, + "physical_state": { + "position": { + "legrest_height": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 3, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "03 26 18 0A 00 00 00 00", + "timestamp": 52428.988603 + } + }, + "longitudinal": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 0, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "00 26 18 07 00 00 00 00", + "timestamp": 52429.015565 + } + }, + "cushion_tilt": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 1, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "01 26 18 08 00 00 00 00", + "timestamp": 52429.041376 + } + }, + "backrest_recline": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 2, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "02 26 18 09 00 00 00 00", + "timestamp": 52429.06754 + } + } + }, + "ventilation": { + "state": "off", + "level": 0, + "feedback_value": 1, + "feedback_frame": { + "frame_id": "0x4FA", + "payload_size": 8, + "data_hex": "09 00 00 00 00 00 00 00", + "timestamp": 52429.075411 + } + }, + "heating": { + "state": "off", + "level": 0, + "feedback_value": 1, + "feedback_frame": { + "frame_id": "0x4FA", + "payload_size": 8, + "data_hex": "09 00 00 00 00 00 00 00", + "timestamp": 52429.075411 + } + }, + "captured_at": "2026-08-01T22:06:33.726921Z", + "source": "physical_can", + "target": { + "device": "seat", + "zone": "front_right", + "instance": "passenger" + }, + "complete": true, + "missing": [] + }, + "finished_at": "2026-08-01T22:06:33.819288Z", + "duration_ms": 23157 + }, + "final_state": { + "id": "FINAL-THERMAL-OFF", + "group": "cleanup", + "utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零", + "started_at": "2026-08-01T22:08:11.634351Z", + "status": "passed", + "errors": [], + "http_status": 201, + "request_id": "req-20260801220811-9d1330", + "provider_used": "local_rules", + "matched_modes": [], + "state_http_status": 200, + "physical_state": { + "position": { + "cushion_tilt": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 1, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "01 26 18 1D 00 00 00 00", + "timestamp": 52552.285415 + } + }, + "backrest_recline": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 2, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "02 26 18 1E 00 00 00 00", + "timestamp": 52552.311577 + } + }, + "legrest_height": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 3, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "03 26 18 1F 00 00 00 00", + "timestamp": 52552.337346 + } + }, + "longitudinal": { + "position_pct": 0, + "position_valid": true, + "motion_state": "stopped", + "fault": false, + "task_state": 6, + "motor_result": 2, + "motor_selector": 0, + "value_x100": 0, + "feedback_frame": { + "frame_id": "0x302", + "payload_size": 8, + "data_hex": "00 26 18 1C 00 00 00 00", + "timestamp": 52552.364354 + } + } + }, + "ventilation": { + "state": "off", + "level": 0, + "feedback_value": 1, + "feedback_frame": { + "frame_id": "0x4FA", + "payload_size": 8, + "data_hex": "09 00 00 00 00 00 00 00", + "timestamp": 52552.372264 + } + }, + "heating": { + "state": "off", + "level": 0, + "feedback_value": 1, + "feedback_frame": { + "frame_id": "0x4FA", + "payload_size": 8, + "data_hex": "09 00 00 00 00 00 00 00", + "timestamp": 52552.372264 + } + }, + "captured_at": "2026-08-01T22:08:37.021239Z", + "source": "physical_can", + "target": { + "device": "seat", + "zone": "front_right", + "instance": "passenger" + }, + "complete": true, + "missing": [] + }, + "finished_at": "2026-08-01T22:08:37.100192Z", + "duration_ms": 25466 + }, + "finished_at": "2026-08-01T22:08:37.100232Z", + "summary": { + "total": 5, + "passed": 5, + "failed": 0, + "safety_blocked": 0, + "executed": 5, + "position_memory_sync_passed": true, + "final_state_passed": true + } +} \ No newline at end of file diff --git a/contracts/schemas/function-config.schema.json b/contracts/schemas/function-config.schema.json index 4181947..64d0f99 100755 --- a/contracts/schemas/function-config.schema.json +++ b/contracts/schemas/function-config.schema.json @@ -26,7 +26,7 @@ "requested_delta_angle_deg": {"type": "number", "minimum": 0}, "requested_delta_position_pct": {"type": "integer", "minimum": 0}, "range_clamped": {"type": "boolean"}, - "requested_duration_ms": {"type": "integer", "minimum": 1}, + "requested_duration_ms": {"type": "integer", "minimum": 0}, "applied_duration_ms": {"type": "integer", "minimum": 1}, "max_duration_ms": {"type": "integer", "minimum": 1}, "min_duration_ms": {"type": "integer", "minimum": 1}, diff --git a/docs/testing/t2g-seat-frontend-test-suite.md b/docs/testing/t2g-seat-frontend-test-suite.md index c30de36..770fb23 100644 --- a/docs/testing/t2g-seat-frontend-test-suite.md +++ b/docs/testing/t2g-seat-frontend-test-suite.md @@ -41,7 +41,7 @@ python .\tools\run_seat_frontend_hil_suite.py ` --output .\artifacts\hil\seat_frontend_hil_latest.json ``` -可用 `--only-group thermal|motion|combined|mode|model|safety` 只执行一组,用 `--start-at CASE-ID` 从指定用例续跑,或重复传入 `--case CASE-ID` 精确复测一个有序子集。每次运行先强制执行四轴物理归零,重建霍尔位置记忆;失败时停止后续位置测试。当前清单共 186 条:129 条热管理/移动/组合输入、33 条模式输入、4 条 GPT-5.6-sol 模糊意图输入和 20 条安全拒绝输入。新增覆盖包括取暖/吹风口语、正常秒数/半秒/毫秒与最小时长钳制、四轴独立端点表达、坐垫与腿托超量程、靠背角度下溢,以及后排/左前/全部座椅、小数/负位置和零时长安全拒绝。每条用例均核对:所有座椅操作为 `passenger/front_right`、物理状态来源为 `physical_can`、目标位置误差不超过 2%、联合孪生值与物理状态相同;模型用例还会防止静默降级为本地规则,时长和超量程用例会核对界面可见的时长上限、实际执行时长和到达端点提示,拒绝用例会比较动作前后物理指纹。调试环境设置 `COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED=false`,执行器和前端均不要求逐次输入硬件令牌。 +可用 `--only-group thermal|motion|combined|mode|model|safety` 只执行一组,用 `--start-at CASE-ID` 从指定用例续跑,或重复传入 `--case CASE-ID` 精确复测一个有序子集。每次运行先强制执行四轴物理归零,重建霍尔位置记忆;失败时停止后续位置测试。当前清单共 186 条:132 条热管理/移动/组合输入、33 条模式输入、6 条 GPT-5.6-sol 模糊/未知模式输入和 15 条安全拒绝输入。体验容错覆盖取暖/吹风口语、正常秒数/半秒/毫秒、零时长按最小安全时长执行、小数位置四舍五入、负位置钳制到近端、四轴独立端点、超量程备用端点,以及模糊舒适和未保存老板模式的 GPT 姿态合成。只有后排/左前/全部座椅等不受支持目标、未知物理能力或真实语义冲突保留安全拒绝。每条用例均核对:所有座椅操作为 `passenger/front_right`、物理状态来源为 `physical_can`、目标位置误差不超过 2%、联合孪生值与物理状态相同;模型用例还会防止静默降级为本地规则,时长、取整和超量程用例会核对界面可见的实际执行提示,拒绝用例会比较动作前后物理指纹。调试环境设置 `COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED=false`,执行器和前端均不要求逐次输入硬件令牌。 ## 真实台架执行顺序 @@ -64,3 +64,5 @@ python .\tools\run_seat_frontend_hil_suite.py ` 2026-08-01 的 M0/M2 无 Hall 与 M1 高电流无 Hall 记录最终定位为水平、坐盆和靠背 Hall 地线断开。2026-08-02 地线恢复、四轴手工归零后,revision 6 的完整自动 HIL 结果为 33/33 通过、0 失败、0 `safety_blocked`:覆盖加热/通风全部档位、M0~M3 双向中间位置、单轴及四轴归零、组合指令和全部 11 个预设模式。临时轴向安全阻塞已从测试清单移除;报告保存于 `artifacts/hil/seat_frontend_hil_20260802_full_retest_run1.json`。 2026-08-02 补齐时长和超量程容错后,扩充后的完整 HIL 为 161/161 通过、0 失败、0 `safety_blocked`,最终清理通过;独立 CAN 复核确认加热/通风均为 0 档、M0~M3 均为 0% 且四轴 `Position_Valid=1`。其中“靠背向后移动70度”、四轴双向超长时长、水平101%、靠背200度以及“打开零重力模式”均在完整序列中通过。完整报告为 `artifacts/hil/seat_frontend_hil_20260802_full_timed_overflow.json`,新增规则的定向 14/14 报告为 `artifacts/hil/seat_frontend_hil_20260802_timed_overflow_targeted.json`。 + +2026-08-02 调整体验容错边界后,软件完整回归为 158/158 通过并包含 248 个参数化子用例;真实台架定向回归为 5/5 通过。定向用例验证了 50.5% 取整为 51%、-10% 钳制到 0% 机械近端、0 秒按 300 ms 最小时长执行,以及 GPT-5.6-sol 将“把座椅调舒服一点”和未保存的“老板模式”分别合成为 80/30/35/10 与 20/30/10/0 四轴目标。预检位置记忆同步和最终清理均通过,独立物理 CAN 复核确认加热/通风为 0 档、M0~M3 为 0%、四轴 `Position_Valid=1`;报告为 `artifacts/hil/seat_frontend_hil_20260802_experience_tolerance_targeted.json`。 diff --git a/domains/seat/src/seat_domain/intent.py b/domains/seat/src/seat_domain/intent.py index cebd19c..b791e34 100755 --- a/domains/seat/src/seat_domain/intent.py +++ b/domains/seat/src/seat_domain/intent.py @@ -2,6 +2,7 @@ from __future__ import annotations +import math import re import unicodedata from copy import deepcopy @@ -20,7 +21,7 @@ STATE_PATTERN = re.compile( LEVEL_PATTERN = re.compile(r"(最高|最低|[1-5一二三四五高中低])\s*档") POSITION_PATTERN_TEMPLATE = ( r"{alias}(?:位置)?(?:调到|移动到|移到|设为|设置为|调至|移动至|到|为)?" - r"(?P\d{{1,3}})%" + r"(?P[+-]?\d{{1,3}}(?:\.\d+)?)%" ) ANGLE_PATTERN_TEMPLATE = ( r"{alias}(?:夹角|角度)?(?:调到|放倒到|抬到|移动到|移到|设为|设置为|" @@ -300,6 +301,16 @@ def _format_seconds(duration_ms: int) -> str: return f"{seconds:g}" +def _round_half_away_from_zero(value: float) -> int: + """Use the integer a user normally expects from a .5 position request.""" + + return math.floor(value + 0.5) if value >= 0 else math.ceil(value - 0.5) + + +def _format_numeric_request(value: float) -> str: + return f"{value:g}" + + def _timed_motion_action( axis: str, direction_token: str, @@ -309,8 +320,8 @@ def _timed_motion_action( operation = _relative_direction(axis, direction_token) if operation is None: return None, f"{direction_token}不适用于该座椅轴,请确认移动方向。" - if requested_duration_ms <= 0: - return None, "座椅按时长移动的时长必须大于0。" + if requested_duration_ms < 0: + return None, "座椅按时长移动的时长不能为负数。" state = _context_motion_state(context, axis) if state is None: return None, "缺少当前座椅位置,无法安全计算按时长移动的方向和备用端点。" @@ -894,12 +905,18 @@ def parse_local_intent( for axis, aliases in MOTION_AXIS_ALIASES.items() if any(alias in clause for alias in aliases) ] - percentages = list(re.finditer(r"(?P\d{1,3})%", clause)) + percentages = list( + re.finditer(r"(?P[+-]?\d{1,3}(?:\.\d+)?)%", clause) + ) shared_position = None if len(clause_axes) > 1 and len(percentages) == 1 and re.search( r"都|全部|均", clause ): - shared_position = int(percentages[0].group("position")) + requested_shared_value = float(percentages[0].group("position")) + rounded_shared_position = _round_half_away_from_zero( + requested_shared_value + ) + shared_position = max(0, min(100, rounded_shared_position)) target_zh = ( _nearest_preceding_target(clause, percentages[0].start()) or "副驾" ) @@ -908,8 +925,6 @@ def parse_local_intent( {"field": "target", "question": PASSENGER_ONLY_QUESTION} ) else: - requested_shared_position = shared_position - shared_position = min(100, shared_position) motion_actions.extend( { "target": "passenger", @@ -920,11 +935,22 @@ def parse_local_intent( { "user_notice": ( f"{MOTION_AXIS_LABELS[axis]}请求位置" - f"{requested_shared_position}%超过最大行程;" - "已采用备用目标并移动到对应机械末端。" + f"{_format_numeric_request(requested_shared_value)}%;" + + ( + f"已取整为{rounded_shared_position}%。" + if requested_shared_value + != rounded_shared_position + else "" + ) + + ( + f" 取整后超过0到100%行程,已采用" + f"{shared_position}%备用目标并移动到对应机械末端。" + if rounded_shared_position != shared_position + else "" + ) ) } - if requested_shared_position > 100 + if requested_shared_value != shared_position else {} ), } @@ -1104,8 +1130,13 @@ def parse_local_intent( break absolute_angle = None if position_match is not None: - requested_position_pct = int(position_match.group("position")) - position_pct = min(100, requested_position_pct) + requested_position_value = float( + position_match.group("position") + ) + requested_position_pct = _round_half_away_from_zero( + requested_position_value + ) + position_pct = max(0, min(100, requested_position_pct)) else: requested_angle = int(angle_match.group("angle")) minimum, maximum = MOTION_ANGLE_RANGES[axis] @@ -1133,11 +1164,21 @@ def parse_local_intent( f"{minimum}到{maximum}度行程;已采用备用目标" f"{absolute_angle}度并移动到{endpoint_label}。" ) - elif requested_position_pct != position_pct: - motion_action["user_notice"] = ( - f"{MOTION_AXIS_LABELS[axis]}请求位置{requested_position_pct}%超过" - "0到100%行程;已采用100%的备用目标并移动到机械末端。" - ) + else: + notices: list[str] = [] + if requested_position_value != requested_position_pct: + notices.append( + f"{MOTION_AXIS_LABELS[axis]}请求位置" + f"{_format_numeric_request(requested_position_value)}%;" + f"已取整为{requested_position_pct}%。" + ) + if requested_position_pct != position_pct: + notices.append( + f"取整后超过0到100%行程;已采用{position_pct}%的备用目标" + "并移动到对应机械末端。" + ) + if notices: + motion_action["user_notice"] = " ".join(notices) motion_actions.append(motion_action) break diff --git a/domains/seat/tests/unit/test_intent.py b/domains/seat/tests/unit/test_intent.py index c8d2322..6c8ecb3 100755 --- a/domains/seat/tests/unit/test_intent.py +++ b/domains/seat/tests/unit/test_intent.py @@ -301,6 +301,29 @@ class LocalIntentParserTests(unittest.TestCase): self.assertTrue(minimum["actions"][0]["motion_intent"]["duration_clamped"]) self.assertIn("最小允许时长为0.3秒", minimum["actions"][0]["user_notice"]) + def test_rounds_decimal_and_clamps_negative_absolute_positions(self) -> None: + decimal = parse_local_intent("副驾座椅水平位置调到50.5%") + negative = parse_local_intent("副驾座椅水平位置调到-10%") + + self.assertEqual(decimal["ambiguities"], []) + self.assertEqual(decimal["actions"][0]["position_pct"], 51) + self.assertIn("取整为51%", decimal["actions"][0]["user_notice"]) + self.assertEqual(negative["ambiguities"], []) + self.assertEqual(negative["actions"][0]["position_pct"], 0) + self.assertIn("0%的备用目标", negative["actions"][0]["user_notice"]) + + def test_zero_duration_uses_minimum_safe_motion_instead_of_rejecting(self) -> None: + result = parse_local_intent( + "副驾靠背向后移动0秒", self.motion_context() + ) + + self.assertEqual(result["ambiguities"], []) + motion = result["actions"][0]["motion_intent"] + self.assertEqual(motion["requested_duration_ms"], 0) + self.assertEqual(motion["applied_duration_ms"], 300) + self.assertTrue(motion["duration_clamped"]) + self.assertIn("已按0.3秒", result["actions"][0]["user_notice"]) + def test_rejects_unsupported_level_and_conflicting_state(self) -> None: unsupported = parse_local_intent("副驾加热开到4档") conflicting = parse_local_intent("打开副驾加热然后关闭副驾加热") diff --git a/src/cognitive_agent/infrastructure/llm/openai_responses.py b/src/cognitive_agent/infrastructure/llm/openai_responses.py index 8ca185b..23a4e2c 100755 --- a/src/cognitive_agent/infrastructure/llm/openai_responses.py +++ b/src/cognitive_agent/infrastructure/llm/openai_responses.py @@ -139,6 +139,11 @@ with this conservative deterministic baseline: longitudinal 80%, cushion tilt 30 remaining upright and reversible. State this inferred ergonomic baseline in decision_summary. Apply the same causal reasoning to paraphrases such as insufficient legroom, feeling squeezed, wanting more space, or asking for a comfortable posture; do not require the user to name a motor or percentage. +When a user simply asks to make the seat more comfortable without numeric targets, use that same +80/30/35/10 ergonomic baseline. For an unsaved "boss", "executive", or equivalent front-seat +clearance mode, infer the conventional intent of moving this controllable front passenger seat out +of the way and use this deterministic baseline: longitudinal 20%, cushion tilt 30%, backrest recline +10%, and legrest height 0%. State the inferred mode meaning and targets in decision_summary. When execution_policy.planning_environment is dry_run_candidate_synthesis, a named preset is not required. For a commonly understood mode, activity, or comfort goal, decompose the goal into the @@ -156,8 +161,10 @@ capabilities as usual. Return every understood goal in requests using an exact capability name from the registry. Use target_scope and operation to preserve the user's intent. Put concrete capability parameters in the -parameters list when they are known or defensibly inferred. Do not drop catalog_only requests; the -runtime router, not the model, decides which domain adapter can execute them. +parameters list when they are known or defensibly inferred. Preserve a catalog_only request when the +user explicitly asks for that exact feature, but do not invent catalog_only capabilities while +synthesizing a vague comfort goal or an unknown named mode. Inferred seat postures must use only the +registered executable seat motion capabilities so the plan can safely reach hardware. For executable capabilities, resolve follow-up and relative intent against runtime state and return absolute target parameter values that the adapter can execute. Do not delegate that semantic decision to downstream code. Catalog-only requests may remain relative when their current state is unavailable. diff --git a/tests/fixtures/seat_frontend_hil_suite.json b/tests/fixtures/seat_frontend_hil_suite.json index 85b3848..723ef25 100644 --- a/tests/fixtures/seat_frontend_hil_suite.json +++ b/tests/fixtures/seat_frontend_hil_suite.json @@ -14,8 +14,8 @@ "coverage_dimensions": { "targets": ["副驾", "副驾驶", "副驾位", "右前座", "乘客座", "省略座位", "后排/左前/全部座椅安全拒绝"], "verbs": ["打开", "开启", "启动", "调到", "设置为", "进入", "切换到", "执行", "关闭", "停止", "归零", "复位"], - "values": ["阿拉伯数字", "中文数字", "全角百分号", "百分之表达", "高/中/低档", "正常秒数/半秒/毫秒", "时长上下限钳制", "绝对角度", "相对角度", "相对百分比", "逐轴机械端点", "超量程备用端点", "小数/负数安全拒绝"], - "functions": ["加热/取暖", "通风/吹风", "水平", "坐盆", "靠背", "腿托", "四轴组合", "相对移动", "按时长移动", "预设模式", "GPT模糊舒适意图"], + "values": ["阿拉伯数字", "中文数字", "全角百分号", "百分之表达", "高/中/低档", "正常秒数/半秒/毫秒", "时长上下限钳制", "零时长最小动作", "绝对角度", "相对角度", "相对百分比", "逐轴机械端点", "超量程备用端点", "小数取整/负数端点容错"], + "functions": ["加热/取暖", "通风/吹风", "水平", "坐盆", "靠背", "腿托", "四轴组合", "相对移动", "按时长移动", "预设模式", "GPT模糊舒适意图", "GPT未知模式姿态合成"], "safety": ["主驾", "前排范围", "越界容错", "缺少状态", "缺少当前位置", "矛盾指令", "未知能力", "多模式冲突", "非端点堵转不容错"] }, "cases": [ @@ -315,7 +315,10 @@ {"id": "MOTION-M1-ABS-OVERFLOW", "group": "motion", "utterance": "坐垫位置调到150%", "expected": {"cushion_tilt": 100}, "expected_notice_contains": ["备用目标", "机械末端"]}, {"id": "MOTION-M2-ABS-ANGLE-UNDERFLOW", "group": "motion", "utterance": "副驾靠背调到80度", "expected": {"backrest_recline": 0}, "expected_notice_contains": ["备用目标", "95度", "最前方"]}, - {"id": "MOTION-M3-ABS-ANGLE-OVERFLOW", "group": "motion", "utterance": "腿托调到200度", "expected": {"legrest_height": 100}, "expected_notice_contains": ["备用目标", "167度", "最上方"]} + {"id": "MOTION-M3-ABS-ANGLE-OVERFLOW", "group": "motion", "utterance": "腿托调到200度", "expected": {"legrest_height": 100}, "expected_notice_contains": ["备用目标", "167度", "最上方"]}, + {"id": "MOTION-M0-ABS-DECIMAL-ROUND", "group": "motion", "utterance": "副驾座椅水平位置调到50.5%", "expected": {"longitudinal": 51}, "expected_notice_contains": ["取整为51%"]}, + {"id": "MOTION-M0-ABS-NEGATIVE-CLAMP", "group": "motion", "utterance": "副驾座椅水平位置调到-10%", "expected": {"longitudinal": 0}, "expected_notice_contains": ["备用目标", "0%", "机械末端"]}, + {"id": "MOTION-M2-TIMED-ZERO-MIN-CLAMP", "group": "motion", "utterance": "副驾靠背向后移动0秒", "expected": {}, "expected_motion": {"axis": "backrest_recline", "mode": "timed", "direction": "increase", "requested_duration_ms": 0, "applied_duration_ms": 300, "max_duration_ms": 30000, "min_duration_ms": 300, "duration_clamped": true}, "expected_notice_contains": ["最小允许时长为0.3秒", "已按0.3秒"]} ], "mode_cases": [ {"mode_id": "easy_entry_exit", "expected": {"longitudinal": 100, "cushion_tilt": 0, "backrest_recline": 0, "legrest_height": 0}, "variants": [ @@ -385,6 +388,20 @@ "provider": "openai", "expected_provider_prefix": "openai:gpt-5.6-sol", "expected": {"longitudinal": 80, "cushion_tilt": 30, "backrest_recline": 35, "legrest_height": 10} + }, + { + "id": "MODEL-FUZZY-COMFORT-ONLY", + "utterance": "把座椅调舒服一点", + "provider": "openai", + "expected_provider_prefix": "openai:gpt-5.6-sol", + "expected": {"longitudinal": 80, "cushion_tilt": 30, "backrest_recline": 35, "legrest_height": 10} + }, + { + "id": "MODEL-UNKNOWN-BOSS-MODE", + "utterance": "打开老板模式", + "provider": "openai", + "expected_provider_prefix": "openai:gpt-5.6-sol", + "expected": {"longitudinal": 20, "cushion_tilt": 30, "backrest_recline": 10, "legrest_height": 0} } ], "rejection_cases": [ @@ -396,18 +413,13 @@ {"id": "SAFE-MISSING-STATE", "utterance": "副驾座椅通风", "expected_status": "needs_confirmation"}, {"id": "SAFE-CONTRADICTORY", "utterance": "打开副驾加热然后关闭副驾加热", "expected_status": "needs_confirmation"}, {"id": "SAFE-UNKNOWN-CAPABILITY", "utterance": "把副驾座椅按摩打开", "expected_status": "needs_confirmation"}, - {"id": "SAFE-UNKNOWN-MODE", "utterance": "打开老板模式", "expected_status": "needs_confirmation"}, {"id": "SAFE-MULTIPLE-MODES", "utterance": "进入办公模式和零重力模式", "expected_status": "needs_confirmation"}, {"id": "SAFE-LEGREST-EXTENSION", "utterance": "把副驾腿托伸长到50%", "expected_status": "needs_confirmation"}, {"id": "SAFE-RELATIVE-WRONG-UNIT", "utterance": "坐垫向前移动10度", "expected_status": "needs_confirmation"}, {"id": "SAFE-RELATIVE-MISSING-AXIS", "utterance": "向前移动10度", "expected_status": "needs_confirmation"}, - {"id": "SAFE-VAGUE", "utterance": "把座椅调舒服一点", "expected_status": "needs_confirmation"}, {"id": "SAFE-UNSUPPORTED-REAR-ROW", "utterance": "打开后排座椅加热", "expected_status": "needs_confirmation"}, {"id": "SAFE-UNSUPPORTED-FRONT-LEFT", "utterance": "打开左前座椅通风", "expected_status": "needs_confirmation"}, - {"id": "SAFE-UNSUPPORTED-ALL-SEATS", "utterance": "打开所有座椅加热", "expected_status": "needs_confirmation"}, - {"id": "SAFE-DECIMAL-POSITION", "utterance": "副驾座椅水平位置调到50.5%", "expected_status": "needs_confirmation"}, - {"id": "SAFE-NEGATIVE-POSITION", "utterance": "副驾座椅水平位置调到-10%", "expected_status": "needs_confirmation"}, - {"id": "SAFE-ZERO-DURATION", "utterance": "副驾靠背向后移动0秒", "expected_status": "needs_confirmation"} + {"id": "SAFE-UNSUPPORTED-ALL-SEATS", "utterance": "打开所有座椅加热", "expected_status": "needs_confirmation"} ], "final_state": { "utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零", diff --git a/tests/integration/test_seat_frontend_input_coverage.py b/tests/integration/test_seat_frontend_input_coverage.py index 26ce274..b12ed0d 100644 --- a/tests/integration/test_seat_frontend_input_coverage.py +++ b/tests/integration/test_seat_frontend_input_coverage.py @@ -60,7 +60,7 @@ class SeatFrontendInputCoverageTests(unittest.TestCase): def test_suite_has_broad_equivalence_and_safety_coverage(self) -> None: self.assertGreaterEqual(len(self.positive_cases), 160) self.assertGreaterEqual(len(self.model_cases), 4) - self.assertGreaterEqual(len(self.rejection_cases), 20) + self.assertGreaterEqual(len(self.rejection_cases), 15) self.assertEqual( {case["group"] for case in self.positive_cases}, {"thermal", "motion", "combined", "mode"}, diff --git a/tests/unit/test_openai_responses.py b/tests/unit/test_openai_responses.py index ce069e0..ebc9dc4 100755 --- a/tests/unit/test_openai_responses.py +++ b/tests/unit/test_openai_responses.py @@ -72,6 +72,9 @@ class OpenAIResponsesTests(unittest.TestCase): self.assertIn("longitudinal 80%", normalized) self.assertIn("backrest recline 35%", normalized) self.assertIn("do not manufacture a driver-seat target", normalized) + self.assertIn("boss", normalized) + self.assertIn("longitudinal 20%", normalized) + self.assertIn("do not invent catalog_only capabilities", normalized) def test_non_streaming_structured_output(self) -> None: response_body = json.dumps({"output_text": json.dumps(INTENT)})