init
This commit is contained in:
Executable
+4
@@ -0,0 +1,4 @@
|
||||
# Application
|
||||
|
||||
实现面向 API/CLI 的用例:提交请求、确认歧义、生成配置、dry-run、执行、取消和查询结果。负责权限、幂等、任务状态和 artifact 生命周期,不包含具体领域转换规则。
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
+63
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
)
|
||||
+1306
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user