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.
- holmes/__init__.py +3 -5
- holmes/clients/robusta_client.py +4 -3
- holmes/common/env_vars.py +18 -2
- holmes/common/openshift.py +1 -1
- holmes/config.py +11 -6
- holmes/core/conversations.py +30 -13
- holmes/core/investigation.py +21 -25
- holmes/core/investigation_structured_output.py +3 -3
- holmes/core/issue.py +1 -1
- holmes/core/llm.py +50 -31
- holmes/core/models.py +19 -17
- holmes/core/openai_formatting.py +1 -1
- holmes/core/prompt.py +47 -2
- holmes/core/runbooks.py +1 -0
- holmes/core/safeguards.py +4 -2
- holmes/core/supabase_dal.py +4 -2
- holmes/core/tool_calling_llm.py +102 -141
- holmes/core/tools.py +19 -28
- holmes/core/tools_utils/token_counting.py +9 -2
- holmes/core/tools_utils/tool_context_window_limiter.py +13 -30
- holmes/core/tools_utils/tool_executor.py +0 -18
- holmes/core/tools_utils/toolset_utils.py +1 -0
- holmes/core/toolset_manager.py +37 -2
- holmes/core/tracing.py +13 -2
- holmes/core/transformers/__init__.py +1 -1
- holmes/core/transformers/base.py +1 -0
- holmes/core/transformers/llm_summarize.py +3 -2
- holmes/core/transformers/registry.py +2 -1
- holmes/core/transformers/transformer.py +1 -0
- holmes/core/truncation/compaction.py +37 -2
- holmes/core/truncation/input_context_window_limiter.py +3 -2
- holmes/interactive.py +52 -8
- holmes/main.py +17 -37
- holmes/plugins/interfaces.py +2 -1
- holmes/plugins/prompts/__init__.py +2 -1
- holmes/plugins/prompts/_fetch_logs.jinja2 +5 -5
- holmes/plugins/prompts/_runbook_instructions.jinja2 +2 -1
- holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
- holmes/plugins/prompts/conversation_history_compaction.jinja2 +2 -1
- holmes/plugins/prompts/generic_ask.jinja2 +0 -2
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -2
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -2
- holmes/plugins/prompts/generic_investigation.jinja2 +0 -2
- holmes/plugins/prompts/investigation_procedure.jinja2 +2 -1
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -2
- holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -2
- holmes/plugins/runbooks/__init__.py +32 -3
- holmes/plugins/sources/github/__init__.py +4 -2
- holmes/plugins/sources/prometheus/models.py +1 -0
- holmes/plugins/toolsets/__init__.py +30 -26
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +13 -12
- holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +3 -2
- holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
- holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +3 -2
- holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +3 -1
- holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +3 -1
- holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +12 -12
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +7 -7
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +7 -7
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -5
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +3 -3
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +7 -7
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +6 -8
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +3 -3
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +3 -3
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +3 -3
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +3 -3
- holmes/plugins/toolsets/azure_sql/utils.py +0 -32
- holmes/plugins/toolsets/bash/argocd/__init__.py +3 -3
- holmes/plugins/toolsets/bash/aws/__init__.py +4 -4
- holmes/plugins/toolsets/bash/azure/__init__.py +4 -4
- holmes/plugins/toolsets/bash/bash_toolset.py +2 -3
- holmes/plugins/toolsets/bash/common/bash.py +19 -9
- holmes/plugins/toolsets/bash/common/bash_command.py +1 -1
- holmes/plugins/toolsets/bash/common/stringify.py +1 -1
- holmes/plugins/toolsets/bash/kubectl/__init__.py +2 -1
- holmes/plugins/toolsets/bash/kubectl/constants.py +0 -1
- holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +3 -4
- holmes/plugins/toolsets/bash/parse_command.py +12 -13
- holmes/plugins/toolsets/connectivity_check.py +124 -0
- holmes/plugins/toolsets/coralogix/api.py +132 -119
- holmes/plugins/toolsets/coralogix/coralogix.jinja2 +14 -0
- holmes/plugins/toolsets/coralogix/toolset_coralogix.py +219 -0
- holmes/plugins/toolsets/coralogix/utils.py +15 -79
- holmes/plugins/toolsets/datadog/datadog_api.py +36 -3
- holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +34 -1
- holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +3 -3
- holmes/plugins/toolsets/datadog/datadog_models.py +59 -0
- holmes/plugins/toolsets/datadog/datadog_url_utils.py +213 -0
- holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +165 -28
- holmes/plugins/toolsets/datadog/toolset_datadog_general.py +71 -28
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +224 -375
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +67 -36
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +360 -343
- holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
- holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
- holmes/plugins/toolsets/git.py +7 -8
- holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
- holmes/plugins/toolsets/grafana/common.py +2 -30
- holmes/plugins/toolsets/grafana/grafana_tempo_api.py +2 -1
- holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +18 -2
- holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +92 -18
- holmes/plugins/toolsets/grafana/loki_api.py +4 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +109 -25
- holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +22 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +201 -33
- holmes/plugins/toolsets/grafana/trace_parser.py +3 -2
- holmes/plugins/toolsets/internet/internet.py +10 -10
- holmes/plugins/toolsets/internet/notion.py +5 -6
- holmes/plugins/toolsets/investigator/core_investigation.py +3 -3
- holmes/plugins/toolsets/investigator/model.py +3 -1
- holmes/plugins/toolsets/json_filter_mixin.py +134 -0
- holmes/plugins/toolsets/kafka.py +12 -7
- holmes/plugins/toolsets/kubernetes.yaml +260 -30
- holmes/plugins/toolsets/kubernetes_logs.py +3 -3
- holmes/plugins/toolsets/logging_utils/logging_api.py +16 -6
- holmes/plugins/toolsets/mcp/toolset_mcp.py +88 -60
- holmes/plugins/toolsets/newrelic/new_relic_api.py +41 -1
- holmes/plugins/toolsets/newrelic/newrelic.jinja2 +24 -0
- holmes/plugins/toolsets/newrelic/newrelic.py +212 -55
- holmes/plugins/toolsets/prometheus/prometheus.py +358 -102
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +11 -3
- holmes/plugins/toolsets/rabbitmq/api.py +23 -4
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +5 -5
- holmes/plugins/toolsets/robusta/robusta.py +5 -5
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +25 -6
- holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +1 -1
- holmes/plugins/toolsets/utils.py +1 -1
- holmes/utils/config_utils.py +1 -1
- holmes/utils/connection_utils.py +31 -0
- holmes/utils/console/result.py +10 -0
- holmes/utils/file_utils.py +2 -1
- holmes/utils/global_instructions.py +10 -26
- holmes/utils/holmes_status.py +4 -3
- holmes/utils/log.py +15 -0
- holmes/utils/markdown_utils.py +2 -3
- holmes/utils/memory_limit.py +58 -0
- holmes/utils/sentry_helper.py +23 -0
- holmes/utils/stream.py +12 -5
- holmes/utils/tags.py +4 -3
- holmes/version.py +3 -1
- {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +12 -10
- holmesgpt-0.18.4.dist-info/RECORD +258 -0
- holmes/plugins/toolsets/aws.yaml +0 -80
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -114
- holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
- holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -736
- holmes/plugins/toolsets/grafana/grafana_api.py +0 -64
- holmes/plugins/toolsets/opensearch/__init__.py +0 -0
- holmes/plugins/toolsets/opensearch/opensearch.py +0 -250
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -215
- holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
- holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
- holmes/utils/keygen_utils.py +0 -6
- holmesgpt-0.16.2a0.dist-info/RECORD +0 -258
- holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_ppl_query_docs.jinja2 +0 -0
- holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist.py +2 -2
- /holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist_instructions.jinja2 +0 -0
- {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/LICENSE +0 -0
- {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
- {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
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
2
3
|
import logging
|
|
3
|
-
|
|
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
|
|
17
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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=
|
|
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:
|
|
112
|
-
nr_account_id:
|
|
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
|
-
|
|
151
|
-
|
|
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
|
}
|