feat: harden seat intent and expand HIL coverage

This commit is contained in:
vee
2026-08-02 05:20:38 +08:00
parent 4aded5c21d
commit 55d0d400cb
62 changed files with 53112 additions and 1748 deletions
+178 -13
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import json
import os
import re
import sys
import time
@@ -22,9 +23,11 @@ if str(SEAT_SOURCE) not in sys.path:
from seat_domain import ( # noqa: E402
HardwareSeatExecutor,
SIMULATED_HARDWARE_FAULTS,
SeatCanMatrix,
SeatCompiler,
T2GSeatCompiler,
SimulatedHardwareSeatExecutor,
VirtualSeatExecutor,
build_function_config,
build_function_preset,
@@ -120,6 +123,10 @@ class SeatDemoService:
)
self.compiler = SeatCompiler(self.domain_root / "can" / "signal-mapping.yaml")
self.executor = VirtualSeatExecutor(self.matrix)
self.simulated_hardware_executor = SimulatedHardwareSeatExecutor(self.matrix)
self.simulated_hardware_enabled = os.getenv(
"COGNITIVE_AGENT_SIMULATED_HARDWARE_ENABLED", "false"
).strip().lower() in {"1", "true", "yes", "on"}
self.hardware_matrix = SeatCanMatrix(
[
self.domain_root / "can" / "seat-thermal-hardware-matrix.yaml",
@@ -131,6 +138,10 @@ class SeatDemoService:
repository_root / "platforms" / "t2g_seat_bench" / "calibration" / "seat.yaml",
)
self.hardware_settings = SeatHardwareSettings.from_env(repository_root)
# Shared by the short-lived hardware executor instances. A process must
# establish a fresh physical four-axis reference before trusting stored
# Hall counts, and a firmware replacement invalidates it again.
self._seat_position_memory_state = {"synchronized": False}
self.openai = OpenAIIntentInterpreter()
self.domain_runtimes = {
domain: ConfiguredDomainRuntime(repository_root, domain)
@@ -171,11 +182,21 @@ class SeatDemoService:
"domain": "cabin",
"platform": "reference_vehicle",
"execution_mode": "dry_run",
"execution_modes": ["dry_run", "hardware"],
"execution_modes": ["dry_run", "simulated_hardware", "hardware"],
"hardware_execution_allowed": self.hardware_settings.configured,
"hardware": self.hardware_settings.public_info(),
"seat_position_memory_synchronized": bool(
self._seat_position_memory_state.get("synchronized", False)
),
"simulated_hardware": {
"enabled": self.simulated_hardware_enabled,
"default_fault": "normal",
"fault_scenarios": list(SIMULATED_HARDWARE_FAULTS),
"physical_can_opened": False,
},
"openai_available": self.openai.available,
"openai_model": self.openai.model,
"openai_responses_endpoint": self.openai.responses_endpoint,
"conversation_memory": True,
"default_vehicle_id": default_vehicle.id,
"vehicle": default_vehicle.public_dict(),
@@ -272,6 +293,10 @@ class SeatDemoService:
auto_home=self.hardware_settings.auto_home_enabled,
home_timeout_s=self.hardware_settings.home_timeout_s,
firmware_guard=self.hardware_settings.firmware_guard(),
synchronize_position_before_first_motion=(
self.hardware_settings.position_sync_enabled
),
position_sync_state=self._seat_position_memory_state,
)
def process(
@@ -282,6 +307,7 @@ class SeatDemoService:
execution_mode: str = "dry_run",
approval_token: str | None = None,
vehicle_id: str | None = None,
simulation_fault: str = "normal",
) -> dict[str, Any]:
utterance = utterance.strip()
if not utterance:
@@ -290,8 +316,21 @@ class SeatDemoService:
raise ValueError("utterance must be at most 1000 characters")
if provider not in {"auto", "openai", "local"}:
raise ValueError("provider must be auto, openai, or local")
if execution_mode not in {"dry_run", "hardware"}:
raise ValueError("execution_mode must be dry_run or hardware")
if execution_mode not in {"dry_run", "simulated_hardware", "hardware"}:
raise ValueError(
"execution_mode must be dry_run, simulated_hardware, or hardware"
)
simulated_hardware_request = execution_mode == "simulated_hardware"
if simulated_hardware_request and not self.simulated_hardware_enabled:
raise HardwareSafetyError(
"simulated hardware is disabled; set "
"COGNITIVE_AGENT_SIMULATED_HARDWARE_ENABLED=true"
)
if simulation_fault not in SIMULATED_HARDWARE_FAULTS:
raise ValueError(
"simulation_fault must be one of "
+ ", ".join(SIMULATED_HARDWARE_FAULTS)
)
mode_save_name = self._parse_mode_profile_save_name(utterance)
mode_restore_name = self._parse_mode_profile_restore_name(utterance)
mode_update = self._parse_mode_profile_update(utterance)
@@ -329,6 +368,8 @@ class SeatDemoService:
"hardware_execution_allowed": (
hardware_request and not control_plane_request
),
"simulated_hardware": simulated_hardware_request,
"simulation_fault": simulation_fault if simulated_hardware_request else None,
"vehicle": vehicle_profile.public_dict(),
"matched_modes": [
profile.public_dict() for profile in matched_modes
@@ -483,6 +524,8 @@ class SeatDemoService:
result,
traces,
hardware_request=hardware_request,
simulated_hardware_request=simulated_hardware_request,
simulation_fault=simulation_fault,
vehicle_profile=vehicle_profile,
)
@@ -763,11 +806,19 @@ class SeatDemoService:
)
+ "板端 Position_Valid 将在发送绝对移动命令前实时检查。"
if hardware_request
else "平台、矩阵和策略均限制为 dry-run;硬件发送路径未启用。"
else (
"模拟台架隔离门禁已通过;仅生成故障注入反馈,物理 CAN、ZLG 与 IAR 路径均未打开。"
if simulated_hardware_request
else "平台、矩阵和策略均限制为 dry-run;硬件发送路径未启用。"
)
),
safety_started,
{
"hardware_execution_allowed": hardware_request,
"simulated_hardware": simulated_hardware_request,
"simulation_fault": (
simulation_fault if simulated_hardware_request else None
),
"position_calibration_gate": (
"runtime_can_preflight" if hardware_request else "not_applicable"
),
@@ -788,6 +839,13 @@ class SeatDemoService:
execution_report = hardware_executor.execute(
command_plan, function_config, context["devices"]
)
elif simulated_hardware_request:
execution_report = self.simulated_hardware_executor.execute(
command_plan,
function_config,
context["devices"],
fault_scenario=simulation_fault,
)
else:
execution_report = self.executor.execute(
command_plan, function_config, context["devices"]
@@ -845,7 +903,15 @@ class SeatDemoService:
else hardware_failure_summary
)
if hardware_request
else "虚拟 CAN 已编码命令帧并生成模拟回读。"
else (
"模拟台架已生成隔离的 HIL 反馈,未访问物理 CAN。"
if simulated_hardware_request and execution_succeeded
else (
f"模拟台架故障已触发:{failed_execution['message']}"
if simulated_hardware_request and failed_execution
else "虚拟 CAN 已编码命令帧并生成模拟回读。"
)
)
)
),
execute_started,
@@ -861,6 +927,14 @@ class SeatDemoService:
},
)
if execution_succeeded:
execution_notices = [
str(item.get("telemetry", {}).get("user_notice"))
for item in execution_results
if item.get("telemetry", {}).get("user_notice")
]
for notice in execution_notices:
if notice not in parsed["decision_summary"]:
parsed["decision_summary"].append(notice)
state_actions = (
self._hardware_feedback_actions(
parsed["actions"], function_config, execution_report
@@ -950,7 +1024,15 @@ class SeatDemoService:
else "未获得满足目标的真实硬件反馈。"
)
if hardware_request
else "虚构反馈帧中的最终状态与请求目标一致。"
else (
"模拟台架反馈与请求目标一致,物理 CAN 始终关闭。"
if simulated_hardware_request and execution_succeeded
else (
f"模拟台架故障反馈:{failed_execution['message']}"
if simulated_hardware_request and failed_execution
else "虚构反馈帧中的最终状态与请求目标一致。"
)
)
)
),
time.perf_counter(),
@@ -958,6 +1040,9 @@ class SeatDemoService:
"command_frame": telemetry.get("command_frame", {}).get("data_hex"),
"feedback_frame": telemetry.get("feedback_frame", {}).get("data_hex"),
"simulated": not hardware_request,
"simulation_fault": (
simulation_fault if simulated_hardware_request else None
),
},
)
self._persist(result)
@@ -1026,9 +1111,16 @@ class SeatDemoService:
action["position_valid"] = bool(
telemetry["position_valid"]
)
if telemetry.get("motion_mode") == "relative_timed_duty":
if telemetry.get("motion_mode") in {
"relative_timed_duty",
"timed_user_request",
}:
action["position_estimated"] = True
action["feedback_basis"] = "relative_hall_confirmed_open_loop"
action["feedback_basis"] = (
"timed_endpoint_or_hall_confirmed_open_loop"
if telemetry.get("motion_mode") == "timed_user_request"
else "relative_hall_confirmed_open_loop"
)
continue
raw = telemetry.get("feedback_value")
if raw is not None:
@@ -1141,6 +1233,35 @@ class SeatDemoService:
return parsed
if self._contains_seat_preset_request(parsed):
return parsed
if len(matched_modes) > 1:
rejected = deepcopy(parsed)
for key in (
"actions",
"domain_actions",
"preset_actions",
"preset_events",
"deferred_requests",
"model_requests",
):
rejected[key] = []
rejected["ambiguities"] = [
{
"field": "mode_profile",
"question": "一条请求只能执行一个座椅功能模式,请确认要执行的模式。",
}
]
rejected["decision_summary"] = [
"同一条请求命中多个功能模式,已停止生成控制指令以避免模式冲突。"
]
return rejected
safety_ambiguities = [
item
for item in parsed.get("ambiguities", [])
if item.get("field") != "capability"
]
if safety_ambiguities:
return parsed
mode_requests = self.mode_profiles.requests_for_profiles(matched_modes)
detected_domains = []
@@ -1969,6 +2090,8 @@ class SeatDemoService:
traces: list[dict[str, Any]],
*,
hardware_request: bool = False,
simulated_hardware_request: bool = False,
simulation_fault: str = "normal",
vehicle_profile: VehicleProfile | None = None,
) -> dict[str, Any]:
"""Run isolated adapters, with only the passenger seat eligible for HIL."""
@@ -2165,11 +2288,19 @@ class SeatDemoService:
(
"副驾座椅已通过 T2G HIL 安全门,其他座舱领域保持虚拟 CAN。"
if hardware_request
else "所有领域均为 dry-run;矩阵缺少实车完整性信息,硬件发送保持禁用。"
else (
"模拟台架仅对座椅结果注入故障;全部领域的物理发送路径保持关闭。"
if simulated_hardware_request
else "所有领域均为 dry-run;矩阵缺少实车完整性信息,硬件发送保持禁用。"
)
),
time.perf_counter(),
{
"hardware_execution_allowed": hardware_request,
"simulated_hardware": simulated_hardware_request,
"simulation_fault": (
simulation_fault if simulated_hardware_request else None
),
"hardware_domains": ["seat"] if hardware_request else [],
"virtual_domains": [
domain
@@ -2190,6 +2321,13 @@ class SeatDemoService:
reports[domain] = self._new_hardware_executor().execute(
plans[domain], config, context["devices"]
)
elif simulated_hardware_request:
reports[domain] = self.simulated_hardware_executor.execute(
plans[domain],
config,
context["devices"],
fault_scenario=simulation_fault,
)
else:
reports[domain] = self.executor.execute(
plans[domain], config, context["devices"]
@@ -2212,8 +2350,9 @@ class SeatDemoService:
if hardware_request
else None
)
simulated_seat = simulated_hardware_request and "seat" in reports
report_spec: dict[str, Any] = {
"mode": "hil" if hardware_request else "dry_run",
"mode": "hil" if hardware_request or simulated_seat else "dry_run",
"status": "succeeded" if execution_succeeded else "failed",
"started_at": execution_started_at,
"finished_at": _now(),
@@ -2229,14 +2368,27 @@ class SeatDemoService:
"name": (
"Cabin orchestration mixed HIL execution report"
if hardware_request
else "Cabin orchestration virtual execution report"
else (
"Cabin orchestration simulated hardware execution report"
if simulated_seat
else "Cabin orchestration virtual execution report"
)
),
"version": "1.0.0",
"request_id": request_id,
"labels": {
"simulation": "mixed" if hardware_request else "true",
"seat_feedback": (
"physical_can" if hardware_request else "simulated"
"physical_can"
if hardware_request
else (
"simulated_hardware"
if simulated_seat
else "simulated"
)
),
"fault_scenario": (
simulation_fault if simulated_seat else "none"
),
"orchestration": "cross_domain"
if len(configs) > 1
@@ -2272,7 +2424,20 @@ class SeatDemoService:
else (
f"副驾座椅真实执行失败:{failed_execution['message']}"
if hardware_request and failed_execution
else f"已虚拟执行 {len(report_results)} 个领域操作并汇总反馈。"
else (
f"模拟台架已执行 {len(report_results)} 个领域操作,物理 CAN 未打开。"
if simulated_seat and execution_succeeded
else (
f"模拟台架故障已触发:{failed_execution['message']}"
if simulated_seat and failed_execution
else (
"本次没有座椅动作,故障注入未应用;"
f"已虚拟执行 {len(report_results)} 个领域操作。"
if simulated_hardware_request
else f"已虚拟执行 {len(report_results)} 个领域操作并汇总反馈。"
)
)
)
)
),
execute_started,
+6 -3
View File
@@ -6,7 +6,7 @@ import os
from pathlib import Path
def load_local_env(path: Path) -> None:
def load_local_env(path: Path, *, override: bool = False) -> None:
if not path.is_file():
return
for raw_line in path.read_text(encoding="utf-8").splitlines():
@@ -18,6 +18,9 @@ def load_local_env(path: Path) -> None:
value = value.strip()
if value[:1] == value[-1:] and value.startswith(("'", '"')):
value = value[1:-1]
if key:
os.environ.setdefault(key, value)
if key and (override or not os.environ.get(key, "").strip()):
# A number of launchers export optional variables as empty strings.
# Treat those exactly like an unset value so a deliberate local
# development configuration is not silently shadowed.
os.environ[key] = value
@@ -88,6 +88,7 @@ class SeatHardwareSettings:
cspybat: Path
auto_flash_enabled: bool
auto_home_enabled: bool
position_sync_enabled: bool
home_timeout_s: int
firmware_probe_timeout_ms: int
firmware_boot_wait_ms: int
@@ -171,6 +172,10 @@ class SeatHardwareSettings:
),
auto_flash_enabled=auto_flash_enabled,
auto_home_enabled=_env_bool("COGNITIVE_AGENT_SEAT_AUTO_HOME_ENABLED"),
position_sync_enabled=_env_bool(
"COGNITIVE_AGENT_SEAT_POSITION_SYNC_ENABLED",
default=hardware_enabled,
),
home_timeout_s=_env_int(
"COGNITIVE_AGENT_SEAT_HOME_TIMEOUT_S", 120
),
@@ -375,12 +380,19 @@ class SeatHardwareSettings:
"physical_target": "t2g_primary_seat",
"target_aliasing": "disabled_front_passenger_only",
"auto_home_enabled": self.auto_home_enabled,
"position_sync_enabled": self.position_sync_enabled,
"position_sync_policy": (
"four_axis_known_range_home_before_first_process_motion_and_after_firmware_change"
if self.position_sync_enabled
else "disabled"
),
"home_timeout_s": self.home_timeout_s,
"limitations": [
"hardware execution requires the unified Seat_Controller_demo image on one development board",
"all seat capabilities are restricted to passenger/front_right",
"M0/M1/M2/M3 map to horizontal, seat-pan, backrest and legrest on one physical seat",
"known-range homing can move an invalid axis to its near endpoint and remains separately disabled by default",
"on-demand recovery of an unexpectedly invalid target axis remains separately disabled by default",
"when position synchronization is enabled, the first process motion and every repository firmware replacement rebuild all four physical Hall references",
"current diagnostics use conservative bench thresholds and do not replace external power limiting",
],
}
@@ -12,9 +12,9 @@ from typing import Any, Callable
from cognitive_agent.infrastructure.can.gateway import CanBus
FIRMWARE_PROTOCOL_SIGNATURE = "seat-controller-smv8-current-diagnostics-v3"
FIRMWARE_PROTOCOL_SIGNATURE = "seat-controller-smv8-current-diagnostics-v6"
FIRMWARE_PROTOCOL_FRAME_IDS = frozenset({0x301, 0x302, 0x303, 0x304, 0x305, 0x4FA})
EXPECTED_FIRMWARE_REVISION = 3
EXPECTED_FIRMWARE_REVISION = 6
EXPECTED_MOTOR_MAX_HALL = (480, 374, 917, 647)
@@ -10,6 +10,13 @@ import urllib.request
from typing import Any
def _env_text(name: str, default: str) -> str:
"""Return a non-blank environment value or its documented default."""
value = os.getenv(name, "").strip()
return value or default
def build_intent_schema(capability_context: dict[str, Any]) -> dict[str, Any]:
domains = [item["domain"] for item in capability_context.get("domains", [])] or ["seat"]
capabilities = [
@@ -122,6 +129,17 @@ An explicit scope in the current utterance overrides conversation history.
When multiple capabilities address the goal equally well, prefer one whose domain is executable.
Never substitute a materially less suitable capability merely because it is executable.
When the registry exposes exactly one executable seat target and the user refers only to "the seat",
"the front cabin", or the occupant without a conflicting target, use that sole executable target; do
not manufacture a driver-seat target. Understand causal and fuzzy ergonomic requests as goals, not as
unsupported phrases. In particular, when the occupant says the front cabin is cramped and asks for a
comfortable seat position without numeric targets, emit the four registered seat motion capabilities
with this conservative deterministic baseline: longitudinal 80%, cushion tilt 30%, backrest recline
35%, and legrest height 10%. This baseline moves the seat rearward for usable occupant space while
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 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
registered atomic capabilities and infer conservative absolute targets from their normalized ranges
@@ -173,12 +191,14 @@ class OpenAIIntentError(RuntimeError):
class OpenAIIntentInterpreter:
def __init__(self) -> None:
self.api_key = os.getenv("OPENAI_API_KEY", "")
self.model = os.getenv("OPENAI_MODEL", "gpt-5.6-sol")
self.base_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1").rstrip("/")
self.api_key = os.getenv("OPENAI_API_KEY", "").strip()
self.model = _env_text("OPENAI_MODEL", "gpt-5.6-sol")
self.base_url = _env_text(
"OPENAI_BASE_URL", "https://api.openai.com/v1"
).rstrip("/")
self.timeout_seconds = float(os.getenv("OPENAI_TIMEOUT_SECONDS", "30"))
self.max_retries = max(0, int(os.getenv("OPENAI_MAX_RETRIES", "2")))
self.reasoning_effort = os.getenv("OPENAI_REASONING_EFFORT", "medium")
self.reasoning_effort = _env_text("OPENAI_REASONING_EFFORT", "medium")
self.streaming = os.getenv("OPENAI_STREAM", "false").strip().lower() in {
"1",
"true",
@@ -190,6 +210,10 @@ class OpenAIIntentInterpreter:
def available(self) -> bool:
return bool(self.api_key)
@property
def responses_endpoint(self) -> str:
return f"{self.base_url}/responses"
def interpret(
self,
utterance: str,
@@ -269,7 +293,7 @@ class OpenAIIntentInterpreter:
def _request_output_text(self, payload: dict[str, Any]) -> str:
request = urllib.request.Request(
f"{self.base_url}/responses",
self.responses_endpoint,
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {self.api_key}",
+20 -6
View File
@@ -20,8 +20,10 @@ from cognitive_agent.config_loader import load_local_env
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
load_local_env(REPOSITORY_ROOT / ".env")
load_local_env(REPOSITORY_ROOT / ".env.auth")
# Repository-local launch configuration is deliberate and must not be shadowed
# by stale variables inherited from an IDE or an earlier model experiment.
load_local_env(REPOSITORY_ROOT / ".env", override=True)
load_local_env(REPOSITORY_ROOT / ".env.auth", override=True)
from cognitive_agent.auth import AuthConfig, AuthRuntime, verify_password # noqa: E402
from cognitive_agent.application.seat_service import SeatDemoService # noqa: E402
@@ -31,12 +33,14 @@ from cognitive_agent.infrastructure.llm.openai_responses import OpenAIIntentErro
STATIC_ROOT = REPOSITORY_ROOT / "apps" / "web-console"
mimetypes.add_type("application/manifest+json", ".webmanifest")
REQUEST_ID_PATTERN = re.compile(r"^req-[a-z0-9-]+$")
PUBLIC_AUTH_PATHS = {
"/login",
"/login.html",
"/login.css",
"/login.js",
"/network.js",
"/api/auth/status",
"/api/auth/login",
}
@@ -182,6 +186,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
if payload.get("vehicle_id")
else None
),
simulation_fault=str(payload.get("simulation_fault", "normal")),
)
self._json(result, HTTPStatus.CREATED)
except HardwareSafetyError as exc:
@@ -370,7 +375,14 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", f"{content_type or 'application/octet-stream'}; charset=utf-8")
self.send_header("Content-Length", str(len(content)))
self.send_header("Cache-Control", "no-store")
cache_control = "no-cache"
if relative_path.startswith(("vendor/", "assets/", "icons/")):
cache_control = "public, max-age=31536000, immutable"
elif candidate.name in {"index.html", "login.html", "service-worker.js"}:
cache_control = "no-cache"
self.send_header("Cache-Control", cache_control)
if candidate.name == "service-worker.js":
self.send_header("Service-Worker-Allowed", "/")
self._send_security_headers()
self.end_headers()
self.wfile.write(content)
@@ -427,10 +439,10 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
self.send_header(
"Content-Security-Policy",
"default-src 'self'; "
"script-src 'self' 'wasm-unsafe-eval' https://unpkg.com https://cdn.jsdelivr.net; "
"script-src 'self' 'wasm-unsafe-eval'; "
"style-src 'self'; "
"img-src 'self' data:; "
"connect-src 'self' https://cdn.jsdelivr.net; "
"connect-src 'self'; "
"object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
)
if self._is_https():
@@ -462,7 +474,9 @@ def main() -> None:
print(f"Cabin orchestration demo: http://{args.host}:{args.port}", flush=True)
hardware = SeatDemoHandler.service.hardware_settings
print(
"Execution modes: dry_run + hardware "
"Execution modes: dry_run + simulated_hardware "
f"({'enabled' if SeatDemoHandler.service.simulated_hardware_enabled else 'locked'}) "
"+ hardware "
f"({'configured' if hardware.configured else 'locked'})",
flush=True,
)