Skip to content

Conversation

@maciejmajek
Copy link
Member

@maciejmajek maciejmajek commented Dec 19, 2025

Purpose

Currently, sending ROS2 messages requires constructing a nested dictionary and passing the message type as a string. This approach has several drawbacks:

  • Cumbersome for complex messages: Many ROS2 messages, such as PoseStamped, Path, or RobotTrajectory, contain multiple nested fields. Manually building dictionaries for these messages quickly becomes verbose and error-prone.
  • No type checking or syntax help: Since dictionaries are untyped, IDEs and linters cannot assist with field names, types, or structure. Mistakes in key names or value types only appear at runtime.
  • Hard to maintain: Any change in message structure requires carefully updating all nested dictionary fields. Large dictionaries make code difficult to read, modify, or extend.
  • Prone to subtle bugs: Missing or misnamed fields may not be immediately obvious, leading to runtime errors or unexpected behavior in ROS2 communication.

Switching to using ROS2 message classes directly eliminates these problems, providing clear, type-checked, and maintainable code. The original idea of passing dictionaries instead of real message objects was intended to make it easier for AI tools or LLMs to generate code without needing to import message definitions. However, with these changes (especially if call_service and start_action are updated to support the same simplifications) RAI will significantly lower the entry barrier for AI developers who have less familiarity with ROS 2, enabling them to work with messages, services, and actions in a straightforward way.

Reducing boilerplate:

from rai.communication.ros2 import ROS2Connector, ROS2Context, ROS2Message
from geometry_msgs.msg import PoseStamped, Point, Quaternion

