holmesgpt 0.13.2__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 (188) hide show
  1. holmes/__init__.py +3 -5
  2. holmes/clients/robusta_client.py +20 -6
  3. holmes/common/env_vars.py +58 -3
  4. holmes/common/openshift.py +1 -1
  5. holmes/config.py +123 -148
  6. holmes/core/conversations.py +71 -15
  7. holmes/core/feedback.py +191 -0
  8. holmes/core/investigation.py +31 -39
  9. holmes/core/investigation_structured_output.py +3 -3
  10. holmes/core/issue.py +1 -1
  11. holmes/core/llm.py +508 -88
  12. holmes/core/models.py +108 -4
  13. holmes/core/openai_formatting.py +14 -1
  14. holmes/core/prompt.py +48 -3
  15. holmes/core/runbooks.py +1 -0
  16. holmes/core/safeguards.py +8 -6
  17. holmes/core/supabase_dal.py +295 -100
  18. holmes/core/tool_calling_llm.py +489 -428
  19. holmes/core/tools.py +325 -56
  20. holmes/core/tools_utils/token_counting.py +21 -0
  21. holmes/core/tools_utils/tool_context_window_limiter.py +40 -0
  22. holmes/core/tools_utils/tool_executor.py +0 -13
  23. holmes/core/tools_utils/toolset_utils.py +1 -0
  24. holmes/core/toolset_manager.py +191 -5
  25. holmes/core/tracing.py +19 -3
  26. holmes/core/transformers/__init__.py +23 -0
  27. holmes/core/transformers/base.py +63 -0
  28. holmes/core/transformers/llm_summarize.py +175 -0
  29. holmes/core/transformers/registry.py +123 -0
  30. holmes/core/transformers/transformer.py +32 -0
  31. holmes/core/truncation/compaction.py +94 -0
  32. holmes/core/truncation/dal_truncation_utils.py +23 -0
  33. holmes/core/truncation/input_context_window_limiter.py +219 -0
  34. holmes/interactive.py +228 -31
  35. holmes/main.py +23 -40
  36. holmes/plugins/interfaces.py +2 -1
  37. holmes/plugins/prompts/__init__.py +2 -1
  38. holmes/plugins/prompts/_fetch_logs.jinja2 +31 -6
  39. holmes/plugins/prompts/_general_instructions.jinja2 +1 -2
  40. holmes/plugins/prompts/_runbook_instructions.jinja2 +24 -12
  41. holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
  42. holmes/plugins/prompts/conversation_history_compaction.jinja2 +89 -0
  43. holmes/plugins/prompts/generic_ask.jinja2 +0 -4
  44. holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -1
  45. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -1
  46. holmes/plugins/prompts/generic_investigation.jinja2 +0 -1
  47. holmes/plugins/prompts/investigation_procedure.jinja2 +50 -1
  48. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -1
  49. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -1
  50. holmes/plugins/runbooks/__init__.py +145 -17
  51. holmes/plugins/runbooks/catalog.json +2 -0
  52. holmes/plugins/sources/github/__init__.py +4 -2
  53. holmes/plugins/sources/prometheus/models.py +1 -0
  54. holmes/plugins/toolsets/__init__.py +44 -27
  55. holmes/plugins/toolsets/aks-node-health.yaml +46 -0
  56. holmes/plugins/toolsets/aks.yaml +64 -0
  57. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +38 -47
  58. holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +3 -2
  59. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
  60. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +3 -2
  61. holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +3 -1
  62. holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +3 -1
  63. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +12 -13
  64. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +15 -12
  65. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +15 -12
  66. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +11 -11
  67. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +11 -9
  68. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +15 -12
  69. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +15 -15
  70. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +11 -8
  71. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +11 -8
  72. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +11 -8
  73. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +11 -8
  74. holmes/plugins/toolsets/azure_sql/utils.py +0 -32
  75. holmes/plugins/toolsets/bash/argocd/__init__.py +3 -3
  76. holmes/plugins/toolsets/bash/aws/__init__.py +4 -4
  77. holmes/plugins/toolsets/bash/azure/__init__.py +4 -4
  78. holmes/plugins/toolsets/bash/bash_toolset.py +11 -15
  79. holmes/plugins/toolsets/bash/common/bash.py +23 -13
  80. holmes/plugins/toolsets/bash/common/bash_command.py +1 -1
  81. holmes/plugins/toolsets/bash/common/stringify.py +1 -1
  82. holmes/plugins/toolsets/bash/kubectl/__init__.py +2 -1
  83. holmes/plugins/toolsets/bash/kubectl/constants.py +0 -1
  84. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +3 -4
  85. holmes/plugins/toolsets/bash/parse_command.py +12 -13
  86. holmes/plugins/toolsets/cilium.yaml +284 -0
  87. holmes/plugins/toolsets/connectivity_check.py +124 -0
  88. holmes/plugins/toolsets/coralogix/api.py +132 -119
  89. holmes/plugins/toolsets/coralogix/coralogix.jinja2 +14 -0
  90. holmes/plugins/toolsets/coralogix/toolset_coralogix.py +219 -0
  91. holmes/plugins/toolsets/coralogix/utils.py +15 -79
  92. holmes/plugins/toolsets/datadog/datadog_api.py +525 -26
  93. holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +55 -11
  94. holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +3 -3
  95. holmes/plugins/toolsets/datadog/datadog_models.py +59 -0
  96. holmes/plugins/toolsets/datadog/datadog_url_utils.py +213 -0
  97. holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +165 -28
  98. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +417 -241
  99. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +234 -214
  100. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +167 -79
  101. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +374 -363
  102. holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
  103. holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
  104. holmes/plugins/toolsets/elasticsearch/opensearch_ppl_query_docs.jinja2 +1616 -0
  105. holmes/plugins/toolsets/elasticsearch/opensearch_query_assist.py +78 -0
  106. holmes/plugins/toolsets/elasticsearch/opensearch_query_assist_instructions.jinja2 +223 -0
  107. holmes/plugins/toolsets/git.py +54 -50
  108. holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
  109. holmes/plugins/toolsets/grafana/common.py +13 -29
  110. holmes/plugins/toolsets/grafana/grafana_tempo_api.py +455 -0
  111. holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +25 -0
  112. holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +191 -0
  113. holmes/plugins/toolsets/grafana/loki_api.py +4 -0
  114. holmes/plugins/toolsets/grafana/toolset_grafana.py +293 -89
  115. holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +49 -0
  116. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
  117. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +820 -292
  118. holmes/plugins/toolsets/grafana/trace_parser.py +4 -3
  119. holmes/plugins/toolsets/internet/internet.py +15 -16
  120. holmes/plugins/toolsets/internet/notion.py +9 -11
  121. holmes/plugins/toolsets/investigator/core_investigation.py +44 -36
  122. holmes/plugins/toolsets/investigator/model.py +3 -1
  123. holmes/plugins/toolsets/json_filter_mixin.py +134 -0
  124. holmes/plugins/toolsets/kafka.py +36 -42
  125. holmes/plugins/toolsets/kubernetes.yaml +317 -113
  126. holmes/plugins/toolsets/kubernetes_logs.py +9 -9
  127. holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
  128. holmes/plugins/toolsets/logging_utils/logging_api.py +94 -8
  129. holmes/plugins/toolsets/mcp/toolset_mcp.py +218 -64
  130. holmes/plugins/toolsets/newrelic/new_relic_api.py +165 -0
  131. holmes/plugins/toolsets/newrelic/newrelic.jinja2 +65 -0
  132. holmes/plugins/toolsets/newrelic/newrelic.py +320 -0
  133. holmes/plugins/toolsets/openshift.yaml +283 -0
  134. holmes/plugins/toolsets/prometheus/prometheus.py +1202 -421
  135. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +54 -5
  136. holmes/plugins/toolsets/prometheus/utils.py +28 -0
  137. holmes/plugins/toolsets/rabbitmq/api.py +23 -4
  138. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +13 -14
  139. holmes/plugins/toolsets/robusta/robusta.py +239 -68
  140. holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +26 -9
  141. holmes/plugins/toolsets/runbook/runbook_fetcher.py +157 -27
  142. holmes/plugins/toolsets/service_discovery.py +1 -1
  143. holmes/plugins/toolsets/servicenow_tables/instructions.jinja2 +83 -0
  144. holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +426 -0
  145. holmes/plugins/toolsets/utils.py +88 -0
  146. holmes/utils/config_utils.py +91 -0
  147. holmes/utils/connection_utils.py +31 -0
  148. holmes/utils/console/result.py +10 -0
  149. holmes/utils/default_toolset_installation_guide.jinja2 +1 -22
  150. holmes/utils/env.py +7 -0
  151. holmes/utils/file_utils.py +2 -1
  152. holmes/utils/global_instructions.py +60 -11
  153. holmes/utils/holmes_status.py +6 -4
  154. holmes/utils/holmes_sync_toolsets.py +0 -2
  155. holmes/utils/krr_utils.py +188 -0
  156. holmes/utils/log.py +15 -0
  157. holmes/utils/markdown_utils.py +2 -3
  158. holmes/utils/memory_limit.py +58 -0
  159. holmes/utils/sentry_helper.py +64 -0
  160. holmes/utils/stream.py +69 -8
  161. holmes/utils/tags.py +4 -3
  162. holmes/version.py +37 -15
  163. holmesgpt-0.18.4.dist-info/LICENSE +178 -0
  164. {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +35 -31
  165. holmesgpt-0.18.4.dist-info/RECORD +258 -0
  166. holmes/core/performance_timing.py +0 -72
  167. holmes/plugins/toolsets/aws.yaml +0 -80
  168. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -112
  169. holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
  170. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -739
  171. holmes/plugins/toolsets/grafana/grafana_api.py +0 -42
  172. holmes/plugins/toolsets/grafana/tempo_api.py +0 -124
  173. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +0 -110
  174. holmes/plugins/toolsets/newrelic.py +0 -231
  175. holmes/plugins/toolsets/opensearch/opensearch.py +0 -257
  176. holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
  177. holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -218
  178. holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
  179. holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
  180. holmes/plugins/toolsets/servicenow/install.md +0 -37
  181. holmes/plugins/toolsets/servicenow/instructions.jinja2 +0 -3
  182. holmes/plugins/toolsets/servicenow/servicenow.py +0 -219
  183. holmes/utils/keygen_utils.py +0 -6
  184. holmesgpt-0.13.2.dist-info/LICENSE.txt +0 -21
  185. holmesgpt-0.13.2.dist-info/RECORD +0 -234
  186. /holmes/plugins/toolsets/{opensearch → newrelic}/__init__.py +0 -0
  187. {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
  188. {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,191 @@
1
+ import json
2
+ import os
3
+ from typing import Dict, Optional, Tuple
4
+ from urllib.parse import quote
5
+
6
+ from holmes.core.tools import (
7
+ StructuredToolResult,
8
+ StructuredToolResultStatus,
9
+ Tool,
10
+ ToolInvokeContext,
11
+ ToolParameter,
12
+ )
13
+ from holmes.plugins.toolsets.consts import (
14
+ STANDARD_END_DATETIME_TOOL_PARAM_DESCRIPTION,
15
+ )
16
+ from holmes.plugins.toolsets.grafana.common import GrafanaConfig, get_base_url
17
+ from holmes.plugins.toolsets.grafana.loki_api import (
18
+ execute_loki_query,
19
+ )
20
+ from holmes.plugins.toolsets.grafana.toolset_grafana import BaseGrafanaToolset
21
+ from holmes.plugins.toolsets.logging_utils.logging_api import (
22
+ DEFAULT_LOG_LIMIT,
23
+ DEFAULT_TIME_SPAN_SECONDS,
24
+ )
25
+ from holmes.plugins.toolsets.utils import (
26
+ process_timestamps_to_rfc3339,
27
+ standard_start_datetime_tool_param_description,
28
+ toolset_name_for_one_liner,
29
+ )
30
+
31
+
32
+ def _build_grafana_loki_explore_url(
33
+ config: GrafanaConfig, query: str, start: str, end: str, limit: int = 100
34
+ ) -> Optional[str]:
35
+ if not config.grafana_datasource_uid:
36
+ return None
37
+ try:
38
+ base_url = config.external_url or config.url
39
+ datasource_uid = config.grafana_datasource_uid or "loki"
40
+
41
+ from_str = start if start else "now-1h"
42
+ to_str = end if end else "now"
43
+
44
+ pane_id = "tmp"
45
+ safe_query = query if query else "{}"
46
+ panes = {
47
+ pane_id: {
48
+ "datasource": datasource_uid,
49
+ "queries": [
50
+ {
51
+ "refId": "A",
52
+ "datasource": {"type": "loki", "uid": datasource_uid},
53
+ "expr": safe_query,
54
+ "queryType": "range",
55
+ "maxLines": limit,
56
+ }
57
+ ],
58
+ "range": {"from": from_str, "to": to_str},
59
+ }
60
+ }
61
+
62
+ panes_encoded = quote(
63
+ json.dumps(panes, separators=(",", ":"), ensure_ascii=False), safe=""
64
+ )
65
+ return f"{base_url}/explore?schemaVersion=1&panes={panes_encoded}&orgId=1"
66
+ except Exception:
67
+ return None
68
+
69
+
70
+ class GrafanaLokiToolset(BaseGrafanaToolset):
71
+ def health_check(self) -> Tuple[bool, str]:
72
+ """Test a dummy query to check if service available."""
73
+ (start, end) = process_timestamps_to_rfc3339(
74
+ start_timestamp=-1,
75
+ end_timestamp=None,
76
+ default_time_span_seconds=DEFAULT_TIME_SPAN_SECONDS,
77
+ )
78
+
79
+ c = self._grafana_config
80
+ try:
81
+ _ = execute_loki_query(
82
+ base_url=get_base_url(c),
83
+ api_key=c.api_key,
84
+ headers=c.headers,
85
+ query='{job="test_endpoint"}',
86
+ start=start,
87
+ end=end,
88
+ limit=1,
89
+ verify_ssl=c.verify_ssl,
90
+ )
91
+ except Exception as e:
92
+ return False, f"Unable to connect to Loki.\n{str(e)}"
93
+ return True, ""
94
+
95
+ def __init__(self):
96
+ super().__init__(
97
+ name="grafana/loki",
98
+ description="Runs loki log queries using Grafana Loki or Loki directly.",
99
+ icon_url="https://grafana.com/media/docs/loki/logo-grafana-loki.png",
100
+ docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/grafanaloki/",
101
+ tools=[],
102
+ )
103
+
104
+ self.tools = [LokiQuery(toolset=self)]
105
+ instructions_filepath = os.path.abspath(
106
+ os.path.join(os.path.dirname(__file__), "instructions.jinja2")
107
+ )
108
+ self._load_llm_instructions(jinja_template=f"file://{instructions_filepath}")
109
+
110
+
111
+ class LokiQuery(Tool):
112
+ toolset: GrafanaLokiToolset
113
+ name: str = "grafana_loki_query"
114
+ description: str = "Run a query against Grafana Loki using LogQL query language."
115
+ parameters: Dict[str, ToolParameter] = {
116
+ "query": ToolParameter(
117
+ description="LogQL query string.",
118
+ type="string",
119
+ required=True,
120
+ ),
121
+ "start": ToolParameter(
122
+ description=standard_start_datetime_tool_param_description(
123
+ DEFAULT_TIME_SPAN_SECONDS
124
+ ),
125
+ type="string",
126
+ required=False,
127
+ ),
128
+ "end": ToolParameter(
129
+ description=STANDARD_END_DATETIME_TOOL_PARAM_DESCRIPTION,
130
+ type="string",
131
+ required=False,
132
+ ),
133
+ "limit": ToolParameter(
134
+ description=f"Maximum number of entries to return (default: {DEFAULT_LOG_LIMIT})",
135
+ type="integer",
136
+ required=False,
137
+ ),
138
+ }
139
+
140
+ def get_parameterized_one_liner(self, params) -> str:
141
+ return f"{toolset_name_for_one_liner(self.toolset.name)}: loki query {params}"
142
+
143
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
144
+ (start, end) = process_timestamps_to_rfc3339(
145
+ start_timestamp=params.get("start"),
146
+ end_timestamp=params.get("end"),
147
+ default_time_span_seconds=DEFAULT_TIME_SPAN_SECONDS,
148
+ )
149
+
150
+ config = self.toolset._grafana_config
151
+ query_str = params.get("query", '{query="no_query_fallback"}')
152
+ try:
153
+ data = execute_loki_query(
154
+ base_url=get_base_url(config),
155
+ api_key=config.api_key,
156
+ headers=config.headers,
157
+ query=query_str,
158
+ start=start,
159
+ end=end,
160
+ limit=params.get("limit") or DEFAULT_LOG_LIMIT,
161
+ verify_ssl=config.verify_ssl,
162
+ )
163
+
164
+ explore_url = _build_grafana_loki_explore_url(
165
+ config,
166
+ query_str,
167
+ start,
168
+ end,
169
+ limit=params.get("limit") or DEFAULT_LOG_LIMIT,
170
+ )
171
+
172
+ if data:
173
+ return StructuredToolResult(
174
+ status=StructuredToolResultStatus.SUCCESS,
175
+ data=data,
176
+ params=params,
177
+ url=explore_url,
178
+ )
179
+ else:
180
+ return StructuredToolResult(
181
+ status=StructuredToolResultStatus.NO_DATA,
182
+ params=params,
183
+ url=explore_url,
184
+ )
185
+ except Exception as e:
186
+ return StructuredToolResult(
187
+ status=StructuredToolResultStatus.ERROR,
188
+ params=params,
189
+ error=str(e),
190
+ url=f"{get_base_url(config)}/loki/api/v1/query_range",
191
+ )
@@ -42,6 +42,7 @@ def execute_loki_query(
42
42
  start: Union[int, str],
43
43
  end: Union[int, str],
44
44
  limit: int,
45
+ verify_ssl: bool = True,
45
46
  ) -> List[Dict]:
46
47
  params = {"query": query, "limit": limit, "start": start, "end": end}
47
48
  try:
@@ -50,6 +51,7 @@ def execute_loki_query(
50
51
  url,
51
52
  headers=build_headers(api_key=api_key, additional_headers=headers),
52
53
  params=params, # type: ignore
54
+ verify=verify_ssl,
53
55
  )
54
56
  response.raise_for_status()
55
57
 
@@ -74,6 +76,7 @@ def query_loki_logs_by_label(
74
76
  label: str,
75
77
  namespace_search_key: str = "namespace",
76
78
  limit: int = 200,
79
+ verify_ssl: bool = True,
77
80
  ) -> List[Dict]:
78
81
  query = f'{{{namespace_search_key}="{namespace}", {label}="{label_value}"}}'
79
82
  if filter:
@@ -86,4 +89,5 @@ def query_loki_logs_by_label(
86
89
  start=start,
87
90
  end=end,
88
91
  limit=limit,
92
+ verify_ssl=verify_ssl,
89
93
  )
@@ -1,127 +1,331 @@
1
- from typing import Dict, List
1
+ import os
2
+ from abc import ABC
3
+ from typing import Any, ClassVar, Dict, Optional, Tuple, Type, cast
2
4
  from urllib.parse import urlencode, urljoin
5
+
6
+ import requests # type: ignore
7
+
3
8
  from holmes.core.tools import (
4
9
  StructuredToolResult,
10
+ StructuredToolResultStatus,
5
11
  Tool,
12
+ ToolInvokeContext,
6
13
  ToolParameter,
7
- ToolResultStatus,
8
14
  )
9
15
  from holmes.plugins.toolsets.grafana.base_grafana_toolset import BaseGrafanaToolset
10
- import requests # type: ignore
11
- import logging
12
-
16
+ from holmes.plugins.toolsets.grafana.common import (
17
+ GrafanaConfig,
18
+ build_headers,
19
+ get_base_url,
20
+ )
21
+ from holmes.plugins.toolsets.json_filter_mixin import JsonFilterMixin
13
22
  from holmes.plugins.toolsets.utils import toolset_name_for_one_liner
14
23
 
15
24
 
16
- class ListAndBuildGrafanaDashboardURLs(Tool):
17
- def __init__(self, toolset: BaseGrafanaToolset):
25
+ class GrafanaDashboardConfig(GrafanaConfig):
26
+ """Configuration specific to Grafana Dashboard toolset."""
27
+
28
+ pass
29
+
30
+
31
+ def _build_grafana_dashboard_url(
32
+ config: GrafanaDashboardConfig,
33
+ uid: Optional[str] = None,
34
+ query_params: Optional[Dict[str, Any]] = None,
35
+ ) -> Optional[str]:
36
+ try:
37
+ base_url = config.external_url or config.url
38
+ if uid:
39
+ return f"{base_url.rstrip('/')}/d/{uid}"
40
+ else:
41
+ query_string = urlencode(query_params, doseq=True) if query_params else ""
42
+ if query_string:
43
+ return f"{base_url.rstrip('/')}/dashboards?{query_string}"
44
+ else:
45
+ return f"{base_url.rstrip('/')}/dashboards"
46
+ except Exception:
47
+ return None
48
+
49
+
50
+ class GrafanaToolset(BaseGrafanaToolset):
51
+ config_class: ClassVar[Type[GrafanaDashboardConfig]] = GrafanaDashboardConfig
52
+
53
+ def __init__(self):
18
54
  super().__init__(
19
- name="list_and_build_grafana_dashboard_urls",
20
- description="Lists all available Grafana dashboard urls",
55
+ name="grafana/dashboards",
56
+ description="Provides tools for interacting with Grafana dashboards",
57
+ icon_url="https://w7.pngwing.com/pngs/434/923/png-transparent-grafana-hd-logo-thumbnail.png",
58
+ docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/grafanadashboards/",
59
+ tools=[
60
+ SearchDashboards(self),
61
+ GetDashboardByUID(self),
62
+ GetHomeDashboard(self),
63
+ GetDashboardTags(self),
64
+ ],
65
+ )
66
+
67
+ self._load_llm_instructions_from_file(
68
+ os.path.dirname(__file__), "toolset_grafana_dashboard.jinja2"
69
+ )
70
+
71
+ def health_check(self) -> Tuple[bool, str]:
72
+ """Test connectivity by invoking GetDashboardTags tool."""
73
+ tool = GetDashboardTags(self)
74
+ try:
75
+ _ = tool._make_grafana_request("api/dashboards/tags", {})
76
+ return True, ""
77
+ except Exception as e:
78
+ return False, f"Failed to connect to Grafana {str(e)}"
79
+
80
+ @property
81
+ def grafana_config(self) -> GrafanaDashboardConfig:
82
+ return cast(GrafanaDashboardConfig, self._grafana_config)
83
+
84
+
85
+ class BaseGrafanaTool(Tool, ABC):
86
+ """Base class for Grafana tools with common HTTP request functionality."""
87
+
88
+ def __init__(self, toolset: GrafanaToolset, *args, **kwargs):
89
+ super().__init__(*args, **kwargs)
90
+ self._toolset = toolset
91
+
92
+ def _make_grafana_request(
93
+ self,
94
+ endpoint: str,
95
+ params: dict,
96
+ query_params: Optional[Dict] = None,
97
+ timeout: int = 30,
98
+ ) -> StructuredToolResult:
99
+ """Make a GET request to Grafana API and return structured result.
100
+
101
+ Args:
102
+ endpoint: API endpoint path (e.g., "/api/search")
103
+ params: Original parameters passed to the tool
104
+ query_params: Optional query parameters for the request
105
+
106
+ Returns:
107
+ StructuredToolResult with the API response data
108
+ """
109
+ base_url = get_base_url(self._toolset.grafana_config)
110
+ if not base_url.endswith("/"):
111
+ base_url += "/"
112
+ url = urljoin(base_url, endpoint)
113
+ headers = build_headers(
114
+ api_key=self._toolset.grafana_config.api_key,
115
+ additional_headers=self._toolset.grafana_config.headers,
116
+ )
117
+
118
+ response = requests.get(
119
+ url,
120
+ headers=headers,
121
+ params=query_params,
122
+ timeout=timeout,
123
+ verify=self._toolset.grafana_config.verify_ssl,
124
+ )
125
+ response.raise_for_status()
126
+ data = response.json()
127
+
128
+ return StructuredToolResult(
129
+ status=StructuredToolResultStatus.SUCCESS,
130
+ data=data,
131
+ url=url,
132
+ params=params,
133
+ )
134
+
135
+
136
+ class SearchDashboards(BaseGrafanaTool):
137
+ def __init__(self, toolset: GrafanaToolset):
138
+ super().__init__(
139
+ toolset=toolset,
140
+ name="grafana_search_dashboards",
141
+ description="Search for Grafana dashboards and folders using the /api/search endpoint",
21
142
  parameters={
22
- "cluster_name": ToolParameter(
23
- description="The cluster name. Defaults to None.",
143
+ "query": ToolParameter(
144
+ description="Search text to filter dashboards",
145
+ type="string",
146
+ required=False,
147
+ ),
148
+ "tag": ToolParameter(
149
+ description="Search dashboards by tag",
24
150
  type="string",
25
151
  required=False,
26
152
  ),
27
- "namespace": ToolParameter(
28
- description="The namespace for filtering dashboards.",
153
+ "type": ToolParameter(
154
+ description="Filter by type: 'dash-folder' or 'dash-db'",
29
155
  type="string",
30
156
  required=False,
31
157
  ),
32
- "node_name": ToolParameter(
33
- description="The node name to filter for node-related dashboards.",
158
+ "dashboardIds": ToolParameter(
159
+ description="List of dashboard IDs to filter (comma-separated)",
34
160
  type="string",
35
161
  required=False,
36
162
  ),
37
- "pod_name": ToolParameter(
38
- description="The pod name to filter dashboards.",
163
+ "dashboardUIDs": ToolParameter(
164
+ description="List of dashboard UIDs to search for (comma-separated)",
39
165
  type="string",
40
166
  required=False,
41
167
  ),
168
+ "folderUIDs": ToolParameter(
169
+ description="List of folder UIDs to search within (comma-separated)",
170
+ type="string",
171
+ required=False,
172
+ ),
173
+ "starred": ToolParameter(
174
+ description="Return only starred dashboards",
175
+ type="boolean",
176
+ required=False,
177
+ ),
178
+ "limit": ToolParameter(
179
+ description="Maximum results (default 1000, max 5000)",
180
+ type="integer",
181
+ required=False,
182
+ ),
183
+ "page": ToolParameter(
184
+ description="Page number for pagination",
185
+ type="integer",
186
+ required=False,
187
+ ),
42
188
  },
43
189
  )
44
- self._toolset = toolset
45
190
 
46
- def _invoke(
47
- self, params: dict, user_approved: bool = False
48
- ) -> StructuredToolResult:
49
- url = urljoin(
50
- self._toolset._grafana_config.url, "/api/search?query=&type=dash-db"
191
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
192
+ query_params = {}
193
+ if params.get("query"):
194
+ query_params["query"] = params["query"]
195
+ if params.get("tag"):
196
+ query_params["tag"] = params["tag"]
197
+ if params.get("type"):
198
+ query_params["type"] = params["type"]
199
+ if params.get("dashboardIds"):
200
+ # Check if dashboardIds also needs to be passed as multiple params
201
+ dashboard_ids = params["dashboardIds"].split(",")
202
+ query_params["dashboardIds"] = [
203
+ dashboard_id.strip()
204
+ for dashboard_id in dashboard_ids
205
+ if dashboard_id.strip()
206
+ ]
207
+ if params.get("dashboardUIDs"):
208
+ # Handle dashboardUIDs as a list - split comma-separated values
209
+ dashboard_uids = params["dashboardUIDs"].split(",")
210
+ query_params["dashboardUIDs"] = [
211
+ uid.strip() for uid in dashboard_uids if uid.strip()
212
+ ]
213
+ if params.get("folderUIDs"):
214
+ # Check if folderUIDs also needs to be passed as multiple params
215
+ folder_uids = params["folderUIDs"].split(",")
216
+ query_params["folderUIDs"] = [
217
+ uid.strip() for uid in folder_uids if uid.strip()
218
+ ]
219
+ if params.get("starred") is not None:
220
+ query_params["starred"] = str(params["starred"]).lower()
221
+ if params.get("limit"):
222
+ query_params["limit"] = params["limit"]
223
+ if params.get("page"):
224
+ query_params["page"] = params["page"]
225
+
226
+ result = self._make_grafana_request("api/search", params, query_params)
227
+
228
+ config = self._toolset.grafana_config
229
+ search_url = _build_grafana_dashboard_url(config, query_params=query_params)
230
+
231
+ if params.get("dashboardUIDs"):
232
+ uids = [
233
+ uid.strip() for uid in params["dashboardUIDs"].split(",") if uid.strip()
234
+ ]
235
+ if len(uids) == 1:
236
+ search_url = _build_grafana_dashboard_url(config, uid=uids[0])
237
+
238
+ return StructuredToolResult(
239
+ status=result.status,
240
+ data=result.data,
241
+ params=result.params,
242
+ url=search_url if search_url else None,
51
243
  )
52
- headers = {"Authorization": f"Bearer {self._toolset._grafana_config.api_key}"}
53
244
 
54
- try:
55
- response = requests.get(url, headers=headers)
56
- response.raise_for_status()
57
- dashboards = response.json()
58
- formatted_dashboards: List[str] = []
59
- base_url = (
60
- self._toolset._grafana_config.external_url
61
- or self._toolset._grafana_config.url
62
- )
63
- for dash in dashboards:
64
- dashboard_url = urljoin(
65
- base_url,
66
- f"/d/{dash['uid']}/{dash['uri'].split('/')[-1]}",
67
- )
68
-
69
- params_dict = {
70
- "var-cluster": params.get("cluster_name", ""),
71
- "var-namespace": params.get("namespace", ""),
72
- "var-pod": params.get("pod_name", ""),
73
- "var-node": params.get("node_name", ""),
74
- "var-datasource": self._toolset._grafana_config.grafana_datasource_uid,
75
- "refresh": "5s",
245
+ def get_parameterized_one_liner(self, params: Dict) -> str:
246
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Search Dashboards"
247
+
248
+
249
+ class GetDashboardByUID(JsonFilterMixin, BaseGrafanaTool):
250
+ def __init__(self, toolset: GrafanaToolset):
251
+ super().__init__(
252
+ toolset=toolset,
253
+ name="grafana_get_dashboard_by_uid",
254
+ description="Get a dashboard by its UID using the /api/dashboards/uid/:uid endpoint",
255
+ parameters=self.extend_parameters(
256
+ {
257
+ "uid": ToolParameter(
258
+ description="The unique identifier of the dashboard",
259
+ type="string",
260
+ required=True,
261
+ )
76
262
  }
263
+ ),
264
+ )
77
265
 
78
- # If filtering for nodes, ensure only node-related dashboards are included
79
- if params.get("node_name") and "node" not in dash["title"].lower():
80
- continue
81
-
82
- # we add all params since if the dashboard isnt configured for a param it will ignore it if it is added
83
- query_string = urlencode({k: v for k, v in params_dict.items() if v})
84
- dashboard_url = (
85
- f"{dashboard_url}?{query_string}" if query_string else dashboard_url
86
- )
87
-
88
- formatted_dashboards.append(
89
- f"Title: {dash['title']}\nURL: {dashboard_url}\n"
90
- )
91
-
92
- return StructuredToolResult(
93
- status=ToolResultStatus.SUCCESS
94
- if formatted_dashboards
95
- else ToolResultStatus.NO_DATA,
96
- data="\n".join(formatted_dashboards)
97
- if formatted_dashboards
98
- else "No dashboards found.",
99
- url=url,
100
- params=params,
101
- )
102
- except requests.RequestException as e:
103
- logging.error(f"Error fetching dashboards: {str(e)}")
104
- return StructuredToolResult(
105
- status=ToolResultStatus.ERROR,
106
- error=f"Error fetching dashboards: {str(e)}",
107
- url=url,
108
- params=params,
109
- )
266
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
267
+ uid = params["uid"]
268
+ result = self._make_grafana_request(f"api/dashboards/uid/{uid}", params)
269
+
270
+ dashboard_url = _build_grafana_dashboard_url(
271
+ self._toolset.grafana_config, uid=uid
272
+ )
273
+
274
+ filtered_result = self.filter_result(result, params)
275
+ filtered_result.url = dashboard_url if dashboard_url else result.url
276
+ return filtered_result
110
277
 
111
278
  def get_parameterized_one_liner(self, params: Dict) -> str:
112
- return (
113
- f"{toolset_name_for_one_liner(self._toolset.name)}: List Grafana Dashboards"
279
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Get Dashboard {params.get('uid', '')}"
280
+
281
+
282
+ class GetHomeDashboard(JsonFilterMixin, BaseGrafanaTool):
283
+ def __init__(self, toolset: GrafanaToolset):
284
+ super().__init__(
285
+ toolset=toolset,
286
+ name="grafana_get_home_dashboard",
287
+ description="Get the home dashboard using the /api/dashboards/home endpoint",
288
+ parameters=self.extend_parameters({}),
114
289
  )
115
290
 
291
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
292
+ result = self._make_grafana_request("api/dashboards/home", params)
293
+ config = self._toolset.grafana_config
294
+ dashboard_url = None
295
+ if isinstance(result.data, dict):
296
+ uid = result.data.get("dashboard", {}).get("uid")
297
+ if uid:
298
+ dashboard_url = _build_grafana_dashboard_url(config, uid=uid)
116
299
 
117
- class GrafanaToolset(BaseGrafanaToolset):
118
- def __init__(self):
300
+ filtered_result = self.filter_result(result, params)
301
+ filtered_result.url = dashboard_url if dashboard_url else None
302
+ return filtered_result
303
+
304
+ def get_parameterized_one_liner(self, params: Dict) -> str:
305
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Get Home Dashboard"
306
+
307
+
308
+ class GetDashboardTags(BaseGrafanaTool):
309
+ def __init__(self, toolset: GrafanaToolset):
119
310
  super().__init__(
120
- name="grafana/grafana",
121
- description="Provides tools for interacting with Grafana dashboards",
122
- icon_url="https://w7.pngwing.com/pngs/434/923/png-transparent-grafana-hd-logo-thumbnail.png",
123
- docs_url="",
124
- tools=[
125
- ListAndBuildGrafanaDashboardURLs(self),
126
- ],
311
+ toolset=toolset,
312
+ name="grafana_get_dashboard_tags",
313
+ description="Get all tags used across dashboards using the /api/dashboards/tags endpoint",
314
+ parameters={},
315
+ )
316
+
317
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
318
+ result = self._make_grafana_request("api/dashboards/tags", params)
319
+
320
+ config = self._toolset.grafana_config
321
+ tags_url = _build_grafana_dashboard_url(config)
322
+
323
+ return StructuredToolResult(
324
+ status=result.status,
325
+ data=result.data,
326
+ params=result.params,
327
+ url=tags_url,
127
328
  )
329
+
330
+ def get_parameterized_one_liner(self, params: Dict) -> str:
331
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Get Dashboard Tags"