holmesgpt 0.16.2a0__py3-none-any.whl → 0.18.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. holmes/__init__.py +3 -5
  2. holmes/clients/robusta_client.py +4 -3
  3. holmes/common/env_vars.py +18 -2
  4. holmes/common/openshift.py +1 -1
  5. holmes/config.py +11 -6
  6. holmes/core/conversations.py +30 -13
  7. holmes/core/investigation.py +21 -25
  8. holmes/core/investigation_structured_output.py +3 -3
  9. holmes/core/issue.py +1 -1
  10. holmes/core/llm.py +50 -31
  11. holmes/core/models.py +19 -17
  12. holmes/core/openai_formatting.py +1 -1
  13. holmes/core/prompt.py +47 -2
  14. holmes/core/runbooks.py +1 -0
  15. holmes/core/safeguards.py +4 -2
  16. holmes/core/supabase_dal.py +4 -2
  17. holmes/core/tool_calling_llm.py +102 -141
  18. holmes/core/tools.py +19 -28
  19. holmes/core/tools_utils/token_counting.py +9 -2
  20. holmes/core/tools_utils/tool_context_window_limiter.py +13 -30
  21. holmes/core/tools_utils/tool_executor.py +0 -18
  22. holmes/core/tools_utils/toolset_utils.py +1 -0
  23. holmes/core/toolset_manager.py +37 -2
  24. holmes/core/tracing.py +13 -2
  25. holmes/core/transformers/__init__.py +1 -1
  26. holmes/core/transformers/base.py +1 -0
  27. holmes/core/transformers/llm_summarize.py +3 -2
  28. holmes/core/transformers/registry.py +2 -1
  29. holmes/core/transformers/transformer.py +1 -0
  30. holmes/core/truncation/compaction.py +37 -2
  31. holmes/core/truncation/input_context_window_limiter.py +3 -2
  32. holmes/interactive.py +52 -8
  33. holmes/main.py +17 -37
  34. holmes/plugins/interfaces.py +2 -1
  35. holmes/plugins/prompts/__init__.py +2 -1
  36. holmes/plugins/prompts/_fetch_logs.jinja2 +5 -5
  37. holmes/plugins/prompts/_runbook_instructions.jinja2 +2 -1
  38. holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
  39. holmes/plugins/prompts/conversation_history_compaction.jinja2 +2 -1
  40. holmes/plugins/prompts/generic_ask.jinja2 +0 -2
  41. holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -2
  42. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -2
  43. holmes/plugins/prompts/generic_investigation.jinja2 +0 -2
  44. holmes/plugins/prompts/investigation_procedure.jinja2 +2 -1
  45. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -2
  46. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -2
  47. holmes/plugins/runbooks/__init__.py +32 -3
  48. holmes/plugins/sources/github/__init__.py +4 -2
  49. holmes/plugins/sources/prometheus/models.py +1 -0
  50. holmes/plugins/toolsets/__init__.py +30 -26
  51. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +13 -12
  52. holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +3 -2
  53. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
  54. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +3 -2
  55. holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +3 -1
  56. holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +3 -1
  57. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +12 -12
  58. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +7 -7
  59. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +7 -7
  60. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -5
  61. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +3 -3
  62. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +7 -7
  63. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +6 -8
  64. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +3 -3
  65. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +3 -3
  66. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +3 -3
  67. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +3 -3
  68. holmes/plugins/toolsets/azure_sql/utils.py +0 -32
  69. holmes/plugins/toolsets/bash/argocd/__init__.py +3 -3
  70. holmes/plugins/toolsets/bash/aws/__init__.py +4 -4
  71. holmes/plugins/toolsets/bash/azure/__init__.py +4 -4
  72. holmes/plugins/toolsets/bash/bash_toolset.py +2 -3
  73. holmes/plugins/toolsets/bash/common/bash.py +19 -9
  74. holmes/plugins/toolsets/bash/common/bash_command.py +1 -1
  75. holmes/plugins/toolsets/bash/common/stringify.py +1 -1
  76. holmes/plugins/toolsets/bash/kubectl/__init__.py +2 -1
  77. holmes/plugins/toolsets/bash/kubectl/constants.py +0 -1
  78. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +3 -4
  79. holmes/plugins/toolsets/bash/parse_command.py +12 -13
  80. holmes/plugins/toolsets/connectivity_check.py +124 -0
  81. holmes/plugins/toolsets/coralogix/api.py +132 -119
  82. holmes/plugins/toolsets/coralogix/coralogix.jinja2 +14 -0
  83. holmes/plugins/toolsets/coralogix/toolset_coralogix.py +219 -0
  84. holmes/plugins/toolsets/coralogix/utils.py +15 -79
  85. holmes/plugins/toolsets/datadog/datadog_api.py +36 -3
  86. holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +34 -1
  87. holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +3 -3
  88. holmes/plugins/toolsets/datadog/datadog_models.py +59 -0
  89. holmes/plugins/toolsets/datadog/datadog_url_utils.py +213 -0
  90. holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +165 -28
  91. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +71 -28
  92. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +224 -375
  93. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +67 -36
  94. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +360 -343
  95. holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
  96. holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
  97. holmes/plugins/toolsets/git.py +7 -8
  98. holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
  99. holmes/plugins/toolsets/grafana/common.py +2 -30
  100. holmes/plugins/toolsets/grafana/grafana_tempo_api.py +2 -1
  101. holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +18 -2
  102. holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +92 -18
  103. holmes/plugins/toolsets/grafana/loki_api.py +4 -0
  104. holmes/plugins/toolsets/grafana/toolset_grafana.py +109 -25
  105. holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +22 -0
  106. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +201 -33
  107. holmes/plugins/toolsets/grafana/trace_parser.py +3 -2
  108. holmes/plugins/toolsets/internet/internet.py +10 -10
  109. holmes/plugins/toolsets/internet/notion.py +5 -6
  110. holmes/plugins/toolsets/investigator/core_investigation.py +3 -3
  111. holmes/plugins/toolsets/investigator/model.py +3 -1
  112. holmes/plugins/toolsets/json_filter_mixin.py +134 -0
  113. holmes/plugins/toolsets/kafka.py +12 -7
  114. holmes/plugins/toolsets/kubernetes.yaml +260 -30
  115. holmes/plugins/toolsets/kubernetes_logs.py +3 -3
  116. holmes/plugins/toolsets/logging_utils/logging_api.py +16 -6
  117. holmes/plugins/toolsets/mcp/toolset_mcp.py +88 -60
  118. holmes/plugins/toolsets/newrelic/new_relic_api.py +41 -1
  119. holmes/plugins/toolsets/newrelic/newrelic.jinja2 +24 -0
  120. holmes/plugins/toolsets/newrelic/newrelic.py +212 -55
  121. holmes/plugins/toolsets/prometheus/prometheus.py +358 -102
  122. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +11 -3
  123. holmes/plugins/toolsets/rabbitmq/api.py +23 -4
  124. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +5 -5
  125. holmes/plugins/toolsets/robusta/robusta.py +5 -5
  126. holmes/plugins/toolsets/runbook/runbook_fetcher.py +25 -6
  127. holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +1 -1
  128. holmes/plugins/toolsets/utils.py +1 -1
  129. holmes/utils/config_utils.py +1 -1
  130. holmes/utils/connection_utils.py +31 -0
  131. holmes/utils/console/result.py +10 -0
  132. holmes/utils/file_utils.py +2 -1
  133. holmes/utils/global_instructions.py +10 -26
  134. holmes/utils/holmes_status.py +4 -3
  135. holmes/utils/log.py +15 -0
  136. holmes/utils/markdown_utils.py +2 -3
  137. holmes/utils/memory_limit.py +58 -0
  138. holmes/utils/sentry_helper.py +23 -0
  139. holmes/utils/stream.py +12 -5
  140. holmes/utils/tags.py +4 -3
  141. holmes/version.py +3 -1
  142. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +12 -10
  143. holmesgpt-0.18.4.dist-info/RECORD +258 -0
  144. holmes/plugins/toolsets/aws.yaml +0 -80
  145. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -114
  146. holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
  147. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -736
  148. holmes/plugins/toolsets/grafana/grafana_api.py +0 -64
  149. holmes/plugins/toolsets/opensearch/__init__.py +0 -0
  150. holmes/plugins/toolsets/opensearch/opensearch.py +0 -250
  151. holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
  152. holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -215
  153. holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
  154. holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
  155. holmes/utils/keygen_utils.py +0 -6
  156. holmesgpt-0.16.2a0.dist-info/RECORD +0 -258
  157. holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_ppl_query_docs.jinja2 +0 -0
  158. holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist.py +2 -2
  159. /holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist_instructions.jinja2 +0 -0
  160. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/LICENSE +0 -0
  161. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
  162. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/entry_points.txt +0 -0
