diff --git a/src/rai_core/rai/tools/numbers.py b/src/rai_core/rai/tools/numbers.py new file mode 100644 index 000000000..fff583176 --- /dev/null +++ b/src/rai_core/rai/tools/numbers.py @@ -0,0 +1,26 @@ +# Copyright (C) 2026 Robotec.AI +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Numeric validation helpers for agent tool inputs.""" + +import math + + +def require_finite_float(value: object, *, name: str = "value") -> float: + if value is True or value is False or not isinstance(value, (int, float)): + raise ValueError(f"{name} must be a finite number") + number = float(value) + if not math.isfinite(number): + raise ValueError(f"{name} must be a finite number") + return number diff --git a/src/rai_core/rai/tools/ros2/navigation/nav2.py b/src/rai_core/rai/tools/ros2/navigation/nav2.py index 0ca4417fe..c95322fca 100644 --- a/src/rai_core/rai/tools/ros2/navigation/nav2.py +++ b/src/rai_core/rai/tools/ros2/navigation/nav2.py @@ -29,6 +29,7 @@ from rai.communication.ros2.connectors import ROS2Connector from rai.messages import MultimodalArtifact from rai.tools.ros2.base import BaseROS2Tool, BaseROS2Toolkit +from rai.tools.numbers import require_finite_float current_action_id: Optional[str] = None current_feedback: Optional[NavigateToPose.Feedback] = None @@ -86,6 +87,10 @@ def on_done(self, result: NavigateToPose.Result) -> None: current_result = result def _run(self, x: float, y: float, z: float, yaw: float) -> str: + x = require_finite_float(x, name="x") + y = require_finite_float(y, name="y") + z = require_finite_float(z, name="z") + yaw = require_finite_float(yaw, name="yaw") pose = PoseStamped() pose.header.frame_id = self.frame_id pose.header.stamp = self.connector.node.get_clock().now().to_msg() diff --git a/tests/tools/test_require_finite_float.py b/tests/tools/test_require_finite_float.py new file mode 100644 index 000000000..65bae01f9 --- /dev/null +++ b/tests/tools/test_require_finite_float.py @@ -0,0 +1,30 @@ +# Copyright (C) 2026 Robotec.AI +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math + +import pytest + +from rai.tools.numbers import require_finite_float + + +@pytest.mark.parametrize("value", [True, False, "1", None, math.nan, math.inf, -math.inf]) +def test_require_finite_float_rejects(value): + with pytest.raises(ValueError, match="finite"): + require_finite_float(value, name="x") + + +def test_require_finite_float_accepts(): + assert require_finite_float(1.5, name="x") == 1.5 + assert require_finite_float(0, name="x") == 0.0