Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/rai_core/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "rai_core"
version = "2.12.2"
version = "2.12.3"
description = "Core functionality for RAI framework"
readme = "README.md"
requires-python = ">=3.10,<3.13"
Expand Down
13 changes: 12 additions & 1 deletion src/rai_core/rai/agents/langchain/core/tool_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,20 @@ def run_one(call: ToolCall):
self.logger.info(f"Running tool: {call['name']}, args: {call['args']}")
artifact = None

tool = self.tools_by_name.get(call["name"])
if tool is None:
error_message = f'Unknown tool: "{call["name"]}"'
self.logger.info(error_message)
return ToolMessage(
content=error_message,
name=call["name"],
tool_call_id=call["id"],
status="error",
)

try:
ts = time.perf_counter()
output = self.tools_by_name[call["name"]].invoke(call, config) # type: ignore
output = tool.invoke(call, config) # type: ignore
te = time.perf_counter() - ts
self.logger.info(
f"Tool {call['name']} completed in {te:.2f} seconds. Tool output: {str(output.content)[:100]}{'...' if len(str(output.content)) > 100 else ''}"
Expand Down
2 changes: 1 addition & 1 deletion src/rai_core/rai/communication/hri_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def to_langchain(self) -> LangchainBaseMessage:
if self.images == [] and self.audios == []:
return AIMessage(content=self.text)
return AIMultimodalMessage(
content=self.text, images=base64_images, audios=base64_images
content=self.text, images=base64_images, audios=base64_audios
)
case _:
raise ValueError(
Expand Down
2 changes: 1 addition & 1 deletion src/rai_core/rai/frontend/configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ def on_wake_word_threshold_change():

# Get the current vendor from config and convert to display name
current_vendor = st.session_state.config.get("asr", {}).get(
"transciption_model", TRANSCRIBE_MODELS[0]
"transcription_model", TRANSCRIBE_MODELS[0]
)

asr_vendor = st.selectbox(
Expand Down
1 change: 1 addition & 0 deletions src/rai_core/rai/messages/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def store_artifacts(
# TODO(boczekbartek): refactor
path = Path(db_path)
if not path.is_file():
path.parent.mkdir(parents=True, exist_ok=True)
artifact_database: dict = {}
with path.open("wb") as file:
pickle.dump(artifact_database, file)
Expand Down
6 changes: 4 additions & 2 deletions src/rai_core/rai/tools/ros2/generic/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,10 @@ def _run(self) -> str:


class StartROS2ActionToolInput(BaseModel):
action_name: str = Field(..., description="The name of the action to start")
action_type: str = Field(..., description="The type of the action")
action_name: str = Field(
..., min_length=1, description="The name of the action to start"
)
action_type: str = Field(..., min_length=1, description="The type of the action")
action_args: Dict[str, Any] = Field(
..., description="The arguments to pass to the action"
)
Expand Down
5 changes: 3 additions & 2 deletions src/rai_core/rai/tools/ros2/generic/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,15 @@ def _run(self) -> str:


class CallROS2ServiceToolInput(BaseModel):
service_name: str = Field(description="The service to call")
service_type: str = Field(description="The type of the service")
service_name: str = Field(min_length=1, description="The service to call")
service_type: str = Field(min_length=1, description="The type of the service")
service_args: Optional[Dict[str, Any]] = Field(
default={},
description="A dictionary mapping each field name of the service request message to its value. For example, for std_srvs/srv/SetBool use {'data': True}.",
)
timeout_sec: float = Field(
default=5.0,
gt=0,
description="The timeout for the service call in seconds",
)

Expand Down
24 changes: 15 additions & 9 deletions src/rai_core/rai/tools/ros2/generic/topics.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ def get_tools(self) -> List[BaseTool]:


class PublishROS2MessageToolInput(BaseModel):
topic: str = Field(..., description="The topic to publish the message to")
topic: str = Field(
..., min_length=1, description="The topic to publish the message to"
)
message: Dict[str, Any] = Field(..., description="The message to publish")
message_type: str = Field(..., description="The type of the message")
message_type: str = Field(..., min_length=1, description="The type of the message")


class PublishROS2MessageTool(BaseROS2Tool):
Expand All @@ -99,8 +101,10 @@ def _run(self, topic: str, message: Dict[str, Any], message_type: str) -> str:


class ReceiveROS2MessageToolInput(BaseModel):
topic: str = Field(..., description="The topic to receive the message from")
timeout_sec: float = Field(1.0, description="The timeout in seconds")
topic: str = Field(
..., min_length=1, description="The topic to receive the message from"
)
timeout_sec: float = Field(1.0, gt=0, description="The timeout in seconds")


class ReceiveROS2MessageTool(BaseROS2Tool):
Expand All @@ -117,8 +121,10 @@ def _run(self, topic: str, timeout_sec: float = 1.0) -> str:


class GetROS2ImageToolInput(BaseModel):
topic: str = Field(..., description="The topic to receive the image from")
timeout_sec: float = Field(1.0, description="The timeout in seconds")
topic: str = Field(
..., min_length=1, description="The topic to receive the image from"
)
timeout_sec: float = Field(1.0, gt=0, description="The timeout in seconds")


class GetROS2ImageTool(BaseROS2Tool):
Expand Down Expand Up @@ -249,9 +255,9 @@ def _run(self, msg_type: str) -> str:


class GetROS2TransformToolInput(BaseModel):
target_frame: str = Field(..., description="The target frame")
source_frame: str = Field(..., description="The source frame")
timeout_sec: float = Field(default=10.0, description="The timeout in seconds")
target_frame: str = Field(..., min_length=1, description="The target frame")
source_frame: str = Field(..., min_length=1, description="The source frame")
timeout_sec: float = Field(default=10.0, gt=0, description="The timeout in seconds")


class GetROS2TransformTool(BaseROS2Tool):
Expand Down
16 changes: 12 additions & 4 deletions src/rai_core/rai/tools/ros2/navigation/nav2.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,18 @@ def get_tools(self) -> List[BaseTool]:


class NavigateToPoseToolInput(BaseModel):
x: float = Field(..., description="The x coordinate of the pose")
y: float = Field(..., description="The y coordinate of the pose")
z: float = Field(..., description="The z coordinate of the pose")
yaw: float = Field(..., description="The yaw angle of the pose")
x: float = Field(
..., allow_inf_nan=False, description="The x coordinate of the pose"
)
y: float = Field(
..., allow_inf_nan=False, description="The y coordinate of the pose"
)
z: float = Field(
..., allow_inf_nan=False, description="The z coordinate of the pose"
)
yaw: float = Field(
..., allow_inf_nan=False, description="The yaw angle of the pose"
)


class NavigateToPoseTool(BaseROS2Tool):
Expand Down
10 changes: 5 additions & 5 deletions src/rai_core/rai/tools/ros2/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ class GetROS2ImageConfiguredTool(BaseROS2Tool):
description: str = "Get the current image from the camera"
response_format: Literal["content", "content_and_artifact"] = "content_and_artifact"

topic: str = Field(..., description="The topic to get the image from")
timeout_sec: float = Field(default=5.0, description="The timeout in seconds")
topic: str = Field(..., min_length=1, description="The topic to get the image from")
timeout_sec: float = Field(default=5.0, gt=0, description="The timeout in seconds")

def model_post_init(self, __context: Any) -> None:
if not self.is_readable(topic=self.topic):
Expand All @@ -53,9 +53,9 @@ class GetROS2TransformConfiguredTool(BaseROS2Tool):
name: str = "get_ros2_robot_position"
description: str = "Get the robot's position"

source_frame: str = Field(..., description="The source frame")
target_frame: str = Field(..., description="The target frame")
timeout_sec: float = Field(default=10.0, description="The timeout in seconds")
source_frame: str = Field(..., min_length=1, description="The source frame")
target_frame: str = Field(..., min_length=1, description="The target frame")
timeout_sec: float = Field(default=10.0, gt=0, description="The timeout in seconds")

def _run(self) -> Any:
tool = GetROS2TransformTool(
Expand Down
2 changes: 1 addition & 1 deletion src/rai_s2s/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "rai_s2s"
version = "1.0.0"
version = "1.0.1"
description = "Speech-to-Speech module for RAI framework"
readme = "README.md"
requires-python = ">=3.10,<3.13"
Expand Down
12 changes: 12 additions & 0 deletions src/rai_s2s/rai_s2s/asr/agents/initialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ class VADConfig:
threshold: float = 0.5
silence_grace_period: float = 0.3

def __post_init__(self):
if not 0.0 < self.threshold <= 1.0:
raise ValueError(f"threshold must be in (0, 1], got {self.threshold}")
if self.silence_grace_period < 0:
raise ValueError(
f"silence_grace_period must not be negative, got {self.silence_grace_period}"
)


@dataclass
class WWConfig:
Expand All @@ -32,6 +40,10 @@ class WWConfig:
threshold: float = 0.01
is_used: bool = False

def __post_init__(self):
if not 0.0 < self.threshold <= 1.0:
raise ValueError(f"threshold must be in (0, 1], got {self.threshold}")


TRANSCRIBE_MODELS = ["LocalWhisper", "FasterWhisper", "OpenAI"]

Expand Down
1 change: 1 addition & 0 deletions tests/agents/langchain/test_tool_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def test_tool_runner_invalid_call():
"Tool output is not a tool message"
)
assert output["messages"][1].status == "error"
assert "Unknown tool" in output["messages"][1].content


def test_tool_runner():
Expand Down
21 changes: 17 additions & 4 deletions tests/communication/test_hri_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,23 @@ def test_to_langchain_ai_multimodal(image, audio):
): # NOTE: update when https://github.com/RobotecAI/rai/issues/370 is resolved
_ = message.to_langchain()

# assert isinstance(langchain_message, AIMultimodalMessage)
# assert langchain_message.content == "Response"
# assert langchain_message.images == ["img"]
# assert langchain_message.audios == ["audio"]

def test_to_langchain_ai_images_only(image):
message = HRIMessage(
text="Response",
images=[image],
audios=[],
message_author="ai",
communication_id=HRIMessage.generate_communication_id(),
seq_no=0,
seq_end=True,
)

langchain_message = message.to_langchain()

assert isinstance(langchain_message, RAIMultimodalMessage)
assert len(langchain_message.images) == 1
assert langchain_message.audios == []


def test_from_langchain_human():
Expand Down
3 changes: 1 addition & 2 deletions tests/messages/test_artifacts_db_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ def test_store_artifacts_honors_db_path(tmp_path: Path):


def test_store_artifacts_creates_file_at_path(tmp_path: Path):
db = tmp_path / "nested" / "db.pkl"
db.parent.mkdir(parents=True)
db = tmp_path / "nested" / "dir" / "db.pkl"
store_artifacts("x", [1], db_path=str(db))
assert get_stored_artifacts("x", db_path=str(db)) == [1]
37 changes: 37 additions & 0 deletions tests/s2s/test_asr_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# 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.

"""VAD and wake word thresholds come from config.toml, where an out-of-range
value silently turns detection permanently on or permanently off."""

import pytest

from rai_s2s.asr.agents.initialization import VADConfig, WWConfig


@pytest.mark.parametrize("config_cls", [VADConfig, WWConfig])
@pytest.mark.parametrize("threshold", [0.0, -0.1, 1.5])
def test_threshold_out_of_range_rejected(config_cls, threshold):
with pytest.raises(ValueError, match="threshold"):
config_cls(threshold=threshold)


def test_negative_silence_grace_period_rejected():
with pytest.raises(ValueError, match="silence_grace_period"):
VADConfig(silence_grace_period=-1.0)


def test_defaults_are_valid():
assert VADConfig().threshold == 0.5
assert WWConfig().threshold == 0.01
Loading
Loading