@@ -5,7 +5,6 @@ from typing import Any, Dict
5
5
 
6
6
  import requests # type: ignore
7
7
 
8
-
9
8
  logger = logging.getLogger(__name__)
10
9
 
11
10
 
@@ -123,3 +122,44 @@ class NewRelicAPI:
123
122
  raise Exception(
124
123
  f"Failed to extract results from NewRelic response: {e}"
125
124
  ) from e
125
+
126
+ def get_organization_accounts(self) -> list:
127
+ """Get all accounts accessible in the organization.
128
+
129
+ Returns:
130
+ list: List of account dictionaries with id and name
131
+
132
+ Raises:
133
+ requests.exceptions.HTTPError: If the API request fails
134
+ Exception: If GraphQL returns errors
135
+ """
136
+ graphql_query = {
137
+ "query": """
138
+ query GetOrganizationAccounts {
139
+ actor {
140
+ organization {
141
+ accountManagement {
142
+ managedAccounts {
143
+ id
144
+ name
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ """
151
+ }
152
+
153
+ logger.info("Querying organization accounts")
154
+ response = self._make_request(graphql_query)
155
+
156
+ # Extract accounts from the nested response
157
+ try:
158
+ accounts = response["data"]["actor"]["organization"]["accountManagement"][
159
+ "managedAccounts"
160
+ ]
161
+ return accounts
162
+ except (KeyError, TypeError) as e:
163
+ raise Exception(
164
+ f"Failed to extract accounts from NewRelic response: {e}"
165
+ ) from e
@@ -1,5 +1,29 @@
1
1
  New Relic provides distributed tracing data along with logs and metrics.
