holmesgpt 0.14.2__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.

Files changed (68) hide show
  1. holmes/__init__.py +1 -1
  2. holmes/common/env_vars.py +6 -0
  3. holmes/config.py +3 -6
  4. holmes/core/conversations.py +12 -2
  5. holmes/core/feedback.py +191 -0
  6. holmes/core/llm.py +16 -12
  7. holmes/core/models.py +101 -1
  8. holmes/core/supabase_dal.py +23 -9
  9. holmes/core/tool_calling_llm.py +197 -15
  10. holmes/core/tools.py +20 -7
  11. holmes/core/tools_utils/token_counting.py +13 -0
  12. holmes/core/tools_utils/tool_context_window_limiter.py +45 -23
  13. holmes/core/tools_utils/tool_executor.py +11 -6
  14. holmes/core/toolset_manager.py +5 -1
  15. holmes/core/truncation/dal_truncation_utils.py +23 -0
  16. holmes/interactive.py +146 -14
  17. holmes/plugins/prompts/_fetch_logs.jinja2 +3 -0
  18. holmes/plugins/runbooks/__init__.py +6 -1
  19. holmes/plugins/toolsets/__init__.py +11 -4
  20. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +9 -20
  21. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +2 -3
  22. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +2 -3
  23. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +6 -4
  24. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +6 -4
  25. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +2 -3
  26. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +6 -4
  27. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +2 -3
  28. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +2 -3
  29. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +2 -3
  30. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +2 -3
  31. holmes/plugins/toolsets/bash/bash_toolset.py +4 -7
  32. holmes/plugins/toolsets/cilium.yaml +284 -0
  33. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +5 -10
  34. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +1 -1
  35. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +6 -13
  36. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +3 -6
  37. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +4 -9
  38. holmes/plugins/toolsets/git.py +14 -12
  39. holmes/plugins/toolsets/grafana/grafana_tempo_api.py +23 -42
  40. holmes/plugins/toolsets/grafana/toolset_grafana.py +2 -3
  41. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +18 -36
  42. holmes/plugins/toolsets/internet/internet.py +2 -3
  43. holmes/plugins/toolsets/internet/notion.py +2 -3
  44. holmes/plugins/toolsets/investigator/core_investigation.py +7 -9
  45. holmes/plugins/toolsets/kafka.py +7 -18
  46. holmes/plugins/toolsets/logging_utils/logging_api.py +79 -3
  47. holmes/plugins/toolsets/mcp/toolset_mcp.py +2 -3
  48. holmes/plugins/toolsets/newrelic/__init__.py +0 -0
  49. holmes/plugins/toolsets/newrelic/new_relic_api.py +125 -0
  50. holmes/plugins/toolsets/newrelic/newrelic.jinja2 +41 -0
  51. holmes/plugins/toolsets/newrelic/newrelic.py +211 -0
  52. holmes/plugins/toolsets/opensearch/opensearch.py +5 -12
  53. holmes/plugins/toolsets/opensearch/opensearch_traces.py +3 -6
  54. holmes/plugins/toolsets/prometheus/prometheus.py +131 -97
  55. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +3 -6
  56. holmes/plugins/toolsets/robusta/robusta.py +4 -9
  57. holmes/plugins/toolsets/runbook/runbook_fetcher.py +93 -13
  58. holmes/plugins/toolsets/servicenow/servicenow.py +5 -10
  59. holmes/utils/sentry_helper.py +1 -1
  60. holmes/utils/stream.py +22 -7
  61. holmes/version.py +34 -14
  62. {holmesgpt-0.14.2.dist-info → holmesgpt-0.14.3a0.dist-info}/METADATA +6 -8
  63. {holmesgpt-0.14.2.dist-info → holmesgpt-0.14.3a0.dist-info}/RECORD +66 -60
  64. holmes/core/tools_utils/data_types.py +0 -81
  65. holmes/plugins/toolsets/newrelic.py +0 -231
  66. {holmesgpt-0.14.2.dist-info → holmesgpt-0.14.3a0.dist-info}/LICENSE.txt +0 -0
  67. {holmesgpt-0.14.2.dist-info → holmesgpt-0.14.3a0.dist-info}/WHEEL +0 -0
  68. {holmesgpt-0.14.2.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, use_post: bool = False):
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 (GET) or body parameters (POST)
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
- if self.use_post:
105
- # POST request with JSON body
106
- response = requests.post(
107
- url,
108
- headers=self.headers,
109
- json=params or {},
110
- timeout=timeout,
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 (or POST if use_post=True)
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
- if self.use_post:
157
- response = requests.post(
158
- url,
159
- headers=self.headers,
160
- timeout=30,
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 (or POST if use_post=True)
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 (or POST if use_post=True)
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 (or POST if use_post=True)
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 (or POST if use_post=True)
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 (or POST if use_post=True)
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 (or POST if use_post=True)
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 (or POST if use_post=True)
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
  )
@@ -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, use_post=TEMPO_API_USE_POST)
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}}}"
@@ -413,10 +409,8 @@ class SearchTracesByQuery(Tool):
413
409
  )
414
410
  self._toolset = toolset
415
411
 
416
- def _invoke(
417
- self, params: Dict, user_approved: bool = False
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
- self, params: Dict, user_approved: bool = False
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
- self, params: Dict, user_approved: bool = False
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
- self, params: Dict, user_approved: bool = False
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
- self, params: Dict, user_approved: bool = False
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
- self, params: Dict, user_approved: bool = False
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
- self, params: Dict, user_approved: bool = False
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 {}
@@ -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,17 +1,21 @@
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
@@ -22,6 +26,11 @@ 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