Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8968e2b
chore: fix seperate → separate typos in bench code (#797)
Bartok9 Jul 22, 2026
fbb88c2
fix(aggregators): avoid KeyError for unknown ROS log levels (#798)
Bartok9 Jul 22, 2026
f672311
fix(ros2): slash-normalize available names in entity waiters (#801)
Bartok9 Jul 22, 2026
3501232
fix(messages): honor db_path in store_artifacts (#804)
Bartok9 Jul 22, 2026
4698adc
fix(aggregators): reject non-positive max_size (#807)
Bartok9 Jul 22, 2026
72ed265
fix(agents): reject non-positive StateBasedConfig interval/workers (#…
Bartok9 Jul 22, 2026
042cc25
fix(agents): reject non-positive HRICallbackHandler max_buffer_size (…
Bartok9 Jul 22, 2026
81932de
fix(agents): reject empty AgentRunner agent list (#818)
Bartok9 Jul 22, 2026
460367f
fix(agents): reject non-positive LangChainAgent max_size (#815)
Bartok9 Jul 22, 2026
ca14da2
fix(ros2): reject non-positive timeout_sec in get_future_result (#808)
Bartok9 Jul 22, 2026
a51057b
fix(aggregators): report original keyframe buffer size in VLM diff su…
Bartok9 Jul 22, 2026
5e0a2bc
fix(ros2): reject non-positive action server result_timeout (#822)
Bartok9 Jul 22, 2026
e3c0ffe
fix(tools): reject non-positive ROS2 CLI command timeout (#817)
Bartok9 Jul 22, 2026
81f5a89
fix(communication): reject non-positive callback_max_workers (#813)
Bartok9 Jul 22, 2026
082be4b
chore: pre-commit & version bump lock (#825)
maciejmajek Jul 22, 2026
e1745a6
fix(ci): license header + align zero-timeout test with #808 (#826)
Bartok9 Jul 22, 2026
c1d1f16
Apply suggestions from code review
maciejmajek Aug 12, 2026
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