Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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_bench/rai_bench/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def test_models(
raise ValueError("Number of passed models must match number of passed vendors")
else:
for bench_conf in benchmark_configs:
# for each bench configuration seperate run folder
# for each bench configuration separate run folder
now = datetime.now()
run_name = f"run_{now.strftime('%Y-%m-%d_%H-%M-%S')}"
for i, model_name in enumerate(model_names):
Expand Down
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.1"
version = "2.12.2"
description = "Core functionality for RAI framework"
readme = "README.md"
requires-python = ">=3.10,<3.13"
Expand Down
2 changes: 2 additions & 0 deletions src/rai_core/rai/agents/langchain/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ def __init__(
max_size: int = 100,
):
super().__init__()
if max_size <= 0:
raise ValueError(f"max_size must be positive, got {max_size!r}")
Comment on lines +118 to +119

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'File map:\n'
git ls-files 'src/rai_core/rai/agents/langchain/agent.py'

printf '\nRelevant lines:\n'
sed -n '1,220p' src/rai_core/rai/agents/langchain/agent.py

printf '\nSearch for max_size usage:\n'
rg -n "max_size" src/rai_core/rai/agents/langchain/agent.py src/rai_core/rai -g '*.py'

printf '\nPython probe for the current guard semantics:\n'
python3 - <<'PY'
def guard(max_size):
    if max_size <= 0:
        return "raise"
    return "ok"

cases = [None, 0, 1, 1.5, True, False, -1]
for case in cases:
    try:
        print(repr(case), "=>", guard(case))
    except Exception as e:
        print(repr(case), "=>", type(e).__name__, str(e))
PY

Repository: RobotecAI/rai

Length of output: 11112


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Search for LangChainAgent tests and callers:\n'
rg -n "LangChainAgent\(" src tests -g '*.py' || true

printf '\nSearch for max_size=None with LangChainAgent or its subclasses:\n'
rg -n "max_size\s*=\s*None|max_size:\s*int\s*\|\s*None" src tests -g '*.py' || true

printf '\nInspect nearby class doc/contract for state_based_agent:\n'
sed -n '1,160p' src/rai_core/rai/agents/langchain/state_based_agent.py

printf '\nInspect aggregator base contract for comparison:\n'
sed -n '1,120p' src/rai_core/rai/aggregators/base.py

Repository: RobotecAI/rai

Length of output: 9010


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' tests/agents/langchain/test_langchain_agent_max_size.py

Repository: RobotecAI/rai

Length of output: 1310


Reject non-integer max_size values. max_size is documented and typed as an int, but True and 1.5 still pass this guard. Require a non-bool positive int and add regression tests for those cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rai_core/rai/agents/langchain/agent.py` around lines 118 - 119, Update
the max_size validation in the surrounding agent initialization logic to require
a positive int that is not a bool, rejecting values such as True and 1.5 while
preserving the existing error for non-positive values. Add regression tests
covering both invalid non-integer cases.

self.logger = logging.getLogger(__name__)
self.agent = runnable
self.stream_response = stream_response
Expand Down
8 changes: 8 additions & 0 deletions src/rai_core/rai/agents/langchain/callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ def __init__(
logger: Optional[logging.Logger] = None,
stream_response: bool = True,
):
if isinstance(max_buffer_size, bool) or not isinstance(max_buffer_size, int):
raise TypeError(
f"max_buffer_size must be a positive int, got {type(max_buffer_size).__name__}"
)
if max_buffer_size <= 0:
raise ValueError(
f"max_buffer_size must be positive, got {max_buffer_size!r}"
)
self.connectors = connectors
self.aggregate_chunks = aggregate_chunks
self.stream_response = stream_response
Expand Down
16 changes: 15 additions & 1 deletion src/rai_core/rai/agents/langchain/state_based_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from langchain_core.language_models import BaseChatModel
from langchain_core.messages import BaseMessage, HumanMessage
from langchain_core.tools import BaseTool
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, field_validator

from rai.agents.langchain.agent import LangChainAgent, newMessageBehaviorType
from rai.agents.langchain.core import ReActAgentState, create_state_based_runnable
Expand All @@ -42,6 +42,20 @@ class StateBasedConfig(BaseModel):
arbitrary_types_allowed=True,
)

@field_validator("time_interval")
@classmethod
def _time_interval_positive(cls, v: float) -> float:
if v <= 0:
raise ValueError(f"time_interval must be positive, got {v!r}")
return v
Comment on lines +45 to +50

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject non-finite time_interval values.

float("nan") passes v <= 0, although NaN is not a valid positive interval. Reject non-finite values with math.isfinite and add a NaN regression test.

Proposed validation
+import math
+
     `@field_validator`("time_interval")
     `@classmethod`
     def _time_interval_positive(cls, v: float) -> float:
-        if v <= 0:
+        if not math.isfinite(v) or v <= 0:
             raise ValueError(f"time_interval must be positive, got {v!r}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@field_validator("time_interval")
@classmethod
def _time_interval_positive(cls, v: float) -> float:
if v <= 0:
raise ValueError(f"time_interval must be positive, got {v!r}")
return v
import math
`@field_validator`("time_interval")
`@classmethod`
def _time_interval_positive(cls, v: float) -> float:
if not math.isfinite(v) or v <= 0:
raise ValueError(f"time_interval must be positive, got {v!r}")
return v
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rai_core/rai/agents/langchain/state_based_agent.py` around lines 45 - 50,
Update the _time_interval_positive validator to reject any non-finite value by
checking math.isfinite(v) alongside the existing positive-value condition, while
preserving the current error behavior for invalid intervals. Add a regression
test covering NaN input.


@field_validator("max_workers")
@classmethod
def _max_workers_positive(cls, v: int) -> int:
if v <= 0:
raise ValueError(f"max_workers must be positive, got {v!r}")
return v


class BaseStateBasedAgent(LangChainAgent, ABC):
"""
Expand Down
6 changes: 6 additions & 0 deletions src/rai_core/rai/agents/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ def wait_for_shutdown(agents: List[BaseAgent]):
an interrupt signal (SIGINT, e.g., Ctrl+C) or SIGTERM. It installs signal handlers to
capture these events and invokes the agent's ``stop()`` method as part of the shutdown process.
"""
if not agents:
raise ValueError("agents must be a non-empty list")
shutdown_event = Event()

def signal_handler(signum, frame):
Expand All @@ -58,6 +60,8 @@ def run_agents(agents: List[BaseAgent]):
Args:
agents: List of agent instances
"""
if not agents:
raise ValueError("agents must be a non-empty list")
logger.info(
"run_agents is an experimental function. \
If you believe that your agents are not running properly, \
Expand All @@ -84,6 +88,8 @@ def __init__(self, agents: List[BaseAgent]):
agents : List[BaseAgent]
List of agent instances to be managed by the runner.
"""
if not agents:
raise ValueError("agents must be a non-empty list")
self.agents = agents
self.logger = logging.getLogger(__name__)

Expand Down
2 changes: 2 additions & 0 deletions src/rai_core/rai/aggregators/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class BaseAggregator(ABC, Generic[T]):

def __init__(self, max_size: int | None = None) -> None:
super().__init__()
if max_size is not None and max_size <= 0:
raise ValueError(f"max_size must be positive or None, got {max_size}")
Comment on lines +31 to +32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use one consistent integer-size validation contract.

Both constructors accept positive floats and booleans as buffer sizes, causing inaccurate queue capacities. Apply the same non-boolean positive-integer validation in both locations, while preserving None as an unbounded sentinel where supported.

  • src/rai_core/rai/aggregators/base.py#L31-L32: reject boolean and non-integer max_size values while retaining None.
  • src/rai_core/rai/agents/langchain/agent.py#L118-L119: reject boolean and non-integer bounded values, and either retain or explicitly remove the existing None sentinel behavior.
📍 Affects 2 files
  • src/rai_core/rai/aggregators/base.py#L31-L32 (this comment)
  • src/rai_core/rai/agents/langchain/agent.py#L118-L119
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rai_core/rai/aggregators/base.py` around lines 31 - 32, Use one
non-boolean positive-integer validation contract for buffer sizes: update the
constructor validation in src/rai_core/rai/aggregators/base.py lines 31-32 to
reject booleans and non-integer max_size values while preserving None as
unbounded; apply the same validation to the bounded buffer-size check in
src/rai_core/rai/agents/langchain/agent.py lines 118-119, explicitly preserving
its existing None sentinel behavior.

self._buffer: Deque[T] = deque()
self.max_size = max_size

Expand Down
10 changes: 7 additions & 3 deletions src/rai_core/rai/aggregators/ros2/aggregators.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def get(self) -> HumanMessage:
prev_parsed = None
counter = 0
for log in msgs:
level = self.levels[log.level]
level = self.levels.get(log.level, str(log.level))
parsed = f"[{log.name}] [{level}] [{log.function}] {log.msg}"
if parsed == prev_parsed:
counter += 1
Expand Down Expand Up @@ -112,7 +112,7 @@ class ROS2ImgDescription(BaseModel):
class ROS2ImgVLMDiffAggregator(BaseAggregator[Image | CompressedImage]):
"""
Returns the LLM analysis of the differences between 3 images in the
aggregation buffer: 1st, midden, last
aggregation buffer: 1st, middle, last
"""

SYSTEM_PROMPT = "You are an expert in image analysis and your speciality is the comparison of 3 images"
Expand Down Expand Up @@ -142,6 +142,7 @@ def get(self) -> HumanMessage | None:
return None

b64_images = [convert_ros_img_to_base64(msg) for msg in msgs]
original_count = len(b64_images)

self.clear_buffer()

Expand All @@ -165,5 +166,8 @@ class ROS2ImgDiffOutput(BaseModel):
llm = self.llm.with_structured_output(ROS2ImgDiffOutput)
response = cast(ROS2ImgDiffOutput, llm.invoke(task))
return HumanMessage(
content=f"Result of the analysis of the {len(b64_images)} keyframes selected from {len(b64_images)} last images:\n{response}"
content=(
f"Result of the analysis of the {len(b64_images)} keyframes "
f"selected from {original_count} last images:\n{response}"
)
)
4 changes: 4 additions & 0 deletions src/rai_core/rai/communication/base_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ class ParametrizedCallback(BaseModel, Generic[T]):

class BaseConnector(Generic[T]):
def __init__(self, callback_max_workers: int = 4):
if callback_max_workers <= 0:
raise ValueError(
f"callback_max_workers must be positive, got {callback_max_workers!r}"
)
self.callback_max_workers = callback_max_workers
self.logger = logging.getLogger(self.__class__.__name__)
self.registered_callbacks: Dict[str, Dict[str, ParametrizedCallback[T]]] = (
Expand Down
11 changes: 10 additions & 1 deletion src/rai_core/rai/communication/ros2/api/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,17 @@ def create_action_server(
The handle for the created action server

Raises:
ValueError: If the action server cannot be created
TypeError: If result_timeout is not a positive number (bool rejected).
ValueError: If the action server cannot be created or result_timeout <= 0.
"""
if isinstance(result_timeout, bool) or not isinstance(
result_timeout, (int, float)
):
raise TypeError(
f"result_timeout must be a positive number, got {type(result_timeout).__name__}"
)
if result_timeout <= 0:
raise ValueError(f"result_timeout must be positive, got {result_timeout!r}")
Comment on lines +155 to +165

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep timeout validation consistent across both APIs.

src/rai_core/rai/communication/ros2/api/action.py#L155-L165 still accepts non-finite timeouts, and src/rai_core/rai/communication/ros2/ros_async.py#L29-L30 also accepts True because it only checks <= 0. Reject non-finite values at both entry points.

📍 Affects 2 files
  • src/rai_core/rai/communication/ros2/api/action.py#L155-L165 (this comment)
  • src/rai_core/rai/communication/ros2/ros_async.py#L29-L30
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rai_core/rai/communication/ros2/api/action.py` around lines 155 - 165,
Update timeout validation in action.py lines 155-165 and ros_async.py lines
29-30 to reject non-finite values at both API entry points, while preserving
rejection of non-positive values and booleans. Use the existing validation paths
and ensure valid positive finite numbers remain accepted.

handle = self._generate_handle()
action_ros_type = import_message_from_str(action_type)
try:
Expand Down
2 changes: 2 additions & 0 deletions src/rai_core/rai/communication/ros2/ros_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ def get_future_result(
future: rclpy.task.Future, timeout_sec: float = 5.0
) -> Any | None:
"""Replaces rclpy.spin_until_future_complete"""
if timeout_sec <= 0:
raise ValueError(f"timeout_sec must be positive, got {timeout_sec}")
result = None
event = threading.Event()

Expand Down
4 changes: 2 additions & 2 deletions src/rai_core/rai/communication/ros2/waiters.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ def get_missing_entities(
callable_get_entities: Callable[[], List[str]],
requested_entities: List[str],
) -> set[str]:
requested_set = set(requested_entities)
available_set = set(callable_get_entities())
requested_set = {ensure_slash(name) for name in requested_entities}
available_set = {ensure_slash(name) for name in callable_get_entities()}
Comment on lines +33 to +34

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize the timeout-reporting path too.

Polling uses normalized sets, but the final timeout calculation still compares normalized requested names with raw available_entities. If only some entities are available without /, the timeout error incorrectly includes those entities as missing.

Proposed fix
-    available_entities = get_entities()
-
-    missing = set(requested) - set(available_entities)
+    missing = get_missing_entities(get_entities, requested)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rai_core/rai/communication/ros2/waiters.py` around lines 33 - 34, Update
the timeout-reporting calculation in the waiter flow to compare normalized
entity names consistently, matching the normalized requested_set and
available_set built from ensure_slash. Reuse the normalized available set when
determining missing entities so names available without a leading slash are not
reported as missing.

return requested_set - available_set


Expand Down
12 changes: 6 additions & 6 deletions src/rai_core/rai/messages/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,18 @@ def store_artifacts(
tool_call_id: str, artifacts: List[Any], db_path="artifact_database.pkl"
):
# TODO(boczekbartek): refactor
db_path = Path(db_path)
if not db_path.is_file():
artifact_database = {}
with open("artifact_database.pkl", "wb") as file:
path = Path(db_path)
if not path.is_file():
artifact_database: dict = {}
with path.open("wb") as file:
pickle.dump(artifact_database, file)
Comment on lines +29 to 33

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix the missing-parent contract in both implementation and test.

store_artifacts only creates the file, while the test creates the parent directory first, so the nested-path failure is hidden.

  • src/rai_core/rai/messages/artifacts.py#L29-L33: call path.parent.mkdir(parents=True, exist_ok=True) before opening the file.
  • tests/messages/test_artifacts_db_path.py#L30-L34: remove db.parent.mkdir(...) so the test verifies that production creates the missing directory.
📍 Affects 2 files
  • src/rai_core/rai/messages/artifacts.py#L29-L33 (this comment)
  • tests/messages/test_artifacts_db_path.py#L30-L34
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rai_core/rai/messages/artifacts.py` around lines 29 - 33, Update
store_artifacts in src/rai_core/rai/messages/artifacts.py to create path.parent
with parents=True and exist_ok=True before opening the database file. In
tests/messages/test_artifacts_db_path.py, remove the test’s db.parent.mkdir call
so the test exercises production directory creation.

with open("artifact_database.pkl", "rb") as file:
with path.open("rb") as file:
artifact_database = pickle.load(file)
if tool_call_id not in artifact_database:
artifact_database[tool_call_id] = artifacts
else:
artifact_database[tool_call_id].extend(artifacts)
with open("artifact_database.pkl", "wb") as file:
with path.open("wb") as file:
pickle.dump(artifact_database, file)


Expand Down
6 changes: 6 additions & 0 deletions src/rai_core/rai/tools/ros2/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@


def run_with_timeout(cmd: List[str], timeout_sec: int):
if isinstance(timeout_sec, bool) or not isinstance(timeout_sec, (int, float)):
raise TypeError(
f"timeout_sec must be a positive number, got {type(timeout_sec).__name__}"
)
if timeout_sec <= 0:
raise ValueError(f"timeout_sec must be positive, got {timeout_sec!r}")
Comment on lines +25 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '1,220p' src/rai_core/rai/tools/ros2/cli.py

Repository: RobotecAI/rai

Length of output: 6071


🏁 Script executed:

python3 - <<'PY'
import math
from threading import Timer

vals = [float('nan'), float('inf'), float('-inf'), 0.1, -1.0, 1]
for v in vals:
    try:
        print(type(v).__name__, v, 'finite=', math.isfinite(v), '<=0=', (v <= 0))
    except Exception as e:
        print(type(v).__name__, v, 'error comparing:', e)

# Probe Timer construction/start behavior with NaN/inf without executing callbacks long-term.
def cb():
    print("callback fired")

for v in [float('nan'), float('inf'), 0.001]:
    t = Timer(v, cb)
    print("Timer", v, "interval attr:", t.interval)
PY

Repository: RobotecAI/rai

Length of output: 445


🏁 Script executed:

python3 - <<'PY'
import math
from threading import Timer

vals = [float('nan'), float('inf'), float('-inf'), 0.1, -1.0, 1]
for v in vals:
    print(type(v).__name__, v, 'finite=', math.isfinite(v), '<=0=', (v <= 0))

for v in [float('nan'), float('inf'), 0.001]:
    t = Timer(v, lambda: None)
    print("Timer", v, "interval attr:", t.interval)
PY

Repository: RobotecAI/rai

Length of output: 445


🏁 Script executed:

python3 - <<'PY'
import time
from threading import Timer

results = []

def mark(label):
    results.append((label, time.monotonic()))

start = time.monotonic()
for label, interval in [("nan", float("nan")), ("inf", float("inf")), ("short", 0.05)]:
    t = Timer(interval, mark, args=(label,))
    t.daemon = True
    t.start()

time.sleep(0.2)
print("elapsed", round(time.monotonic() - start, 3))
print("results", results)
PY

Repository: RobotecAI/rai

Length of output: 826


Reject non-finite timeout values. float("nan") bypasses the <= 0 check and reaches Timer, where it can fire immediately; float("inf") raises an OverflowError in the timer thread. Add math.isfinite(timeout_sec) here to reject both.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rai_core/rai/tools/ros2/cli.py` around lines 25 - 30, Update the
timeout_sec validation in the ROS2 CLI timeout handling to reject non-finite
numeric values by checking math.isfinite(timeout_sec) alongside the existing
type and positive-value checks. Preserve the current TypeError behavior for
invalid types and raise the appropriate validation error before creating the
Timer for NaN or infinity.

proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
timer = Timer(timeout_sec, proc.kill)
try:
Expand Down
39 changes: 39 additions & 0 deletions tests/agents/langchain/test_hri_callback_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 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 pytest
from rai.agents.langchain.callback import HRICallbackHandler


def test_hri_callback_handler_rejects_nonpositive_max_buffer_size():
with pytest.raises(ValueError, match="max_buffer_size must be positive"):
HRICallbackHandler(connectors={}, max_buffer_size=0)
with pytest.raises(ValueError, match="max_buffer_size must be positive"):
HRICallbackHandler(connectors={}, max_buffer_size=-1)


def test_hri_callback_handler_rejects_non_int_max_buffer_size():
with pytest.raises(TypeError, match="max_buffer_size must be a positive int"):
HRICallbackHandler(connectors={}, max_buffer_size=False)
with pytest.raises(TypeError, match="max_buffer_size must be a positive int"):
HRICallbackHandler(connectors={}, max_buffer_size=1.5) # type: ignore[arg-type]
with pytest.raises(TypeError, match="max_buffer_size must be a positive int"):
HRICallbackHandler(connectors={}, max_buffer_size="200") # type: ignore[arg-type]


def test_hri_callback_handler_accepts_positive_max_buffer_size():
handler = HRICallbackHandler(connectors={}, max_buffer_size=1)
assert handler.max_buffer_size == 1
default_handler = HRICallbackHandler(connectors={})
assert default_handler.max_buffer_size == 200
37 changes: 37 additions & 0 deletions tests/agents/langchain/test_langchain_agent_max_size.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# 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.

from unittest.mock import MagicMock

import pytest
from rai.agents.langchain.agent import LangChainAgent


def _make_agent(max_size: int) -> LangChainAgent:
return LangChainAgent(
target_connectors={},
runnable=MagicMock(),
max_size=max_size,
)


def test_max_size_must_be_positive():
for bad in (0, -1, False):
with pytest.raises(ValueError, match="max_size must be positive"):
_make_agent(bad) # type: ignore[arg-type]


def test_max_size_accepts_positive():
agent = _make_agent(1)
assert agent.max_size == 1
38 changes: 38 additions & 0 deletions tests/agents/langchain/test_state_based_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 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 pytest
from pydantic import ValidationError
from rai.agents.langchain.state_based_agent import StateBasedConfig


def test_time_interval_must_be_positive():
for bad in (0, -1.0, 0.0):
with pytest.raises(ValidationError):
StateBasedConfig(aggregators={}, time_interval=bad)


def test_max_workers_must_be_positive():
for bad in (0, -1):
with pytest.raises(ValidationError):
StateBasedConfig(aggregators={}, max_workers=bad)


def test_defaults_and_positive_ok():
cfg = StateBasedConfig(aggregators={})
assert cfg.time_interval == 5.0
assert cfg.max_workers == 8
cfg2 = StateBasedConfig(aggregators={}, time_interval=0.1, max_workers=1)
assert cfg2.time_interval == 0.1
assert cfg2.max_workers == 1
23 changes: 23 additions & 0 deletions tests/agents/test_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,26 @@ def fake_signal(signum, handler):

assert thread.is_alive() is False
assert all(agent.stop_called for agent in agents)


def test_agent_runner_rejects_empty_agents():
import pytest

with pytest.raises(ValueError, match="non-empty"):
AgentRunner([])


def test_wait_for_shutdown_rejects_empty_agents():
import pytest
from rai.agents.runner import wait_for_shutdown

with pytest.raises(ValueError, match="non-empty"):
wait_for_shutdown([])


def test_run_agents_rejects_empty_agents():
import pytest
from rai.agents.runner import run_agents

with pytest.raises(ValueError, match="non-empty"):
run_agents([])
Loading
Loading