feat: tolerate recoverable seat intent inputs
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
import unicodedata
|
||||
from copy import deepcopy
|
||||
@@ -20,7 +21,7 @@ STATE_PATTERN = re.compile(
|
||||
LEVEL_PATTERN = re.compile(r"(最高|最低|[1-5一二三四五高中低])\s*档")
|
||||
POSITION_PATTERN_TEMPLATE = (
|
||||
r"{alias}(?:位置)?(?:调到|移动到|移到|设为|设置为|调至|移动至|到|为)?"
|
||||
r"(?P<position>\d{{1,3}})%"
|
||||
r"(?P<position>[+-]?\d{{1,3}}(?:\.\d+)?)%"
|
||||
)
|
||||
ANGLE_PATTERN_TEMPLATE = (
|
||||
r"{alias}(?:夹角|角度)?(?:调到|放倒到|抬到|移动到|移到|设为|设置为|"
|
||||
@@ -300,6 +301,16 @@ def _format_seconds(duration_ms: int) -> str:
|
||||
return f"{seconds:g}"
|
||||
|
||||
|
||||
def _round_half_away_from_zero(value: float) -> int:
|
||||
"""Use the integer a user normally expects from a .5 position request."""
|
||||
|
||||
return math.floor(value + 0.5) if value >= 0 else math.ceil(value - 0.5)
|
||||
|
||||
|
||||
def _format_numeric_request(value: float) -> str:
|
||||
return f"{value:g}"
|
||||
|
||||
|
||||
def _timed_motion_action(
|
||||
axis: str,
|
||||
direction_token: str,
|
||||
@@ -309,8 +320,8 @@ def _timed_motion_action(
|
||||
operation = _relative_direction(axis, direction_token)
|
||||
if operation is None:
|
||||
return None, f"{direction_token}不适用于该座椅轴,请确认移动方向。"
|
||||
if requested_duration_ms <= 0:
|
||||
return None, "座椅按时长移动的时长必须大于0。"
|
||||
if requested_duration_ms < 0:
|
||||
return None, "座椅按时长移动的时长不能为负数。"
|
||||
state = _context_motion_state(context, axis)
|
||||
if state is None:
|
||||
return None, "缺少当前座椅位置,无法安全计算按时长移动的方向和备用端点。"
|
||||
@@ -894,12 +905,18 @@ def parse_local_intent(
|
||||
for axis, aliases in MOTION_AXIS_ALIASES.items()
|
||||
if any(alias in clause for alias in aliases)
|
||||
]
|
||||
percentages = list(re.finditer(r"(?P<position>\d{1,3})%", clause))
|
||||
percentages = list(
|
||||
re.finditer(r"(?P<position>[+-]?\d{1,3}(?:\.\d+)?)%", clause)
|
||||
)
|
||||
shared_position = None
|
||||
if len(clause_axes) > 1 and len(percentages) == 1 and re.search(
|
||||
r"都|全部|均", clause
|
||||
):
|
||||
shared_position = int(percentages[0].group("position"))
|
||||
requested_shared_value = float(percentages[0].group("position"))
|
||||
rounded_shared_position = _round_half_away_from_zero(
|
||||
requested_shared_value
|
||||
)
|
||||
shared_position = max(0, min(100, rounded_shared_position))
|
||||
target_zh = (
|
||||
_nearest_preceding_target(clause, percentages[0].start()) or "副驾"
|
||||
)
|
||||
@@ -908,8 +925,6 @@ def parse_local_intent(
|
||||
{"field": "target", "question": PASSENGER_ONLY_QUESTION}
|
||||
)
|
||||
else:
|
||||
requested_shared_position = shared_position
|
||||
shared_position = min(100, shared_position)
|
||||
motion_actions.extend(
|
||||
{
|
||||
"target": "passenger",
|
||||
@@ -920,11 +935,22 @@ def parse_local_intent(
|
||||
{
|
||||
"user_notice": (
|
||||
f"{MOTION_AXIS_LABELS[axis]}请求位置"
|
||||
f"{requested_shared_position}%超过最大行程;"
|
||||
"已采用备用目标并移动到对应机械末端。"
|
||||
f"{_format_numeric_request(requested_shared_value)}%;"
|
||||
+ (
|
||||
f"已取整为{rounded_shared_position}%。"
|
||||
if requested_shared_value
|
||||
!= rounded_shared_position
|
||||
else ""
|
||||
)
|
||||
+ (
|
||||
f" 取整后超过0到100%行程,已采用"
|
||||
f"{shared_position}%备用目标并移动到对应机械末端。"
|
||||
if rounded_shared_position != shared_position
|
||||
else ""
|
||||
)
|
||||
)
|
||||
}
|
||||
if requested_shared_position > 100
|
||||
if requested_shared_value != shared_position
|
||||
else {}
|
||||
),
|
||||
}
|
||||
@@ -1104,8 +1130,13 @@ def parse_local_intent(
|
||||
break
|
||||
absolute_angle = None
|
||||
if position_match is not None:
|
||||
requested_position_pct = int(position_match.group("position"))
|
||||
position_pct = min(100, requested_position_pct)
|
||||
requested_position_value = float(
|
||||
position_match.group("position")
|
||||
)
|
||||
requested_position_pct = _round_half_away_from_zero(
|
||||
requested_position_value
|
||||
)
|
||||
position_pct = max(0, min(100, requested_position_pct))
|
||||
else:
|
||||
requested_angle = int(angle_match.group("angle"))
|
||||
minimum, maximum = MOTION_ANGLE_RANGES[axis]
|
||||
@@ -1133,11 +1164,21 @@ def parse_local_intent(
|
||||
f"{minimum}到{maximum}度行程;已采用备用目标"
|
||||
f"{absolute_angle}度并移动到{endpoint_label}。"
|
||||
)
|
||||
elif requested_position_pct != position_pct:
|
||||
motion_action["user_notice"] = (
|
||||
f"{MOTION_AXIS_LABELS[axis]}请求位置{requested_position_pct}%超过"
|
||||
"0到100%行程;已采用100%的备用目标并移动到机械末端。"
|
||||
)
|
||||
else:
|
||||
notices: list[str] = []
|
||||
if requested_position_value != requested_position_pct:
|
||||
notices.append(
|
||||
f"{MOTION_AXIS_LABELS[axis]}请求位置"
|
||||
f"{_format_numeric_request(requested_position_value)}%;"
|
||||
f"已取整为{requested_position_pct}%。"
|
||||
)
|
||||
if requested_position_pct != position_pct:
|
||||
notices.append(
|
||||
f"取整后超过0到100%行程;已采用{position_pct}%的备用目标"
|
||||
"并移动到对应机械末端。"
|
||||
)
|
||||
if notices:
|
||||
motion_action["user_notice"] = " ".join(notices)
|
||||
motion_actions.append(motion_action)
|
||||
break
|
||||
|
||||
|
||||
@@ -301,6 +301,29 @@ class LocalIntentParserTests(unittest.TestCase):
|
||||
self.assertTrue(minimum["actions"][0]["motion_intent"]["duration_clamped"])
|
||||
self.assertIn("最小允许时长为0.3秒", minimum["actions"][0]["user_notice"])
|
||||
|
||||
def test_rounds_decimal_and_clamps_negative_absolute_positions(self) -> None:
|
||||
decimal = parse_local_intent("副驾座椅水平位置调到50.5%")
|
||||
negative = parse_local_intent("副驾座椅水平位置调到-10%")
|
||||
|
||||
self.assertEqual(decimal["ambiguities"], [])
|
||||
self.assertEqual(decimal["actions"][0]["position_pct"], 51)
|
||||
self.assertIn("取整为51%", decimal["actions"][0]["user_notice"])
|
||||
self.assertEqual(negative["ambiguities"], [])
|
||||
self.assertEqual(negative["actions"][0]["position_pct"], 0)
|
||||
self.assertIn("0%的备用目标", negative["actions"][0]["user_notice"])
|
||||
|
||||
def test_zero_duration_uses_minimum_safe_motion_instead_of_rejecting(self) -> None:
|
||||
result = parse_local_intent(
|
||||
"副驾靠背向后移动0秒", self.motion_context()
|
||||
)
|
||||
|
||||
self.assertEqual(result["ambiguities"], [])
|
||||
motion = result["actions"][0]["motion_intent"]
|
||||
self.assertEqual(motion["requested_duration_ms"], 0)
|
||||
self.assertEqual(motion["applied_duration_ms"], 300)
|
||||
self.assertTrue(motion["duration_clamped"])
|
||||
self.assertIn("已按0.3秒", result["actions"][0]["user_notice"])
|
||||
|
||||
def test_rejects_unsupported_level_and_conflicting_state(self) -> None:
|
||||
unsupported = parse_local_intent("副驾加热开到4档")
|
||||
conflicting = parse_local_intent("打开副驾加热然后关闭副驾加热")
|
||||
|
||||
Reference in New Issue
Block a user