2
2
 
3
+ {% if config.enable_multi_account %}
4
+ **MULTI-ACCOUNT MODE**: You have access to multiple New Relic accounts in this organization.
5
+
6
+ ### Important Multi-Account Workflow
7
+
8
+ **Each NRQL query MUST include the account_id parameter.**
9
+ 1. A New Relic account ID is a numeric identifier, typically a 6–8 digit integer (e.g., 1234567).
10
+
11
+ **Here's how to determine which account_id to use**
12
+
13
+ 1. **ALWAYS Check context first**: Look for common new relic labels or tags with the account id or name such as `nrAccountId` `accountId` or `account` in the provided context
14
+ (e.g., from alerts, traces, or previous queries). If found, use that value.
15
+
16
+ 2. **ALWAYS CHECK if Account name provided**: If the user mentions a specific account name (e.g., "Production Account", "Staging"):
17
+ - YOU MUST First call `newrelic_list_organization_accounts` to get the list of all accounts
18
+ - Find the matching account by name and use its ID
19
+
20
+ 3. **No account specified**: If you can't find any account ID or name based on the context of the question.
21
+ - Use the function newrelic_execute_nrql_query default account id value as the account ID.
22
+ - Let the user know you have used the default account.
23
+
24
+ **Important**: The context may contain account IDs in various places - check trace data, alert metadata, or previous query results for `nrAccountId`, `accountId`, `account.id` or similar fields.
25
+
26
+ {% endif %}
3
27
  Assume every application has New Relic tracing data.
4
28
 
5
29
  Use `nrql_query` to run a NRQL query.
@@ -1,37 +1,78 @@
1
- import os
1
+ import base64
2
+ import json
2
3
  import logging
3
- from typing import Any, Optional, Dict
4
+ import os
5
+ from typing import Any, Dict, Optional
6
+
7
+ from pydantic import BaseModel
8
+
4
9
  from holmes.core.tools import (
5
10
  CallablePrerequisite,
11
+ StructuredToolResult,
12
+ StructuredToolResultStatus,
6
13
  Tool,
7
14
  ToolInvokeContext,
8
15
  ToolParameter,
9
16
  Toolset,
10
17
  ToolsetTag,
11
18
  )
12
- from pydantic import BaseModel
13
- from holmes.core.tools import StructuredToolResult, StructuredToolResultStatus
14
- from holmes.plugins.toolsets.utils import toolset_name_for_one_liner
15
19
  from holmes.plugins.toolsets.newrelic.new_relic_api import NewRelicAPI
