This commit is contained in:
orin
2026-07-30 08:45:38 +08:00
commit 25b24ac4cd
1333 changed files with 170092 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
# Application
实现面向 API/CLI 的用例:提交请求、确认歧义、生成配置、dry-run、执行、取消和查询结果。负责权限、幂等、任务状态和 artifact 生命周期,不包含具体领域转换规则。
+63
View File
@@ -0,0 +1,63 @@
"""Load model-facing capability context from domain-owned catalogs."""
from __future__ import annotations
from pathlib import Path
from typing import Any
import yaml
class CapabilityRegistry:
"""Expose domain catalogs without embedding domain semantics in prompts or code."""
def __init__(self, repository_root: Path, executable_domains: set[str]) -> None:
self.root = repository_root
self.executable_domains = set(executable_domains)
self._snapshot = self._load()
def snapshot(self) -> dict[str, Any]:
return self._snapshot
def _load(self) -> dict[str, Any]:
domains = []
for catalog_path in sorted(
(self.root / "domains").glob("*/config/capabilities.yaml")
):
if catalog_path.parts[-3] == "_template":
continue
catalog = yaml.safe_load(catalog_path.read_text(encoding="utf-8"))
domain = catalog["spec"]["domain"]
domains.append(
{
"domain": domain,
"availability": (
"executable" if domain in self.executable_domains else "catalog_only"
),
"capabilities": catalog["spec"]["capabilities"],
}
)
return {
"interaction_context": {
"surface": "driver_console",
"speaker_default_scope": "driver",
},
"execution_policy": {
"executable_domains": sorted(self.executable_domains),
"catalog_only_behavior": "return_as_deferred_request",
"planning_environment": "dry_run_candidate_synthesis",
"named_preset_required": False,
"fuzzy_goal_behavior": (
"decompose a commonly understood user goal into executable atomic "
"capabilities and infer conservative absolute targets"
),
"capability_selection": (
"prefer_executable_when_equally_suitable; never replace a clearly better "
"capability solely because it is executable"
),
"executable_request_parameters": (
"resolve relative intent against runtime state and return absolute target values"
),
},
"domains": domains,
}
+346
View File
@@ -0,0 +1,346 @@
"""Thread-safe in-memory conversation and virtual actuator state."""
from __future__ import annotations
import json
from copy import deepcopy
from datetime import UTC, datetime
from pathlib import Path
from threading import RLock
from typing import Any
from cognitive_agent.application.seat_calibration import position_pct_to_angle
TARGETS = ("driver", "passenger")
THERMAL_FUNCTIONS = ("ventilation", "heating")
MOTION_INITIAL_POSITIONS = {
"longitudinal": 50,
"cushion_tilt": 50,
"backrest_recline": 20,
"legrest_height": 0,
"legrest_extension": 0,
}
CLIMATE_ZONES = (
"vehicle",
"front_row",
"rear_row",
"driver",
"passenger",
"rear_left",
"rear_right",
)
def _now() -> str:
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
def _motion_state(axis: str, position: int) -> dict[str, Any]:
value: dict[str, Any] = {
"position_pct": position,
"motion_state": "stopped",
"fault": False,
}
angle = position_pct_to_angle(axis, position)
if angle is not None:
value["angle_deg"] = angle
return value
def _new_state(
conversation_id: str,
presets: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
return {
"conversation_id": conversation_id,
"turn_count": 0,
"last_target": None,
"updated_at": _now(),
"devices": {
target: {
**{
function: {"state": "off", "level": 0}
for function in THERMAL_FUNCTIONS
},
"position": {
axis: _motion_state(axis, position)
for axis, position in MOTION_INITIAL_POSITIONS.items()
},
}
for target in TARGETS
},
"domain_devices": {
"climate": {
"power": "off",
"ac": "off",
"fan_level": 0,
"cycle_mode": "external",
"front_defrost": "off",
"zones": {
zone: {
"power": "off",
"temperature_c": 24,
"outlet_mode": "face",
}
for zone in CLIMATE_ZONES
},
},
"roof": {
"shade_position_pct": 0,
"transmittance_pct": 100,
"automatic_dimming": "off",
"motion_state": "stopped",
"fault": False,
},
"ambient_light": {
"power": "off",
"color_hex": "#74D9FF",
"brightness_pct": 0,
"mode": "off",
},
},
"presets": list(presets or []),
"recent_turns": [],
}
class ConversationStateStore:
"""Keep isolated dry-run state for each browser conversation."""
def __init__(
self,
max_conversations: int = 256,
preset_root: Path | None = None,
) -> None:
self._states: dict[str, dict[str, Any]] = {}
self._max_conversations = max_conversations
self.preset_root = preset_root
self._lock = RLock()
def _preset_path(self, conversation_id: str) -> Path | None:
if self.preset_root is None:
return None
return self.preset_root / f"{conversation_id}.json"
def _load_presets(self, conversation_id: str) -> list[dict[str, Any]]:
path = self._preset_path(conversation_id)
if path is None or not path.is_file():
return []
try:
document = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return []
presets = document.get("presets", [])
return presets if isinstance(presets, list) else []
def _persist_presets(self, conversation_id: str, presets: list[dict[str, Any]]) -> None:
path = self._preset_path(conversation_id)
if path is None:
return
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(".json.tmp")
temporary.write_text(
json.dumps(
{
"conversation_id": conversation_id,
"presets": presets,
},
ensure_ascii=False,
indent=2,
)
+ "\n",
encoding="utf-8",
)
temporary.replace(path)
def _state(self, conversation_id: str) -> dict[str, Any]:
state = self._states.get(conversation_id)
if state is not None:
return state
if len(self._states) >= self._max_conversations:
oldest_id = next(iter(self._states))
del self._states[oldest_id]
state = _new_state(conversation_id, self._load_presets(conversation_id))
self._states[conversation_id] = state
return state
def snapshot(self, conversation_id: str) -> dict[str, Any]:
with self._lock:
state = self._state(conversation_id)
return deepcopy(state)
def apply_successful_actions(
self,
conversation_id: str,
utterance: str,
actions: list[dict[str, Any]],
decision_summary: list[str],
) -> dict[str, Any]:
with self._lock:
state = self._state(conversation_id)
for action in actions:
if action.get("domain") in {
"climate",
"roof",
"ambient_light",
}:
self._apply_domain_action(state, action)
state["last_target"] = action["target"]
continue
target = action["target"]
if action.get("action_type") == "position":
axis = action["axis"]
position_state = _motion_state(axis, action["position_pct"])
if action.get("angle_deg") is not None:
position_state["angle_deg"] = action["angle_deg"]
if action.get("position_valid") is not None:
position_state["position_valid"] = bool(
action["position_valid"]
)
if action.get("position_estimated") is not None:
position_state["position_estimated"] = bool(
action["position_estimated"]
)
if action.get("feedback_basis") is not None:
position_state["feedback_basis"] = str(
action["feedback_basis"]
)
state["devices"][target]["position"][axis] = position_state
state["last_target"] = target
continue
function = action["function"]
requested_state = action["state"]
previous = state["devices"][target][function]
if requested_state == "off":
level = 0
else:
level = action.get("requested_level") or previous["level"] or 1
state["devices"][target][function] = {
"state": requested_state,
"level": level,
}
state["last_target"] = target
state["turn_count"] += 1
state["updated_at"] = _now()
state["recent_turns"].append(
{
"utterance": utterance,
"actions": deepcopy(actions),
"decision_summary": list(decision_summary),
}
)
state["recent_turns"] = state["recent_turns"][-6:]
return deepcopy(state)
@staticmethod
def _apply_domain_action(
state: dict[str, Any],
action: dict[str, Any],
) -> None:
domain = action["domain"]
capability = action["capability"]
target = action["target"]
parameters = action["parameters"]
device = state["domain_devices"][domain]
if domain == "climate":
if capability == "climate.set_power":
device["zones"][target]["power"] = parameters["state"]
if target == "vehicle":
device["power"] = parameters["state"]
for zone in device["zones"].values():
zone["power"] = parameters["state"]
elif capability == "climate.set_temperature":
device["zones"][target]["temperature_c"] = parameters[
"temperature_c"
]
if target == "vehicle":
for zone in device["zones"].values():
zone["temperature_c"] = parameters["temperature_c"]
elif capability == "climate.set_fan_level":
device["fan_level"] = parameters["level"]
elif capability == "climate.set_ac":
device["ac"] = parameters["state"]
elif capability == "climate.set_cycle_mode":
device["cycle_mode"] = parameters["mode"]
elif capability == "climate.set_front_defrost":
device["front_defrost"] = parameters["state"]
elif capability == "climate.set_outlet_mode":
device["zones"][target]["outlet_mode"] = parameters["mode"]
return
if domain == "roof":
if capability == "roof.set_shade_position":
device["shade_position_pct"] = parameters["position_pct"]
elif capability == "roof.set_transmittance":
device["transmittance_pct"] = parameters["transmittance_pct"]
elif capability == "roof.set_automatic_dimming":
device["automatic_dimming"] = parameters["state"]
return
if capability == "ambient_light.set_power":
device["power"] = parameters["state"]
elif capability == "ambient_light.set_illumination":
device["color_hex"] = parameters["color_hex"]
device["brightness_pct"] = parameters["brightness_pct"]
device["power"] = "on" if parameters["brightness_pct"] > 0 else "off"
device["mode"] = "custom"
elif capability == "ambient_light.set_mode":
device["mode"] = parameters["mode"]
if parameters["mode"] in {"on", "off"}:
device["power"] = parameters["mode"]
def save_preset(
self,
conversation_id: str,
utterance: str,
target: str,
preset_name: str,
function_preset: dict[str, Any],
decision_summary: list[str],
) -> dict[str, Any]:
with self._lock:
state = self._state(conversation_id)
entry = {
"name": preset_name,
"target": target,
"saved_at": _now(),
"function_preset": deepcopy(function_preset),
}
expected_name = preset_name.casefold()
state["presets"] = [
item
for item in state["presets"]
if not (
item.get("target") == target
and str(item.get("name", "")).casefold() == expected_name
)
]
state["presets"].append(entry)
state["last_target"] = target
state["turn_count"] += 1
state["updated_at"] = _now()
state["recent_turns"].append(
{
"utterance": utterance,
"actions": [
{
"action_type": "preset_save",
"target": target,
"preset_name": preset_name,
}
],
"decision_summary": list(decision_summary),
}
)
state["recent_turns"] = state["recent_turns"][-6:]
self._persist_presets(conversation_id, state["presets"])
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))
self._states[conversation_id] = state
return deepcopy(state)
+307
View File
@@ -0,0 +1,307 @@
"""Configuration generation, deterministic compilation, and dry-run execution."""
from __future__ import annotations
import hashlib
import json
import math
import re
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
import yaml
from cognitive_agent.infrastructure.can import CanMatrix
HEX_COLOR = re.compile(r"^#[0-9A-Fa-f]{6}$")
def _now() -> str:
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
class ConfiguredDomainRuntime:
"""Domain-isolated runtime backed entirely by domain-owned YAML documents."""
compiler_version = "1.0.0"
def __init__(self, repository_root: Path, domain: str):
self.domain = domain
self.root = repository_root / "domains" / domain
self.catalog = yaml.safe_load(
(self.root / "config" / "capabilities.yaml").read_text(encoding="utf-8")
)
self.mapping = yaml.safe_load(
(self.root / "can" / "signal-mapping.yaml").read_text(encoding="utf-8")
)
self.matrix = CanMatrix(self.root / "can" / "can-matrix.yaml")
self.capabilities = {
item["name"]: item for item in self.catalog["spec"]["capabilities"]
}
def runtime_info(self) -> dict[str, Any]:
return {
"domain": self.domain,
"hardware_execution_allowed": self.matrix.hardware_execution_allowed,
"capabilities": self.catalog["spec"]["capabilities"],
"matrix": {
"messages": len(self.matrix.data["messages"]),
"signals": sum(
len(message["signals"])
for message in self.matrix.data["messages"]
),
"source": self.matrix.data["source"],
"limitations": self.matrix.data["limitations"],
},
}
def build_function_config(
self,
actions: list[dict[str, Any]],
request_id: str,
provider: str,
) -> dict[str, Any]:
operations = []
for index, action in enumerate(actions, start=1):
capability_suffix = action["capability"].split(".", 1)[1]
operation_id = f"{action['target']}_{capability_suffix}_{index}"
operations.append(
{
"id": operation_id[:63],
"capability": action["capability"],
"target": {
"device": self.capabilities[action["capability"]][
"target_devices"
][0],
"zone": action["target"],
},
"parameters": dict(action["parameters"]),
"timeout_ms": 8000 if self.domain == "roof" else 4000,
"failure_policy": "abort",
}
)
return {
"apiVersion": "cognitive-agent/v1alpha1",
"kind": "FunctionConfig",
"metadata": {
"id": f"{self.domain}-{request_id}",
"name": f"{self.domain.replace('_', ' ').title()} function",
"version": "1.0.0",
"request_id": request_id,
"generated_at": _now(),
"labels": {
"provider": provider,
"execution_scope": "virtual_only",
"matrix_source": "supplied_xlsx",
},
},
"spec": {
"domain": self.domain,
"operations": operations,
"assumptions": [
"The supplied matrix is partial; this configuration is eligible for virtual execution only.",
"Sub-ID placement and LSB0/Intel encoding are inferred from the workbook byte annotations.",
],
},
}
def compile(self, function_config: dict[str, Any]) -> dict[str, Any]:
commands: list[dict[str, Any]] = []
for operation_index, operation in enumerate(
function_config["spec"]["operations"], start=1
):
mappings = self._mappings_for(operation)
grouped: dict[tuple[str, str], dict[str, Any]] = {}
covered_parameters: set[str] = set()
completion: dict[str, Any] | None = None
for mapping in mappings:
parameter = mapping["parameter"]
if parameter not in operation["parameters"]:
continue
covered_parameters.add(parameter)
key = (mapping["bus"], mapping["message"])
signals = grouped.setdefault(key, {"signals": {}})["signals"]
signals.update(
self._compile_value(
mapping,
operation["parameters"][parameter],
)
)
if completion is None and mapping.get("feedback_message"):
feedback_value = self._compile_feedback(
mapping,
operation["parameters"][parameter],
)
completion = {
"message": mapping["feedback_message"],
"signal": mapping["feedback_signal"],
"operator": "eq",
"value": feedback_value,
"timeout_ms": int(operation.get("timeout_ms", 4000)),
}
missing = set(operation["parameters"]) - covered_parameters
if missing:
raise ValueError(
f"No {self.domain} signal mapping for {operation['capability']} "
f"parameters: {', '.join(sorted(missing))}"
)
for group_index, ((bus, message), group) in enumerate(
grouped.items(), start=1
):
command_id = (
f"{self.domain}_{operation_index}_{group_index}_control"
)[:63]
command: dict[str, Any] = {
"id": command_id,
"operation_ref": operation["id"],
"bus": bus,
"message": message,
"signals": dict(sorted(group["signals"].items())),
"send_offset_ms": (operation_index - 1) * 20,
}
if completion is not None and group_index == 1:
command["completion"] = completion
commands.append(command)
canonical = json.dumps(
function_config, ensure_ascii=False, sort_keys=True
).encode("utf-8")
metadata = function_config["metadata"]
return {
"apiVersion": "cognitive-agent/v1alpha1",
"kind": "CommandPlan",
"metadata": {
"id": f"plan-{self.domain}-{metadata['request_id']}",
"name": f"{self.domain.replace('_', ' ').title()} CAN command plan",
"version": "1.0.0",
"request_id": metadata["request_id"],
"labels": {"execution_scope": "virtual_only"},
},
"spec": {
"domain": self.domain,
"platform": self.mapping["spec"]["platform"],
"source": {
"config_id": metadata["id"],
"config_version": metadata["version"],
"config_sha256": hashlib.sha256(canonical).hexdigest(),
"compiler_version": self.compiler_version,
"mapping_version": self.mapping["metadata"]["version"],
},
"commands": commands,
},
}
def execute(
self,
command_plan: dict[str, Any],
function_config: dict[str, Any],
) -> dict[str, Any]:
started_at = _now()
operations = {
item["id"]: item for item in function_config["spec"]["operations"]
}
results = []
for command in command_plan["spec"]["commands"]:
operation = operations[command["operation_ref"]]
command_frame = self.matrix.encode(
command["message"], command["signals"]
)
completion = command.get("completion")
feedback_frame = None
if completion is not None:
feedback_frame = self.matrix.encode(
completion["message"],
{completion["signal"]: int(completion["value"])},
)
results.append(
{
"ref": operation["id"],
"domain": self.domain,
"status": "succeeded",
"message": (
f"虚拟 CAN 已执行 {operation['capability']},"
+ ("模拟反馈与目标一致。" if feedback_frame else "矩阵未提供直接结果反馈。")
),
"telemetry": {
"target": operation["target"]["zone"],
"requested_parameters": operation["parameters"],
"command_frame": command_frame,
"feedback_frame": feedback_frame,
"feedback_available": feedback_frame is not None,
"matrix_source": "supplied_xlsx",
"simulated": True,
},
}
)
request_id = function_config["metadata"]["request_id"]
return {
"apiVersion": "cognitive-agent/v1alpha1",
"kind": "ExecutionReport",
"metadata": {
"id": f"report-{self.domain}-{request_id}",
"name": f"{self.domain.replace('_', ' ').title()} virtual execution report",
"version": "1.0.0",
"request_id": request_id,
"labels": {"simulation": "true"},
},
"spec": {
"mode": "dry_run",
"status": "succeeded",
"started_at": started_at,
"finished_at": _now(),
"results": results,
},
}
def _mappings_for(self, operation: dict[str, Any]) -> list[dict[str, Any]]:
matches = [
item
for item in self.mapping["spec"]["mappings"]
if item["capability"] == operation["capability"]
and item.get("target_zone") == operation["target"]["zone"]
]
if not matches:
raise ValueError(
f"No {self.domain} mapping for capability={operation['capability']}, "
f"zone={operation['target']['zone']}"
)
return matches
@staticmethod
def _compile_value(mapping: dict[str, Any], value: Any) -> dict[str, int]:
handler = mapping["compiler_handler"]
if handler == "compile_enum":
try:
raw = mapping["value_map"][str(value)]
except KeyError as exc:
raise ValueError(f"Unmapped enum value: {value!r}") from exc
return {mapping["signal"]: int(raw)}
if handler == "compile_integer":
return {mapping["signal"]: int(value)}
if handler == "compile_level_plus_one":
return {mapping["signal"]: int(value) + 1}
if handler == "compile_hex_rgb":
color = str(value)
if not HEX_COLOR.fullmatch(color):
raise ValueError("color_hex must use #RRGGBB")
channels = [int(color[index : index + 2], 16) for index in (1, 3, 5)]
return dict(zip(mapping["signals"], channels, strict=True))
raise ValueError(f"Unsupported compiler handler: {handler}")
@staticmethod
def _compile_feedback(mapping: dict[str, Any], value: Any) -> int:
handler = mapping.get("feedback_handler", mapping["compiler_handler"])
if handler == "compile_enum":
values = mapping.get("feedback_value_map", mapping.get("value_map", {}))
return int(values[str(value)])
if handler == "compile_integer":
return int(value)
if handler == "compile_temperature_feedback":
return int(value) - 16
if handler == "compile_transmittance_bucket":
percentage = int(value)
return 1 if percentage == 0 else min(11, math.ceil(percentage / 10) + 1)
raise ValueError(f"Unsupported feedback handler: {handler}")
+64
View File
@@ -0,0 +1,64 @@
"""Expose the gesture model and safety configuration bundled with this project."""
from __future__ import annotations
from pathlib import Path
from typing import Any
import yaml
DEFAULT_ACTION_COPY = {
"POINT_UP": {"action": "SEAT_UP", "utterance": "副驾驶座椅坐垫前端升高一点"},
"POINT_DOWN": {"action": "SEAT_DOWN", "utterance": "副驾驶座椅坐垫前端降低一点"},
"POINT_RIGHT": {"action": "SEAT_FORWARD", "utterance": "副驾驶座椅向前移动一点"},
"POINT_LEFT": {"action": "SEAT_BACKWARD", "utterance": "副驾驶座椅向后移动一点"},
"THUMB_UP": {"action": "BACKREST_FORWARD", "utterance": "副驾驶靠背向前调一点"},
"THUMB_DOWN": {"action": "BACKREST_BACKWARD", "utterance": "副驾驶靠背向后调一点"},
"GUN_UP": {"action": "LEGREST_EXTEND", "utterance": "副驾驶腿托伸出一点"},
"GUN_DOWN": {"action": "LEGREST_RETRACT", "utterance": "副驾驶腿托收回一点"},
"VICTORY_RIGHT": {"action": "LEGREST_EXTEND", "utterance": "副驾驶腿托伸出一点"},
"VICTORY_LEFT": {"action": "LEGREST_RETRACT", "utterance": "副驾驶腿托收回一点"},
}
class GestureCapabilityBridge:
"""Expose the repository-owned gesture assets to the browser runtime."""
def __init__(self, repository_root: Path, asset_root: Path | None = None) -> None:
self.repository_root = repository_root.resolve()
self.asset_root = (
asset_root or self.repository_root / "apps" / "web-console" / "assets" / "gesture"
).resolve()
self.config_path = self.repository_root / "config" / "gesture" / "default.yaml"
self.model_path = self.asset_root / "gesture_recognizer.task"
@property
def available(self) -> bool:
return self.config_path.is_file() and self.model_path.is_file()
def runtime_info(self) -> dict[str, Any]:
recognition: dict[str, Any] = {}
if self.config_path.is_file():
try:
payload = yaml.safe_load(self.config_path.read_text(encoding="utf-8")) or {}
recognition = dict(payload.get("recognition", {}))
except (OSError, TypeError, ValueError, yaml.YAMLError):
recognition = {}
defaults = {
"action_hold_ms": 600,
"min_action_frames": 8,
"arm_hold_ms": 1000,
"armed_idle_timeout_ms": 3000,
"max_continuous_motion_ms": 1500,
"neutral_release_ms": 350,
}
safety = {name: int(recognition.get(name, value)) for name, value in defaults.items()}
return {
"available": self.available,
"model_available": self.model_path.is_file(),
"source": "cognitive-agent-bundled-assets",
"asset_strategy": "bundled",
"safety": safety,
"actions": DEFAULT_ACTION_COPY,
}
+532
View File
@@ -0,0 +1,532 @@
"""Translate a generic model plan into domain adapter inputs without semantic rewriting."""
from __future__ import annotations
from copy import deepcopy
import re
from typing import Any
from cognitive_agent.application.seat_calibration import (
ANGLE_CALIBRATIONS,
angle_to_position_pct,
)
SEAT_CAPABILITIES = {
"seat.set_ventilation": "ventilation",
"seat.set_heating": "heating",
}
SEAT_MOTION_CAPABILITIES = {
"seat.set_longitudinal_position": "longitudinal",
"seat.set_cushion_tilt": "cushion_tilt",
"seat.set_backrest_recline": "backrest_recline",
"seat.set_legrest_height": "legrest_height",
"seat.set_legrest_extension": "legrest_extension",
}
SAVE_PRESET_CAPABILITY = "seat.save_posture_preset"
APPLY_PRESET_CAPABILITY = "seat.apply_posture_preset"
HEX_COLOR = re.compile(r"^#[0-9A-Fa-f]{6}$")
class ModelPlanAdapter:
"""Perform structural capability routing; user-intent decisions remain model-owned."""
def __init__(self, capability_context: dict[str, Any]) -> None:
self.capabilities: dict[str, tuple[str, str, dict[str, Any]]] = {}
for domain in capability_context.get("domains", []):
for capability in domain.get("capabilities", []):
self.capabilities[capability["name"]] = (
domain["domain"],
domain["availability"],
capability,
)
def adapt(
self,
plan: dict[str, Any],
context: dict[str, Any] | None = None,
) -> dict[str, Any]:
actions: list[dict[str, Any]] = []
domain_actions: list[dict[str, Any]] = []
deferred: list[dict[str, Any]] = []
preset_actions: list[dict[str, Any]] = []
preset_events: list[dict[str, Any]] = []
ambiguities = list(plan.get("ambiguities", []))
context = context or {}
for index, request in enumerate(plan.get("requests", [])):
capability_name = str(request.get("capability"))
registered = self.capabilities.get(capability_name)
if registered is None or registered[0] != request.get("domain"):
ambiguities.append(
{
"field": f"requests[{index}].capability",
"question": "模型请求的领域与能力注册表不一致。",
}
)
continue
domain, availability, capability = registered
parameters = {
str(item["name"]): item.get("value")
for item in request.get("parameters", [])
}
allowed_parameters = {item["name"] for item in capability.get("parameters", [])}
unknown_parameters = set(parameters) - allowed_parameters
if unknown_parameters:
ambiguities.append(
{
"field": f"requests[{index}].parameters",
"question": (
f"能力 {capability_name} 不支持参数 "
f"{', '.join(sorted(unknown_parameters))}。"
),
}
)
continue
if availability != "executable":
values = [value for value in parameters.values() if value is not None]
deferred.append(
{
"domain": domain,
"capability": capability_name,
"target_scope": request["target_scope"],
"desired_change": request["operation"],
"requested_value": values[0] if len(values) == 1 else None,
"parameters": parameters,
"reason": request["reason"],
}
)
continue
if domain != "seat":
generic_action = self._generic_action(
index,
request,
parameters,
capability,
ambiguities,
)
if generic_action is not None:
domain_actions.append(generic_action)
continue
if capability_name == SAVE_PRESET_CAPABILITY:
preset_action = self._preset_action(
index, request, parameters, ambiguities
)
if preset_action is not None:
preset_actions.append(preset_action)
continue
if capability_name == APPLY_PRESET_CAPABILITY:
preset_action = self._preset_action(
index, request, parameters, ambiguities
)
if preset_action is None:
continue
preset = self._find_preset(
context.get("presets", []),
preset_action["target"],
preset_action["preset_name"],
)
if preset is None:
available = [
str(item.get("name"))
for item in context.get("presets", [])
if item.get("target") == preset_action["target"]
]
suffix = (
f"当前可用预设:{', '.join(available)}。"
if available
else "当前座位尚未保存任何预设。"
)
ambiguities.append(
{
"field": f"requests[{index}].parameters.preset_name",
"question": (
f"未找到 {preset_action['target']} 的预设"
f"“{preset_action['preset_name']}”。{suffix}"
),
}
)
continue
for operation in preset["function_preset"]["spec"]["operations"]:
operation_parameters = dict(operation["parameters"])
action = self._seat_action(
index,
{
"capability": operation["capability"],
"target_scope": preset_action["target"],
"operation": "set",
},
operation_parameters,
ambiguities,
context,
)
if action is not None:
actions.append(action)
preset_events.append(
{
"action": "apply",
"target": preset_action["target"],
"preset_name": preset_action["preset_name"],
}
)
continue
action = self._seat_action(
index, request, parameters, ambiguities, context
)
if action is not None:
actions.append(action)
return {
"detected_domains": list(plan.get("detected_domains", ["seat"])),
"actions": actions,
"domain_actions": domain_actions,
"preset_actions": preset_actions,
"preset_events": preset_events,
"deferred_requests": deferred,
"ambiguities": ambiguities,
"decision_summary": list(plan.get("decision_summary", [])),
"confidence": float(plan.get("confidence", 0)),
"model_requests": deepcopy(plan.get("requests", [])),
}
@staticmethod
def _generic_action(
index: int,
request: dict[str, Any],
parameters: dict[str, Any],
capability: dict[str, Any],
ambiguities: list[dict[str, str]],
) -> dict[str, Any] | None:
target = request.get("target_scope")
if target not in capability.get("target_scopes", []):
ambiguities.append(
{
"field": f"requests[{index}].target_scope",
"question": (
f"{request['capability']} 不支持目标范围 {target!r}。"
),
}
)
return None
parameter_specs = {
item["name"]: item for item in capability.get("parameters", [])
}
state_spec = parameter_specs.get("state")
if state_spec is not None and parameters.get("state") is None:
inferred_state = {
"activate": "on",
"deactivate": "off",
}.get(request.get("operation"))
if inferred_state is not None:
parameters["state"] = inferred_state
for name, spec in parameter_specs.items():
value = parameters.get(name)
if spec.get("required") and value is None:
ambiguities.append(
{
"field": f"requests[{index}].parameters.{name}",
"question": (
f"{request['capability']} 需要模型给出参数 {name} 的绝对目标值。"
),
}
)
return None
if value is None:
continue
expected_type = spec.get("type")
numeric = (
isinstance(value, (int, float))
and not isinstance(value, bool)
)
type_valid = {
"number": numeric,
"integer": numeric and float(value).is_integer(),
"string": isinstance(value, str),
"boolean": isinstance(value, bool),
"object": isinstance(value, dict),
"array": isinstance(value, list),
}.get(str(expected_type), False)
if not type_valid:
ambiguities.append(
{
"field": f"requests[{index}].parameters.{name}",
"question": f"参数 {name} 的类型必须是 {expected_type}。",
}
)
return None
if numeric and (
("minimum" in spec and float(value) < float(spec["minimum"]))
or ("maximum" in spec and float(value) > float(spec["maximum"]))
):
ambiguities.append(
{
"field": f"requests[{index}].parameters.{name}",
"question": (
f"参数 {name} 必须位于 "
f"{spec.get('minimum', '-∞')} 到 {spec.get('maximum', '+∞')} 之间。"
),
}
)
return None
if spec.get("allowed_values") and value not in spec["allowed_values"]:
ambiguities.append(
{
"field": f"requests[{index}].parameters.{name}",
"question": (
f"参数 {name} 必须是 "
f"{', '.join(map(str, spec['allowed_values']))} 之一。"
),
}
)
return None
if name == "color_hex" and not HEX_COLOR.fullmatch(str(value)):
ambiguities.append(
{
"field": f"requests[{index}].parameters.color_hex",
"question": "氛围灯颜色必须是 #RRGGBB 格式。",
}
)
return None
normalized = {}
for name, value in parameters.items():
spec = parameter_specs[name]
normalized[name] = (
int(value)
if spec.get("type") == "integer"
else value.upper()
if name == "color_hex"
else value
)
return {
"domain": request["domain"],
"capability": request["capability"],
"target": str(target),
"parameters": normalized,
"operation": request.get("operation", "set"),
"reason": request.get("reason", ""),
}
@staticmethod
def _preset_action(
index: int,
request: dict[str, Any],
parameters: dict[str, Any],
ambiguities: list[dict[str, str]],
) -> dict[str, str] | None:
target = request.get("target_scope")
if target not in {"driver", "passenger"}:
ambiguities.append(
{
"field": f"requests[{index}].target_scope",
"question": "座椅姿态预设需要明确主驾或副驾。",
}
)
return None
preset_name = parameters.get("preset_name")
if (
not isinstance(preset_name, str)
or not preset_name.strip()
or len(preset_name.strip()) > 64
):
ambiguities.append(
{
"field": f"requests[{index}].parameters.preset_name",
"question": "保存或调用座椅姿态预设需要提供 1-64 个字符的名称。",
}
)
return None
return {"target": target, "preset_name": preset_name.strip()}
@staticmethod
def _find_preset(
presets: list[dict[str, Any]],
target: str,
preset_name: str,
) -> dict[str, Any] | None:
expected = preset_name.casefold()
return next(
(
preset
for preset in presets
if preset.get("target") == target
and str(preset.get("name", "")).casefold() == expected
),
None,
)
@staticmethod
def _seat_action(
index: int,
request: dict[str, Any],
parameters: dict[str, Any],
ambiguities: list[dict[str, str]],
context: dict[str, Any],
) -> dict[str, Any] | None:
target = request.get("target_scope")
if target not in {"driver", "passenger"}:
ambiguities.append(
{
"field": f"requests[{index}].target_scope",
"question": "座椅执行适配器需要 driver 或 passenger 目标。",
}
)
return None
capability = request["capability"]
if capability in SEAT_MOTION_CAPABILITIES:
axis = SEAT_MOTION_CAPABILITIES[capability]
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
):
ambiguities.append(
{
"field": f"requests[{index}].parameters.angle_deg",
"question": (
f"{capability} 的夹角必须在 "
f"{calibration.minimum_deg:g}-{calibration.maximum_deg:g} 度之间。"
),
}
)
return None
exact_angle: int | float = (
int(angle) if float(angle).is_integer() else float(angle)
)
action = {
"action_type": "position",
"target": target,
"axis": axis,
"position_pct": angle_to_position_pct(capability, float(angle)),
"angle_deg": exact_angle,
}
return ModelPlanAdapter._preserve_relative_motion(
index, request, action, context, ambiguities
)
position = parameters.get("position_pct")
if (
not isinstance(position, (int, float))
or isinstance(position, bool)
or not float(position).is_integer()
or not 0 <= float(position) <= 100
):
ambiguities.append(
{
"field": f"requests[{index}].parameters.position_pct",
"question": "座椅位置动作需要模型给出 0-100 的绝对整数目标。",
}
)
return None
action = {
"action_type": "position",
"target": target,
"axis": axis,
"position_pct": int(position),
}
return ModelPlanAdapter._preserve_relative_motion(
index, request, action, context, ambiguities
)
state = parameters.get("state")
if state is None:
state = {"activate": "on", "deactivate": "off"}.get(request.get("operation"))
if state not in {"on", "off"}:
ambiguities.append(
{
"field": f"requests[{index}].parameters.state",
"question": "座椅动作需要明确的 on/off 目标状态。",
}
)
return None
level = parameters.get("level")
return {
"target": target,
"function": SEAT_CAPABILITIES[capability],
"state": state,
"requested_level": (
int(level)
if isinstance(level, (int, float)) and not isinstance(level, bool)
else None
),
}
@staticmethod
def _preserve_relative_motion(
index: int,
request: dict[str, Any],
action: dict[str, Any],
context: dict[str, Any],
ambiguities: list[dict[str, str]],
) -> dict[str, Any] | None:
"""Keep the model's relative intent after it resolves an absolute target.
The absolute target remains the public capability value. This context is
also needed by the hardware compiler to choose the bounded timed/Hall path
when the controller has not yet acquired an absolute endpoint calibration.
"""
requested_operation = str(request.get("operation", "set"))
if requested_operation not in {"increase", "decrease"}:
return action
target = str(action["target"])
axis = str(action["axis"])
previous = (
context.get("devices", {})
.get(target, {})
.get("position", {})
.get(axis)
)
if not isinstance(previous, dict):
return action
start_position = previous.get("position_pct")
if not isinstance(start_position, (int, float)) or isinstance(
start_position, bool
):
return action
target_position = int(action["position_pct"])
signed_delta = target_position - int(start_position)
expected_sign = 1 if requested_operation == "increase" else -1
if signed_delta * expected_sign < 0:
ambiguities.append(
{
"field": f"requests[{index}].operation",
"question": (
"模型给出的相对移动方向与其解析出的绝对目标相互矛盾,"
"无法安全生成电机方向。"
),
}
)
return None
motion_intent: dict[str, Any] = {
"mode": "relative",
"direction": requested_operation,
"start_position_pct": int(start_position),
"delta_position_pct": abs(signed_delta),
}
start_angle = previous.get("angle_deg")
target_angle = action.get("angle_deg")
if (
isinstance(start_angle, (int, float))
and not isinstance(start_angle, bool)
and isinstance(target_angle, (int, float))
and not isinstance(target_angle, bool)
):
motion_intent["start_angle_deg"] = float(start_angle)
motion_intent["delta_angle_deg"] = abs(
float(target_angle) - float(start_angle)
)
action["motion_intent"] = motion_intent
return action
+51
View File
@@ -0,0 +1,51 @@
"""Virtual seat posture calibration shared by planning and state handling."""
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class AngleCalibration:
axis: str
minimum_deg: float
maximum_deg: float
ANGLE_CALIBRATIONS = {
"seat.set_backrest_recline": AngleCalibration(
axis="backrest_recline",
minimum_deg=95,
maximum_deg=140,
),
"seat.set_legrest_height": AngleCalibration(
axis="legrest_height",
minimum_deg=95,
maximum_deg=160,
),
}
AXIS_ANGLE_CALIBRATIONS = {
calibration.axis: calibration for calibration in ANGLE_CALIBRATIONS.values()
}
def angle_to_position_pct(capability: str, angle_deg: float) -> int:
calibration = ANGLE_CALIBRATIONS[capability]
normalized = (
(float(angle_deg) - calibration.minimum_deg)
/ (calibration.maximum_deg - calibration.minimum_deg)
)
return round(normalized * 100)
def position_pct_to_angle(axis: str, position_pct: float) -> float | None:
calibration = AXIS_ANGLE_CALIBRATIONS.get(axis)
if calibration is None:
return None
return round(
calibration.minimum_deg
+ (calibration.maximum_deg - calibration.minimum_deg)
* float(position_pct)
/ 100,
1,
)
File diff suppressed because it is too large Load Diff