Skip to content

fix(toolset): fix toolset config is not initialized when check prerequisites is not called - #535

Merged
mainred merged 1 commit into
masterfrom
fix-bash-toolset
Jul 1, 2025
Merged

mainred merged 1 commit into
masterfrom
fix-bash-toolset

Conversation

@mainred

@mainred mainred commented Jun 18, 2025

Copy link
Copy Markdown
Collaborator

We rely on check prerequisites to initialize the toolset configuration, but when the toolset is loaded from the cache, the prerequisites is not checked, and toolset configuration is not initialized.
Fix this by adding init_config method to initialize the toolset configuration when check prerequisites is not called.
fix: #532

@coderabbitai

coderabbitai Bot commented Jun 18, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

This change updates holmes/core/toolset_manager.py by clarifying the docstring of load_toolset_with_status to explain the toolset loading order and conflict checks. It adds prerequisite checks for enabled and CLI toolsets and introduces a redundant assignment in add_or_merge_onto_toolsets without affecting functionality.

Changes

File(s) Change Summary
holmes/core/toolset_manager.py Expanded docstring for load_toolset_with_status, added prerequisite checks for enabled and CLI toolsets, added conflict error raising, and inserted a redundant assignment in add_or_merge_onto_toolsets.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ToolsetManager
    participant DAL
    participant Toolset

    User->>ToolsetManager: load_toolset_with_status(...)
    ToolsetManager->>ToolsetManager: Load built-in toolsets
    ToolsetManager->>ToolsetManager: Load custom toolsets from config (override built-ins)
    ToolsetManager->>ToolsetManager: Load custom toolsets from CLI
    ToolsetManager->>ToolsetManager: Check for conflicts (raise error if found)
    ToolsetManager->>DAL: (optional) Load cached toolset statuses
    ToolsetManager->>Toolset: For each enabled toolset from cache, call check_prerequisites()
    ToolsetManager->>Toolset: For each CLI toolset, call check_prerequisites()
    ToolsetManager-->>User: Return list of toolsets with status
Loading

Assessment against linked issues

Objective Addressed Explanation
Ensure bash toolset and related tools execute successfully without attribute errors (#532) ✅

Assessment against linked issues: Out-of-scope changes

No out-of-scope changes found.

Possibly related PRs

Suggested reviewers

  • nherment

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7ff6076cb010c1d018372e54d40369b26865e7d and 07c5a5c.

📒 Files selected for processing (1)
  • holmes/core/toolset_manager.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • holmes/core/toolset_manager.py
⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.12)
  • GitHub Check: build (3.10)
  • GitHub Check: build (3.12)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.10)
  • GitHub Check: llm_evals
✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 8

🔭 Outside diff range comments (9)
holmes/plugins/toolsets/mcp/toolset_mcp.py (1)

104-112: init_server_tools signature is now incompatible with the new CallablePrerequisite invocation

Toolset.check_prerequisites was changed to execute callables with no arguments, yet init_server_tools still expects config: dict[str, Any]. This will raise
TypeError: init_server_tools() missing 1 required positional argument: 'config'
as soon as the prerequisite is evaluated.

-    # used as a CallablePrerequisite, config added for that case.
-    def init_server_tools(self, config: dict[str, Any]) -> Tuple[bool, str]:
+    # used as a CallablePrerequisite
+    def init_server_tools(self) -> Tuple[bool, str]:

Also remove the now-unused config doc-comment above the method.

holmes/core/tools.py (1)

288-290: Type hint is out of sync with runtime behaviour

CallablePrerequisite.callable is still typed as Callable[[dict[str, Any]], …], but callables are now invoked with zero arguments (see line 415). Static analysers / IDEs will flag every compliant callable as type-incompatible.

-class CallablePrerequisite(BaseModel):
-    callable: Callable[[dict[str, Any]], Tuple[bool, str]]
+class CallablePrerequisite(BaseModel):
+    callable: Callable[[], Tuple[bool, str]]
holmes/plugins/toolsets/opensearch/opensearch.py (1)

210-229: Config casting & client list accumulation can break subsequent calls

  • Casting: OpenSearchConfig(**self.config) will crash on the second run for
    the same reasons mentioned in other toolsets.
  • Clients: self.clients is never cleared; every run appends duplicates.
-        try:
-            os_config = OpenSearchConfig(**self.config)
+        try:
+            if isinstance(self.config, dict):
+                self.config = OpenSearchConfig(**self.config)
+            os_config: OpenSearchConfig = self.config
+
+            # idempotent – start fresh
+            self.clients.clear()

Apply the same isinstance guard in init_config() (247-253).

holmes/plugins/toolsets/grafana/base_grafana_toolset.py (1)

34-45: Guard against re-casting config and drop redundant debug

Same pattern: after the first init_config() call, self.config is a
GrafanaConfig instance; re-casting will fail.

-            self.init_config()
+            self.init_config()

and in init_config() (55-57):

-        self._grafana_config = self.config_class(**self.config)
+        if isinstance(self.config, dict):
+            self._grafana_config = self.config_class(**self.config)
+        else:
+            self._grafana_config = self.config

Also, the debug message on line 36 may reveal internal toolset names; consider
raising at INFO level only when troubleshooting.

holmes/plugins/toolsets/prometheus/prometheus.py (1)

802-814: init_config fails when self.config is already a PrometheusConfig instance

PrometheusConfig(**self.config) expects a mapping, but self.config
might already be a parsed model (e.g., after cache reload).
This crashes with TypeError: PrometheusConfig object is not a mapping.

-        if self.config:
-            self.config = PrometheusConfig(**self.config)
+        if self.config:
+            if isinstance(self.config, PrometheusConfig):
+                # Already parsed – nothing to do
+                pass
+            else:
+                self.config = PrometheusConfig(**self.config)
holmes/plugins/toolsets/newrelic.py (2)

205-218: prerequisites_callable() can explode on the second call

toolset_manager.init_config() is invoked once during load, and prerequisites_callable() calls self.init_config() again.
If self.config was already converted to a NewrelicConfig instance by the first call, the second call raises TypeError: NewrelicConfig object is not a mapping (see pylint E1134).

Either make init_config() idempotent or guard against the model instance here.

-        if not self.config:
-            return False, "No configuration provided"
-
-        try:
-            self.init_config()
+        try:
+            # Initialise only once – safe even if already initialised
+            self.init_config()
+            if not self.config:
+                return False, "No configuration provided"

223-227: init_config() not idempotent & trips on **self.config

When self.config is already a NewrelicConfig object, NewrelicConfig(**self.config) fails. Make the method accept both dict and NewrelicConfig, persist the parsed model, and become a no-op on subsequent calls.

-    def init_config(self):
-        nr_config = NewrelicConfig(**self.config)
-        self.nr_account_id = nr_config.nr_account_id
-        self.nr_api_key = nr_config.nr_api_key
+    def init_config(self):
+        """
+        Idempotently populate nr_account_id / nr_api_key.
+        Accepts either a raw dict or an already-parsed NewrelicConfig.
+        """
+        if not self.config:
+            return
+        if isinstance(self.config, NewrelicConfig):
+            nr_config = self.config
+        else:
+            nr_config = NewrelicConfig(**self.config)  # type: ignore[arg-type]
+            self.config = nr_config
+
+        self.nr_account_id = nr_config.nr_account_id
+        self.nr_api_key = nr_config.nr_api_key
holmes/plugins/toolsets/kafka.py (1)

502-537: Duplication & E1134 in prerequisites_callable()

The entire client-construction block is duplicated in init_config().
Running both methods back-to-back creates clients twice and still fails if self.config is an already-parsed KafkaConfig (same **model issue).

Refactor to delegate all heavy lifting to init_config() and make the latter resilient to repeated invocations.

