Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

mainred
Copy link
Collaborator

@mainred mainred commented Jun 18, 2025

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

Copy link
Contributor

coderabbitai bot commented Jun 18, 2025

Walkthrough

This change adds an abstract init_config method to the Toolset base class and implements it in subclasses, centralizing configuration initialization. The load_toolset_with_status method now calls init_config for enabled toolsets. Multiple toolsets refactor their prerequisites_callable methods to call init_config for configuration setup, improving consistency and error handling. Tests and utilities are updated with no-op init_config methods for compatibility.

Changes

File(s) Change Summary
holmes/core/tools.py, holmes/core/toolset_manager.py Added abstract init_config method to Toolset and subclasses; updated toolset loading to call init_config; refined config attribute typing; expanded docstring for load_toolset_with_status.
holmes/plugins/toolsets/bash/bash_toolset.py, holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py, holmes/plugins/toolsets/datadog.py, holmes/plugins/toolsets/git.py, holmes/plugins/toolsets/grafana/base_grafana_toolset.py, holmes/plugins/toolsets/internet/internet.py, holmes/plugins/toolsets/kafka.py, holmes/plugins/toolsets/mcp/toolset_mcp.py, holmes/plugins/toolsets/newrelic.py, holmes/plugins/toolsets/opensearch/opensearch.py, holmes/plugins/toolsets/opensearch/opensearch_utils.py, holmes/plugins/toolsets/prometheus/prometheus.py, holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py, holmes/plugins/toolsets/robusta/robusta.py, holmes/plugins/toolsets/runbook/runbook_fetcher.py Refactored prerequisites_callable to call new init_config method for configuration initialization; replaced direct config assignments with typed_config attributes; added exception handling in some toolsets; reordered imports for clarity.
holmes/plugins/toolsets/opensearch/opensearch_logs.py, holmes/plugins/toolsets/opensearch/opensearch_traces.py Replaced all references from opensearch_config to typed_config for configuration access; reordered imports; no functional changes.
holmes/plugins/toolsets/grafana/toolset_grafana.py Refactored _invoke method to return StructuredToolResult instead of string; uses typed_config and improved error handling with logging.
holmes/plugins/toolsets/grafana/toolset_grafana_loki.py Added typed_config attribute and init_config method; refactored prerequisites_callable to use init_config.
holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py Changed grafana_config property to return typed_config instead of _grafana_config.
holmes/plugins/toolsets/kubernetes_logs.py Added no-op init_config method; reformatted return statement in fetch_pod_logs.
tests/llm/utils/mock_toolset.py, tests/plugins/prompt/test_toolsets_instructions.py, tests/test_check_prerequisites.py, tests/test_holmes_sync_toolsets.py Added no-op init_config methods to mock/sample toolsets; simplified test assertions to match updated callable signatures.
tests/test_mcp_toolset.py Renamed a test function for clarity; updated calls to init_server_tools to remove explicit config argument.
tests/utils/toolsets.py Updated callable function signatures to accept dict[str, Any] parameter named onfig or config for consistency.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ToolsetManager
    participant Toolset
    participant Tool

    User->>ToolsetManager: Load toolsets
    ToolsetManager->>Toolset: Instantiate Toolset
    ToolsetManager->>Toolset: Call init_config(config)
    ToolsetManager->>Toolset: Call check_prerequisites()
    Toolset->>Tool: For each prerequisite, call callable()
    Toolset->>ToolsetManager: Prerequisite check result
Loading

Assessment against linked issues

