holmesgpt 0.14.1a0__py3-none-any.whl → 0.14.3a0__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.
Potentially problematic release.
This version of holmesgpt might be problematic. Click here for more details.
- holmes/__init__.py +1 -1
- holmes/clients/robusta_client.py +5 -2
- holmes/common/env_vars.py +8 -2
- holmes/config.py +4 -7
- holmes/core/conversations.py +12 -2
- holmes/core/feedback.py +191 -0
- holmes/core/llm.py +52 -10
- holmes/core/models.py +101 -1
- holmes/core/supabase_dal.py +23 -9
- holmes/core/tool_calling_llm.py +206 -16
- holmes/core/tools.py +20 -7
- holmes/core/tools_utils/token_counting.py +13 -0
- holmes/core/tools_utils/tool_context_window_limiter.py +45 -23
- holmes/core/tools_utils/tool_executor.py +11 -6
- holmes/core/toolset_manager.py +7 -3
- holmes/core/truncation/dal_truncation_utils.py +23 -0
- holmes/interactive.py +146 -14
- holmes/plugins/prompts/_fetch_logs.jinja2 +13 -1
- holmes/plugins/runbooks/__init__.py +6 -1
- holmes/plugins/toolsets/__init__.py +11 -4
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +9 -20
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +2 -3
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +2 -3
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +6 -4
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +6 -4
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +2 -3
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +6 -4
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +2 -3
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +2 -3
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +2 -3
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +2 -3
- holmes/plugins/toolsets/bash/bash_toolset.py +4 -7
- holmes/plugins/toolsets/cilium.yaml +284 -0
- holmes/plugins/toolsets/datadog/datadog_api.py +490 -24
- holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +21 -10
- holmes/plugins/toolsets/datadog/toolset_datadog_general.py +333 -199
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +181 -9
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +80 -22
- holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +5 -8
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +7 -12
- holmes/plugins/toolsets/git.py +14 -12
- holmes/plugins/toolsets/grafana/grafana_tempo_api.py +23 -42
- holmes/plugins/toolsets/grafana/toolset_grafana.py +2 -3
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +2 -1
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +21 -39
- holmes/plugins/toolsets/internet/internet.py +2 -3
- holmes/plugins/toolsets/internet/notion.py +2 -3
- holmes/plugins/toolsets/investigator/core_investigation.py +7 -9
- holmes/plugins/toolsets/kafka.py +7 -18
- holmes/plugins/toolsets/logging_utils/logging_api.py +80 -4
- holmes/plugins/toolsets/mcp/toolset_mcp.py +2 -3
- holmes/plugins/toolsets/newrelic/__init__.py +0 -0
- holmes/plugins/toolsets/newrelic/new_relic_api.py +125 -0
- holmes/plugins/toolsets/newrelic/newrelic.jinja2 +41 -0
- holmes/plugins/toolsets/newrelic/newrelic.py +211 -0
- holmes/plugins/toolsets/opensearch/opensearch.py +5 -12
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +3 -6
- holmes/plugins/toolsets/prometheus/prometheus.py +808 -419
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +27 -11
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +3 -6
- holmes/plugins/toolsets/robusta/robusta.py +4 -9
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +93 -13
- holmes/plugins/toolsets/servicenow/servicenow.py +5 -10
- holmes/utils/sentry_helper.py +1 -1
- holmes/utils/stream.py +22 -7
- holmes/version.py +34 -14
- {holmesgpt-0.14.1a0.dist-info → holmesgpt-0.14.3a0.dist-info}/METADATA +7 -9
- {holmesgpt-0.14.1a0.dist-info → holmesgpt-0.14.3a0.dist-info}/RECORD +71 -65
- holmes/core/tools_utils/data_types.py +0 -81
- holmes/plugins/toolsets/newrelic.py +0 -231
- {holmesgpt-0.14.1a0.dist-info → holmesgpt-0.14.3a0.dist-info}/LICENSE.txt +0 -0
- {holmesgpt-0.14.1a0.dist-info → holmesgpt-0.14.3a0.dist-info}/WHEEL +0 -0
- {holmesgpt-0.14.1a0.dist-info → holmesgpt-0.14.3a0.dist-info}/entry_points.txt +0 -0
|
@@ -46,21 +46,18 @@ class TempoAPIError(Exception):
|
|
|
46
46
|
class GrafanaTempoAPI:
|
|
47
47
|
"""Python wrapper for Grafana Tempo REST API.
|
|
48
48
|
|
|
49
|
-
This class provides a clean interface to all Tempo API endpoints
|
|
50
|
-
supporting both GET and POST methods based on configuration.
|
|
49
|
+
This class provides a clean interface to all Tempo API endpoints.
|
|
51
50
|
"""
|
|
52
51
|
|
|
53
|
-
def __init__(self, config: GrafanaTempoConfig
|
|
52
|
+
def __init__(self, config: GrafanaTempoConfig):
|
|
54
53
|
"""Initialize the Tempo API wrapper.
|
|
55
54
|
|
|
56
55
|
Args:
|
|
57
56
|
config: GrafanaTempoConfig instance with connection details
|
|
58
|
-
use_post: If True, use POST method for API calls. Defaults to False (GET).
|
|
59
57
|
"""
|
|
60
58
|
self.config = config
|
|
61
59
|
self.base_url = get_base_url(config)
|
|
62
60
|
self.headers = build_headers(config.api_key, config.headers)
|
|
63
|
-
self.use_post = use_post
|
|
64
61
|
|
|
65
62
|
def _make_request(
|
|
66
63
|
self,
|
|
@@ -74,7 +71,7 @@ class GrafanaTempoAPI:
|
|
|
74
71
|
|
|
75
72
|
Args:
|
|
76
73
|
endpoint: API endpoint path (e.g., "/api/echo")
|
|
77
|
-
params: Query parameters
|
|
74
|
+
params: Query parameters
|
|
78
75
|
path_params: Parameters to substitute in the endpoint path
|
|
79
76
|
timeout: Request timeout in seconds
|
|
80
77
|
retries: Number of retry attempts
|
|
@@ -101,22 +98,13 @@ class GrafanaTempoAPI:
|
|
|
101
98
|
and e.response.status_code < 500,
|
|
102
99
|
)
|
|
103
100
|
def make_request():
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
112
|
-
else:
|
|
113
|
-
# GET request with query parameters
|
|
114
|
-
response = requests.get(
|
|
115
|
-
url,
|
|
116
|
-
headers=self.headers,
|
|
117
|
-
params=params,
|
|
118
|
-
timeout=timeout,
|
|
119
|
-
)
|
|
101
|
+
# GET request with query parameters
|
|
102
|
+
response = requests.get(
|
|
103
|
+
url,
|
|
104
|
+
headers=self.headers,
|
|
105
|
+
params=params,
|
|
106
|
+
timeout=timeout,
|
|
107
|
+
)
|
|
120
108
|
response.raise_for_status()
|
|
121
109
|
return response.json()
|
|
122
110
|
|
|
@@ -145,7 +133,7 @@ class GrafanaTempoAPI:
|
|
|
145
133
|
"""Query the echo endpoint to check Tempo status.
|
|
146
134
|
|
|
147
135
|
API Endpoint: GET /api/echo
|
|
148
|
-
HTTP Method: GET
|
|
136
|
+
HTTP Method: GET
|
|
149
137
|
|
|
150
138
|
Returns:
|
|
151
139
|
bool: True if endpoint returns 200 status code, False otherwise
|
|
@@ -153,18 +141,11 @@ class GrafanaTempoAPI:
|
|
|
153
141
|
url = f"{self.base_url}/api/echo"
|
|
154
142
|
|
|
155
143
|
try:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
)
|
|
162
|
-
else:
|
|
163
|
-
response = requests.get(
|
|
164
|
-
url,
|
|
165
|
-
headers=self.headers,
|
|
166
|
-
timeout=30,
|
|
167
|
-
)
|
|
144
|
+
response = requests.get(
|
|
145
|
+
url,
|
|
146
|
+
headers=self.headers,
|
|
147
|
+
timeout=30,
|
|
148
|
+
)
|
|
168
149
|
|
|
169
150
|
# Just check status code, don't try to parse JSON
|
|
170
151
|
return response.status_code == 200
|
|
@@ -182,7 +163,7 @@ class GrafanaTempoAPI:
|
|
|
182
163
|
"""Query a trace by its ID.
|
|
183
164
|
|
|
184
165
|
API Endpoint: GET /api/v2/traces/{trace_id}
|
|
185
|
-
HTTP Method: GET
|
|
166
|
+
HTTP Method: GET
|
|
186
167
|
|
|
187
168
|
Args:
|
|
188
169
|
trace_id: The trace ID to retrieve
|
|
@@ -250,7 +231,7 @@ class GrafanaTempoAPI:
|
|
|
250
231
|
"""Search for traces using tag-based search.
|
|
251
232
|
|
|
252
233
|
API Endpoint: GET /api/search
|
|
253
|
-
HTTP Method: GET
|
|
234
|
+
HTTP Method: GET
|
|
254
235
|
|
|
255
236
|
Args:
|
|
256
237
|
tags: logfmt-encoded span/process attributes (required)
|
|
@@ -291,7 +272,7 @@ class GrafanaTempoAPI:
|
|
|
291
272
|
"""Search for traces using TraceQL query.
|
|
292
273
|
|
|
293
274
|
API Endpoint: GET /api/search
|
|
294
|
-
HTTP Method: GET
|
|
275
|
+
HTTP Method: GET
|
|
295
276
|
|
|
296
277
|
Note: minDuration and maxDuration are not supported with TraceQL queries.
|
|
297
278
|
Use the TraceQL query syntax to filter by duration instead.
|
|
@@ -326,7 +307,7 @@ class GrafanaTempoAPI:
|
|
|
326
307
|
"""Search for available tag names.
|
|
327
308
|
|
|
328
309
|
API Endpoint: GET /api/v2/search/tags
|
|
329
|
-
HTTP Method: GET
|
|
310
|
+
HTTP Method: GET
|
|
330
311
|
|
|
331
312
|
Args:
|
|
332
313
|
scope: Optional scope filter ("resource", "span", or "intrinsic")
|
|
@@ -367,7 +348,7 @@ class GrafanaTempoAPI:
|
|
|
367
348
|
"""Search for values of a specific tag with optional TraceQL filtering.
|
|
368
349
|
|
|
369
350
|
API Endpoint: GET /api/v2/search/tag/{tag}/values
|
|
370
|
-
HTTP Method: GET
|
|
351
|
+
HTTP Method: GET
|
|
371
352
|
|
|
372
353
|
Args:
|
|
373
354
|
tag: The tag name to get values for (required)
|
|
@@ -410,7 +391,7 @@ class GrafanaTempoAPI:
|
|
|
410
391
|
Computes a single value across the entire time range.
|
|
411
392
|
|
|
412
393
|
API Endpoint: GET /api/metrics/query
|
|
413
|
-
HTTP Method: GET
|
|
394
|
+
HTTP Method: GET
|
|
414
395
|
|
|
415
396
|
Args:
|
|
416
397
|
q: TraceQL metrics query (required)
|
|
@@ -445,7 +426,7 @@ class GrafanaTempoAPI:
|
|
|
445
426
|
Returns metrics computed at regular intervals over the time range.
|
|
446
427
|
|
|
447
428
|
API Endpoint: GET /api/metrics/query_range
|
|
448
|
-
HTTP Method: GET
|
|
429
|
+
HTTP Method: GET
|
|
449
430
|
|
|
450
431
|
Args:
|
|
451
432
|
q: TraceQL metrics query (required)
|
|
@@ -3,6 +3,7 @@ from urllib.parse import urlencode, urljoin
|
|
|
3
3
|
from holmes.core.tools import (
|
|
4
4
|
StructuredToolResult,
|
|
5
5
|
Tool,
|
|
6
|
+
ToolInvokeContext,
|
|
6
7
|
ToolParameter,
|
|
7
8
|
StructuredToolResultStatus,
|
|
8
9
|
)
|
|
@@ -43,9 +44,7 @@ class ListAndBuildGrafanaDashboardURLs(Tool):
|
|
|
43
44
|
)
|
|
44
45
|
self._toolset = toolset
|
|
45
46
|
|
|
46
|
-
def _invoke(
|
|
47
|
-
self, params: dict, user_approved: bool = False
|
|
48
|
-
) -> StructuredToolResult:
|
|
47
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
49
48
|
url = urljoin(
|
|
50
49
|
self._toolset._grafana_config.url, "/api/search?query=&type=dash-db"
|
|
51
50
|
)
|
|
@@ -14,6 +14,7 @@ from holmes.plugins.toolsets.logging_utils.logging_api import (
|
|
|
14
14
|
LoggingCapability,
|
|
15
15
|
PodLoggingTool,
|
|
16
16
|
DEFAULT_TIME_SPAN_SECONDS,
|
|
17
|
+
DEFAULT_LOG_LIMIT,
|
|
17
18
|
)
|
|
18
19
|
from holmes.plugins.toolsets.utils import (
|
|
19
20
|
process_timestamps_to_rfc3339,
|
|
@@ -94,7 +95,7 @@ class GrafanaLokiToolset(BasePodLoggingToolset):
|
|
|
94
95
|
label_value=params.pod_name,
|
|
95
96
|
start=start,
|
|
96
97
|
end=end,
|
|
97
|
-
limit=params.limit or
|
|
98
|
+
limit=params.limit or DEFAULT_LOG_LIMIT,
|
|
98
99
|
)
|
|
99
100
|
if logs:
|
|
100
101
|
logs.sort(key=lambda x: x["timestamp"])
|
|
@@ -7,6 +7,7 @@ from holmes.common.env_vars import load_bool, MAX_GRAPH_POINTS
|
|
|
7
7
|
from holmes.core.tools import (
|
|
8
8
|
StructuredToolResult,
|
|
9
9
|
Tool,
|
|
10
|
+
ToolInvokeContext,
|
|
10
11
|
ToolParameter,
|
|
11
12
|
StructuredToolResultStatus,
|
|
12
13
|
)
|
|
@@ -29,7 +30,6 @@ from holmes.plugins.toolsets.utils import (
|
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
TEMPO_LABELS_ADD_PREFIX = load_bool("TEMPO_LABELS_ADD_PREFIX", True)
|
|
32
|
-
TEMPO_API_USE_POST = False # Use GET method for direct API mapping
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class BaseGrafanaTempoToolset(BaseGrafanaToolset):
|
|
@@ -56,7 +56,7 @@ class BaseGrafanaTempoToolset(BaseGrafanaToolset):
|
|
|
56
56
|
|
|
57
57
|
# Then check Tempo-specific echo endpoint
|
|
58
58
|
try:
|
|
59
|
-
api = GrafanaTempoAPI(self.grafana_config
|
|
59
|
+
api = GrafanaTempoAPI(self.grafana_config)
|
|
60
60
|
if api.query_echo_endpoint():
|
|
61
61
|
return True, "Successfully connected to Tempo"
|
|
62
62
|
else:
|
|
@@ -195,9 +195,7 @@ Examples:
|
|
|
195
195
|
|
|
196
196
|
return f"At least one of the following argument is expected but none were set: {expected_params}"
|
|
197
197
|
|
|
198
|
-
def _invoke(
|
|
199
|
-
self, params: dict, user_approved: bool = False
|
|
200
|
-
) -> StructuredToolResult:
|
|
198
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
201
199
|
try:
|
|
202
200
|
# Build query
|
|
203
201
|
if params.get("base_query"):
|
|
@@ -231,9 +229,7 @@ Examples:
|
|
|
231
229
|
start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
|
|
232
230
|
|
|
233
231
|
# Create API instance
|
|
234
|
-
api = GrafanaTempoAPI(
|
|
235
|
-
self._toolset.grafana_config, use_post=TEMPO_API_USE_POST
|
|
236
|
-
)
|
|
232
|
+
api = GrafanaTempoAPI(self._toolset.grafana_config)
|
|
237
233
|
|
|
238
234
|
# Step 1: Get all trace summaries
|
|
239
235
|
stats_query = f"{{{base_query}}}"
|
|
@@ -242,9 +238,9 @@ Examples:
|
|
|
242
238
|
import logging
|
|
243
239
|
|
|
244
240
|
logger = logging.getLogger(__name__)
|
|
245
|
-
logger.
|
|
241
|
+
logger.debug(f"Tempo query: {stats_query}")
|
|
246
242
|
|
|
247
|
-
logger.
|
|
243
|
+
logger.debug(f"start: {start}, end: {end}")
|
|
248
244
|
|
|
249
245
|
all_traces_response = api.search_traces_by_query(
|
|
250
246
|
q=stats_query,
|
|
@@ -253,7 +249,7 @@ Examples:
|
|
|
253
249
|
limit=1000,
|
|
254
250
|
)
|
|
255
251
|
|
|
256
|
-
logger.
|
|
252
|
+
logger.debug(f"Response: {all_traces_response}")
|
|
257
253
|
|
|
258
254
|
traces = all_traces_response.get("traces", [])
|
|
259
255
|
if not traces:
|
|
@@ -413,10 +409,8 @@ class SearchTracesByQuery(Tool):
|
|
|
413
409
|
)
|
|
414
410
|
self._toolset = toolset
|
|
415
411
|
|
|
416
|
-
def _invoke(
|
|
417
|
-
|
|
418
|
-
) -> StructuredToolResult:
|
|
419
|
-
api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
|
|
412
|
+
def _invoke(self, params: Dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
413
|
+
api = GrafanaTempoAPI(self._toolset.grafana_config)
|
|
420
414
|
|
|
421
415
|
start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
|
|
422
416
|
|
|
@@ -495,10 +489,8 @@ class SearchTracesByTags(Tool):
|
|
|
495
489
|
)
|
|
496
490
|
self._toolset = toolset
|
|
497
491
|
|
|
498
|
-
def _invoke(
|
|
499
|
-
|
|
500
|
-
) -> StructuredToolResult:
|
|
501
|
-
api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
|
|
492
|
+
def _invoke(self, params: Dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
493
|
+
api = GrafanaTempoAPI(self._toolset.grafana_config)
|
|
502
494
|
|
|
503
495
|
start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
|
|
504
496
|
|
|
@@ -559,10 +551,8 @@ class QueryTraceById(Tool):
|
|
|
559
551
|
)
|
|
560
552
|
self._toolset = toolset
|
|
561
553
|
|
|
562
|
-
def _invoke(
|
|
563
|
-
|
|
564
|
-
) -> StructuredToolResult:
|
|
565
|
-
api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
|
|
554
|
+
def _invoke(self, params: Dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
555
|
+
api = GrafanaTempoAPI(self._toolset.grafana_config)
|
|
566
556
|
|
|
567
557
|
start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
|
|
568
558
|
|
|
@@ -636,10 +626,8 @@ class SearchTagNames(Tool):
|
|
|
636
626
|
)
|
|
637
627
|
self._toolset = toolset
|
|
638
628
|
|
|
639
|
-
def _invoke(
|
|
640
|
-
|
|
641
|
-
) -> StructuredToolResult:
|
|
642
|
-
api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
|
|
629
|
+
def _invoke(self, params: Dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
630
|
+
api = GrafanaTempoAPI(self._toolset.grafana_config)
|
|
643
631
|
|
|
644
632
|
start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
|
|
645
633
|
|
|
@@ -714,10 +702,8 @@ class SearchTagValues(Tool):
|
|
|
714
702
|
)
|
|
715
703
|
self._toolset = toolset
|
|
716
704
|
|
|
717
|
-
def _invoke(
|
|
718
|
-
|
|
719
|
-
) -> StructuredToolResult:
|
|
720
|
-
api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
|
|
705
|
+
def _invoke(self, params: Dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
706
|
+
api = GrafanaTempoAPI(self._toolset.grafana_config)
|
|
721
707
|
|
|
722
708
|
start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
|
|
723
709
|
|
|
@@ -794,10 +780,8 @@ class QueryMetricsInstant(Tool):
|
|
|
794
780
|
)
|
|
795
781
|
self._toolset = toolset
|
|
796
782
|
|
|
797
|
-
def _invoke(
|
|
798
|
-
|
|
799
|
-
) -> StructuredToolResult:
|
|
800
|
-
api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
|
|
783
|
+
def _invoke(self, params: Dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
784
|
+
api = GrafanaTempoAPI(self._toolset.grafana_config)
|
|
801
785
|
|
|
802
786
|
start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
|
|
803
787
|
|
|
@@ -880,10 +864,8 @@ class QueryMetricsRange(Tool):
|
|
|
880
864
|
)
|
|
881
865
|
self._toolset = toolset
|
|
882
866
|
|
|
883
|
-
def _invoke(
|
|
884
|
-
|
|
885
|
-
) -> StructuredToolResult:
|
|
886
|
-
api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
|
|
867
|
+
def _invoke(self, params: Dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
868
|
+
api = GrafanaTempoAPI(self._toolset.grafana_config)
|
|
887
869
|
|
|
888
870
|
start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
|
|
889
871
|
|
|
@@ -6,6 +6,7 @@ from typing import Any, Optional, Tuple, Dict, List
|
|
|
6
6
|
from requests import RequestException, Timeout # type: ignore
|
|
7
7
|
from holmes.core.tools import (
|
|
8
8
|
Tool,
|
|
9
|
+
ToolInvokeContext,
|
|
9
10
|
ToolParameter,
|
|
10
11
|
Toolset,
|
|
11
12
|
ToolsetTag,
|
|
@@ -186,9 +187,7 @@ class FetchWebpage(Tool):
|
|
|
186
187
|
toolset=toolset, # type: ignore
|
|
187
188
|
)
|
|
188
189
|
|
|
189
|
-
def _invoke(
|
|
190
|
-
self, params: dict, user_approved: bool = False
|
|
191
|
-
) -> StructuredToolResult:
|
|
190
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
192
191
|
url: str = params["url"]
|
|
193
192
|
|
|
194
193
|
additional_headers = (
|
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
from typing import Any, Dict, Tuple
|
|
5
5
|
from holmes.core.tools import (
|
|
6
6
|
Tool,
|
|
7
|
+
ToolInvokeContext,
|
|
7
8
|
ToolParameter,
|
|
8
9
|
ToolsetTag,
|
|
9
10
|
)
|
|
@@ -44,9 +45,7 @@ class FetchNotion(Tool):
|
|
|
44
45
|
return f"https://api.notion.com/v1/blocks/{notion_id}/children"
|
|
45
46
|
return url # Return original URL if no match is found
|
|
46
47
|
|
|
47
|
-
def _invoke(
|
|
48
|
-
self, params: dict, user_approved: bool = False
|
|
49
|
-
) -> StructuredToolResult:
|
|
48
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
50
49
|
url: str = params["url"]
|
|
51
50
|
|
|
52
51
|
# Get headers from the toolset configuration
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
3
|
from typing import Any, Dict
|
|
4
|
-
|
|
5
4
|
from uuid import uuid4
|
|
5
|
+
|
|
6
6
|
from holmes.core.todo_tasks_formatter import format_tasks
|
|
7
7
|
from holmes.core.tools import (
|
|
8
|
-
Toolset,
|
|
9
|
-
ToolsetTag,
|
|
10
|
-
ToolParameter,
|
|
11
|
-
Tool,
|
|
12
8
|
StructuredToolResult,
|
|
13
9
|
StructuredToolResultStatus,
|
|
10
|
+
Tool,
|
|
11
|
+
ToolInvokeContext,
|
|
12
|
+
ToolParameter,
|
|
13
|
+
Toolset,
|
|
14
|
+
ToolsetTag,
|
|
14
15
|
)
|
|
15
16
|
from holmes.plugins.toolsets.investigator.model import Task, TaskStatus
|
|
16
17
|
|
|
@@ -74,9 +75,7 @@ class TodoWriteTool(Tool):
|
|
|
74
75
|
|
|
75
76
|
logging.info(separator)
|
|
76
77
|
|
|
77
|
-
def _invoke(
|
|
78
|
-
self, params: dict, user_approved: bool = False
|
|
79
|
-
) -> StructuredToolResult:
|
|
78
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
80
79
|
try:
|
|
81
80
|
todos_data = params.get("todos", [])
|
|
82
81
|
|
|
@@ -133,7 +132,6 @@ class CoreInvestigationToolset(Toolset):
|
|
|
133
132
|
tags=[ToolsetTag.CORE],
|
|
134
133
|
is_default=True,
|
|
135
134
|
)
|
|
136
|
-
logging.info("Core investigation toolset loaded")
|
|
137
135
|
|
|
138
136
|
def get_example_config(self) -> Dict[str, Any]:
|
|
139
137
|
return {}
|
holmes/plugins/toolsets/kafka.py
CHANGED
|
@@ -27,6 +27,7 @@ from holmes.core.tools import (
|
|
|
27
27
|
CallablePrerequisite,
|
|
28
28
|
StructuredToolResult,
|
|
29
29
|
Tool,
|
|
30
|
+
ToolInvokeContext,
|
|
30
31
|
ToolParameter,
|
|
31
32
|
StructuredToolResultStatus,
|
|
32
33
|
Toolset,
|
|
@@ -153,9 +154,7 @@ class ListKafkaConsumers(BaseKafkaTool):
|
|
|
153
154
|
toolset=toolset,
|
|
154
155
|
)
|
|
155
156
|
|
|
156
|
-
def _invoke(
|
|
157
|
-
self, params: dict, user_approved: bool = False
|
|
158
|
-
) -> StructuredToolResult:
|
|
157
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
159
158
|
try:
|
|
160
159
|
kafka_cluster_name = get_param_or_raise(params, "kafka_cluster_name")
|
|
161
160
|
client = self.get_kafka_client(kafka_cluster_name)
|
|
@@ -228,9 +227,7 @@ class DescribeConsumerGroup(BaseKafkaTool):
|
|
|
228
227
|
toolset=toolset,
|
|
229
228
|
)
|
|
230
229
|
|
|
231
|
-
def _invoke(
|
|
232
|
-
self, params: dict, user_approved: bool = False
|
|
233
|
-
) -> StructuredToolResult:
|
|
230
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
234
231
|
group_id = params["group_id"]
|
|
235
232
|
try:
|
|
236
233
|
kafka_cluster_name = get_param_or_raise(params, "kafka_cluster_name")
|
|
@@ -286,9 +283,7 @@ class ListTopics(BaseKafkaTool):
|
|
|
286
283
|
toolset=toolset,
|
|
287
284
|
)
|
|
288
285
|
|
|
289
|
-
def _invoke(
|
|
290
|
-
self, params: dict, user_approved: bool = False
|
|
291
|
-
) -> StructuredToolResult:
|
|
286
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
292
287
|
try:
|
|
293
288
|
kafka_cluster_name = get_param_or_raise(params, "kafka_cluster_name")
|
|
294
289
|
client = self.get_kafka_client(kafka_cluster_name)
|
|
@@ -344,9 +339,7 @@ class DescribeTopic(BaseKafkaTool):
|
|
|
344
339
|
toolset=toolset,
|
|
345
340
|
)
|
|
346
341
|
|
|
347
|
-
def _invoke(
|
|
348
|
-
self, params: dict, user_approved: bool = False
|
|
349
|
-
) -> StructuredToolResult:
|
|
342
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
350
343
|
topic_name = params["topic_name"]
|
|
351
344
|
try:
|
|
352
345
|
kafka_cluster_name = get_param_or_raise(params, "kafka_cluster_name")
|
|
@@ -469,9 +462,7 @@ class FindConsumerGroupsByTopic(BaseKafkaTool):
|
|
|
469
462
|
toolset=toolset,
|
|
470
463
|
)
|
|
471
464
|
|
|
472
|
-
def _invoke(
|
|
473
|
-
self, params: dict, user_approved: bool = False
|
|
474
|
-
) -> StructuredToolResult:
|
|
465
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
475
466
|
topic_name = params["topic_name"]
|
|
476
467
|
try:
|
|
477
468
|
kafka_cluster_name = get_param_or_raise(params, "kafka_cluster_name")
|
|
@@ -559,9 +550,7 @@ class ListKafkaClusters(BaseKafkaTool):
|
|
|
559
550
|
toolset=toolset,
|
|
560
551
|
)
|
|
561
552
|
|
|
562
|
-
def _invoke(
|
|
563
|
-
self, params: dict, user_approved: bool = False
|
|
564
|
-
) -> StructuredToolResult:
|
|
553
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
565
554
|
cluster_names = list(self.toolset.clients.keys())
|
|
566
555
|
return StructuredToolResult(
|
|
567
556
|
status=StructuredToolResultStatus.SUCCESS,
|
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from datetime import datetime, timedelta
|
|
3
3
|
import logging
|
|
4
|
+
from math import ceil
|
|
4
5
|
from typing import Optional, Set
|
|
5
6
|
from enum import Enum
|
|
6
7
|
|
|
7
8
|
from pydantic import BaseModel, field_validator
|
|
8
9
|
from datetime import timezone
|
|
10
|
+
from holmes.core.llm import LLM
|
|
9
11
|
from holmes.core.tools import (
|
|
10
12
|
StructuredToolResult,
|
|
11
13
|
Tool,
|
|
14
|
+
ToolInvokeContext,
|
|
12
15
|
ToolParameter,
|
|
13
16
|
Toolset,
|
|
14
17
|
)
|
|
18
|
+
from holmes.core.tools_utils.token_counting import count_tool_response_tokens
|
|
15
19
|
from holmes.plugins.toolsets.utils import get_param_or_raise
|
|
16
20
|
|
|
17
21
|
# Default values for log fetching
|
|
18
22
|
DEFAULT_LOG_LIMIT = 100
|
|
19
23
|
SECONDS_PER_DAY = 24 * 60 * 60
|
|
20
24
|
DEFAULT_TIME_SPAN_SECONDS = 7 * SECONDS_PER_DAY # 1 week in seconds
|
|
21
|
-
DEFAULT_GRAPH_TIME_SPAN_SECONDS = 1 *
|
|
25
|
+
DEFAULT_GRAPH_TIME_SPAN_SECONDS = 1 * 60 * 60 # 1 hour in seconds
|
|
22
26
|
|
|
23
27
|
POD_LOGGING_TOOL_NAME = "fetch_pod_logs"
|
|
24
28
|
|
|
29
|
+
TRUNCATION_PROMPT_PREFIX = "[... PREVIOUS LOGS ABOVE THIS LINE HAVE BEEN TRUNCATED]"
|
|
30
|
+
MIN_NUMBER_OF_CHARACTERS_TO_TRUNCATE: int = (
|
|
31
|
+
50 + len(TRUNCATION_PROMPT_PREFIX)
|
|
32
|
+
) # prevents the truncation algorithm from going too slow once the actual token count gets close to the expected limit
|
|
33
|
+
|
|
25
34
|
|
|
26
35
|
class LoggingCapability(str, Enum):
|
|
27
36
|
"""Optional advanced logging capabilities"""
|
|
@@ -74,6 +83,68 @@ class BasePodLoggingToolset(Toolset, ABC):
|
|
|
74
83
|
return ""
|
|
75
84
|
|
|
76
85
|
|
|
86
|
+
def truncate_logs(
|
|
87
|
+
logging_structured_tool_result: StructuredToolResult,
|
|
88
|
+
llm: LLM,
|
|
89
|
+
token_limit: int,
|
|
90
|
+
structured_params: FetchPodLogsParams,
|
|
91
|
+
):
|
|
92
|
+
original_token_count = count_tool_response_tokens(
|
|
93
|
+
llm=llm, structured_tool_result=logging_structured_tool_result
|
|
94
|
+
)
|
|
95
|
+
token_count = original_token_count
|
|
96
|
+
text = None
|
|
97
|
+
while token_count > token_limit:
|
|
98
|
+
# Loop because we are counting tokens but trimming characters. This means we try to trim a number of
|
|
99
|
+
# characters proportional to the number of tokens but we may still have too many tokens
|
|
100
|
+
if not text:
|
|
101
|
+
text = logging_structured_tool_result.get_stringified_data()
|
|
102
|
+
if not text:
|
|
103
|
+
# Weird scenario where the result exceeds the token allowance but there is not data.
|
|
104
|
+
# Exit and do nothing because I don't know how to handle such scenario.
|
|
105
|
+
logging.warning(
|
|
106
|
+
f"The calculated token count for logs is {token_count} but the limit is {token_limit}. However the data field is empty so there are no logs to truncate."
|
|
107
|
+
)
|
|
108
|
+
return
|
|
109
|
+
ratio = token_count / token_limit
|
|
110
|
+
character_count = len(text)
|
|
111
|
+
number_of_characters_to_truncate = character_count - ceil(
|
|
112
|
+
character_count / ratio
|
|
113
|
+
)
|
|
114
|
+
number_of_characters_to_truncate = max(
|
|
115
|
+
MIN_NUMBER_OF_CHARACTERS_TO_TRUNCATE, number_of_characters_to_truncate
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if len(text) <= number_of_characters_to_truncate:
|
|
119
|
+
logging.warning(
|
|
120
|
+
f"The calculated token count for logs is {token_count} (max allowed tokens={token_limit}) but the logs are only {len(text)} characters which is below the intended truncation of {number_of_characters_to_truncate} characters. Logs will no longer be truncated"
|
|
121
|
+
)
|
|
122
|
+
return
|
|
123
|
+
else:
|
|
124
|
+
linefeed_truncation_offset = max(
|
|
125
|
+
text[number_of_characters_to_truncate:].find("\n"), 0
|
|
126
|
+
) # keep log lines atomic
|
|
127
|
+
|
|
128
|
+
# Tentatively add the truncation prefix.
|
|
129
|
+
# When counting tokens, we want to include the TRUNCATION_PROMPT_PREFIX because it will be part of the tool response.
|
|
130
|
+
# Because we're truncating based on character counts but ultimately checking tokens count,
|
|
131
|
+
# it is possible that the character truncation is incorrect and more need to be truncated.
|
|
132
|
+
# This will be caught in the next iteration and the truncation prefix will be truncated
|
|
133
|
+
# because MIN_NUMBER_OF_CHARACTERS_TO_TRUNCATE cannot be smaller than TRUNCATION_PROMPT_PREFIX
|
|
134
|
+
text = (
|
|
135
|
+
TRUNCATION_PROMPT_PREFIX
|
|
136
|
+
+ text[number_of_characters_to_truncate + linefeed_truncation_offset :]
|
|
137
|
+
)
|
|
138
|
+
logging_structured_tool_result.data = text
|
|
139
|
+
token_count = count_tool_response_tokens(
|
|
140
|
+
llm=llm, structured_tool_result=logging_structured_tool_result
|
|
141
|
+
)
|
|
142
|
+
if token_count < original_token_count:
|
|
143
|
+
logging.info(
|
|
144
|
+
f"Logs for pod {structured_params.pod_name}/{structured_params.namespace} have been truncated from {original_token_count} tokens down to {token_count} tokens."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
77
148
|
class PodLoggingTool(Tool):
|
|
78
149
|
"""Common tool for fetching pod logs across different logging backends"""
|
|
79
150
|
|
|
@@ -175,9 +246,7 @@ If you hit the log limit and see lots of repetitive INFO logs, use exclude_filte
|
|
|
175
246
|
|
|
176
247
|
return params
|
|
177
248
|
|
|
178
|
-
def _invoke(
|
|
179
|
-
self, params: dict, user_approved: bool = False
|
|
180
|
-
) -> StructuredToolResult:
|
|
249
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
181
250
|
structured_params = FetchPodLogsParams(
|
|
182
251
|
namespace=get_param_or_raise(params, "namespace"),
|
|
183
252
|
pod_name=get_param_or_raise(params, "pod_name"),
|
|
@@ -192,6 +261,13 @@ If you hit the log limit and see lots of repetitive INFO logs, use exclude_filte
|
|
|
192
261
|
params=structured_params,
|
|
193
262
|
)
|
|
194
263
|
|
|
264
|
+
truncate_logs(
|
|
265
|
+
logging_structured_tool_result=result,
|
|
266
|
+
llm=context.llm,
|
|
267
|
+
token_limit=context.max_token_count,
|
|
268
|
+
structured_params=structured_params,
|
|
269
|
+
)
|
|
270
|
+
|
|
195
271
|
return result
|
|
196
272
|
|
|
197
273
|
def get_parameterized_one_liner(self, params: dict) -> str:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from holmes.core.tools import (
|
|
2
|
+
ToolInvokeContext,
|
|
2
3
|
Toolset,
|
|
3
4
|
Tool,
|
|
4
5
|
ToolParameter,
|
|
@@ -24,9 +25,7 @@ class RemoteMCPTool(Tool):
|
|
|
24
25
|
url: str
|
|
25
26
|
headers: Optional[Dict[str, str]] = None
|
|
26
27
|
|
|
27
|
-
def _invoke(
|
|
28
|
-
self, params: dict, user_approved: bool = False
|
|
29
|
-
) -> StructuredToolResult:
|
|
28
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
30
29
|
try:
|
|
31
30
|
return asyncio.run(self._invoke_async(params))
|
|
32
31
|
except Exception as e:
|
|
File without changes
|