16
- import yaml
17
- from holmes.utils.keygen_utils import generate_random_key
20
+ from holmes.plugins.toolsets.utils import toolset_name_for_one_liner
21
+
22
+
23
+ def _build_newrelic_query_url(
24
+ base_url: str,
25
+ account_id: str,
26
+ nrql_query: str,
27
+ ) -> Optional[str]:
28
+ """Build a New Relic query URL for the NRQL query builder.
29
+
30
+ Note: URL links to queries are not officially supported by New Relic, so we are using
31
+ a workaround to open their overlay to the query builder with the query pre-filled.
32
+ This uses the dashboard launcher with an overlay parameter to open the query builder nerdlet.
33
+
34
+ """
35
+ try:
36
+ account_id_int = int(account_id) if isinstance(account_id, str) else account_id
37
+
38
+ overlay = {
39
+ "nerdletId": "data-exploration.query-builder",
40
+ "initialActiveInterface": "nrqlEditor",
41
+ "initialQueries": [
42
+ {
43
+ "accountId": account_id_int,
44
+ "nrql": nrql_query,
45
+ }
46
+ ],
47
+ }
48
+
49
+ overlay_json = json.dumps(overlay, separators=(",", ":"))
50
+ overlay_base64 = base64.b64encode(overlay_json.encode("utf-8")).decode("utf-8")
51
+
52
+ pane = {
53
+ "nerdletId": "dashboards.list",
54
+ "entityDomain": "VIZ",
55
+ "entityType": "DASHBOARD",
56
+ }
57
+ pane_json = json.dumps(pane, separators=(",", ":"))
58
+ pane_base64 = base64.b64encode(pane_json.encode("utf-8")).decode("utf-8")
59
+
60
+ url = (
61
+ f"{base_url}/launcher/dashboards.launcher"
62
+ f"?pane={pane_base64}"
63
+ f"&overlay={overlay_base64}"
64
+ )
65
+
66
+ return url
67
+ except Exception:
68
+ return None
18
69
 
19
70
 
20
71
  class ExecuteNRQLQuery(Tool):
21
72
  def __init__(self, toolset: "NewRelicToolset"):
