This commit is contained in:
orin
2026-07-30 14:09:04 +08:00
parent 25b24ac4cd
commit 49f5c49c53
35 changed files with 4078 additions and 48 deletions
@@ -339,6 +339,55 @@ class ConversationStateStore:
self._persist_presets(conversation_id, state["presets"])
return deepcopy(state)
def save_mode_profile(
self,
conversation_id: str,
utterance: str,
mode_profile: dict[str, Any],
source_actions: list[dict[str, Any]],
decision_summary: list[str],
) -> dict[str, Any]:
with self._lock:
state = self._state(conversation_id)
state["turn_count"] += 1
state["updated_at"] = _now()
state["recent_turns"].append(
{
"utterance": utterance,
"actions": deepcopy(source_actions),
"decision_summary": list(decision_summary),
"control_event": "mode_profile_save",
"mode_profile": deepcopy(mode_profile),
}
)
state["recent_turns"] = state["recent_turns"][-6:]
return deepcopy(state)
def record_mode_profile_event(
self,
conversation_id: str,
utterance: str,
mode_profile: dict[str, Any],
source_actions: list[dict[str, Any]],
decision_summary: list[str],
control_event: str,
) -> dict[str, Any]:
with self._lock:
state = self._state(conversation_id)
state["turn_count"] += 1
state["updated_at"] = _now()
state["recent_turns"].append(
{
"utterance": utterance,
"actions": deepcopy(source_actions),
"decision_summary": list(decision_summary),
"control_event": control_event,
"mode_profile": deepcopy(mode_profile),
}
)
state["recent_turns"] = state["recent_turns"][-6:]
return deepcopy(state)
def reset(self, conversation_id: str) -> dict[str, Any]:
with self._lock:
state = _new_state(conversation_id, self._load_presets(conversation_id))
@@ -0,0 +1,435 @@
"""Repository-backed cabin mode profiles."""
from __future__ import annotations
import hashlib
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import yaml
_WHITESPACE = re.compile(r"\s+")
_NON_ASCII_SLUG = re.compile(r"[^a-z0-9]+")
def _normalize_match_text(value: str) -> str:
return _WHITESPACE.sub("", value.casefold())
def _clean_name(value: str) -> str:
cleaned = value.strip().strip(",。,.!!")
if not cleaned:
raise ValueError("mode name must not be empty")
if len(cleaned) > 64:
raise ValueError("mode name must be at most 64 characters")
return cleaned
def _mode_aliases(name: str, aliases: list[str] | None = None) -> list[str]:
values = [name, *(aliases or [])]
if name.endswith("模式") and len(name) > 2:
values.append(name.removesuffix("模式"))
else:
values.append(f"{name}模式")
normalized: set[str] = set()
unique = []
for value in values:
candidate = str(value).strip()
key = _normalize_match_text(candidate)
if not candidate or key in normalized:
continue
normalized.add(key)
unique.append(candidate)
return unique
def _slugify_mode_id(name: str) -> str:
ascii_slug = _NON_ASCII_SLUG.sub("-", name.casefold()).strip("-")
digest = hashlib.sha256(name.encode("utf-8")).hexdigest()[:10]
if ascii_slug:
return ascii_slug if len(ascii_slug) >= 3 else f"{ascii_slug}-{digest[:6]}"
return f"mode-{digest}"
@dataclass(frozen=True)
class ModeOperation:
domain: str
capability: str
target_scope: str
parameters: dict[str, Any]
operation: str = "set"
reason: str = ""
@classmethod
def from_dict(cls, document: dict[str, Any]) -> "ModeOperation":
parameters = document.get("parameters", {})
if not isinstance(parameters, dict):
raise ValueError("mode operation parameters must be an object")
return cls(
domain=str(document["domain"]),
capability=str(document["capability"]),
target_scope=str(document["target_scope"]),
operation=str(document.get("operation", "set")),
parameters=dict(parameters),
reason=str(document.get("reason", "")),
)
def to_request(self, mode_name: str) -> dict[str, Any]:
return {
"domain": self.domain,
"capability": self.capability,
"target_scope": self.target_scope,
"operation": self.operation,
"parameters": [
{"name": name, "value": value}
for name, value in self.parameters.items()
],
"reason": self.reason or f"Matched mode profile {mode_name}.",
}
def to_yaml(self) -> dict[str, Any]:
return {
"domain": self.domain,
"capability": self.capability,
"target_scope": self.target_scope,
"operation": self.operation,
"parameters": dict(self.parameters),
"reason": self.reason,
}
@dataclass(frozen=True)
class ModeProfile:
id: str
name: str
aliases: tuple[str, ...]
description: str
icon: str
operations: tuple[ModeOperation, ...]
source: str
common: bool = True
vehicle_ids: tuple[str, ...] = ()
@classmethod
def from_yaml(cls, path: Path, profile_root: Path | None = None) -> "ModeProfile":
document = yaml.safe_load(path.read_text(encoding="utf-8"))
if not isinstance(document, dict):
raise ValueError(f"{path} must contain a YAML object")
operations = document.get("operations", [])
if not isinstance(operations, list) or not operations:
raise ValueError(f"{path} must contain at least one operation")
name = _clean_name(str(document["name"]))
common, vehicle_ids = _mode_scope(document, path, profile_root)
aliases = _mode_aliases(
name,
[str(alias) for alias in document.get("aliases", [])],
)
return cls(
id=str(document["id"]),
name=name,
aliases=tuple(aliases),
description=str(document.get("description", "")),
icon=str(document.get("icon", "bookmark")),
operations=tuple(ModeOperation.from_dict(item) for item in operations),
source=str(path),
common=common,
vehicle_ids=vehicle_ids,
)
def public_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"name": self.name,
"aliases": list(self.aliases),
"description": self.description,
"icon": self.icon,
"domains": sorted({operation.domain for operation in self.operations}),
"operation_count": len(self.operations),
"scope": {
"common": self.common,
"vehicle_ids": list(self.vehicle_ids),
},
}
class ModeProfileRegistry:
"""Load, match, and persist long-term cabin mode profiles."""
def __init__(self, profile_root: Path):
self.profile_root = Path(profile_root)
self._profiles: list[ModeProfile] = []
self.reload()
def reload(self) -> None:
if not self.profile_root.exists():
self._profiles = []
return
self._profiles = [
ModeProfile.from_yaml(path, self.profile_root)
for path in sorted(self.profile_root.rglob("*.yaml"))
if path.is_file()
]
def profiles(self) -> list[ModeProfile]:
return list(self._profiles)
def public_modes(self, vehicle_id: str | None = None) -> list[dict[str, Any]]:
return [
profile.public_dict()
for profile in self._profiles_for_vehicle(vehicle_id)
]
def match_text(
self,
utterance: str,
vehicle_id: str | None = None,
) -> list[ModeProfile]:
haystack = _normalize_match_text(utterance)
selected = {
profile.id: profile
for profile in self._profiles_for_vehicle(vehicle_id)
}
matches = []
matched_ids: set[str] = set()
for profile in self._visible_candidates_for_vehicle(vehicle_id):
candidates = [profile.id, profile.name, *profile.aliases]
if any(
candidate
and _normalize_match_text(str(candidate)) in haystack
for candidate in candidates
):
if profile.id in matched_ids:
continue
matched_ids.add(profile.id)
matches.append(selected[profile.id])
return matches
@staticmethod
def requests_for_profiles(profiles: list[ModeProfile]) -> list[dict[str, Any]]:
return [
operation.to_request(profile.name)
for profile in profiles
for operation in profile.operations
]
def save_mode(
self,
name: str,
operations: list[dict[str, Any] | ModeOperation],
mode_id: str | None = None,
aliases: list[str] | None = None,
vehicle_id: str | None = None,
) -> ModeProfile:
clean_name = _clean_name(name)
canonical_operations = [
operation
if isinstance(operation, ModeOperation)
else ModeOperation.from_dict(operation)
for operation in operations
]
if not canonical_operations:
raise ValueError("mode profile must contain at least one operation")
profile_id = mode_id or _slugify_mode_id(clean_name)
scope = (
{"vehicle_ids": [vehicle_id]}
if vehicle_id
else {"common": True, "vehicle_ids": []}
)
document = {
"id": profile_id,
"name": clean_name,
"aliases": _mode_aliases(clean_name, aliases),
"description": "用户固化的长期模式配置。",
"icon": "bookmark",
"scope": scope,
"operations": [
operation.to_yaml() for operation in canonical_operations
],
}
path = (
self.profile_root / "vehicles" / vehicle_id / "user" / f"{profile_id}.yaml"
if vehicle_id
else self.profile_root / "common" / "user" / f"{profile_id}.yaml"
)
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(
yaml.safe_dump(document, allow_unicode=True, sort_keys=False),
encoding="utf-8",
)
temporary.replace(path)
self.reload()
return ModeProfile.from_yaml(path, self.profile_root)
def update_mode(
self,
profile: ModeProfile,
operations: list[dict[str, Any] | ModeOperation],
) -> ModeProfile:
canonical_updates = _canonical_operations(operations)
if not canonical_updates:
raise ValueError("mode profile update must contain at least one operation")
path = Path(profile.source)
if not path.is_file():
raise ValueError(f"mode profile source does not exist: {path}")
backup_path = _initial_backup_path(path)
if not backup_path.exists():
backup_path.write_text(path.read_text(encoding="utf-8"), encoding="utf-8")
merged_operations = _merge_operations(profile.operations, canonical_updates)
document = _mode_profile_document(profile, merged_operations)
_replace_yaml(path, document)
self.reload()
return ModeProfile.from_yaml(path, self.profile_root)
def restore_initial_mode(self, profile: ModeProfile) -> ModeProfile:
path = Path(profile.source)
backup_path = _initial_backup_path(path)
if not backup_path.is_file():
raise ValueError(f"no initial mode backup found: {backup_path}")
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(backup_path.read_text(encoding="utf-8"), encoding="utf-8")
temporary.replace(path)
self.reload()
return ModeProfile.from_yaml(path, self.profile_root)
def _profiles_for_vehicle(self, vehicle_id: str | None) -> list[ModeProfile]:
selected: dict[str, ModeProfile] = {}
order: list[str] = []
for profile in self._visible_candidates_for_vehicle(vehicle_id):
if profile.id not in selected:
order.append(profile.id)
selected[profile.id] = profile
continue
current = selected[profile.id]
if _profile_rank(profile, vehicle_id) >= _profile_rank(current, vehicle_id):
selected[profile.id] = profile
return [selected[profile_id] for profile_id in order]
def _visible_candidates_for_vehicle(
self,
vehicle_id: str | None,
) -> list[ModeProfile]:
return [
profile
for profile in self._profiles
if _is_visible_for_vehicle(profile, vehicle_id)
]
def _canonical_operations(
operations: list[dict[str, Any] | ModeOperation],
) -> list[ModeOperation]:
return [
operation
if isinstance(operation, ModeOperation)
else ModeOperation.from_dict(operation)
for operation in operations
]
def _operation_signature(operation: ModeOperation) -> tuple[str, str, str]:
return (operation.domain, operation.capability, operation.target_scope)
def _merge_operations(
existing: tuple[ModeOperation, ...],
updates: list[ModeOperation],
) -> list[ModeOperation]:
merged = list(existing)
positions = {
_operation_signature(operation): index
for index, operation in enumerate(merged)
}
for operation in updates:
signature = _operation_signature(operation)
index = positions.get(signature)
if index is None:
positions[signature] = len(merged)
merged.append(operation)
continue
merged[index] = operation
return merged
def _initial_backup_path(path: Path) -> Path:
return path.with_suffix(path.suffix + ".initial.bak")
def _mode_profile_document(
profile: ModeProfile,
operations: list[ModeOperation],
) -> dict[str, Any]:
scope: dict[str, Any] = {
"common": profile.common,
"vehicle_ids": list(profile.vehicle_ids),
}
return {
"id": profile.id,
"name": profile.name,
"aliases": list(profile.aliases),
"description": profile.description,
"icon": profile.icon,
"scope": scope,
"operations": [operation.to_yaml() for operation in operations],
}
def _replace_yaml(path: Path, document: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(
yaml.safe_dump(document, allow_unicode=True, sort_keys=False),
encoding="utf-8",
)
temporary.replace(path)
def _mode_scope(
document: dict[str, Any],
path: Path,
profile_root: Path | None,
) -> tuple[bool, tuple[str, ...]]:
scope = document.get("scope")
if isinstance(scope, dict):
vehicle_ids = tuple(
str(vehicle_id).strip()
for vehicle_id in scope.get("vehicle_ids", [])
if str(vehicle_id).strip()
)
common = bool(scope.get("common", not vehicle_ids))
return common, vehicle_ids
if profile_root is not None:
try:
parts = path.relative_to(profile_root).parts
except ValueError:
parts = ()
if len(parts) >= 3 and parts[0] == "vehicles":
return False, (parts[1],)
if parts and parts[0] == "common":
return True, ()
return True, ()
def _is_visible_for_vehicle(
profile: ModeProfile,
vehicle_id: str | None,
) -> bool:
if vehicle_id is None:
return True
return profile.common or vehicle_id in profile.vehicle_ids
def _profile_rank(profile: ModeProfile, vehicle_id: str | None) -> int:
if vehicle_id is not None and vehicle_id in profile.vehicle_ids:
return 2
if profile.common:
return 1
return 0
@@ -10,6 +10,7 @@ from cognitive_agent.application.seat_calibration import (
ANGLE_CALIBRATIONS,
angle_to_position_pct,
)
from cognitive_agent.application.vehicle_profile import VehicleProfile
SEAT_CAPABILITIES = {
@@ -23,6 +24,7 @@ SEAT_MOTION_CAPABILITIES = {
"seat.set_legrest_height": "legrest_height",
"seat.set_legrest_extension": "legrest_extension",
}
SEAT_RELATIVE_PHYSICAL_PARAMETERS = {"distance_mm", "angle_delta_deg"}
SAVE_PRESET_CAPABILITY = "seat.save_posture_preset"
APPLY_PRESET_CAPABILITY = "seat.apply_posture_preset"
HEX_COLOR = re.compile(r"^#[0-9A-Fa-f]{6}$")
@@ -45,6 +47,7 @@ class ModelPlanAdapter:
self,
plan: dict[str, Any],
context: dict[str, Any] | None = None,
vehicle_profile: VehicleProfile | None = None,
) -> dict[str, Any]:
actions: list[dict[str, Any]] = []
domain_actions: list[dict[str, Any]] = []
@@ -73,6 +76,11 @@ class ModelPlanAdapter:
}
allowed_parameters = {item["name"] for item in capability.get("parameters", [])}
unknown_parameters = set(parameters) - allowed_parameters
if (
domain == "seat"
and capability_name in SEAT_MOTION_CAPABILITIES
):
unknown_parameters -= SEAT_RELATIVE_PHYSICAL_PARAMETERS
if unknown_parameters:
ambiguities.append(
{
@@ -162,6 +170,7 @@ class ModelPlanAdapter:
operation_parameters,
ambiguities,
context,
vehicle_profile,
)
if action is not None:
actions.append(action)
@@ -174,7 +183,7 @@ class ModelPlanAdapter:
)
continue
action = self._seat_action(
index, request, parameters, ambiguities, context
index, request, parameters, ambiguities, context, vehicle_profile
)
if action is not None:
actions.append(action)
@@ -367,6 +376,7 @@ class ModelPlanAdapter:
parameters: dict[str, Any],
ambiguities: list[dict[str, str]],
context: dict[str, Any],
vehicle_profile: VehicleProfile | None = None,
) -> dict[str, Any] | None:
target = request.get("target_scope")
if target not in {"driver", "passenger"}:
@@ -381,21 +391,68 @@ class ModelPlanAdapter:
capability = request["capability"]
if capability in SEAT_MOTION_CAPABILITIES:
axis = SEAT_MOTION_CAPABILITIES[capability]
physical_action = ModelPlanAdapter._relative_physical_motion_action(
index,
request,
parameters,
ambiguities,
context,
target,
axis,
vehicle_profile,
)
if physical_action is not None:
return physical_action
angle = parameters.get("angle_deg")
if angle is not None and capability in ANGLE_CALIBRATIONS:
calibration = ANGLE_CALIBRATIONS[capability]
if (
not isinstance(angle, (int, float))
or isinstance(angle, bool)
or not calibration.minimum_deg <= float(angle) <= calibration.maximum_deg
):
if angle is not None:
if not isinstance(angle, (int, float)) or isinstance(angle, bool):
ambiguities.append(
{
"field": f"requests[{index}].parameters.angle_deg",
"question": (
f"{capability} 的夹角必须在 "
f"{calibration.minimum_deg:g}-{calibration.maximum_deg:g} 度之间。"
),
"question": f"{capability} 的夹角必须是数字。",
}
)
return None
profile_motor = (
vehicle_profile.seat_motors.get(axis)
if vehicle_profile is not None
else None
)
if profile_motor is not None and profile_motor.display_angle:
if not profile_motor.contains_display_angle(float(angle)):
display_angle = profile_motor.display_angle
ambiguities.append(
{
"field": f"requests[{index}].parameters.angle_deg",
"question": (
f"{capability} 的夹角必须在 "
f"{display_angle['zero_deg']:g}-{display_angle['max_deg']:g} 度之间。"
),
}
)
return None
position_pct = profile_motor.position_pct_for_display_angle(
float(angle)
)
elif capability in ANGLE_CALIBRATIONS:
calibration = ANGLE_CALIBRATIONS[capability]
if not calibration.minimum_deg <= float(angle) <= calibration.maximum_deg:
ambiguities.append(
{
"field": f"requests[{index}].parameters.angle_deg",
"question": (
f"{capability} 的夹角必须在 "
f"{calibration.minimum_deg:g}-{calibration.maximum_deg:g} 度之间。"
),
}
)
return None
position_pct = angle_to_position_pct(capability, float(angle))
else:
ambiguities.append(
{
"field": f"requests[{index}].parameters.position_pct",
"question": "当前车型未给出该座椅轴的角度标定,需要 0-100 的绝对百分比。",
}
)
return None
@@ -406,7 +463,7 @@ class ModelPlanAdapter:
"action_type": "position",
"target": target,
"axis": axis,
"position_pct": angle_to_position_pct(capability, float(angle)),
"position_pct": position_pct,
"angle_deg": exact_angle,
}
return ModelPlanAdapter._preserve_relative_motion(
@@ -460,6 +517,157 @@ class ModelPlanAdapter:
),
}
@staticmethod
def _relative_physical_motion_action(
index: int,
request: dict[str, Any],
parameters: dict[str, Any],
ambiguities: list[dict[str, str]],
context: dict[str, Any],
target: str,
axis: str,
vehicle_profile: VehicleProfile | None,
) -> dict[str, Any] | None:
physical_parameters = [
name
for name in ("distance_mm", "angle_delta_deg")
if parameters.get(name) is not None
]
if not physical_parameters:
return None
if len(physical_parameters) > 1:
ambiguities.append(
{
"field": f"requests[{index}].parameters",
"question": "单个座椅轴相对移动只能提供一种物理增量。",
}
)
return None
if vehicle_profile is None:
ambiguities.append(
{
"field": f"requests[{index}].parameters.position_pct",
"question": "缺少车型座椅量程,无法把相对物理移动换算为绝对百分比。",
}
)
return None
parameter_name = physical_parameters[0]
delta = parameters[parameter_name]
if (
not isinstance(delta, (int, float))
or isinstance(delta, bool)
or float(delta) <= 0
):
ambiguities.append(
{
"field": f"requests[{index}].parameters.{parameter_name}",
"question": "座椅相对物理移动量必须是正数。",
}
)
return None
motor = vehicle_profile.seat_motors.get(axis)
if motor is None:
ambiguities.append(
{
"field": f"requests[{index}].capability",
"question": f"车型 {vehicle_profile.name} 未配置 {axis} 轴量程。",
}
)
return None
expected_unit = "mm" if parameter_name == "distance_mm" else "degree"
if motor.unit != expected_unit:
ambiguities.append(
{
"field": f"requests[{index}].parameters.{parameter_name}",
"question": (
f"{axis} 轴使用 {motor.unit} 标定,不能用 {expected_unit} 增量换算。"
),
}
)
return None
direction = str(request.get("operation", "set"))
if direction not in {"increase", "decrease"}:
ambiguities.append(
{
"field": f"requests[{index}].operation",
"question": "相对物理移动需要 increase 或 decrease 方向。",
}
)
return None
previous = (
context.get("devices", {})
.get(target, {})
.get("position", {})
.get(axis)
)
if not isinstance(previous, dict):
ambiguities.append(
{
"field": f"requests[{index}].target_scope",
"question": "缺少当前座椅姿态,无法安全执行相对物理移动。",
}
)
return None
current_pct = previous.get("position_pct")
if (
not isinstance(current_pct, (int, float))
or isinstance(current_pct, bool)
):
ambiguities.append(
{
"field": f"requests[{index}].target_scope",
"question": "当前座椅百分比位置无效,无法安全执行相对物理移动。",
}
)
return None
position_pct = vehicle_profile.relative_target_pct(
axis,
current_pct=current_pct,
delta=float(delta),
direction=direction,
)
action: dict[str, Any] = {
"action_type": "position",
"target": target,
"axis": axis,
"position_pct": position_pct,
}
if parameter_name == "angle_delta_deg":
action["angle_deg"] = ModelPlanAdapter._relative_target_angle(
previous,
motor,
position_pct,
float(delta),
direction,
)
return ModelPlanAdapter._preserve_relative_motion(
index, request, action, context, ambiguities
)
@staticmethod
def _relative_target_angle(
previous: dict[str, Any],
motor: Any,
position_pct: int,
delta_deg: float,
direction: str,
) -> int | float:
start_angle = previous.get("angle_deg")
if (
isinstance(start_angle, (int, float))
and not isinstance(start_angle, bool)
):
signed_delta = delta_deg if direction == "increase" else -delta_deg
return _clean_number(float(start_angle) + signed_delta)
display_angle = motor.display_angle_for_pct(position_pct)
if display_angle is None:
return _clean_number(delta_deg)
return _clean_number(display_angle)
@staticmethod
def _preserve_relative_motion(
index: int,
@@ -530,3 +738,7 @@ class ModelPlanAdapter:
)
action["motion_intent"] = motion_intent
return action
def _clean_number(value: float) -> int | float:
return int(value) if float(value).is_integer() else round(float(value), 1)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,166 @@
"""Vehicle-specific seat calibration profiles."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import yaml
@dataclass(frozen=True)
class SeatMotorProfile:
id: str
name: str
unit: str
range: float
reference_zero: str
positive_direction: str
hall_total_pulses: int
display_angle: dict[str, Any] | None = None
def delta_to_pct(self, delta: float) -> float:
if self.range <= 0:
raise ValueError(f"motor {self.id} range must be positive")
return float(delta) / self.range * 100
def display_angle_for_pct(self, position_pct: int) -> float | None:
if not self.display_angle:
return None
zero = float(self.display_angle["zero_deg"])
maximum = float(self.display_angle["max_deg"])
return zero + (maximum - zero) * _clamp_pct(position_pct) / 100
def position_pct_for_display_angle(self, angle_deg: int | float) -> int:
if not self.display_angle:
raise ValueError(f"motor {self.id} does not define a display angle range")
zero = float(self.display_angle["zero_deg"])
maximum = float(self.display_angle["max_deg"])
if maximum <= zero:
raise ValueError(f"motor {self.id} display angle range must be positive")
normalized = (float(angle_deg) - zero) / (maximum - zero)
return _clamp_pct(round(normalized * 100))
def contains_display_angle(self, angle_deg: int | float) -> bool:
if not self.display_angle:
return False
zero = float(self.display_angle["zero_deg"])
maximum = float(self.display_angle["max_deg"])
return zero <= float(angle_deg) <= maximum
def public_dict(self) -> dict[str, Any]:
payload: dict[str, Any] = {
"id": self.id,
"name": self.name,
"unit": self.unit,
"range": _public_number(self.range),
"reference_zero": self.reference_zero,
"positive_direction": self.positive_direction,
"hall_total_pulses": self.hall_total_pulses,
}
if self.display_angle is not None:
payload["display_angle"] = dict(self.display_angle)
return payload
@dataclass(frozen=True)
class VehicleProfile:
id: str
name: str
seat_motors: dict[str, SeatMotorProfile]
def relative_target_pct(
self,
axis: str,
*,
current_pct: int | float,
delta: int | float,
direction: str,
) -> int:
motor = self.seat_motors[axis]
delta_pct = motor.delta_to_pct(float(delta))
if direction == "decrease":
delta_pct = -delta_pct
elif direction != "increase":
raise ValueError(f"unsupported relative direction: {direction}")
return _clamp_pct(round(float(current_pct) + delta_pct))
def display_angle_for_pct(self, axis: str, position_pct: int) -> float | None:
return self.seat_motors[axis].display_angle_for_pct(position_pct)
def public_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"name": self.name,
"seat": {
"motors": {
key: motor.public_dict()
for key, motor in self.seat_motors.items()
}
},
}
class VehicleProfileRegistry:
def __init__(self, profile_root: Path) -> None:
self.profile_root = profile_root
self._profiles = self._load_profiles(profile_root)
def get(self, vehicle_id: str | None) -> VehicleProfile:
resolved = vehicle_id or self.default().id
try:
return self._profiles[resolved]
except KeyError as exc:
raise ValueError(f"unknown vehicle profile: {resolved}") from exc
def default(self) -> VehicleProfile:
if "denza_n7" in self._profiles:
return self._profiles["denza_n7"]
try:
return next(iter(self._profiles.values()))
except StopIteration as exc:
raise ValueError(f"no vehicle profiles found in {self.profile_root}") from exc
def public_profiles(self) -> list[dict[str, Any]]:
return [profile.public_dict() for profile in self._profiles.values()]
@staticmethod
def _load_profiles(profile_root: Path) -> dict[str, VehicleProfile]:
profiles: dict[str, VehicleProfile] = {}
for path in sorted(profile_root.glob("*.yaml")):
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
profile = _parse_vehicle_profile(data)
profiles[profile.id] = profile
return profiles
def _parse_vehicle_profile(data: dict[str, Any]) -> VehicleProfile:
seat = data.get("seat") or {}
motors = seat.get("motors") or {}
seat_motors = {
str(axis): SeatMotorProfile(
id=str(axis),
name=str(payload["name"]),
unit=str(payload["unit"]),
range=float(payload["range"]),
reference_zero=str(payload["reference_zero"]),
positive_direction=str(payload["positive_direction"]),
hall_total_pulses=int(payload["hall_total_pulses"]),
display_angle=payload.get("display_angle"),
)
for axis, payload in motors.items()
}
return VehicleProfile(
id=str(data["id"]),
name=str(data["name"]),
seat_motors=seat_motors,
)
def _clamp_pct(value: int | float) -> int:
return max(0, min(100, int(value)))
def _public_number(value: float) -> int | float:
return int(value) if value.is_integer() else value
@@ -129,6 +129,13 @@ and current state. The absence of a named preset is never, by itself, an ambigui
assumptions in decision_summary. Safety validation and hardware eligibility are downstream concerns;
do not withhold a dry-run candidate merely because a capability is marked high risk.
Runtime context may include mode_profiles. These are trusted long-term mode configuration data,
not new capabilities. If the user names one of those profiles, the deterministic runtime will inject
that profile's operations after planning. In that case, return only the user's explicit additional
goals that are not already represented by the named profile, and mention the named profile in
decision_summary. If the user names no existing profile, synthesize a temporary plan from atomic
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
+10 -3
View File
@@ -145,9 +145,16 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
str(payload.get("provider", "auto")),
str(payload["conversation_id"]) if payload.get("conversation_id") else None,
str(payload.get("execution_mode", "dry_run")),
str(payload["approval_token"])
if payload.get("approval_token")
else None,
approval_token=(
str(payload["approval_token"])
if payload.get("approval_token")
else None
),
vehicle_id=(
str(payload["vehicle_id"])
if payload.get("vehicle_id")
else None
),
)
self._json(result, HTTPStatus.CREATED)
except HardwareSafetyError as exc: