Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
486 changes: 265 additions & 221 deletions poetry.lock

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions src/rai_bench/rai_bench/tool_calling_agent/mocked_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import numpy as np
import numpy.typing as npt
from langchain_core.tools import BaseTool
from pydantic import BaseModel, ValidationError, computed_field
from pydantic import BaseModel, SkipValidation, ValidationError, computed_field
from rai.communication.ros2.api.conversion import import_message_from_str
from rai.communication.ros2.connectors import ROS2Connector
from rai.communication.ros2.messages import ROS2Message
Expand Down Expand Up @@ -474,7 +474,9 @@ class MockGetROS2ActionFeedbackTool(GetROS2ActionFeedbackTool):
connector: ROS2Connector = MagicMock(spec=ROS2Connector)
available_feedbacks: Dict[str, List[Any]] = {}
internal_action_id_mapping: Dict[str, str] = {}
action_feedbacks_store_lock: Lock = Lock()
# SkipValidation wrapper prevents Pydantic from attempting to validate the Lock object,
# which is a C built-in and cannot be validated as a Python type
action_feedbacks_store_lock: SkipValidation[Lock] = Lock()

def _run(self, action_id: str) -> str:
if action_id not in self.internal_action_id_mapping:
Expand All @@ -489,7 +491,9 @@ def _run(self, action_id: str) -> str:
class MockGetROS2ActionResultTool(GetROS2ActionResultTool):
available_results: Dict[str, Any] = {}
internal_action_id_mapping: Dict[str, str] = {}
action_results_store_lock: Lock = Lock()
# SkipValidation wrapper prevents Pydantic from attempting to validate the Lock object,
# which is a C built-in and cannot be validated as a Python type
action_results_store_lock: SkipValidation[Lock] = Lock()

def _run(self, action_id: str) -> str:
if action_id not in self.internal_action_id_mapping:
Expand Down
2 changes: 1 addition & 1 deletion src/rai_bringup/launch/openset.launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def generate_launch_description():
[
ExecuteProcess(
cmd=["python", "run_perception_agents.py"],
cwd="src/rai_extensions/rai_perception/scripts",
cwd="src/rai_extensions/rai_perception/rai_perception/scripts",
output="screen",
),
]
Expand Down
2 changes: 1 addition & 1 deletion src/rai_core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "rai_core"
version = "2.5.9"
version = "2.5.10"
description = "Core functionality for RAI framework"
authors = ["Maciej Majek <maciej.majek@robotec.ai>", "Bartłomiej Boczek <bartlomiej.boczek@robotec.ai>", "Kajetan Rachwał <kajetan.rachwal@robotec.ai>"]
readme = "README.md"
Expand Down
3 changes: 2 additions & 1 deletion src/rai_core/rai/communication/ros2/api/conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,6 @@ def convert_ros_img_to_base64(
)
else:
cv_image = cv2.cvtColor(cv_image, cv2.COLOR_BGR2RGB)
image_data = cv2.imencode(".png", cv_image)[1].tostring() # type: ignore
# Use tobytes() instead of deprecated tostring() (deprecated since NumPy 1.9.0, removed in 2.0.0)
image_data = cv2.imencode(".png", cv_image)[1].tobytes() # type: ignore
return base64.b64encode(image_data).decode("utf-8") # type: ignore
2 changes: 1 addition & 1 deletion src/rai_extensions/rai_perception/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ torch = "^2.3.1"
torchvision = "^0.18.1"
rf-groundingdino = "^0.2.0"
# TODO(juliaj): test-pypi is for testing purpose only, update to rai-gsam2 info once it is published to pypi
rai-gsam2 = { version = "1.0.0", source = "test-pypi" }
rai-gsam2 = { version = "1.0.1", source = "test-pypi" }
rai_core = ">=2.0.0.a2,<3.0.0"

[build-system]
Expand Down
10 changes: 7 additions & 3 deletions tests/agents/langchain/test_langchain_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,13 @@ class TestTracingConfiguration:

def test_tracing_with_missing_config_file(self):
"""Test that tracing gracefully handles missing config.toml file in langchain context."""
# This should not crash even without config.toml
callbacks = get_tracing_callbacks()
assert len(callbacks) == 0
# Mock load_config to simulate missing config file scenario.
# Without mocking, get_tracing_callbacks() would load the workspace's config.toml,
# If tracing is enabled, it'll prevent us from testing how it handles missing config files.
with patch("rai.initialization.model_initialization.load_config") as mock_load:
mock_load.side_effect = FileNotFoundError("Config file not found")
callbacks = get_tracing_callbacks()
assert len(callbacks) == 0