Objective Addressed Explanation
Fix Bash toolset so that tools execute successfully and do not fail due to config being a dict instead of a config object (#532)
Ensure toolset configuration is initialized properly before tool invocation (#532)
Remove reliance on config dictionary arguments in toolset prerequisites and use instance state instead (#532)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Import statement reordering and grouping in multiple files (e.g., holmes/plugins/toolsets/opensearch/opensearch_logs.py, holmes/plugins/toolsets/mcp/toolset_mcp.py) These are stylistic changes and do not relate directly to the objectives of fixing Bash toolset config handling or the callable signature refactor.

Possibly related PRs

Suggested labels

enhancement

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 fd882fd and 48f0f94.

📒 Files selected for processing (3)
  • holmes/plugins/toolsets/kafka.py (1 hunks)
  • holmes/plugins/toolsets/opensearch/opensearch.py (2 hunks)
  • tests/plugins/toolsets/test_toolset_utils.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/plugins/toolsets/test_toolset_utils.py
  • holmes/plugins/toolsets/kafka.py
  • holmes/plugins/toolsets/opensearch/opensearch.py
⏰ 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.12)
  • GitHub Check: build (3.10)
  • GitHub Check: build (3.11)
  • 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.

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: 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.

@mainred mainred force-pushed the fix-bash-toolset branch from 0a6de93 to 98e9260 Compare June 18, 2025 05:24
@nherment
Copy link
Collaborator

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
Copy link
Collaborator Author

mainred commented Jun 18, 2025

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
Collaborator

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
Collaborator

Also note that the pre-commit checks fail

def prerequisites_callable(self) -> Tuple[bool, str]:
if not self.config and not (
os.getenv("GIT_REPO") and os.getenv("GIT_CREDENTIALS")
):
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks like the env checks belong to init_config

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think I have changed git toolset. It was reverted somehow.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We can discuss. In prerequisite check, we check if the config and env is correct. in init_config, should we also check these prerequisites? Or assume the enabled toolset has meet the requirement.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you can assume it has met the requirements because you want to skip the prerequisites liveness checks.
There is some caveats that the config dict may not match the expected pydantic BaseModel and in that case the toolset should go in the proper 'failed' state with a meaningful error message.

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: 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 12e0d76.

📒 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.

@mainred mainred force-pushed the fix-bash-toolset branch 2 times, most recently from 0aa4810 to ace3b7a Compare June 18, 2025 13:49
@mainred
Copy link
Collaborator Author

mainred commented Jun 20, 2025

@nherment could you please take another look?

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

♻️ 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 1d89436 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.

Copy link
Collaborator

@nherment nherment left a comment

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.

if not config:
return True, ""
self.additional_headers = config.get("additional_headers", {})
def prerequisites_callable(self) -> Tuple[bool, str]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

prerequisites_callable can be removed if it's not checking anything

dd_config = DatadogConfig(**config)
self.dd_api_key = dd_config.dd_api_key
self.dd_app_key = dd_config.dd_app_key
self.init_config()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it not better to call toolset.init_config() within toolset.check_prerequisites()?

What happens where there are multiple prerequisites (possible by design)? There is an implicit guarantee thyat prerequisites are run in order but it's just not ideal to have to call init_config within a callable prerequisite.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Once init_config is called inside check_prerequisites it is important to catch any config validation error and return an appropriate error message that helps users remediating the error.

return False, TOOLSET_CONFIG_MISSING_ERROR
errors = []
try:
kafka_config = KafkaConfig(**config)
kafka_config = KafkaConfig(**self.config)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be an indication that the current design is incorrect.

Why does prerequisites callable need to initialize and validate the config when it's the role of init_config? It's repeated code and requires consistent error handling (user should see the same error for the same misconfiguration).

Rather here the pattern should be the one established. init_config validates and creates a typed config which is then used in prerequisites_callable.

admin_config["sasl.password"] = cluster.kafka_password

client = AdminClient(admin_config)
self.clients[cluster.name] = client # Store in dictionary
Copy link
Collaborator

Choose a reason for hiding this comment

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

The code here is a copy paste of the code in prerequisites_callable. It should be extracted in its a private method to avoid duplication.

return False, "Missing opensearch traces URL. Check your config"
elif not config and env_url:
elif not self.config and env_url:
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's the role of init_config to consolidate a valid config so there is no longer any need to check for env vars here so the code here is duplicated unnecessarily .

Copy link
Collaborator

@nherment nherment left a comment

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

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: 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

Copy link
Collaborator

@nherment nherment left a comment

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!

admin_config["sasl.password"] = cluster.kafka_password

client = AdminClient(admin_config)
self.clients[cluster.name] = client
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's great that the code is no longer duplicated. However I think there is a regression: the reporting of configuration errors is no longer complete as the code now would only report a failure for the first cluster that is misconfigued as opposed to all of them.

One of our big focus is to smooth the onboarding and ease of configuration for our users and I feel this is a backward step.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I agree. But I did not find a better way to organize all the errors since errors are raised in init_config, and it seems to be no better way to let init_config pass the errors to prerequisites_callable

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's not great but would it work to gather all errors and then raise an exception? It's not too different than how a pydantic validation error would behave. It's also somewhat simple to implement.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done. thanks.

cluster_kwargs = cluster.model_dump()
client = OpenSearchClient(**cluster_kwargs)
if client.client.cluster.health(params={"timeout": 5}):
self.clients.append(client)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Putting the health check inside the config is fine by me be isn't what you want to avoid by separating the config?

What is important though is to make sure that if more than one cluster cannot be initialized that all the failures are reported to the user for the same reason that I mentioned about the Kafka clusters initialisation.

Looks like there is a bug here as well and I've recorded it separately: there is no error if a cluster reports unhealthy but it won't be part of the initialized clients.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@arikalon1 I think we need your opinion around handling these cases.

Copy link
Contributor

@arikalon1 arikalon1 Jun 26, 2025

Choose a reason for hiding this comment

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

@mainred I think we need to be aligned with the previous behavior
I think previously, it was checking health on all clusters.
If a cluster is healthy - it's added to the list.
If not - the error is returned.

Currently, I think there are 2 issues:

  1. The loop in line 240, is not with try/except - so any error will not be caught and will stop the execution
  2. I think there's another loop, in line 247 - looks like almost the same code. Is it duplicated? or, is this on purpose?

@@ -25,7 +24,7 @@


class BaseBashExecutorToolset(Toolset):
config: Optional[BashExecutorConfig] = None
typed_config: Optional[BashExecutorConfig] = None
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Instead of reusing config, which might be confused with Toolset.config, I use typed_config for specific toolsets for a typed config.
FYI @arikalon1 We had discussions around this variable naming, but now we are on the same page to use typed_config to be specific.

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: 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)

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: 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.

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: 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
Copy link
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

return False, str(e)
return True, ""

def init_config(self, config: Optional[dict[str, Any]]):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Another concern is related to init_config.
prerequisites_callable calls init_config and can catch the exception when config is not well set. But in init_config, which can be also called when the status is cached and prerequisites_callable is not called https://github.com/robusta-dev/holmesgpt/pull/535/files#diff-bfd1b7e4ad5810c10197015d94b5d2aa9f3ad74a937f531f25954306858b08b0R246. The exception is not well handled, since we assume the prerequisites have been checked, but it is not also so considering some configurations are set by environment variables, and environment variables can be cleaned up after console restarts.

cc @arikalon1 @nherment

Copy link
Contributor

Choose a reason for hiding this comment

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

@mainred with init_config, if the configuration is invalid, it should always raise an exception? And then the tool is not initialized?

@@ -130,6 +131,12 @@ def __init__(self, toolset: "OpenSearchTracesToolset"):
def _invoke(self, params: Any) -> StructuredToolResult:
err_msg = ""
try:
if not self._toolset.typed_config:
Copy link
Contributor

Choose a reason for hiding this comment

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

@mainred Why do we need this check?
How can we get to _invoke, if no config was provided?
Wouldn't the toolset be disabled?

def coralogix_config(self) -> Optional[CoralogixConfig]:
return self.config
def init_config(self, config: Optional[dict[str, Any]]):
if not config:
Copy link
Contributor

Choose a reason for hiding this comment

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

@mainred here, for example, init_config doesn't raise an exception when it's not initialized.
Will the Coralogix tool appear enabled or disabled?

errors.append(message)

return len(self.clients) > 0, "\n".join(errors)
self.init_config(config)
Copy link
Contributor

Choose a reason for hiding this comment

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

@mainred here as well
If no config, init_config doesn't raise an exception. But we return True.
Is this correct?

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