-        errors = []
-        try:
-            kafka_config = KafkaConfig(**self.config)
-            for cluster in kafka_config.kafka_clusters:
-                ...
-            return len(self.clients) > 0, "\n".join(errors)
+        try:
+            self.init_config()          # idempotent
+            return (len(self.clients) > 0, "")
holmes/plugins/toolsets/opensearch/opensearch_utils.py (1)

32-48: prerequisites_callable() repeats config parsing & still suffers from **model

The logic here mirrors init_config() and will fail when self.config is already an OpenSearchIndexConfig. Delegate to init_config() and rely on the casted model for the health check.

-        env_url = os.environ.get("OPENSEARCH_LOGS_URL", None)
-        ...
-        else:
-            self.config = OpenSearchIndexConfig(**self.config)
-            return opensearch_health_check(self.config)
+        self.init_config()
+        if not self.config:
+            return False, "Missing opensearch traces URL. Check your config"
+        return opensearch_health_check(self.opensearch_config)
🧹 Nitpick comments (6)
holmes/plugins/toolsets/datetime.py (1)

50-51: Consider adding at least a brief doc-string or comment to init_config

init_config is currently a no-op (pass). Even one-liner guidance (e.g. “# nothing to initialise for now”) helps future maintainers understand that the emptiness is intentional rather than forgotten logic.

holmes/plugins/toolsets/robusta/robusta.py (1)

237-238: Optional doc-string for init_config

Same remark as for other toolsets: a tiny comment stating “nothing to initialise” clarifies intent.

holmes/plugins/toolsets/internet/internet.py (1)

258-261: Guard against double-initialisation

If init_config() is invoked twice (e.g. once by ToolsetManager and again from
prerequisites_callable), the second call re-reads self.config but does not
check whether self.config is already a concrete object instead of a mapping.
While harmless here (dict .get() exists), mirroring the pattern used in other
toolsets keeps behaviour consistent:

-        if self.config:
-            self.additional_headers = self.config.get("additional_headers", {})
+        if isinstance(self.config, dict):
+            self.additional_headers = self.config.get("additional_headers", {})
holmes/plugins/toolsets/bash/bash_toolset.py (1)

202-205: prerequisites_callable should rely solely on init_config()

Because ToolsetManager already calls init_config() on load, calling it again
here is redundant and may mask errors that occur only on the first call.
Consider removing the extra invocation or ensuring idempotency.

holmes/core/toolset_manager.py (1)

401-406: Duplicate dictionary assignment introduces dead code

Both lines assign the same Toolset instance to existing_toolsets_by_name[new_toolset.name].
The second assignment is redundant, adds noise to the diff, and may confuse future readers.

-            else:
-                existing_toolsets_by_name[new_toolset.name] = new_toolset
-                existing_toolsets_by_name[new_toolset.name] = new_toolset
+            else:
+                existing_toolsets_by_name[new_toolset.name] = new_toolset
holmes/plugins/toolsets/opensearch/opensearch_utils.py (1)

54-67: Make init_config() tolerant of repeated calls

OpenSearchIndexConfig(**self.config) explodes on the second call. Handle both dict and model types and avoid overwriting an existing parsed model.

-        else:
-            self.config = OpenSearchIndexConfig(**self.config)
+        elif self.config and not isinstance(self.config, OpenSearchIndexConfig):
+            # First time: parse dict ➜ model, store for future idempotent calls
+            self.config = OpenSearchIndexConfig(**self.config)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fea7861 and 966a1d4.

📒 Files selected for processing (18)
  • holmes/core/tools.py (4 hunks)
  • holmes/core/toolset_manager.py (5 hunks)
  • holmes/plugins/toolsets/bash/bash_toolset.py (2 hunks)
  • holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py (1 hunks)
  • holmes/plugins/toolsets/datadog.py (2 hunks)
  • holmes/plugins/toolsets/datetime.py (2 hunks)
  • holmes/plugins/toolsets/git.py (3 hunks)
  • holmes/plugins/toolsets/grafana/base_grafana_toolset.py (3 hunks)
  • holmes/plugins/toolsets/internet/internet.py (2 hunks)
  • holmes/plugins/toolsets/kafka.py (2 hunks)
  • holmes/plugins/toolsets/mcp/toolset_mcp.py (2 hunks)
  • holmes/plugins/toolsets/newrelic.py (3 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch.py (2 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_logs.py (1 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_utils.py (3 hunks)
  • holmes/plugins/toolsets/prometheus/prometheus.py (2 hunks)
  • holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py (3 hunks)
  • holmes/plugins/toolsets/robusta/robusta.py (2 hunks)
🧰 Additional context used
🪛 Pylint (3.3.7)
holmes/plugins/toolsets/prometheus/prometheus.py

[error] 803-803: Access to member 'config' before its definition line 804

(E0203)

holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py

[refactor] 202-202: Either all return statements in a function should return an expression, or none of them should.

(R1710)


[error] 203-203: Access to member 'config' before its definition line 226

(E0203)

holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py

[refactor] 149-152: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it

(R1705)

holmes/plugins/toolsets/newrelic.py

[error] 224-224: Non-mapping value self.config is used in a mapping context

(E1134)

holmes/plugins/toolsets/opensearch/opensearch.py

[error] 215-215: Non-mapping value self.config is used in a mapping context

(E1134)


[error] 248-248: Non-mapping value self.config is used in a mapping context

(E1134)

holmes/plugins/toolsets/datadog.py

[error] 154-154: Non-mapping value self.config is used in a mapping context

(E1134)

holmes/plugins/toolsets/kafka.py

[error] 507-507: Non-mapping value self.config is used in a mapping context

(E1134)


[error] 542-542: Non-mapping value self.config is used in a mapping context

(E1134)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build (3.12)
🔇 Additional comments (4)
holmes/plugins/toolsets/opensearch/opensearch_logs.py (1)

1-23: Import-only changes look fine

Re-ordering and grouping imports does not alter behaviour and keeps the file PEP-8 compliant.

holmes/core/tools.py (1)

413-417: Missing fallback for legacy callables

(enabled, error_message) = prereq.callable() presumes the new signature. If any external/third-party toolset still passes a one-arg function, a TypeError will bubble up and mark the whole toolset as FAILED.

Add a small compatibility shim:

try:
-    (enabled, error_message) = prereq.callable()
+    try:
+        (enabled, error_message) = prereq.callable()
+    except TypeError:
+        # backward-compat: legacy callables expected config
+        (enabled, error_message) = prereq.callable(self.config)  # type: ignore[arg-type]

This preserves backwards compatibility while encouraging migration.

holmes/plugins/toolsets/grafana/base_grafana_toolset.py (1)

55-57: Idempotent init_config

See diff above – making the method safe for multiple invocations prevents
unexpected TypeErrors during runtime.

holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py (1)

202-226: Mixed return paths – some return a value, some don’t

init_config now has code paths that return early and others that fall
through, leading to inconsistent return types (None vs Tuple[bool,str])
and pylint warning R1710.
After adopting the above fix, remove all explicit return statements and
let the function return None.

Comment thread holmes/plugins/toolsets/internet/internet.py Outdated
Comment thread holmes/plugins/toolsets/bash/bash_toolset.py Outdated
Comment thread holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py Outdated
Comment thread holmes/plugins/toolsets/opensearch/opensearch.py Outdated
Comment thread holmes/plugins/toolsets/git.py Outdated
Comment thread holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py Outdated
Comment thread holmes/plugins/toolsets/datadog.py Outdated
Comment thread holmes/plugins/toolsets/kafka.py Outdated
@mainred
mainred force-pushed the fix-bash-toolset branch from 0a6de93 to 98e9260 Compare June 18, 2025 05:24
@nherment

Copy link
Copy Markdown
Contributor

Thank you for this @mainred.
There is already a mechanism to initialize the toolsets which consists in calling check_prerequisites(). Why not reuse that?

I agree the current approach is not ideal and we want to steer away from it but I also think that adding another function to initialize the config will make toolset initialization more confusing than it already is.

@mainred

mainred commented Jun 18, 2025

Copy link
Copy Markdown
Collaborator Author

check_prerequisites takes time, while our toolset cache is to reduce the loading time taken by the check_prerequisites. So I instead create a init_config method, and assume that the check has been done during the status refresh stage

@nherment

Copy link
Copy Markdown
Contributor

check_prerequisites takes time, while our toolset cache is to reduce the loading time taken by the check_prerequisites. So I instead create a init_config method, and assume that the check has been done during the status refresh stage

ok makes sense.

In that case the current change can be improve by making sure the init_config method is idempotent. For example in the case of prometheus, calling init_config twice would raise an Exception because the line self.config = PrometheusConfig(**self.config) will fail if self.config is already a PrometheusConfig.

What about changing init_config to a setter by passing the config dict as an argument: def init_config(config:dict): .... This separates the config from the time consuming prereq checks but maintain the current (flawed) logic without introducing new potential bugs.

@nherment

Copy link
Copy Markdown
Contributor

Also note that the pre-commit checks fail

Comment thread holmes/plugins/toolsets/git.py Outdated
Comment thread holmes/plugins/toolsets/git.py Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🔭 Outside diff range comments (2)
tests/utils/toolsets.py (1)

4-14: Signatures still use the old config parameter – will break once CallablePrerequisite expects a zero-arg callable

According to the refactor, prerequisite callables are now expected to have the type Callable[[], Tuple[bool, str]].
callable_success, callable_failure_with_message, and callable_failure_no_message still accept a config argument, so the moment the new typing (or the framework) enforces the arity, these functions will raise a TypeError.

Proposed quick fix:

-from typing import Any, Dict, Tuple
+from typing import Tuple
@@
-def callable_success(config: Dict[str, Any]) -> Tuple[bool, str]:
-    return True, ""
+def callable_success() -> Tuple[bool, str]:
+    return True, ""
@@
-def callable_failure_with_message(config: Dict[str, Any]) -> Tuple[bool, str]:
-    return False, "Callable check failed"
+def callable_failure_with_message() -> Tuple[bool, str]:
+    return False, "Callable check failed"
@@
-def callable_failure_no_message(config: Dict[str, Any]) -> Tuple[bool, str]:
-    return False, ""
+def callable_failure_no_message() -> Tuple[bool, str]:
+    return False, ""

(You can remove the unused Any/Dict imports after the change.)

tests/test_mcp_toolset.py (1)

55-58: Typo in test URL – use colon, not slash

"http://0.0.0.0/3005" is not a valid host-and-port URL; the port separator must be :.
Although the URL is not dereferenced in the happy-path thanks to the monkey-patch, leaving an invalid URI is misleading and risks future copy-paste errors.

-        url="http://0.0.0.0/3005",
+        url="http://0.0.0.0:3005",
🧹 Nitpick comments (2)
tests/plugins/prompt/test_toolsets_instructions.py (1)

41-42: Add a small docstring to clarify the no-op init_config

A one-liner docstring will make it explicit that the method is intentionally empty for this mock implementation and prevents “missing-docstring” linter noise.

     def init_config(self):
-        pass
+        """MockToolset never needs extra configuration."""
+        pass
tests/test_mcp_toolset.py (1)

1-4: Minor import-ordering nit

PEP 8 groups standard-library, third-party and local imports separately.
mcp.types and holmes.* are both third-party/local, so the blank line after the first import is superfluous.

Non-blocking, but running ruff/flake8 –-select I will autofix this.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 71ea9a4 and 12e0d76405d01e708d0396e538aaf24050eae4d7.

📒 Files selected for processing (6)
  • tests/llm/utils/mock_toolset.py (1 hunks)
  • tests/plugins/prompt/test_toolsets_instructions.py (2 hunks)
  • tests/test_check_prerequisites.py (1 hunks)
  • tests/test_holmes_sync_toolsets.py (1 hunks)
  • tests/test_mcp_toolset.py (3 hunks)
  • tests/utils/toolsets.py (2 hunks)
✅ Files skipped from review due to trivial changes (3)
  • tests/llm/utils/mock_toolset.py
  • tests/test_check_prerequisites.py
  • tests/test_holmes_sync_toolsets.py
⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.12)
  • GitHub Check: build (3.10)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.12)
  • GitHub Check: build (3.10)
  • GitHub Check: build (3.12)
🔇 Additional comments (1)
tests/plugins/prompt/test_toolsets_instructions.py (1)

10-10: Good import-grouping improvement

load_and_render_prompt is now grouped after the stdlib/third-party/first-party block, matching the typical isort layout. Nice tidy-up.

Comment thread tests/utils/toolsets.py Outdated
Comment thread tests/test_mcp_toolset.py Outdated
@mainred
mainred force-pushed the fix-bash-toolset branch 2 times, most recently from 0aa4810 to ace3b7a Compare June 18, 2025 13:49
@mainred

mainred commented Jun 20, 2025

Copy link
Copy Markdown
Collaborator Author

@nherment could you please take another look?

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

♻️ Duplicate comments (5)
holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py (2)

146-153: Remove unnecessary else block and ensure idempotent configuration initialization.

The else block after the return statement is unnecessary and the current implementation will fail on repeated calls.

Apply this fix to address both issues:

     def prerequisites_callable(self) -> Tuple[bool, str]:
         if not self.config:
             return False, TOOLSET_CONFIG_MISSING_ERROR
         self.init_config()
-        if self.config.api_key:
-            return health_check(domain=self.config.domain, api_key=self.config.api_key)
-        else:
-            return False, "Missing configuration field 'api_key'"
+        if not self.config.api_key:
+            return False, "Missing configuration field 'api_key'"
+        return health_check(domain=self.config.domain, api_key=self.config.api_key)

155-160: Make init_config idempotent to prevent double-casting errors.

The current implementation will fail if called multiple times because CoralogixConfig(**self.config) expects a dictionary but self.config becomes a CoralogixConfig instance after the first call.

     def init_config(self):
         if not self.config:
             logging.error("The coralogix/logs toolset is not configured")
             return
-        self.config = CoralogixConfig(**self.config)
+        if isinstance(self.config, dict):
+            self.config = CoralogixConfig(**self.config)
holmes/plugins/toolsets/opensearch/opensearch.py (1)

210-232: Centralize client creation to avoid duplication and ensure type safety.

Both prerequisites_callable and init_config create OpenSearchClient instances, causing redundant work and multiple connections. The type error occurs because self.config may not be a mapping.

Centralize client creation in init_config and modify prerequisites_callable to delegate:

     def prerequisites_callable(self) -> Tuple[bool, str]:
         if not self.config:
             return False, TOOLSET_CONFIG_MISSING_ERROR
-
-        try:
-            os_config = OpenSearchConfig(**self.config)
-            errors = []
-            for cluster in os_config.opensearch_clusters:
-                try:
-                    logging.info("Setting up OpenSearch client")
-                    cluster_kwargs = cluster.model_dump()
-                    client = OpenSearchClient(**cluster_kwargs)
-                    if client.client.cluster.health(params={"timeout": 5}):
-                        self.clients.append(client)
-                except Exception as e:
-                    message = f"Failed to set up opensearch client {str(cluster.hosts)}. {str(e)}"
-                    logging.exception(message)
-                    errors.append(message)
-
-            return len(self.clients) > 0, "\n".join(errors)
-        except Exception as e:
-            logging.exception("Failed to set up OpenSearch toolset")
-            return False, str(e)
+        
+        self.init_config()
+        return len(self.clients) > 0, "Failed to connect to any OpenSearch clusters" if len(self.clients) == 0 else ""
holmes/plugins/toolsets/kafka.py (1)

541-560: Make init_config idempotent and prevent connection leaks.

The current implementation recreates Kafka clients on every call, causing connection leaks and potential errors with double-casting.

     def init_config(self):
-        kafka_config = KafkaConfig(**self.config)
+        if not self.config:
+            return
+        
+        if isinstance(self.config, KafkaConfig):
+            kafka_config = self.config
+        elif isinstance(self.config, dict):
+            kafka_config = KafkaConfig(**self.config)
+            self.config = kafka_config
+        else:
+            return
 
         for cluster in kafka_config.kafka_clusters:
+            # Skip if client already exists to prevent connection leaks
+            if cluster.name in self.clients:
+                continue
+                
             admin_config = {
                 "bootstrap.servers": cluster.kafka_broker,
                 "client.id": cluster.kafka_client_id,
             }
 
             if cluster.kafka_security_protocol:
                 admin_config["security.protocol"] = cluster.kafka_security_protocol
             if cluster.kafka_sasl_mechanism:
                 admin_config["sasl.mechanisms"] = cluster.kafka_sasl_mechanism
             if cluster.kafka_username and cluster.kafka_password:
                 admin_config["sasl.username"] = cluster.kafka_username
                 admin_config["sasl.password"] = cluster.kafka_password
 
             client = AdminClient(admin_config)
-            self.clients[cluster.name] = client  # Store in dictionary
+            self.clients[cluster.name] = client
holmes/plugins/toolsets/datadog.py (1)

153-156: Make init_config idempotent and handle different config types safely.

The current implementation will fail if self.config is already a DatadogConfig instance, as DatadogConfig(**self.config) expects a mapping.

     def init_config(self):
-        dd_config = DatadogConfig(**self.config)
+        if isinstance(self.config, DatadogConfig):
+            dd_config = self.config
+        elif isinstance(self.config, dict):
+            dd_config = DatadogConfig(**self.config)
+        else:
+            dd_config = DatadogConfig()
+            
         self.dd_api_key = dd_config.dd_api_key
         self.dd_app_key = dd_config.dd_app_key
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d8943621091072ba3dceb2e6d9b03937b7c3d2a and 16db3b7.

📒 Files selected for processing (23)
  • holmes/core/tools.py (5 hunks)
  • holmes/core/toolset_manager.py (5 hunks)
  • holmes/plugins/toolsets/bash/bash_toolset.py (2 hunks)
  • holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py (2 hunks)
  • holmes/plugins/toolsets/datadog.py (2 hunks)
  • holmes/plugins/toolsets/git.py (3 hunks)
  • holmes/plugins/toolsets/grafana/base_grafana_toolset.py (3 hunks)
  • holmes/plugins/toolsets/internet/internet.py (2 hunks)
  • holmes/plugins/toolsets/kafka.py (2 hunks)
  • holmes/plugins/toolsets/mcp/toolset_mcp.py (3 hunks)
  • holmes/plugins/toolsets/newrelic.py (3 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch.py (2 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_logs.py (1 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_utils.py (3 hunks)
  • holmes/plugins/toolsets/prometheus/prometheus.py (2 hunks)
  • holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py (3 hunks)
  • holmes/plugins/toolsets/robusta/robusta.py (2 hunks)
  • tests/llm/utils/mock_toolset.py (1 hunks)
  • tests/plugins/prompt/test_toolsets_instructions.py (2 hunks)
  • tests/test_check_prerequisites.py (2 hunks)
  • tests/test_holmes_sync_toolsets.py (1 hunks)
  • tests/test_mcp_toolset.py (3 hunks)
  • tests/utils/toolsets.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (17)
  • tests/test_holmes_sync_toolsets.py
  • tests/plugins/prompt/test_toolsets_instructions.py
  • tests/test_check_prerequisites.py
  • tests/llm/utils/mock_toolset.py
  • holmes/plugins/toolsets/robusta/robusta.py
  • holmes/plugins/toolsets/opensearch/opensearch_logs.py
  • holmes/plugins/toolsets/bash/bash_toolset.py
  • tests/test_mcp_toolset.py
  • holmes/plugins/toolsets/git.py
  • holmes/plugins/toolsets/internet/internet.py
  • holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py
  • holmes/core/toolset_manager.py
  • holmes/plugins/toolsets/opensearch/opensearch_utils.py
  • holmes/plugins/toolsets/grafana/base_grafana_toolset.py
  • tests/utils/toolsets.py
  • holmes/plugins/toolsets/mcp/toolset_mcp.py
  • holmes/core/tools.py
🧰 Additional context used
🪛 Pylint (3.3.7)
holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py

[refactor] 150-153: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it

(R1705)

holmes/plugins/toolsets/datadog.py

[error] 154-154: Non-mapping value self.config is used in a mapping context

(E1134)

holmes/plugins/toolsets/kafka.py

[error] 507-507: Non-mapping value self.config is used in a mapping context

(E1134)


[error] 542-542: Non-mapping value self.config is used in a mapping context

(E1134)

holmes/plugins/toolsets/newrelic.py

[error] 227-227: Non-mapping value self.config is used in a mapping context

(E1134)

holmes/plugins/toolsets/opensearch/opensearch.py

[error] 215-215: Non-mapping value self.config is used in a mapping context

(E1134)


[error] 248-248: Non-mapping value self.config is used in a mapping context

(E1134)

holmes/plugins/toolsets/prometheus/prometheus.py

[error] 803-803: Access to member 'config' before its definition line 804

(E0203)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build (3.12)
🔇 Additional comments (5)
holmes/plugins/toolsets/opensearch/opensearch.py (1)

247-253: Add error handling and health checks to init_config.

Move the complete client setup logic with health checks and error handling to init_config:

     def init_config(self):
-        os_config = OpenSearchConfig(**self.config)
-        for cluster in os_config.opensearch_clusters:
-            logging.info("Setting up OpenSearch client")
-            cluster_kwargs = cluster.model_dump()
-            client = OpenSearchClient(**cluster_kwargs)
-            self.clients.append(client)
+        if not self.config:
+            return
+        
+        if isinstance(self.config, dict):
+            os_config = OpenSearchConfig(**self.config)
+        else:
+            return
+            
+        for cluster in os_config.opensearch_clusters:
+            try:
+                logging.info("Setting up OpenSearch client")
+                cluster_kwargs = cluster.model_dump()
+                client = OpenSearchClient(**cluster_kwargs)
+                if client.client.cluster.health(params={"timeout": 5}):
+                    self.clients.append(client)
+            except Exception as e:
+                logging.exception(f"Failed to set up opensearch client {str(cluster.hosts)}. {str(e)}")
holmes/plugins/toolsets/prometheus/prometheus.py (1)

802-813: Make init_config idempotent and type-safe.

The current implementation has the same double-casting issue as other toolsets and the pylint error indicates accessing self.config before proper definition.

     def init_config(self):
-        if self.config:
-            self.config = PrometheusConfig(**self.config)
+        if self.config and isinstance(self.config, dict):
+            self.config = PrometheusConfig(**self.config)
+        elif not self.config:
+            prometheus_url = os.environ.get("PROMETHEUS_URL")
+            if not prometheus_url:
+                prometheus_url = self.auto_detect_prometheus_url()
+            self.config = PrometheusConfig(
+                prometheus_url=prometheus_url,
+                headers=add_prometheus_auth(os.environ.get("PROMETHEUS_AUTH_HEADER")),
+            )
+            logging.info(f"Prometheus auto discovered at url {prometheus_url}")

This ensures the method is idempotent and handles the case where self.config is already a PrometheusConfig instance.

holmes/plugins/toolsets/newrelic.py (3)

2-5: Import reordering looks good.

The imports have been properly organized with standard library imports first, followed by third-party imports, then local imports.


9-9: Import additions are correctly placed.

The StructuredToolResult and ToolResultStatus imports have been moved to an appropriate location in the import list.

Also applies to: 12-12


205-218: Method signature change aligns with the refactor pattern.

The removal of the config parameter from prerequisites_callable and the reliance on self.config is consistent with the broader refactor described in the PR objectives. The logic correctly checks for configuration presence and calls the new init_config method.

Comment thread holmes/plugins/toolsets/newrelic.py Outdated

@nherment nherment left a comment

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.

HI @mainred , a few more comments, including some that were made before. I'm happy to jump on a call and go over them and/or the PR with you.

The main issue is the use of self.config = TypedConfig(**self.config) which mutates the type of self.config. In other words:

  • The design of init_config should be resilient to being called multiple times and provide the same outcome
  • toolset.config (or wherever the config is stored) should always be typed and not a dict. Some of the toolsets 'fix' that by saving the typed config in a different field. I understand that some of that is actually legacy code and not something you introduced. However the introduction of the toolset caching and this PR change the code enough that the overall implementation should address this legacy.

Also, in some cases, code is duplicated between init_config and the prerequisites_callable which should be avoided. prerequisites_callable should not have to initialize or validate the config.
Given the pattern is the same across all toolsets, it would be better to always call init_config at the beginning of check_prerequisites.

Comment thread holmes/plugins/toolsets/internet/internet.py Outdated
Comment thread holmes/core/tools.py Outdated
Comment thread holmes/plugins/toolsets/bash/bash_toolset.py Outdated
Comment thread holmes/plugins/toolsets/datadog.py Outdated
Comment thread holmes/plugins/toolsets/kafka.py Outdated
Comment thread holmes/plugins/toolsets/kafka.py Outdated
Comment thread holmes/plugins/toolsets/opensearch/opensearch_utils.py Outdated

@nherment nherment left a comment

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.

making ti clear that I'm requesting changes. Happy to jump on a call and discuss these

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

♻️ Duplicate comments (1)
holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py (1)

159-165: Remove unnecessary else block after return

The else branch is unnecessary since the return statement already exits the function.

-            if self.typed_config.api_key:  # type: ignore
-                return health_check(
-                    domain=self.typed_config.domain,  # type: ignore
-                    api_key=self.typed_config.api_key,  # type: ignore
-                )
-            else:
-                return False, "Missing configuration field 'api_key'"
+            if not self.typed_config.api_key:  # type: ignore
+                return False, "Missing configuration field 'api_key'"
+            return health_check(
+                domain=self.typed_config.domain,  # type: ignore
+                api_key=self.typed_config.api_key,  # type: ignore
+            )
🧹 Nitpick comments (2)
holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py (1)

170-171: Consider making init_config idempotent

While the current implementation works correctly, consider adding a guard to make it idempotent as mentioned in the retrieved learnings.

 def init_config(self, config: dict[str, Any]):
-    self.typed_config = CoralogixConfig(**config)
+    if not isinstance(self.typed_config, CoralogixConfig):
+        self.typed_config = CoralogixConfig(**config)
holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py (1)

37-45: Simplify control flow by removing unnecessary elif

The elif after return can be simplified to if for better readability.

-        elif not cluster_id and len(cluster_ids) > 0:
+        if not cluster_id and len(cluster_ids) > 0:
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 512e03b and b9a7392.

📒 Files selected for processing (26)
  • holmes/core/tools.py (6 hunks)
  • holmes/core/toolset_manager.py (5 hunks)
  • holmes/plugins/toolsets/bash/bash_toolset.py (5 hunks)
  • holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py (5 hunks)
  • holmes/plugins/toolsets/datadog.py (2 hunks)
  • holmes/plugins/toolsets/git.py (3 hunks)
  • holmes/plugins/toolsets/grafana/base_grafana_toolset.py (2 hunks)
  • holmes/plugins/toolsets/internet/internet.py (2 hunks)
  • holmes/plugins/toolsets/kafka.py (1 hunks)
  • holmes/plugins/toolsets/mcp/toolset_mcp.py (2 hunks)
  • holmes/plugins/toolsets/newrelic.py (2 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch.py (2 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_logs.py (7 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_traces.py (4 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_utils.py (3 hunks)
  • holmes/plugins/toolsets/prometheus/prometheus.py (15 hunks)
  • holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py (7 hunks)
  • holmes/plugins/toolsets/robusta/robusta.py (2 hunks)
  • holmes/plugins/toolsets/runbook/runbook_fetcher.py (1 hunks)
  • tests/core/test_toolset_manager.py (1 hunks)
  • tests/llm/utils/mock_toolset.py (1 hunks)
  • tests/plugins/prompt/test_toolsets_instructions.py (2 hunks)
  • tests/test_check_prerequisites.py (2 hunks)
  • tests/test_holmes_sync_toolsets.py (1 hunks)
  • tests/test_mcp_toolset.py (3 hunks)
  • tests/utils/toolsets.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/core/test_toolset_manager.py
🚧 Files skipped from review as they are similar to previous changes (22)
  • tests/llm/utils/mock_toolset.py
  • tests/test_check_prerequisites.py
  • tests/plugins/prompt/test_toolsets_instructions.py
  • holmes/plugins/toolsets/opensearch/opensearch_logs.py
  • tests/test_holmes_sync_toolsets.py
  • holmes/plugins/toolsets/runbook/runbook_fetcher.py
  • holmes/plugins/toolsets/git.py
  • holmes/plugins/toolsets/mcp/toolset_mcp.py
  • tests/test_mcp_toolset.py
  • holmes/plugins/toolsets/internet/internet.py
  • holmes/plugins/toolsets/robusta/robusta.py
  • holmes/plugins/toolsets/grafana/base_grafana_toolset.py
  • holmes/core/toolset_manager.py
  • holmes/plugins/toolsets/datadog.py
  • holmes/core/tools.py
  • holmes/plugins/toolsets/bash/bash_toolset.py
  • holmes/plugins/toolsets/opensearch/opensearch_utils.py
  • holmes/plugins/toolsets/opensearch/opensearch.py
  • holmes/plugins/toolsets/newrelic.py
  • holmes/plugins/toolsets/prometheus/prometheus.py
  • tests/utils/toolsets.py
  • holmes/plugins/toolsets/kafka.py
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: nherment
PR: robusta-dev/holmesgpt#535
File: holmes/plugins/toolsets/bash/bash_toolset.py:207-209
Timestamp: 2025-06-24T05:51:04.531Z
Learning: The init_config method in toolsets should be idempotent - safely callable multiple times without errors. self.config should maintain consistent typing (not alternate between dict and config object types) throughout the object lifecycle.
🪛 Pylint (3.3.7)
holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py

[refactor] 159-165: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it

(R1705)

holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py

[refactor] 37-45: Unnecessary "elif" after "return", remove the leading "el" from "elif"

(R1705)

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: build (3.12)
  • GitHub Check: build (3.10)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.10)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.12)
  • GitHub Check: build (3.12)
🔇 Additional comments (4)
holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py (1)

1-1: Well-structured refactoring for configuration management

The addition of the logging import, typed configuration attribute, and centralized init_config method aligns well with the PR objectives. The error handling in prerequisites_callable is robust and the separation of concerns is clear.

Also applies to: 34-34, 156-171

holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py (2)

213-231: Excellent environment variable fallback implementation

The init_config method elegantly handles the case where no clusters are configured by falling back to environment variables with sensible defaults. The logging message provides good visibility into the configuration source.


123-123: Clean refactoring to typed configuration

The introduction of typed_config and the delegation to init_config in the prerequisites method improves type safety and follows the established pattern from the PR objectives.

Also applies to: 158-162

holmes/plugins/toolsets/opensearch/opensearch_traces.py (1)

1-1: Import reordering appears clean.

The import statements have been reorganized logically, grouping standard library imports first and third-party imports together. The changes look appropriate for code organization.

Also applies to: 3-3, 8-8, 12-15

Comment thread holmes/plugins/toolsets/opensearch/opensearch_traces.py Outdated
Comment thread holmes/plugins/toolsets/opensearch/opensearch_traces.py

@nherment nherment left a comment

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.

Hi @mainred ,

There is a previous comment (albeit minor) that is unaddressed: https://github.com/robusta-dev/holmesgpt/pull/535/files#r2162958028

Some other comments, mostly around not ignoring type checks and around reporting config errors to users.

Overall the approach looks good. Well done!

Comment thread holmes/plugins/toolsets/grafana/base_grafana_toolset.py Outdated
Comment thread holmes/plugins/toolsets/kafka.py Outdated
Comment thread holmes/plugins/toolsets/opensearch/opensearch.py Outdated
Comment thread holmes/plugins/toolsets/opensearch/opensearch_traces.py Outdated
Comment thread holmes/plugins/toolsets/opensearch/opensearch_logs.py Outdated
Comment thread tests/test_mcp_toolset.py Outdated
Comment thread holmes/core/toolset_manager.py Outdated
Comment thread holmes/plugins/toolsets/bash/bash_toolset.py Outdated
Comment thread holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py Outdated
Comment thread holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🔭 Outside diff range comments (1)
holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py (1)

147-167: Duplicate environment variable logic and non-idempotent init_config.

The environment variable fallback logic is duplicated between prerequisites_callable (lines 148-155) and init_config (lines 219-233), which could lead to inconsistent behavior. Additionally, the init_config method is not idempotent as required by the retrieved learnings.

Refactor to eliminate duplication and ensure idempotency:

 def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
-    if not config or not config.get("clusters"):
-        # Attempt to load from environment variables as fallback
-        env_url = os.environ.get("RABBITMQ_MANAGEMENT_URL")
-        if not env_url:
-            return (
-                False,
-                "RabbitMQ toolset is misconfigured. 'management_url' is required.",
-            )
-
     try:
         self.init_config(config)
         if not self.typed_config:
             return (
                 False,
-                "RabbitMQ toolset is misconfigured.",
+                "RabbitMQ toolset is misconfigured. 'management_url' is required.",
             )
     except Exception as e:
         return (False, f"Failed to parse RabbitMQ configuration: {str(e)}")

     return self._check_clusters_config(self.typed_config)
♻️ Duplicate comments (2)
holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py (1)

7-7: Remove the type ignore comment as requested.

Based on past review feedback, this type ignore comment should be removed.

-from requests import RequestException  # type: ignore
+from requests import RequestException
holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py (1)

160-166: Remove unnecessary else block after return.

The else block is unnecessary since the return statement already exits the function.

-            if self.typed_config.api_key:
-                return health_check(
-                    domain=self.typed_config.domain,
-                    api_key=self.typed_config.api_key,
-                )
-            else:
-                return False, "Missing configuration field 'api_key'"
+            if not self.typed_config.api_key:
+                return False, "Missing configuration field 'api_key'"
+            return health_check(
+                domain=self.typed_config.domain,
+                api_key=self.typed_config.api_key,
+            )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b9a7392 and 37b8abc.

📒 Files selected for processing (10)
  • holmes/core/tools.py (6 hunks)
  • holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py (5 hunks)
  • holmes/plugins/toolsets/datadog.py (2 hunks)
  • holmes/plugins/toolsets/git.py (3 hunks)
  • holmes/plugins/toolsets/grafana/base_grafana_toolset.py (2 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_logs.py (7 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_traces.py (4 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_utils.py (3 hunks)
  • holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py (7 hunks)
  • tests/utils/toolsets.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • holmes/plugins/toolsets/grafana/base_grafana_toolset.py
  • holmes/plugins/toolsets/git.py
  • holmes/plugins/toolsets/opensearch/opensearch_traces.py
  • holmes/plugins/toolsets/opensearch/opensearch_logs.py
  • holmes/plugins/toolsets/datadog.py
  • holmes/core/tools.py
  • holmes/plugins/toolsets/opensearch/opensearch_utils.py
  • tests/utils/toolsets.py
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: nherment
PR: robusta-dev/holmesgpt#535
File: holmes/plugins/toolsets/bash/bash_toolset.py:207-209
Timestamp: 2025-06-24T05:51:04.531Z
Learning: The init_config method in toolsets should be idempotent - safely callable multiple times without errors. self.config should maintain consistent typing (not alternate between dict and config object types) throughout the object lifecycle.
🪛 Pylint (3.3.7)
holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py

[refactor] 160-166: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it

(R1705)

holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py

[refactor] 37-45: Unnecessary "elif" after "return", remove the leading "el" from "elif"

(R1705)

Comment thread holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py Outdated
Comment thread holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 37b8abc and deb2393.

📒 Files selected for processing (12)
  • holmes/core/tools.py (6 hunks)
  • holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py (3 hunks)
  • holmes/plugins/toolsets/git.py (3 hunks)
  • holmes/plugins/toolsets/grafana/base_grafana_toolset.py (3 hunks)
  • holmes/plugins/toolsets/grafana/toolset_grafana.py (4 hunks)
  • holmes/plugins/toolsets/grafana/toolset_grafana_loki.py (3 hunks)
  • holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py (1 hunks)
  • holmes/plugins/toolsets/kubernetes_logs.py (3 hunks)
  • holmes/plugins/toolsets/logging_utils/logging_api.py (1 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_logs.py (7 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_traces.py (5 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_utils.py (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • holmes/plugins/toolsets/logging_utils/logging_api.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • holmes/plugins/toolsets/git.py
  • holmes/plugins/toolsets/grafana/base_grafana_toolset.py
  • holmes/core/tools.py
  • holmes/plugins/toolsets/opensearch/opensearch_logs.py
  • holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py
  • holmes/plugins/toolsets/opensearch/opensearch_traces.py
  • holmes/plugins/toolsets/opensearch/opensearch_utils.py
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: nherment
PR: robusta-dev/holmesgpt#535
File: holmes/plugins/toolsets/bash/bash_toolset.py:207-209
Timestamp: 2025-06-24T05:51:04.531Z
Learning: The init_config method in toolsets should be idempotent - safely callable multiple times without errors. self.config should maintain consistent typing (not alternate between dict and config object types) throughout the object lifecycle.
⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: build (3.12)
  • GitHub Check: build (3.10)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.12)
  • GitHub Check: build (3.10)
  • GitHub Check: build (3.11)
  • GitHub Check: llm_evals
🔇 Additional comments (8)
holmes/plugins/toolsets/kubernetes_logs.py (3)

3-4: LGTM: Import formatting improvements.

The import statement reordering and additional blank line improve code formatting and readability without any functional impact.


133-137: LGTM: Improved readability with multiline formatting.

The reformatting of the StructuredToolResult construction to use multiline parentheses enhances code readability while maintaining identical functionality.


259-261: LGTM: Appropriate no-op implementation of init_config.

The no-op implementation is consistent with the architectural pattern across toolsets and makes sense for this Kubernetes logs toolset, which already handles its initialization in the __init__ method through client setup and health checks. The method signature correctly matches the abstract interface requirements and is idempotent as required.

holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py (1)

58-58: Verify that typed_config is properly initialized in the base class.

The change from self._grafana_config to self.typed_config assumes that typed_config is available and properly initialized. Since this class inherits from BaseGrafanaToolset, ensure that the base class implements the init_config method and properly initializes typed_config.

#!/bin/bash
# Description: Verify that BaseGrafanaToolset implements typed_config and init_config
# Expected: Find typed_config attribute and init_config method in BaseGrafanaToolset

echo "Checking BaseGrafanaToolset implementation..."
rg -A 10 "class BaseGrafanaToolset" holmes/plugins/toolsets/grafana/base_grafana_toolset.py

echo "Checking for typed_config attribute in BaseGrafanaToolset..."
rg "typed_config" holmes/plugins/toolsets/grafana/base_grafana_toolset.py

echo "Checking for init_config method in BaseGrafanaToolset..."
rg -A 5 "def init_config" holmes/plugins/toolsets/grafana/base_grafana_toolset.py
holmes/plugins/toolsets/grafana/toolset_grafana.py (3)

48-53: Good error handling for missing configuration.

The check for typed_config existence and returning a structured error result is well-implemented and follows the pattern established in the PR.


54-55: Consistent migration from _grafana_config to typed_config.

All references to the configuration have been consistently updated to use typed_config. The changes maintain the same functionality while following the new standardized configuration pattern.

Also applies to: 63-64, 77-77


94-98: Verify the impact of changing return type from string to StructuredToolResult.

The method now returns StructuredToolResult instead of a plain string. This is a breaking change that may impact existing callers of this method.

#!/bin/bash
# Description: Find all usages of ListAndBuildGrafanaDashboardURLs._invoke to check for compatibility
# Expected: All callers should handle StructuredToolResult return type

echo "Searching for direct calls to _invoke method..."
rg -A 3 "ListAndBuildGrafanaDashboardURLs.*_invoke"

echo "Searching for instantiation and usage of ListAndBuildGrafanaDashboardURLs..."
rg -A 5 "ListAndBuildGrafanaDashboardURLs"

echo "Checking if this tool is used through the Tool framework which might handle StructuredToolResult..."
rg -A 5 "list_and_build_grafana_dashboard_urls"

Also applies to: 101-105

holmes/plugins/toolsets/grafana/toolset_grafana_loki.py (1)

37-37: Proper declaration of typed_config attribute.

The declaration of typed_config as Optional[GrafanaLokiConfig] is correct and follows the established pattern for this refactoring.

Comment thread holmes/plugins/toolsets/grafana/toolset_grafana_loki.py Outdated
Comment thread holmes/plugins/toolsets/grafana/toolset_grafana_loki.py Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 0

♻️ Duplicate comments (4)
holmes/plugins/toolsets/prometheus/prometheus.py (1)

829-841: Make init_config method idempotent and address error handling regression

The init_config method needs to be idempotent to safely handle multiple calls. Additionally, there's a regression in error handling for auto-discovery that should be addressed.

Apply this diff to make the method idempotent and improve error handling:

 def init_config(self, config: Optional[dict[str, Any]]):
+    # Make idempotent - don't reinitialize if already set
+    if self.typed_config is not None:
+        return
+        
     if config:
         self.typed_config = PrometheusConfig(**config)
     else:
         prometheus_url = os.environ.get("PROMETHEUS_URL")
         if not prometheus_url:
             prometheus_url = self.auto_detect_prometheus_url()
+        if not prometheus_url:
+            raise ValueError("Unable to auto-detect prometheus. Define prometheus_url in the configuration for tool prometheus/metrics")
         self.typed_config = PrometheusConfig(
             prometheus_url=prometheus_url,
             headers=add_prometheus_auth(os.environ.get("PROMETHEUS_AUTH_HEADER")),
         )
-        logging.info(f"Prometheus auto discovered at url {prometheus_url}")
+        if prometheus_url:
+            logging.info(f"Prometheus auto discovered at url {prometheus_url}")
holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py (3)

7-7: Remove unnecessary type ignore comment

-from requests import RequestException  # type: ignore
+from requests import RequestException

147-167: Fix error handling logic in prerequisites_callable

The current logic has issues with error handling when environment variables are missing. The method checks for missing environment variables but doesn't handle the case properly.

Apply this diff to fix the error handling:

 def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
-    if not config or not config.get("clusters"):
-        # Attempt to load from environment variables as fallback
-        env_url = os.environ.get("RABBITMQ_MANAGEMENT_URL")
-        if not env_url:
-            return (
-                False,
-                "RabbitMQ toolset is misconfigured. 'management_url' is required.",
-            )
-
     try:
         self.init_config(config)
         if not self.typed_config:
             return (
                 False,
                 "RabbitMQ toolset is misconfigured.",
             )
     except Exception as e:
         return (False, f"Failed to parse RabbitMQ configuration: {str(e)}")

     return self._check_clusters_config(self.typed_config)

218-236: Make init_config idempotent and handle missing environment variables properly

The method should be idempotent and properly handle the case where required environment variables are missing.

Apply this diff to address the issues:

 def init_config(self, config: Optional[dict[str, Any]]):
+    # Make idempotent - don't reinitialize if already set
+    if self.typed_config is not None:
+        return
+        
     if not config or not config.get("clusters"):
         # Attempt to load from environment variables as fallback
         env_url = os.environ.get("RABBITMQ_MANAGEMENT_URL")
+        if not env_url:
+            raise ValueError("RabbitMQ toolset is misconfigured. 'management_url' is required.")
         env_user = os.environ.get("RABBITMQ_USERNAME", "guest")
         env_pass = os.environ.get("RABBITMQ_PASSWORD", "guest")
         config = {
             "clusters": [
                 {
                     "id": "rabbitmq",
                     "management_url": env_url,
                     "username": env_user,
                     "password": env_pass,
                 }
             ]
         }
         logging.info("Loaded RabbitMQ config from environment variables.")

     self.typed_config = RabbitMQConfig(**config)
🧹 Nitpick comments (1)
holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py (1)

34-53: Consider simplifying the conditional logic

The static analysis tool suggests removing unnecessary elif statements after return.

     if not cluster_id and len(cluster_ids) == 1:
         # cluster id is optional if there is only one configured
         return self.toolset.typed_config.clusters[0]
-    elif not cluster_id and len(cluster_ids) > 0:
+    if not cluster_id and len(cluster_ids) > 0:
         raise ValueError(
             f"No cluster is configured. Possible cluster_id values are: {', '.join(cluster_ids)}"
         )
-    elif not cluster_id:
+    if not cluster_id:
         raise ValueError("No cluster is configured")
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between deb2393 and 2780520.

📒 Files selected for processing (23)
  • holmes/core/tools.py (6 hunks)
  • holmes/plugins/toolsets/bash/bash_toolset.py (5 hunks)
  • holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py (4 hunks)
  • holmes/plugins/toolsets/datadog.py (2 hunks)
  • holmes/plugins/toolsets/git.py (3 hunks)
  • holmes/plugins/toolsets/grafana/base_grafana_toolset.py (3 hunks)
  • holmes/plugins/toolsets/grafana/toolset_grafana_loki.py (3 hunks)
  • holmes/plugins/toolsets/internet/internet.py (2 hunks)
  • holmes/plugins/toolsets/kafka.py (1 hunks)
  • holmes/plugins/toolsets/kubernetes_logs.py (3 hunks)
  • holmes/plugins/toolsets/mcp/toolset_mcp.py (2 hunks)
  • holmes/plugins/toolsets/newrelic.py (2 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch.py (2 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_logs.py (7 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch_utils.py (3 hunks)
  • holmes/plugins/toolsets/prometheus/prometheus.py (15 hunks)
  • holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py (7 hunks)
  • holmes/plugins/toolsets/robusta/robusta.py (2 hunks)
  • holmes/plugins/toolsets/runbook/runbook_fetcher.py (2 hunks)
  • tests/llm/utils/mock_toolset.py (2 hunks)
  • tests/plugins/prompt/test_toolsets_instructions.py (2 hunks)
  • tests/test_check_prerequisites.py (3 hunks)
  • tests/test_holmes_sync_toolsets.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (21)
  • tests/llm/utils/mock_toolset.py
  • tests/test_check_prerequisites.py
  • tests/test_holmes_sync_toolsets.py
  • tests/plugins/prompt/test_toolsets_instructions.py
  • holmes/plugins/toolsets/mcp/toolset_mcp.py
  • holmes/plugins/toolsets/runbook/runbook_fetcher.py
  • holmes/plugins/toolsets/robusta/robusta.py
  • holmes/plugins/toolsets/kubernetes_logs.py
  • holmes/plugins/toolsets/git.py
  • holmes/plugins/toolsets/grafana/base_grafana_toolset.py
  • holmes/plugins/toolsets/internet/internet.py
  • holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py
  • holmes/plugins/toolsets/datadog.py
  • holmes/plugins/toolsets/bash/bash_toolset.py
  • holmes/plugins/toolsets/newrelic.py
  • holmes/plugins/toolsets/opensearch/opensearch_utils.py
  • holmes/plugins/toolsets/kafka.py
  • holmes/core/tools.py
  • holmes/plugins/toolsets/opensearch/opensearch_logs.py
  • holmes/plugins/toolsets/opensearch/opensearch.py
  • holmes/plugins/toolsets/grafana/toolset_grafana_loki.py
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: nherment
PR: robusta-dev/holmesgpt#535
File: holmes/plugins/toolsets/bash/bash_toolset.py:207-209
Timestamp: 2025-06-24T05:51:04.531Z
Learning: The init_config method in toolsets should be idempotent - safely callable multiple times without errors. self.config should maintain consistent typing (not alternate between dict and config object types) throughout the object lifecycle.
🪛 Pylint (3.3.7)
holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py

[refactor] 37-45: Unnecessary "elif" after "return", remove the leading "el" from "elif"

(R1705)

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: build (3.10)
  • GitHub Check: build (3.12)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.10)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.12)
  • GitHub Check: llm_evals
🔇 Additional comments (4)
holmes/plugins/toolsets/prometheus/prometheus.py (3)

748-748: LGTM: Typed configuration attribute added

The addition of the typed configuration attribute follows the established pattern for centralizing configuration handling.


292-295: Consistent configuration validation across tools

All tools now properly validate the presence of typed_config and prometheus_url before proceeding. This ensures consistent error handling across the toolset.

Also applies to: 386-389, 494-497, 633-636


776-776: Configuration initialization properly delegated

The prerequisites callable now correctly delegates configuration initialization to the init_config method, separating concerns appropriately.

holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py (1)

123-123: LGTM: Typed configuration attribute added

The addition of the typed configuration attribute follows the established pattern for centralizing configuration handling.

@mainred
mainred force-pushed the fix-bash-toolset branch from fd882fd to 48f0f94 Compare June 25, 2025 10:10
Comment thread holmes/plugins/toolsets/bash/bash_toolset.py Outdated
Comment thread holmes/plugins/toolsets/opensearch/opensearch_traces.py Outdated
Comment thread holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py Outdated
Comment thread holmes/plugins/toolsets/kafka.py Outdated
@mainred
mainred force-pushed the fix-bash-toolset branch 2 times, most recently from 9c6321b to e7ff607 Compare June 29, 2025 14:37
@mainred
mainred force-pushed the fix-bash-toolset branch from e7ff607 to 07c5a5c Compare June 29, 2025 14:56
@github-actions

Copy link
Copy Markdown
Contributor

Results of HolmesGPT evals

Test suite Test case Status
ask_holmes 01_how_many_pods ⚠️
ask_holmes 02_what_is_wrong_with_pod ✅
ask_holmes 03_what_is_the_command_to_port_forward ✅
ask_holmes 04_related_k8s_events ✅
ask_holmes 05_image_version ✅
ask_holmes 06_explain_issue ✅
ask_holmes 07_high_latency ✅
ask_holmes 08_sock_shop_frontend ⚠️
ask_holmes 09_crashpod ✅
ask_holmes 10_image_pull_backoff ✅
ask_holmes 11_init_containers ✅
ask_holmes 12_job_crashing ✅
ask_holmes 13_pending_node_selector ✅
ask_holmes 14_pending_resources ✅
ask_holmes 15_failed_readiness_probe ✅
ask_holmes 16_failed_no_toolset_found ✅
ask_holmes 17_oom_kill ✅
ask_holmes 18_crash_looping_v2 ✅
ask_holmes 19_detect_missing_app_details ✅
ask_holmes 20_long_log_file_search ✅
ask_holmes 21_job_fail_curl_no_svc_account ⚠️
ask_holmes 22_high_latency_dbi_down ⚠️
ask_holmes 23_app_error_in_current_logs ✅
ask_holmes 24_misconfigured_pvc ✅
ask_holmes 25_misconfigured_ingress_class ⚠️
ask_holmes 26_multi_container_logs ✅
ask_holmes 27_permissions_error_no_helm_tools ✅
ask_holmes 28_permissions_error_helm_tools_enabled ✅
ask_holmes 29_events_from_alert_manager ✅
ask_holmes 30_basic_promql_graph_cluster_memory ✅
ask_holmes 31_basic_promql_graph_pod_memory ✅
ask_holmes 32_basic_promql_graph_pod_cpu ✅
ask_holmes 33_http_latency_graph ✅
ask_holmes 34_memory_graph ✅
ask_holmes 35_tempo ✅
ask_holmes 36_argocd_find_resource ✅
ask_holmes 37_argocd_wrong_namespace ⚠️
ask_holmes 38_rabbitmq_split_head ✅
ask_holmes 39_failed_toolset ⚠️
ask_holmes 40_disabled_toolset ✅
ask_holmes 41_setup_argo ✅
ask_holmes 42_dns_issues_result_all_tools ⚠️
ask_holmes 42_dns_issues_result_new_tools ⚠️
ask_holmes 42_dns_issues_result_old_tools ⚠️
ask_holmes 42_dns_issues_steps_new_all_tools ⚠️
ask_holmes 42_dns_issues_steps_new_tools ⚠️
ask_holmes 42_dns_issues_steps_old_tools ⚠️
ask_holmes 43_current_datetime_from_prompt ✅
ask_holmes 43_slack_deployment_logs ✅
ask_holmes 44_slack_statefulset_logs ✅
ask_holmes 45_fetch_deployment_logs_simple ✅
ask_holmes 46_job_crashing_no_longer_exists ⚠️
ask_holmes 47_truncated_logs_context_window ⚠️
ask_holmes 48_logs_since_thursday ⚠️
ask_holmes 49_logs_since_last_week ⚠️
ask_holmes 50_logs_since_specific_date ⚠️
ask_holmes 51_logs_summarize_errors ✅
ask_holmes 52_logs_login_issues ⚠️
ask_holmes 53_logs_find_term ✅
investigate 01_oom_kill ✅
investigate 02_crashloop_backoff ✅
investigate 03_cpu_throttling ✅
investigate 04_image_pull_backoff ✅
investigate 05_crashpod ✅
investigate 06_job_failure ✅
investigate 07_job_syntax_error ✅
investigate 08_memory_pressure ✅
investigate 09_high_latency ✅
investigate 10_KubeDeploymentReplicasMismatch ✅
investigate 11_KubePodCrashLooping ✅
investigate 12_KubePodNotReady ✅
investigate 13_Watchdog ✅
investigate 14_tempo ✅
investigate 15_dns_resolution ⚠️

Legend

  • ✅ the test was successful
  • ⚠️ the test failed but is known to be flakky or known to fail
  • ❌ the test failed and should be fixed before merging the PR

@nherment nherment left a comment

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.

Looks good, much simpler! If it's needed to further speed up the toolsets prerequisites, they can be run in parallel

@mainred
mainred merged commit 07c2a73 into master Jul 1, 2025
@mainred
mainred deleted the fix-bash-toolset branch July 1, 2025 00:02
aantn pushed a commit that referenced this pull request Jul 20, 2025
This is a follow-up PR for
#535

In this PR, we run toolset prerequisite check in parallel, also we mute
the toolset status log when the toolset is loaded from cache. This help
ask command output less verbose log, and bring a cleaner UI to the user.
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.

bash toolset breaks

3 participants