def test_tracing_with_config_file_present(self, test_config_toml):
"""Test that tracing works when config.toml is present in langchain context."""
Expand Down
27 changes: 27 additions & 0 deletions tests/initialization/test_model_initialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import os
from pathlib import Path

import pytest
Expand Down Expand Up @@ -124,3 +125,29 @@ def test_get_llm_model_unknown_vendor_raises(tmp_path, monkeypatch):

with pytest.raises(AttributeError, match="has no attribute 'unsupported'"):
model_initialization.get_llm_model("simple_model", config_path=str(config_path))


def test_load_config_default_path(tmp_path):
"""Test that load_config() defaults to './config.toml' in current directory."""
# Create a config.toml in a temporary directory
_ = write_config(tmp_path / "config.toml", CONFIG_TEMPLATE)

# Change to that directory to test default path behavior
original_cwd = os.getcwd()
try:
os.chdir(tmp_path)

# Call load_config without arguments - should load from current directory
config = model_initialization.load_config()

# Verify config was loaded correctly
assert config.vendor.simple_model == "openai"
assert config.vendor.complex_model == "aws"
assert config.openai.simple_model == "gpt-4o-mini"
assert config.aws.region_name == "us-west-2"
assert config.tracing.project == "rai"
assert config.tracing.langfuse.use_langfuse is False

finally:
# Restore original directory
os.chdir(original_cwd)
10 changes: 7 additions & 3 deletions tests/initialization/test_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ class TestInitializationTracing:

def test_tracing_with_missing_config_file(self):
"""Test that tracing gracefully handles missing config.toml file."""
# This should not crash even without config.toml
callbacks = get_tracing_callbacks()
assert len(callbacks) == 0
# Mock load_config to simulate missing config file scenario.
# Without mocking, get_tracing_callbacks() would load the workspace's config.toml,
# If tracing is enabled, it'll prevent us from testing how it handles missing config files.
with patch("rai.initialization.model_initialization.load_config") as mock_load:
mock_load.side_effect = FileNotFoundError("Config file not found")
callbacks = get_tracing_callbacks()
assert len(callbacks) == 0

def test_tracing_with_config_file_present_tracing_disabled(self, test_config_toml):
"""Test that tracing works when config.toml is present but tracing is disabled."""
Expand Down
13 changes: 13 additions & 0 deletions tests/rai_bringup/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (C) 2025 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.
76 changes: 76 additions & 0 deletions tests/rai_bringup/test_openset_launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright (C) 2025 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 importlib.util
import subprocess
import sys
import time
from pathlib import Path

import pytest

# Import the launch file directly by path
launch_file_path = (
Path(__file__).parent.parent.parent
/ "src"
/ "rai_bringup"
/ "launch"
/ "openset.launch.py"
)
spec = importlib.util.spec_from_file_location("openset_launch", launch_file_path)
openset_launch = importlib.util.module_from_spec(spec)
sys.modules["openset_launch"] = openset_launch
spec.loader.exec_module(openset_launch)
generate_launch_description = openset_launch.generate_launch_description


class TestOpensetLaunch:
"""Test cases for openset.launch.py"""

@pytest.mark.timeout(10)
def test_launch_process_starts_without_immediate_crash(self):
"""Test that the launch process starts without immediately crashing (catches basic errors such as incorrect path to the Python script)."""
process = subprocess.Popen(
["ros2", "launch", "rai_bringup", "openset.launch.py"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)

try:
# Wait a few seconds and verify process hasn't crashed
time.sleep(3)
poll_result = process.poll()

# If poll() returns None, process is still running (good)
# If it returns a code, process has exited (likely an error)
if poll_result is not None:
stdout, stderr = process.communicate(timeout=1)
error_msg = (
f"Launch process exited prematurely with code {poll_result}\n"
)
if stderr:
error_msg += f"STDERR:\n{stderr}"
if stdout:
error_msg += f"\nSTDOUT:\n{stdout}"
assert False, error_msg

finally:
# Terminate the launch process
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.wait()