22
- super().__init__(
23
- name="newrelic_execute_nrql_query",
24
- description="Get Traces, APM, Spans, Logs and more by executing a NRQL query in New Relic. "
25
- "Returns the result of the NRQL function. "
26
- "⚠️ CRITICAL: NRQL silently returns empty results for invalid queries instead of errors. "
27
- "If you get empty results, your query likely has issues such as: "
28
- "1) Wrong attribute names (use SELECT keyset() first to verify), "
29
- "2) Type mismatches (string vs numeric fields), "
30
- "3) Wrong event type. "
31
- "Always verify attribute names and types before querying.",
32
- parameters={
33
- "query": ToolParameter(
34
- description="""The NRQL query string to execute.
73
+ parameters = {
74
+ "query": ToolParameter(
75
+ description="""The NRQL query string to execute.
35
76
 
36
77
  MANDATORY: Before querying any event type, ALWAYS run `SELECT keyset() FROM <EventType> SINCE <timeframe>` to discover available attributes. Never use attributes without confirming they exist first. Make sure to remember which fields are stringKeys, numericKeys or booleanKeys as this will be important in subsequent queries.
37
78
 
@@ -58,48 +99,78 @@ SELECT count(*), average(duration) FROM Transaction FACET transactionType
58
99
  SELECT count(*), transactionType FROM Transaction FACET transactionType
59
100
  ```
60
101
  """,
61
- type="string",
62
- required=True,
63
- ),
64
- "description": ToolParameter(
65
- description="A breif 6 word human understandable description of the query you are running.",
66
- type="string",
67
- required=True,
68
- ),
69
- "query_type": ToolParameter(
70
- description="Either 'Metrics', 'Logs', 'Traces', 'Discover Attributes' or 'Other'.",
71
- type="string",
72
- required=True,
102
+ type="string",
103
+ required=True,
104
+ ),
105
+ "description": ToolParameter(
106
+ description="A brief 6 word human understandable description of the query you are running.",
107
+ type="string",
108
+ required=True,
109
+ ),
110
+ "query_type": ToolParameter(
111
+ description="Either 'Metrics', 'Logs', 'Traces', 'Discover Attributes' or 'Other'.",
112
+ type="string",
113
+ required=True,
114
+ ),
115
+ }
116
+
117
+ # Add account_id parameter only in multi-account mode
118
+ if toolset.enable_multi_account:
119
+ parameters["account_id"] = ToolParameter(
120
+ description=(
121
+ f"A New Relic account ID is a numeric identifier, typically a 6-8 digit integer (e.g., 1234567). It contains only digits, has no prefixes or separators, and uniquely identifies a New Relic account. default: {toolset.nr_account_id}"
73
122
  ),
74
- },
123
+ type="integer",
124
+ required=True,
125
+ )
126
+
127
+ super().__init__(
128
+ name="newrelic_execute_nrql_query",
129
+ description="Get Traces, APM, Spans, Logs and more by executing a NRQL query in New Relic. "
130
+ "Returns the result of the NRQL function. "
131
+ "⚠️ CRITICAL: NRQL silently returns empty results for invalid queries instead of errors. "
132
+ "If you get empty results, your query likely has issues such as: "
133
+ "1) Wrong attribute names (use SELECT keyset() first to verify), "
134
+ "2) Type mismatches (string vs numeric fields), "
135
+ "3) Wrong event type. "
136
+ "Always verify attribute names and types before querying.",
137
+ parameters=parameters,
75
138
  )
76
139
  self._toolset = toolset
77
140
 
78
141
  def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
79
- if not self._toolset.nr_api_key or not self._toolset.nr_account_id:
80
- raise ValueError("NewRelic API key or account ID is not configured")
142
+ if self._toolset.enable_multi_account:
143
+ account_id = params.get("account_id") or self._toolset.nr_account_id
144
+ account_id = str(account_id)
145
+ else:
146
+ account_id = self._toolset.nr_account_id
81
147
 
82
- api = NewRelicAPI(
83
- api_key=self._toolset.nr_api_key,
84
- account_id=self._toolset.nr_account_id,
85
- is_eu_datacenter=self._toolset.is_eu_datacenter,
86
- )
148
+ if not account_id:
149
+ raise ValueError("NewRelic account ID is not configured")
150
+
151
+ api = self._toolset.create_api_client(account_id)
87
152
 
88
153
  query = params["query"]
89
154
  result = api.execute_nrql_query(query)
90
155
 
91
156
  result_with_key = {
92
- "random_key": generate_random_key(),
93
- "tool_name": self.name,
94
157
  "query": query,
95
158
  "data": result,
96
159
  "is_eu": self._toolset.is_eu_datacenter,
97
160
  }
98
- final_result = yaml.dump(result_with_key, default_flow_style=False)
161
+
162
+ # Build New Relic query URL
163
+ explore_url = _build_newrelic_query_url(
164
+ base_url=self._toolset.base_url,
165
+ account_id=account_id,
166
+ nrql_query=query,
167
+ )
168
+
99
169
  return StructuredToolResult(
100
170
  status=StructuredToolResultStatus.SUCCESS,
101
- data=final_result,
171
+ data=result_with_key,
102
172
  params=params,
173
+ url=explore_url,
103
174
  )
104
175
 
105
176
  def get_parameterized_one_liner(self, params) -> str:
@@ -107,16 +178,100 @@ SELECT count(*), transactionType FROM Transaction FACET transactionType
107
178
  return f"{toolset_name_for_one_liner(self._toolset.name)}: Execute NRQL ({description})"
108
179
 
109
180
 
181
+ class ListOrganizationAccounts(Tool):
182
+ def __init__(self, toolset: "NewRelicToolset"):
183
+ super().__init__(
184
+ name="newrelic_list_organization_accounts",
185
+ description=(
186
+ "List all account names and IDs accessible in the New Relic organization. "
187
+ "Use this tool to:\n"
188
+ "1. Find the account ID when given an account name\n"
189
+ "2. Map account names to IDs for running NRQL queries\n"
190
+ "Returns a list of accounts with 'id' and 'name' fields."
191
+ ),
192
+ parameters={},
193
+ )
194
+ self._toolset = toolset
195
+
196
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
197
+ api = self._toolset.create_api_client(
198
+ account_id="0"
199
+ ) # organization query does not need account_id
200
+
201
+ accounts = api.get_organization_accounts()
202
+
203
+ result_with_key = {
204
+ "accounts": accounts,
205
+ "total_count": len(accounts),
206
+ "is_eu": self._toolset.is_eu_datacenter,
207
+ }
208
+
209
+ # Build New Relic accounts URL
210
+ accounts_url = (
211
+ f"{self._toolset.base_url}/admin-portal/organizations/organization-detail"
212
+ )
213
+
214
+ return StructuredToolResult(
215
+ status=StructuredToolResultStatus.SUCCESS,
216
+ data=result_with_key,
217
+ params=params,
218
+ url=accounts_url,
219
+ )
220
+
221
+ def get_parameterized_one_liner(self, params) -> str:
222
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: List organization accounts"
223
+
224
+
110
225
  class NewrelicConfig(BaseModel):
111
- nr_api_key: Optional[str] = None
112
- nr_account_id: Optional[str] = None
226
+ nr_api_key: str
227
+ nr_account_id: str
113
228
  is_eu_datacenter: Optional[bool] = False
229
+ enable_multi_account: Optional[bool] = False
114
230
 
115
231
 
116
232
  class NewRelicToolset(Toolset):
117
233
  nr_api_key: Optional[str] = None
118
234
  nr_account_id: Optional[str] = None
119
235
  is_eu_datacenter: bool = False
236
+ enable_multi_account: bool = False
237
+
238
+ @property
239
+ def base_url(self) -> str:
240
+ """Get the New Relic base URL based on datacenter region."""
241
+ return (
242
+ "https://one.eu.newrelic.com"
243
+ if self.is_eu_datacenter
244
+ else "https://one.newrelic.com"
245
+ )
246
+
247
+ def create_api_client(self, account_id: Optional[str] = None) -> NewRelicAPI:
248
+ """Create a NewRelicAPI client instance.
249
+
250
+ Args:
251
+ account_id: Account ID to use. If None, uses the default from config.
252
+ Set to "0" for organization-level queries.
253
+
254
+ Returns:
255
+ Configured NewRelicAPI instance
256
+
257
+ Raises:
258
+ ValueError: If API key is not configured
259
+ """
260
+ if not self.nr_api_key:
261
+ raise ValueError("NewRelic API key is not configured")
262
+
263
+ effective_account_id = (
264
+ account_id if account_id is not None else self.nr_account_id
265
+ )
266
+
267
+ if not effective_account_id:
268
+ raise ValueError("NewRelic Account id is not configured")
269
+
270
+ return NewRelicAPI(
271
+ api_key=self.nr_api_key,
272
+ account_id=effective_account_id,
273
+ is_eu_datacenter=self.is_eu_datacenter,
274
+ )
120
275
 
121
276
  def __init__(self):
122
277
  super().__init__(
@@ -125,15 +280,9 @@ class NewRelicToolset(Toolset):
125
280
  docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/newrelic/",
126
281
  icon_url="https://companieslogo.com/img/orig/NEWR-de5fcb2e.png?t=1720244493",
127
282
  prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)], # type: ignore
128
- tools=[
129
- ExecuteNRQLQuery(self),
130
- ],
283
+ tools=[],
131
284
  tags=[ToolsetTag.CORE],
132
285
  )
133
- template_file_path = os.path.abspath(
134
- os.path.join(os.path.dirname(__file__), "newrelic.jinja2")
135
- )
136
- self._load_llm_instructions(jinja_template=f"file://{template_file_path}")
137
286
 
138
287
  def prerequisites_callable(
139
288
  self, config: dict[str, Any]
@@ -146,9 +295,16 @@ class NewRelicToolset(Toolset):
146
295
  self.nr_account_id = nr_config.nr_account_id
147
296
  self.nr_api_key = nr_config.nr_api_key
148
297
  self.is_eu_datacenter = nr_config.is_eu_datacenter or False
298
+ self.enable_multi_account = nr_config.enable_multi_account or False
149
299
 
150
- if not self.nr_account_id or not self.nr_api_key:
151
- return False, "New Relic account ID or API key is missing"
300
+ # Tool uses enable_multi_account flag.
301
+ self.tools = [ExecuteNRQLQuery(self)]
302
+ if self.enable_multi_account:
303
+ self.tools.append(ListOrganizationAccounts(self))
304
+ template_file_path = os.path.abspath(
305
+ os.path.join(os.path.dirname(__file__), "newrelic.jinja2")
306
+ )
307
+ self._load_llm_instructions(jinja_template=f"file://{template_file_path}")
152
308
 
153
309
  return True, None
154
310
  except Exception as e:
@@ -160,4 +316,5 @@ class NewRelicToolset(Toolset):
160
316
  "nr_api_key": "NRAK-XXXXXXXXXXXXXXXXXXXXXXXXXX",
161
317
  "nr_account_id": "1234567",
162
318
  "is_eu_datacenter": False,
319
+ "enable_multi_account": False,
163
320
  }