init
This commit is contained in:
+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
|
||||
Reference in New Issue
Block a user