# before: requires building a large dictionary manually, requires passing the msg_type and use of ROS2Message
with ROS2Context():
    connector = ROS2Connector()
    connector.send_message(
        target="/pose_topic",
        message=ROS2Message(payload={
            "header": {
                "stamp": {"sec": 0, "nanosec": 0},
                "frame_id": "map"
            },
            "pose": {
                "position": {"x": 1.0, "y": 2.0, "z": 3.0},
                "orientation": {"x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0}
            }
        }),
        msg_type="geometry_msgs/msg/PoseStamped"
    )

# after: directly using ROS2 message class, much simpler and easier to maintain, no need to pass the msg_type and use of ROS2Message
with ROS2Context():
    connector = ROS2Connector()
    pose_msg = PoseStamped()
    pose_msg.header.frame_id = "map"
    pose_msg.pose.position = Point(x=1.0, y=2.0, z=3.0)
    pose_msg.pose.orientation = Quaternion(x=0.0, y=0.0, z=0.0, w=1.0)
    
    connector.send_message(target="/pose_topic", message=pose_msg)

Proposed Changes

Allow passing IROS2Message into send_message method. Msg cls validation by rosidl_runtime_py

Issues

TBD: Implementation for messages is easy, implementation for services and actions is hard. We are currently passing a dictionary and msg_type e.g. "std_srvs/srv/SetBool". Then we use the msg_type to create a Request cls and fill it with the passed dictionary.

A simpler api, coherent with new send_messages should allow:

connector.service_call(target='/my_service', message=SetBool.Request(data=True))

But to create a service client we also need to know the underlying base class (SetBool, not a SetBool.Request). Since SetBool.Request is not linked to SetBool, we will either need to keep passing msg_type via a string/cls or hack our way out with some magic.

Testing

Extended test suite

Summary by CodeRabbit

Release Notes

  • New Features

    • Send pre-formed ROS2 messages directly without requiring explicit type specification
    • Type parameter is now optional when message structure is unambiguous
  • Bug Fixes

    • Enhanced validation and error handling for invalid message inputs and missing type information
  • Documentation

    • Updated ROS2 connector examples demonstrating message sending patterns and type inference
  • Chores

    • Version bumped to 2.7.0

✏️ Tip: You can customize this high-level summary in your review settings.

@codecov
Copy link

codecov bot commented Dec 19, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 65.53%. Comparing base (074af8e) to head (864f99c).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #741      +/-   ##
==========================================
+ Coverage   65.34%   65.53%   +0.18%     
==========================================
  Files          78       78              
  Lines        3388     3406      +18     
==========================================
+ Hits         2214     2232      +18     
  Misses       1174     1174              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@maciejmajek
Copy link
Member Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 22, 2025

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 22, 2025

Walkthrough

Changes expand ROS2 message handling in the API and connectors to support passing pre-formed ROS2Message objects alongside dictionary payloads, making the msg_type parameter optional. Version bumped from 2.6.0 to 2.7.0. Added utility methods for message type classification and comprehensive test coverage for message publishing scenarios and error handling.

Changes

Cohort / File(s) Summary
Documentation Updates
docs/API_documentation/connectors/ROS_2_Connectors.md
Added explicit ROS2Message import examples and demonstrated raw ROS2 message usage with target parameter and dictionary-based messaging with msg_type.
Package Metadata
src/rai_core/pyproject.toml
Version bump from 2.6.0 to 2.7.0 in tool.poetry configuration.
ROS2 API Type Utilities
src/rai_core/rai/communication/ros2/api/base.py
Imported ROS2 utility predicates (is_action, is_message, is_service) and added three public static helper methods: is_ros2_message(), is_ros2_service(), is_ros2_action() delegating to underlying rosidl utilities.
ROS2 Topic Publishing
src/rai_core/rai/communication/ros2/api/topic.py
Modified publish() signature to accept IROS2Message | Dict[str, Any] for msg_content and str | None (default None) for msg_type. Added type-aware branching: pass through ROS2Message objects directly, build from dict + explicit msg_type, or raise ValueError for missing/invalid combinations.
ROS2 Base Connector
src/rai_core/rai/communication/ros2/connectors/base.py
Updated send_message() signature from message: T, msg_type: str to message: T | IROS2Message, msg_type: str | None = None. Added logic to extract payload from ROS2Message or publish raw ROS2 messages, preserving QoS and targeting behavior.
API Tests
tests/communication/ros2/test_api.py
Added type classification tests for is_ros2_message/is_ros2_service/is_ros2_action. Extended message publish test with actual_type inference validation. Introduced tests for invalid/missing msg_type, wrong QoS, and error handling. Reduced sleep duration from 1.0 to 0.1 seconds. Added imports for ROS2 message types (Point, Pose, PoseStamped, etc.).
Connector Tests
tests/communication/ros2/test_connectors.py
Added pytest.mark.parametrize with message_content, msg_type, and actual_type combinations for send_message testing. Updated test flow to validate dynamic type reception. Expanded imports for additional message types (String, Header, Point, Pose variants, Quaternion). Reduced sleep to 0.1 seconds.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Optional parameter semantics: Review msg_type: str | None = None changes across topic.py and connectors/base.py to ensure backward compatibility; verify error handling for None cases (ValueError raised when dict without msg_type is passed).
  • Type union handling: Validate branching logic in publish() method (topic.py) that dispatches on isinstance(msg_content, IROS2Message) vs Dict vs invalid types.
  • Type inference flow: Confirm message payload extraction from ROS2Message objects preserves message content and semantics.
  • Test parametrization: Verify parametrized test coverage (test_connectors.py and test_api.py) sufficiently exercises edge cases (empty dicts, None types, invalid message specs).
  • Consistency across layers: Ensure connector base.py and API topic.py changes are aligned in type handling and error messages.

Possibly related PRs

Suggested reviewers

  • rachwalk
  • boczekbartek

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive The PR description covers purpose, proposed changes, and testing, but the Issues section is marked TBD and contains incomplete implementation notes rather than linked issue numbers. Replace the TBD Issues section with actual linked GitHub issue numbers (e.g., Closes #XXX). Move implementation notes to a separate discussion section or include them in the testing section for clarity.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: allow passing ros2 types in send_message' clearly and specifically describes the main feature addition in this PR—enabling ROS2 message type instances to be passed directly to send_message, reducing boilerplate.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/true-ros2-messages

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/rai_core/rai/communication/ros2/api/topic.py (1)

150-158: Update docstring to reflect new parameter type.

The docstring for msg_content (line 154) still describes it as "Dictionary containing the message content", but the parameter now also accepts IROS2Message instances directly.

🔎 Suggested docstring update
         """Publish a message to a ROS2 topic.

         Args:
             topic: Name of the topic to publish to
-            msg_content: Dictionary containing the message content
+            msg_content: ROS2 message instance or dictionary containing the message content
             msg_type: ROS2 message type as string (e.g. 'std_msgs/msg/String')
             auto_qos_matching: Whether to automatically match QoS with subscribers
             qos_profile: Optional custom QoS profile to use
🧹 Nitpick comments (1)
tests/communication/ros2/test_connectors.py (1)

90-96: Type hint for message_content is too restrictive.

The parametrized cases include both ROS2Message wrappers and direct ROS2 message instances (e.g., String(), Pose()). The type hint should reflect this union to avoid misleading developers and improve IDE/type-checker accuracy.

🔎 Suggested fix
+from typing import Any
...
 def test_ros2_connector_send_message(
     ros_setup: None,
     request: pytest.FixtureRequest,
-    message_content: ROS2Message,
+    message_content: ROS2Message | Any,  # ROS2Message or native ROS2 message instance
     msg_type: str | None,
     actual_type: type,
 ):

Alternatively, if IROS2Message is the protocol/interface for native ROS2 messages, you could use ROS2Message | IROS2Message.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 074af8e and 864f99c.

📒 Files selected for processing (7)
  • docs/API_documentation/connectors/ROS_2_Connectors.md
  • src/rai_core/pyproject.toml
  • src/rai_core/rai/communication/ros2/api/base.py
  • src/rai_core/rai/communication/ros2/api/topic.py
  • src/rai_core/rai/communication/ros2/connectors/base.py
  • tests/communication/ros2/test_api.py
  • tests/communication/ros2/test_connectors.py
🧰 Additional context used
🧠 Learnings (10)
📓 Common learnings
Learnt from: maciejmajek
Repo: RobotecAI/rai PR: 379
File: tests/communication/ros2/test_connectors.py:50-50
Timestamp: 2025-01-21T11:24:27.687Z
Learning: In ROS2 connector implementation, topics are typically static and well-discovered before running any method of the API. The discovery mechanism in tests is an edge case, as it's not representative of the typical usage pattern.
Learnt from: maciejmajek
Repo: RobotecAI/rai PR: 416
File: src/rai_core/rai/communication/ros2/connectors.py:241-267
Timestamp: 2025-02-10T21:35:25.819Z
Learning: When dealing with ROS2 message conversions, prefer using `rosidl_runtime_py.convert.message_to_ordereddict` directly instead of wrapping it in a utility function to avoid unnecessary abstractions and potential circular imports.
Learnt from: boczekbartek
Repo: RobotecAI/rai PR: 335
File: src/rai/rai/tools/ros/native_actions.py:0-0
Timestamp: 2025-01-08T13:36:27.469Z
Learning: In the RAI codebase, ROS2 messages are converted to string representation to make them more LLM-friendly, as seen in the `GetMsgFromTopic._run` method. This design decision helps in better processing and understanding of the messages by the language model.
📚 Learning: 2025-02-10T21:35:25.819Z
Learnt from: maciejmajek
Repo: RobotecAI/rai PR: 416
File: src/rai_core/rai/communication/ros2/connectors.py:241-267
Timestamp: 2025-02-10T21:35:25.819Z
Learning: Avoid circular imports in the ROS2 connector modules by placing utility functions in dedicated utility modules that don't import from the connector modules.

Applied to files:

  • docs/API_documentation/connectors/ROS_2_Connectors.md
  • src/rai_core/rai/communication/ros2/connectors/base.py
📚 Learning: 2025-01-21T11:24:27.687Z
Learnt from: maciejmajek
Repo: RobotecAI/rai PR: 379
File: tests/communication/ros2/test_connectors.py:50-50
Timestamp: 2025-01-21T11:24:27.687Z
Learning: In ROS2 connector implementation, topics are typically static and well-discovered before running any method of the API. The discovery mechanism in tests is an edge case, as it's not representative of the typical usage pattern.

Applied to files:

  • docs/API_documentation/connectors/ROS_2_Connectors.md
  • src/rai_core/rai/communication/ros2/connectors/base.py
  • tests/communication/ros2/test_api.py
  • tests/communication/ros2/test_connectors.py
📚 Learning: 2025-01-08T13:36:27.469Z
Learnt from: boczekbartek
Repo: RobotecAI/rai PR: 335
File: src/rai/rai/tools/ros/native_actions.py:0-0
Timestamp: 2025-01-08T13:36:27.469Z
Learning: In the RAI codebase, ROS2 messages are converted to string representation to make them more LLM-friendly, as seen in the `GetMsgFromTopic._run` method. This design decision helps in better processing and understanding of the messages by the language model.

Applied to files:

  • docs/API_documentation/connectors/ROS_2_Connectors.md
  • src/rai_core/rai/communication/ros2/api/topic.py
  • src/rai_core/rai/communication/ros2/api/base.py
📚 Learning: 2025-01-16T16:40:58.528Z
Learnt from: maciejmajek
Repo: RobotecAI/rai PR: 367
File: src/rai/rai/communication/ros2/api.py:370-373
Timestamp: 2025-01-16T16:40:58.528Z
Learning: The ROS2 high-level API in `src/rai/rai/communication/ros2/api.py` is intentionally designed to be synchronous for simpler usage, without requiring callback handling.

Applied to files:

  • src/rai_core/rai/communication/ros2/connectors/base.py
  • src/rai_core/rai/communication/ros2/api/topic.py
  • tests/communication/ros2/test_api.py
📚 Learning: 2025-01-08T13:42:45.228Z
Learnt from: boczekbartek
Repo: RobotecAI/rai PR: 335
File: src/rai/rai/node.py:0-0
Timestamp: 2025-01-08T13:42:45.228Z
Learning: In the RAI codebase's ROS2 tools that interact with LLMs, all messages (including error messages) from `get_raw_message_from_topic` are intentionally returned as strings. This design pattern ensures consistent string-based communication with the LLM, where successful responses are converted using `str()` and error messages are returned directly as strings. This is demonstrated in the tool implementation where `return str(msg), {}` is used for regular messages and error messages are passed through unchanged.

Applied to files:

  • src/rai_core/rai/communication/ros2/api/topic.py
  • src/rai_core/rai/communication/ros2/api/base.py
📚 Learning: 2025-02-10T21:35:25.819Z
Learnt from: maciejmajek
Repo: RobotecAI/rai PR: 416
File: src/rai_core/rai/communication/ros2/connectors.py:241-267
Timestamp: 2025-02-10T21:35:25.819Z
Learning: When dealing with ROS2 message conversions, prefer using `rosidl_runtime_py.convert.message_to_ordereddict` directly instead of wrapping it in a utility function to avoid unnecessary abstractions and potential circular imports.

Applied to files:

  • src/rai_core/rai/communication/ros2/api/topic.py
📚 Learning: 2025-04-07T16:16:36.242Z
Learnt from: maciejmajek
Repo: RobotecAI/rai PR: 508
File: src/rai_core/rai/tools/ros2/nav2.py:34-38
Timestamp: 2025-04-07T16:16:36.242Z
Learning: The tools in src/rai_core/rai/tools/ros2/nav2.py are intentionally simplified versions of the more generic ROS2 tools, with a simplified implementation approach that uses global variables.

Applied to files:

  • tests/communication/ros2/test_api.py
📚 Learning: 2025-01-24T13:42:58.814Z
Learnt from: maciejmajek
Repo: RobotecAI/rai PR: 383
File: tests/tools/ros2/test_topic_tools.py:84-105
Timestamp: 2025-01-24T13:42:58.814Z
Learning: The ROS2 node discovery in the test suite is asynchronous, and the current implementation doesn't wait for topic discovery. This is a known limitation that causes tests like `test_receive_message_tool` and `test_receive_image_tool` to be marked as `xfail`. A proper discovery mechanism with timeout will be implemented in the future to ensure reliable topic communication.

Applied to files:

  • tests/communication/ros2/test_api.py
  • tests/communication/ros2/test_connectors.py
📚 Learning: 2025-01-30T16:48:57.540Z
Learnt from: rachwalk
Repo: RobotecAI/rai PR: 393
File: tests/communication/sounds_device/test_connector.py:68-77
Timestamp: 2025-01-30T16:48:57.540Z
Learning: When testing audio processing in Python, using mock audio data (e.g., b"mock_audio_data") is preferred over loading real audio files to ensure test reliability, speed, and isolation, unless there's a specific need to test file format compatibility or audio properties.

Applied to files:

  • tests/communication/ros2/test_connectors.py
🧬 Code graph analysis (2)
src/rai_core/rai/communication/ros2/connectors/base.py (3)
src/rai_core/rai/communication/ros2/api/base.py (1)
  • IROS2Message (50-53)
src/rai_core/rai/communication/ros2/messages.py (1)
  • ROS2Message (41-42)
src/rai_core/rai/communication/ros2/api/topic.py (1)
  • publish (141-175)
src/rai_core/rai/communication/ros2/api/topic.py (1)
src/rai_core/rai/communication/ros2/api/base.py (3)
  • IROS2Message (50-53)
  • is_ros2_message (144-145)
  • build_ros2_msg (108-118)
🪛 Ruff (0.14.10)
src/rai_core/rai/communication/ros2/api/topic.py

171-171: Avoid specifying long messages outside the exception class

(TRY003)


173-173: Avoid specifying long messages outside the exception class

(TRY003)

tests/communication/ros2/test_api.py

117-117: Unused function argument: ros_setup

(ARG001)


148-148: Unused function argument: ros_setup

(ARG001)


224-224: Unused function argument: ros_setup

(ARG001)


250-250: Unused function argument: ros_setup

(ARG001)


282-282: Unused function argument: ros_setup

(ARG001)

tests/communication/ros2/test_connectors.py

91-91: Unused function argument: ros_setup

(ARG001)

🔇 Additional comments (13)
src/rai_core/pyproject.toml (1)

7-7: LGTM!

The minor version bump from 2.6.0 to 2.7.0 appropriately reflects the addition of new backward-compatible functionality (allowing ROS2 message types directly in send_message).

docs/API_documentation/connectors/ROS_2_Connectors.md (1)

36-52: LGTM!

The documentation clearly demonstrates both usage patterns:

  1. Raw ROS2 message with inferred type (lines 41-45)
  2. Dictionary payload with explicit msg_type (lines 47-52)

This aligns well with the implementation changes and provides good guidance for users.

src/rai_core/rai/communication/ros2/api/base.py (1)

143-153: LGTM!

Clean static helper methods that wrap rosidl_runtime_py utilities, providing a consistent API surface within the RAI framework. The delegation pattern keeps the implementation simple while centralizing ROS2 type detection logic.

src/rai_core/rai/communication/ros2/api/topic.py (1)

166-175: LGTM!

The type-aware branching logic correctly handles all cases:

  • Direct ROS2 message passthrough
  • Dictionary with explicit msg_type for building
  • Appropriate errors for missing type or invalid content

The implementation aligns well with the IROS2Message protocol and build_ros2_msg helper.

src/rai_core/rai/communication/ros2/connectors/base.py (1)

279-289: LGTM!

The logic correctly distinguishes between:

  • ROS2Message (wrapper class with payload dict) → extracts the payload
  • Raw ROS2 messages (matching IROS2Message protocol) → passed directly

The delegation to _topic_api.publish then handles the type-aware branching consistently.

tests/communication/ros2/test_connectors.py (2)

22-44: LGTM!

The new imports align well with the parametrized test cases, bringing in the necessary ROS2 message types (Time, Point, Pose, PoseWithCovariance, etc.) to exercise the expanded message-sending scenarios.


97-110: LGTM!

The test body correctly validates the new message-sending behavior:

  • Dynamic type handling via actual_type parameter
  • Optional msg_type for both wrapped and direct messages
  • Proper resource cleanup in the finally block
tests/communication/ros2/test_api.py (6)

25-47: LGTM!

The new imports appropriately support the expanded test coverage for type classification (BaseROS2API) and various message types (Point, Pose, PoseStamped, Header, String, etc.).


64-121: Excellent test coverage for type classification utilities.

The parametrization thoroughly exercises is_ros2_message, is_ros2_service, and is_ros2_action with a comprehensive set of inputs including edge cases (empty values, nested messages) and correct classification of service request/response instances as messages vs service classes as services.


124-171: LGTM!

Well-structured parametrized test that validates both dictionary-based publishing (with explicit msg_type) and direct ROS2 message publishing. The Any type hint for message_content correctly reflects the flexible input types.


216-246: LGTM!

Good extension of the QoS error test to cover both dictionary-based and direct ROS2 message inputs, ensuring consistent error handling across message formats.


249-267: LGTM!

Important test case that validates the API correctly requires msg_type when publishing dictionary payloads, which aligns with the PR's design where direct ROS2 messages can infer their type but dictionaries cannot.


270-302: LGTM!

Comprehensive edge case coverage for invalid inputs. The test correctly validates that the API rejects non-dict, non-ROS2-message types (tuples, None, plain strings) with appropriate ValueError exceptions, ensuring robust input validation.

Comment on lines +266 to +271
message : T | IROS2Message
The message to send. Can be a subclass of ROS2Message (payload is a dict) or any ROS2 message.
target : str
The target topic name.
msg_type : str
The ROS2 message type.
msg_type : str | None, optional
The ROS2 message type. If None, the message type will be inferred from the message content. Must be provided if msg_content is a ROS2Message subclass.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor docstring inconsistency.

Line 271 references msg_content but the parameter is named message. Consider updating for consistency.

🔎 Suggested fix
         msg_type : str | None, optional
-            The ROS2 message type. If None, the message type will be inferred from the message content. Must be provided if msg_content is a ROS2Message subclass.
+            The ROS2 message type. If None, the message type will be inferred from the message content. Must be provided if message is a ROS2Message subclass (with dict payload).
📝 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
message : T | IROS2Message
The message to send. Can be a subclass of ROS2Message (payload is a dict) or any ROS2 message.
target : str
The target topic name.
msg_type : str
The ROS2 message type.
msg_type : str | None, optional
The ROS2 message type. If None, the message type will be inferred from the message content. Must be provided if msg_content is a ROS2Message subclass.
message : T | IROS2Message
The message to send. Can be a subclass of ROS2Message (payload is a dict) or any ROS2 message.
target : str
The target topic name.
msg_type : str | None, optional
The ROS2 message type. If None, the message type will be inferred from the message content. Must be provided if message is a ROS2Message subclass (with dict payload).
🤖 Prompt for AI Agents
In src/rai_core/rai/communication/ros2/connectors/base.py around lines 266 to
271, the docstring uses `msg_content` but the parameter is named `message`;
update the docstring to reference `message` instead of `msg_content` and
rephrase the final sentence to read something like: "Must be provided if message
is a ROS2Message subclass." Ensure the parameter description consistently uses
`message` and matches the function signature.

@Juliaj
Copy link
Collaborator

Juliaj commented Dec 29, 2025

Thanks for adding the context. While I really like that your PR to make RAI friendly to human developers, I think it ought not to make it harder for LLM agents.

  • From my perspective, what makes APIs friendly for humans: static types give us autocomplete and catch errors early, and using ROS2 message classes directly (instead of dicts) reduces boilerplate and makes code more readable. This PR drives toward that direction. That said, there's a learning curve for existing ROS2 developers who are used to working with standard ROS2 message classes, so we should keep that transition cost in mind.

  • On the flip side, I think what makes things easier for LLMs is a bit different. From your PR description I infer that schemas/ nested dicts are often simpler for LLM than constructing object hierarchies with imports. Correct me if I am wrong.

If these are true, there's a bit of a tension here. And what should we do ?

Looking at the RAI connector layer, I think we could implement a hybrid API (or dual support) to serve both audiences: developers + LLM agents. If you like the idea, find a reference implementation here, including the support to service/action. Basically,

  • The dual support API uses Python introspection to infer service/action types from class instances. Methods in BaseROS2API (extract_service_class_from_request() and extract_action_class_from_goal()) use __qualname__ and __module__ to extract base classes from nested Request/Response/Goal instances, making msg_type optional for typed (human-friendly) usage while requiring it for dict-based LLM support.

Things you may not like, look for the TODO in code above. Some of the arguments is overloaded and no type safety check.

Now, does this hybrid API design violate any principles? Hmmm ...

  • On SRP: BaseROS2API does carry multiple responsibilities - message/service/action building from dicts and through class extraction, the new introspection methods we added (extract_service_class_from_request, extract_action_class_from_goal etc), type checking utilities like is_ros2_message etc, message conversion utilities, topic type discovery, and QoS adaptation. It looks like it violates SRP, but these are all related ROS2 utility operations. Splitting them up would create unnecessary fragmentation, so I think it's reasonable to keep them together.

  • On detection logic: At first glance it looks like we're duplicating detection logic in both mixins and API classes, but they're actually complementary. Mixins use isinstance(message, ROS2Message) to detect wrapper objects with dict payloads - this handles connector-level type conversion between ROS2Message (wrapper) and IROS2Message (direct ROS2 classes). API classes use _is_nested_instance() with __qualname__ introspection to detect nested Request/Response/Goal class instances - this handles ROS2-specific introspection to extract base service/action classes. They serve different purposes at different layers, so we're okay, I think.

  • On coupling: The dependency flow is unidirectional and existed before dual support: Tools → Connectors (Mixins) → API Classes → BaseROS2API. Mixins depend on API classes for introspection methods like extract_service_class_from_request and extract_action_class_from_goal. API classes inherit from BaseROS2API for shared utilities. Tools depend on connectors for the high-level interface. No circular dependencies or inappropriate coupling here.

There are some open source projects like Pydantic which accepts both dicts and model instances in many methods, and LangChain which uses mixins for tool and agent composition. What makes RAI unique is that it combines a few patterns together that aren't often seen in other projects. We're using Protocol for type hints while using introspection to extract parent classes from nested types. We support both dicts and classes at API, connector, and tool layers. And we're working with generated classes that can't be modified, requiring structural typing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants