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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. holmes/__init__.py +3 -5
  2. holmes/clients/robusta_client.py +4 -3
  3. holmes/common/env_vars.py +18 -2
  4. holmes/common/openshift.py +1 -1
  5. holmes/config.py +11 -6
  6. holmes/core/conversations.py +30 -13
  7. holmes/core/investigation.py +21 -25
  8. holmes/core/investigation_structured_output.py +3 -3
  9. holmes/core/issue.py +1 -1
  10. holmes/core/llm.py +50 -31
  11. holmes/core/models.py +19 -17
  12. holmes/core/openai_formatting.py +1 -1
  13. holmes/core/prompt.py +47 -2
  14. holmes/core/runbooks.py +1 -0
  15. holmes/core/safeguards.py +4 -2
  16. holmes/core/supabase_dal.py +4 -2
  17. holmes/core/tool_calling_llm.py +102 -141
  18. holmes/core/tools.py +19 -28
  19. holmes/core/tools_utils/token_counting.py +9 -2
  20. holmes/core/tools_utils/tool_context_window_limiter.py +13 -30
  21. holmes/core/tools_utils/tool_executor.py +0 -18
  22. holmes/core/tools_utils/toolset_utils.py +1 -0
  23. holmes/core/toolset_manager.py +37 -2
  24. holmes/core/tracing.py +13 -2
  25. holmes/core/transformers/__init__.py +1 -1
  26. holmes/core/transformers/base.py +1 -0
  27. holmes/core/transformers/llm_summarize.py +3 -2
  28. holmes/core/transformers/registry.py +2 -1
  29. holmes/core/transformers/transformer.py +1 -0
  30. holmes/core/truncation/compaction.py +37 -2
  31. holmes/core/truncation/input_context_window_limiter.py +3 -2
  32. holmes/interactive.py +52 -8
  33. holmes/main.py +17 -37
  34. holmes/plugins/interfaces.py +2 -1
  35. holmes/plugins/prompts/__init__.py +2 -1
  36. holmes/plugins/prompts/_fetch_logs.jinja2 +5 -5
  37. holmes/plugins/prompts/_runbook_instructions.jinja2 +2 -1
  38. holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
  39. holmes/plugins/prompts/conversation_history_compaction.jinja2 +2 -1
  40. holmes/plugins/prompts/generic_ask.jinja2 +0 -2
  41. holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -2
  42. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -2
  43. holmes/plugins/prompts/generic_investigation.jinja2 +0 -2
  44. holmes/plugins/prompts/investigation_procedure.jinja2 +2 -1
  45. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -2
  46. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -2
  47. holmes/plugins/runbooks/__init__.py +32 -3
  48. holmes/plugins/sources/github/__init__.py +4 -2
  49. holmes/plugins/sources/prometheus/models.py +1 -0
  50. holmes/plugins/toolsets/__init__.py +30 -26
  51. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +13 -12
  52. holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +3 -2
  53. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
  54. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +3 -2
  55. holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +3 -1
  56. holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +3 -1
  57. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +12 -12
  58. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +7 -7
  59. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +7 -7
  60. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -5
  61. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +3 -3
  62. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +7 -7
  63. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +6 -8
  64. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +3 -3
  65. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +3 -3
  66. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +3 -3
  67. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +3 -3
  68. holmes/plugins/toolsets/azure_sql/utils.py +0 -32
  69. holmes/plugins/toolsets/bash/argocd/__init__.py +3 -3
  70. holmes/plugins/toolsets/bash/aws/__init__.py +4 -4
  71. holmes/plugins/toolsets/bash/azure/__init__.py +4 -4
  72. holmes/plugins/toolsets/bash/bash_toolset.py +2 -3
  73. holmes/plugins/toolsets/bash/common/bash.py +19 -9
  74. holmes/plugins/toolsets/bash/common/bash_command.py +1 -1
  75. holmes/plugins/toolsets/bash/common/stringify.py +1 -1
  76. holmes/plugins/toolsets/bash/kubectl/__init__.py +2 -1
  77. holmes/plugins/toolsets/bash/kubectl/constants.py +0 -1
  78. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +3 -4
  79. holmes/plugins/toolsets/bash/parse_command.py +12 -13
  80. holmes/plugins/toolsets/connectivity_check.py +124 -0
  81. holmes/plugins/toolsets/coralogix/api.py +132 -119
  82. holmes/plugins/toolsets/coralogix/coralogix.jinja2 +14 -0
  83. holmes/plugins/toolsets/coralogix/toolset_coralogix.py +219 -0
  84. holmes/plugins/toolsets/coralogix/utils.py +15 -79
  85. holmes/plugins/toolsets/datadog/datadog_api.py +36 -3
  86. holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +34 -1
  87. holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +3 -3
  88. holmes/plugins/toolsets/datadog/datadog_models.py +59 -0
  89. holmes/plugins/toolsets/datadog/datadog_url_utils.py +213 -0
  90. holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +165 -28
  91. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +71 -28
  92. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +224 -375
  93. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +67 -36
  94. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +360 -343
  95. holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
  96. holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
  97. holmes/plugins/toolsets/git.py +7 -8
  98. holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
  99. holmes/plugins/toolsets/grafana/common.py +2 -30
  100. holmes/plugins/toolsets/grafana/grafana_tempo_api.py +2 -1
  101. holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +18 -2
  102. holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +92 -18
  103. holmes/plugins/toolsets/grafana/loki_api.py +4 -0
  104. holmes/plugins/toolsets/grafana/toolset_grafana.py +109 -25
  105. holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +22 -0
  106. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +201 -33
  107. holmes/plugins/toolsets/grafana/trace_parser.py +3 -2
  108. holmes/plugins/toolsets/internet/internet.py +10 -10
  109. holmes/plugins/toolsets/internet/notion.py +5 -6
  110. holmes/plugins/toolsets/investigator/core_investigation.py +3 -3
  111. holmes/plugins/toolsets/investigator/model.py +3 -1
  112. holmes/plugins/toolsets/json_filter_mixin.py +134 -0
  113. holmes/plugins/toolsets/kafka.py +12 -7
  114. holmes/plugins/toolsets/kubernetes.yaml +260 -30
  115. holmes/plugins/toolsets/kubernetes_logs.py +3 -3
  116. holmes/plugins/toolsets/logging_utils/logging_api.py +16 -6
  117. holmes/plugins/toolsets/mcp/toolset_mcp.py +88 -60
  118. holmes/plugins/toolsets/newrelic/new_relic_api.py +41 -1
  119. holmes/plugins/toolsets/newrelic/newrelic.jinja2 +24 -0
  120. holmes/plugins/toolsets/newrelic/newrelic.py +212 -55
  121. holmes/plugins/toolsets/prometheus/prometheus.py +358 -102
  122. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +11 -3
  123. holmes/plugins/toolsets/rabbitmq/api.py +23 -4
  124. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +5 -5
  125. holmes/plugins/toolsets/robusta/robusta.py +5 -5
  126. holmes/plugins/toolsets/runbook/runbook_fetcher.py +25 -6
  127. holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +1 -1
  128. holmes/plugins/toolsets/utils.py +1 -1
  129. holmes/utils/config_utils.py +1 -1
  130. holmes/utils/connection_utils.py +31 -0
  131. holmes/utils/console/result.py +10 -0
  132. holmes/utils/file_utils.py +2 -1
  133. holmes/utils/global_instructions.py +10 -26
  134. holmes/utils/holmes_status.py +4 -3
  135. holmes/utils/log.py +15 -0
  136. holmes/utils/markdown_utils.py +2 -3
  137. holmes/utils/memory_limit.py +58 -0
  138. holmes/utils/sentry_helper.py +23 -0
  139. holmes/utils/stream.py +12 -5
  140. holmes/utils/tags.py +4 -3
  141. holmes/version.py +3 -1
  142. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +12 -10
  143. holmesgpt-0.18.4.dist-info/RECORD +258 -0
  144. holmes/plugins/toolsets/aws.yaml +0 -80
  145. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -114
  146. holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
  147. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -736
  148. holmes/plugins/toolsets/grafana/grafana_api.py +0 -64
  149. holmes/plugins/toolsets/opensearch/__init__.py +0 -0
  150. holmes/plugins/toolsets/opensearch/opensearch.py +0 -250
  151. holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
  152. holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -215
  153. holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
  154. holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
  155. holmes/utils/keygen_utils.py +0 -6
  156. holmesgpt-0.16.2a0.dist-info/RECORD +0 -258
  157. holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_ppl_query_docs.jinja2 +0 -0
  158. holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist.py +2 -2
  159. /holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist_instructions.jinja2 +0 -0
  160. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/LICENSE +0 -0
  161. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
  162. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/entry_points.txt +0 -0
@@ -1,15 +1,17 @@
1
+ import json
1
2
  import os
2
- from typing import Any, Dict, Tuple, cast, List
3
+ import time
4
+ import uuid
5
+ from typing import Any, Dict, List, Optional, Tuple, cast
6
+ from urllib.parse import quote
3
7
 
4
- import yaml # type: ignore
5
-
6
- from holmes.common.env_vars import load_bool, MAX_GRAPH_POINTS
8
+ from holmes.common.env_vars import MAX_GRAPH_POINTS, load_bool
7
9
  from holmes.core.tools import (
8
10
  StructuredToolResult,
11
+ StructuredToolResultStatus,
9
12
  Tool,
10
13
  ToolInvokeContext,
11
14
  ToolParameter,
12
- StructuredToolResultStatus,
13
15
  )
14
16
  from holmes.plugins.toolsets.consts import STANDARD_END_DATETIME_TOOL_PARAM_DESCRIPTION
15
17
  from holmes.plugins.toolsets.grafana.base_grafana_toolset import BaseGrafanaToolset
@@ -21,17 +23,116 @@ from holmes.plugins.toolsets.logging_utils.logging_api import (
21
23
  DEFAULT_GRAPH_TIME_SPAN_SECONDS,
22
24
  )
23
25
  from holmes.plugins.toolsets.utils import (
24
- toolset_name_for_one_liner,
25
- process_timestamps_to_int,
26
- standard_start_datetime_tool_param_description,
27
26
  adjust_step_for_max_points,
28
- seconds_to_duration_string,
29
27
  duration_string_to_seconds,
28
+ process_timestamps_to_int,
29
+ seconds_to_duration_string,
30
+ standard_start_datetime_tool_param_description,
31
+ toolset_name_for_one_liner,
30
32
  )
31
33
 
32
34
  TEMPO_LABELS_ADD_PREFIX = load_bool("TEMPO_LABELS_ADD_PREFIX", True)
33
35
 
34
36
 
37
+ def _build_grafana_explore_tempo_url(
38
+ config: GrafanaTempoConfig,
39
+ query: Optional[str] = None,
40
+ start: Optional[int] = None,
41
+ end: Optional[int] = None,
42
+ limit: int = 20,
43
+ trace_id: Optional[str] = None,
44
+ filters: Optional[List[Dict[str, Any]]] = None,
45
+ tags: Optional[str] = None,
46
+ ) -> Optional[str]:
47
+ if not config.grafana_datasource_uid:
48
+ return None
49
+ try:
50
+ base_url = config.external_url or config.url
51
+ datasource_uid = config.grafana_datasource_uid
52
+ now_s = int(time.time())
53
+ start_ts = start if start else now_s - 3600
54
+ end_ts = end if end else now_s
55
+ start_delta = max(0, now_s - start_ts)
56
+ end_delta = max(0, now_s - end_ts)
57
+ from_str = f"now-{start_delta}s" if start_delta > 0 else "now-1h"
58
+ to_str = "now" if end_delta == 0 else f"now-{end_delta}s"
59
+ pane_id = "tmp"
60
+
61
+ if trace_id:
62
+ # Direct trace ID lookup - query is just the traceID string
63
+ query_obj = {
64
+ "refId": "A",
65
+ "datasource": {"type": "tempo", "uid": datasource_uid},
66
+ "queryType": "traceql",
67
+ "limit": limit,
68
+ "tableType": "traces",
69
+ "metricsQueryType": "range",
70
+ "query": trace_id,
71
+ }
72
+ elif tags:
73
+ # Build filters from tag name
74
+ scope = "resource" if tags.startswith("resource.") else "span"
75
+ filter_id = str(uuid.uuid4())[:8]
76
+ filters = [
77
+ {
78
+ "id": filter_id,
79
+ "operator": "=",
80
+ "scope": scope,
81
+ "tag": tags,
82
+ "value": [],
83
+ }
84
+ ]
85
+ query_obj = {
86
+ "refId": "A",
87
+ "datasource": {"type": "tempo", "uid": datasource_uid},
88
+ "queryType": "traceqlSearch",
89
+ "limit": limit,
90
+ "tableType": "traces",
91
+ "metricsQueryType": "range",
92
+ "query": "",
93
+ "filters": filters,
94
+ }
95
+ elif filters:
96
+ # Tag filters - use traceqlSearch with filters array
97
+ query_obj = {
98
+ "refId": "A",
99
+ "datasource": {"type": "tempo", "uid": datasource_uid},
100
+ "queryType": "traceqlSearch",
101
+ "limit": limit,
102
+ "tableType": "traces",
103
+ "metricsQueryType": "range",
104
+ "query": "",
105
+ "filters": filters,
106
+ }
107
+ else:
108
+ # Regular TraceQL query
109
+ safe_query = query if query else "{}"
110
+ query_obj = {
111
+ "refId": "A",
112
+ "datasource": {"type": "tempo", "uid": datasource_uid},
113
+ "queryType": "traceql",
114
+ "limit": limit,
115
+ "tableType": "traces",
116
+ "metricsQueryType": "range",
117
+ "query": safe_query,
118
+ }
119
+
120
+ panes = {
121
+ pane_id: {
122
+ "datasource": datasource_uid,
123
+ "queries": [query_obj],
124
+ "range": {"from": from_str, "to": to_str},
125
+ }
126
+ }
127
+
128
+ panes_encoded = quote(
129
+ json.dumps(panes, separators=(",", ":"), ensure_ascii=False), safe=""
130
+ )
131
+ return f"{base_url}/explore?schemaVersion=1&panes={panes_encoded}&orgId=1"
132
+ except Exception:
133
+ return None
134
+
135
+
35
136
  class BaseGrafanaTempoToolset(BaseGrafanaToolset):
36
137
  config_class = GrafanaTempoConfig
37
138
 
@@ -47,22 +148,17 @@ class BaseGrafanaTempoToolset(BaseGrafanaToolset):
47
148
  def grafana_config(self) -> GrafanaTempoConfig:
48
149
  return cast(GrafanaTempoConfig, self._grafana_config)
49
150
 
50
- def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
51
- """Check Tempo connectivity using the echo endpoint."""
52
- # First call parent to validate config
53
- success, msg = super().prerequisites_callable(config)
54
- if not success:
55
- return success, msg
56
-
57
- # Then check Tempo-specific echo endpoint
151
+ def health_check(self) -> Tuple[bool, str]:
152
+ """Test a dummy query to check if service available."""
58
153
  try:
59
- api = GrafanaTempoAPI(self.grafana_config)
60
- if api.query_echo_endpoint():
61
- return True, "Successfully connected to Tempo"
62
- else:
63
- return False, "Failed to connect to Tempo echo endpoint"
154
+ _ = GrafanaTempoAPI(self.grafana_config).search_traces_by_query(
155
+ q='{ .service.name = "test-endpoint" }',
156
+ limit=1,
157
+ )
64
158
  except Exception as e:
65
- return False, f"Failed to connect to Tempo: {str(e)}"
159
+ return False, f"Unable to connect to Tempo.\n{str(e)}"
160
+
161
+ return True, ""
66
162
 
67
163
  def build_k8s_filters(
68
164
  self, params: Dict[str, Any], use_exact_match: bool
@@ -335,11 +431,18 @@ Examples:
335
431
  ],
336
432
  }
337
433
 
338
- # Return as YAML for readability
434
+ explore_url = _build_grafana_explore_tempo_url(
435
+ self._toolset.grafana_config,
436
+ query=f"{{{base_query}}}",
437
+ start=start,
438
+ end=end,
439
+ )
440
+
339
441
  return StructuredToolResult(
340
442
  status=StructuredToolResultStatus.SUCCESS,
341
- data=yaml.dump(result, default_flow_style=False, sort_keys=False),
443
+ data=result,
342
444
  params=params,
445
+ url=explore_url,
343
446
  )
344
447
 
345
448
  except Exception as e:
@@ -428,10 +531,20 @@ class SearchTracesByQuery(Tool):
428
531
  end=end,
429
532
  spss=params.get("spss"),
430
533
  )
534
+
535
+ explore_url = _build_grafana_explore_tempo_url(
536
+ self._toolset.grafana_config,
537
+ query=params["q"],
538
+ start=start,
539
+ end=end,
540
+ limit=params.get("limit") or 20,
541
+ )
542
+
431
543
  return StructuredToolResult(
432
544
  status=StructuredToolResultStatus.SUCCESS,
433
- data=yaml.dump(result, default_flow_style=False),
545
+ data=result,
434
546
  params=params,
547
+ url=explore_url,
435
548
  )
436
549
  except Exception as e:
437
550
  return StructuredToolResult(
@@ -510,10 +623,21 @@ class SearchTracesByTags(Tool):
510
623
  end=end,
511
624
  spss=params.get("spss"),
512
625
  )
626
+
627
+ tag_filters = params["tags"].replace(" ", " && ")
628
+ explore_url = _build_grafana_explore_tempo_url(
629
+ self._toolset.grafana_config,
630
+ query=f"{{{tag_filters}}}",
631
+ start=start,
632
+ end=end,
633
+ limit=params.get("limit") or 20,
634
+ )
635
+
513
636
  return StructuredToolResult(
514
637
  status=StructuredToolResultStatus.SUCCESS,
515
- data=yaml.dump(result, default_flow_style=False),
638
+ data=result,
516
639
  params=params,
640
+ url=explore_url,
517
641
  )
518
642
  except Exception as e:
519
643
  return StructuredToolResult(
@@ -569,11 +693,18 @@ class QueryTraceById(Tool):
569
693
  end=end,
570
694
  )
571
695
 
572
- # Return raw trace data as YAML for readability
696
+ explore_url = _build_grafana_explore_tempo_url(
697
+ self._toolset.grafana_config,
698
+ trace_id=params["trace_id"],
699
+ start=start,
700
+ end=end,
701
+ )
702
+
573
703
  return StructuredToolResult(
574
704
  status=StructuredToolResultStatus.SUCCESS,
575
- data=yaml.dump(trace_data, default_flow_style=False),
705
+ data=trace_data,
576
706
  params=params,
707
+ url=explore_url,
577
708
  )
578
709
  except Exception as e:
579
710
  return StructuredToolResult(
@@ -646,10 +777,20 @@ class SearchTagNames(Tool):
646
777
  limit=params.get("limit"),
647
778
  max_stale_values=params.get("max_stale_values"),
648
779
  )
780
+
781
+ query_filter = params.get("q") or "{}"
782
+ explore_url = _build_grafana_explore_tempo_url(
783
+ self._toolset.grafana_config,
784
+ query=query_filter,
785
+ start=start,
786
+ end=end,
787
+ )
788
+
649
789
  return StructuredToolResult(
650
790
  status=StructuredToolResultStatus.SUCCESS,
651
- data=yaml.dump(result, default_flow_style=False),
791
+ data=result,
652
792
  params=params,
793
+ url=explore_url,
653
794
  )
654
795
  except Exception as e:
655
796
  return StructuredToolResult(
@@ -722,10 +863,19 @@ class SearchTagValues(Tool):
722
863
  limit=params.get("limit"),
723
864
  max_stale_values=params.get("max_stale_values"),
724
865
  )
866
+
867
+ explore_url = _build_grafana_explore_tempo_url(
868
+ self._toolset.grafana_config,
869
+ start=start,
870
+ end=end,
871
+ tags=params["tag"],
872
+ )
873
+
725
874
  return StructuredToolResult(
726
875
  status=StructuredToolResultStatus.SUCCESS,
727
- data=yaml.dump(result, default_flow_style=False),
876
+ data=result,
728
877
  params=params,
878
+ url=explore_url,
729
879
  )
730
880
  except Exception as e:
731
881
  return StructuredToolResult(
@@ -797,10 +947,19 @@ class QueryMetricsInstant(Tool):
797
947
  start=start,
798
948
  end=end,
799
949
  )
950
+
951
+ explore_url = _build_grafana_explore_tempo_url(
952
+ self._toolset.grafana_config,
953
+ query=params["q"],
954
+ start=start,
955
+ end=end,
956
+ )
957
+
800
958
  return StructuredToolResult(
801
959
  status=StructuredToolResultStatus.SUCCESS,
802
- data=yaml.dump(result, default_flow_style=False),
960
+ data=result,
803
961
  params=params,
962
+ url=explore_url,
804
963
  )
805
964
  except Exception as e:
806
965
  return StructuredToolResult(
@@ -893,10 +1052,19 @@ class QueryMetricsRange(Tool):
893
1052
  end=end,
894
1053
  exemplars=params.get("exemplars"),
895
1054
  )
1055
+
1056
+ explore_url = _build_grafana_explore_tempo_url(
1057
+ self._toolset.grafana_config,
1058
+ query=params["q"],
1059
+ start=start,
1060
+ end=end,
1061
+ )
1062
+
896
1063
  return StructuredToolResult(
897
1064
  status=StructuredToolResultStatus.SUCCESS,
898
- data=yaml.dump(result, default_flow_style=False),
1065
+ data=result,
899
1066
  params=params,
1067
+ url=explore_url,
900
1068
  )
901
1069
  except Exception as e:
902
1070
  return StructuredToolResult(
@@ -1,6 +1,7 @@
1
- from typing import Dict, List, Optional, Any
2
- from dataclasses import dataclass, field
3
1
  import base64
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, Dict, List, Optional
4
+
4
5
  from holmes.plugins.toolsets.utils import unix_nano_to_rfc3339
5
6
 
6
7
 
@@ -1,25 +1,25 @@
1
- import re
2
- import os
3
1
  import logging
4
- from typing import Any, Optional, Tuple, Dict, List
2
+ import os
3
+ import re
4
+ from typing import Any, Dict, List, Optional, Tuple
5
5
 
6
+ import requests # type: ignore
7
+ from bs4 import BeautifulSoup
8
+ from markdownify import markdownify
6
9
  from requests import RequestException, Timeout # type: ignore
10
+
7
11
  from holmes.core.tools import (
12
+ CallablePrerequisite,
13
+ StructuredToolResult,
14
+ StructuredToolResultStatus,
8
15
  Tool,
9
16
  ToolInvokeContext,
10
17
  ToolParameter,
11
18
  Toolset,
12
19
  ToolsetTag,
13
- CallablePrerequisite,
14
20
  )
15
- from markdownify import markdownify
16
- from bs4 import BeautifulSoup
17
-
18
- import requests # type: ignore
19
- from holmes.core.tools import StructuredToolResult, StructuredToolResultStatus
20
21
  from holmes.plugins.toolsets.utils import toolset_name_for_one_liner
21
22
 
22
-
23
23
  # TODO: change and make it holmes
24
24
  INTERNET_TOOLSET_USER_AGENT = os.environ.get(
25
25
  "INTERNET_TOOLSET_USER_AGENT",
@@ -1,8 +1,11 @@
1
- import re
2
- import logging
3
1
  import json
2
+ import logging
3
+ import re
4
4
  from typing import Any, Dict, Tuple
5
+
5
6
  from holmes.core.tools import (
7
+ StructuredToolResult,
8
+ StructuredToolResultStatus,
6
9
  Tool,
7
10
  ToolInvokeContext,
8
11
  ToolParameter,
@@ -12,10 +15,6 @@ from holmes.plugins.toolsets.internet.internet import (
12
15
  InternetBaseToolset,
13
16
  scrape,
14
17
  )
15
- from holmes.core.tools import (
16
- StructuredToolResult,
17
- StructuredToolResultStatus,
18
- )
19
18
  from holmes.plugins.toolsets.utils import toolset_name_for_one_liner
20
19
 
21
20
 
@@ -1,7 +1,6 @@
1
1
  import logging
2
2
  import os
3
3
  from typing import Any, Dict
4
-
5
4
  from uuid import uuid4
6
5
 
7
6
  from holmes.core.todo_tasks_formatter import format_tasks
@@ -39,7 +38,7 @@ class TodoWriteTool(Tool):
39
38
  description: str = "Save investigation tasks to break down complex problems into manageable sub-tasks. ALWAYS provide the COMPLETE list of all tasks, not just the ones being updated."
40
39
  parameters: Dict[str, ToolParameter] = {
41
40
  "todos": ToolParameter(
42
- description="COMPLETE list of ALL tasks on the task list. Each task should have: id (string), content (string), status (pending/in_progress/completed)",
41
+ description="COMPLETE list of ALL tasks on the task list. Each task should have: id (string), content (string), status (pending/in_progress/completed/failed)",
43
42
  type="array",
44
43
  required=True,
45
44
  items=ToolParameter(
@@ -50,7 +49,7 @@ class TodoWriteTool(Tool):
50
49
  "status": ToolParameter(
51
50
  type="string",
52
51
  required=True,
53
- enum=["pending", "in_progress", "completed"],
52
+ enum=["pending", "in_progress", "completed", "failed"],
54
53
  ),
55
54
  },
56
55
  ),
@@ -67,6 +66,7 @@ class TodoWriteTool(Tool):
67
66
  "pending": "[ ]",
68
67
  "in_progress": "[~]",
69
68
  "completed": "[✓]",
69
+ "failed": "[✗]",
70
70
  }
71
71
 
72
72
  max_id_width = max(len(str(task.id)) for task in tasks)
@@ -1,12 +1,14 @@
1
1
  from enum import Enum
2
- from pydantic import BaseModel, Field
3
2
  from uuid import uuid4
4
3
 
4
+ from pydantic import BaseModel, Field
5
+
5
6
 
6
7
  class TaskStatus(str, Enum):
7
8
  PENDING = "pending"
8
9
  IN_PROGRESS = "in_progress"
9
10
  COMPLETED = "completed"
11
+ FAILED = "failed"
10
12
 
11
13
 
12
14
  class Task(BaseModel):
@@ -0,0 +1,134 @@
1
+ import json
2
+ import logging
3
+ from typing import Any, Dict, Optional, Tuple
4
+
5
+ import jq
6
+
7
+ from holmes.core.tools import (
8
+ StructuredToolResult,
9
+ StructuredToolResultStatus,
10
+ ToolParameter,
11
+ )
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def _truncate_to_depth(value: Any, max_depth: Optional[int], current_depth: int = 0):
17
+ """Recursively truncate dictionaries/lists beyond the requested depth."""
18
+ if max_depth is None or max_depth < 0:
19
+ return value
20
+
21
+ if current_depth >= max_depth:
22
+ if isinstance(value, (dict, list)):
23
+ return f"...truncated at depth {max_depth}"
24
+ return value
25
+
26
+ if isinstance(value, dict):
27
+ return {
28
+ key: _truncate_to_depth(sub_value, max_depth, current_depth + 1)
29
+ for key, sub_value in value.items()
30
+ }
31
+ if isinstance(value, list):
32
+ return [
33
+ _truncate_to_depth(item, max_depth, current_depth + 1) for item in value
34
+ ]
35
+
36
+ return value
37
+
38
+
39
+ def _apply_jq_filter(data: Any, expression: str) -> Tuple[Optional[Any], Optional[str]]:
40
+ try:
41
+ compiled = jq.compile(expression)
42
+ matches = compiled.input(data).all()
43
+ if len(matches) == 1:
44
+ return matches[0], None
45
+ return matches, None
46
+ except Exception as exc: # pragma: no cover - defensive
47
+ logger.debug("Failed to apply jq filter", exc_info=exc)
48
+ return None, f"Invalid jq expression: {exc}"
49
+
50
+
51
+ class JsonFilterMixin:
52
+ """Opt-in mixin for tools that return JSON and want filtering controls."""
53
+
54
+ filter_parameters: Dict[str, ToolParameter] = {
55
+ "max_depth": ToolParameter(
56
+ description="Maximum nesting depth to return from the JSON (0 returns only top-level keys). Leave empty for full response.",
57
+ type="integer",
58
+ required=False,
59
+ ),
60
+ "jq": ToolParameter(
61
+ description="Optional jq expression to extract specific parts of the JSON. Supports full jq syntax including filters, slicing, transformations, and more (e.g., '.items[] | select(.price > 10)', '.items[0:5]', '.items[].name').",
62
+ type="string",
63
+ required=False,
64
+ ),
65
+ }
66
+
67
+ @classmethod
68
+ def extend_parameters(
69
+ cls, existing: Dict[str, ToolParameter]
70
+ ) -> Dict[str, ToolParameter]:
71
+ merged = dict(cls.filter_parameters)
72
+ merged.update(existing)
73
+ return merged
74
+
75
+ def _filter_result_data(self, data: Any, params: Dict) -> Tuple[Any, Optional[str]]:
76
+ parsed_data = data
77
+ if isinstance(data, str):
78
+ try:
79
+ parsed_data = json.loads(data)
80
+ except Exception:
81
+ # Not JSON, leave as-is
82
+ return data, None
83
+
84
+ if params.get("jq"):
85
+ parsed_data, error = _apply_jq_filter(parsed_data, params["jq"])
86
+ if error:
87
+ return None, error
88
+
89
+ parsed_data = _truncate_to_depth(parsed_data, params.get("max_depth"))
90
+ return parsed_data, None
91
+
92
+ @staticmethod
93
+ def _safe_string(value: Any) -> Optional[str]:
94
+ if value is None:
95
+ return None
96
+ if isinstance(value, str):
97
+ return value
98
+ try:
99
+ return str(value)
100
+ except Exception:
101
+ return None
102
+
103
+ def filter_result(
104
+ self, result: StructuredToolResult, params: Dict
105
+ ) -> StructuredToolResult:
106
+ base_result = result if isinstance(result, StructuredToolResult) else None
107
+ if base_result is None:
108
+ base_result = StructuredToolResult(
109
+ status=getattr(result, "status", StructuredToolResultStatus.SUCCESS),
110
+ data=getattr(result, "data", None),
111
+ params=getattr(result, "params", params),
112
+ url=self._safe_string(getattr(result, "url", None)),
113
+ invocation=self._safe_string(getattr(result, "invocation", None)),
114
+ icon_url=self._safe_string(getattr(result, "icon_url", None)),
115
+ )
116
+ else:
117
+ # Normalize string fields to avoid MagicMock validation failures
118
+ base_result.url = self._safe_string(base_result.url)
119
+ base_result.invocation = self._safe_string(base_result.invocation)
120
+ base_result.icon_url = self._safe_string(base_result.icon_url)
121
+
122
+ filtered_data, error = self._filter_result_data(base_result.data, params)
123
+ if error:
124
+ return StructuredToolResult(
125
+ status=StructuredToolResultStatus.ERROR,
126
+ error=error,
127
+ params=params,
128
+ url=base_result.url,
129
+ invocation=base_result.invocation,
130
+ icon_url=base_result.icon_url,
131
+ )
132
+
133
+ base_result.data = filtered_data
134
+ return base_result
@@ -1,7 +1,10 @@
1
1
  import logging
2
+ from enum import Enum
2
3
  from typing import Any, Dict, List, Optional, Tuple, Union
3
4
 
4
5
  import yaml # type: ignore
6
+ from confluent_kafka import Consumer
7
+ from confluent_kafka._model import Node
5
8
  from confluent_kafka.admin import (
6
9
  AdminClient,
7
10
  BrokerMetadata,
@@ -17,19 +20,16 @@ from confluent_kafka.admin import (
17
20
  PartitionMetadata,
18
21
  TopicMetadata,
19
22
  )
20
- from confluent_kafka import Consumer
21
- from confluent_kafka._model import Node
22
- from enum import Enum
23
23
  from confluent_kafka.admin import _TopicPartition as TopicPartition
24
24
  from pydantic import BaseModel, ConfigDict
25
25
 
26
26
  from holmes.core.tools import (
27
27
  CallablePrerequisite,
28
28
  StructuredToolResult,
29
+ StructuredToolResultStatus,
29
30
  Tool,
30
31
  ToolInvokeContext,
31
32
  ToolParameter,
32
- StructuredToolResultStatus,
33
33
  Toolset,
34
34
  ToolsetTag,
35
35
  )
@@ -245,7 +245,7 @@ class DescribeConsumerGroup(BaseKafkaTool):
245
245
  group_metadata = futures.get(group_id).result()
246
246
  return StructuredToolResult(
247
247
  status=StructuredToolResultStatus.SUCCESS,
248
- data=yaml.dump(convert_to_dict(group_metadata)),
248
+ data=convert_to_dict(group_metadata),
249
249
  params=params,
250
250
  )
251
251
  else:
@@ -297,7 +297,7 @@ class ListTopics(BaseKafkaTool):
297
297
  topics = client.list_topics()
298
298
  return StructuredToolResult(
299
299
  status=StructuredToolResultStatus.SUCCESS,
300
- data=yaml.dump(convert_to_dict(topics)),
300
+ data=convert_to_dict(topics),
301
301
  params=params,
302
302
  )
303
303
  except Exception as e:
@@ -367,7 +367,7 @@ class DescribeTopic(BaseKafkaTool):
367
367
 
368
368
  return StructuredToolResult(
369
369
  status=StructuredToolResultStatus.SUCCESS,
370
- data=yaml.dump(result),
370
+ data=result,
371
371
  params=params,
372
372
  )
373
373
  except Exception as e:
@@ -599,6 +599,8 @@ class KafkaToolset(Toolset):
599
599
  admin_config = {
600
600
  "bootstrap.servers": cluster.kafka_broker,
601
601
  "client.id": cluster.kafka_client_id,
602
+ "socket.timeout.ms": 15000, # 15 second timeout
603
+ "api.version.request.timeout.ms": 15000, # 15 second API version timeout
602
604
  }
603
605
 
604
606
  if cluster.kafka_security_protocol:
@@ -612,6 +614,9 @@ class KafkaToolset(Toolset):
612
614
  admin_config["sasl.password"] = cluster.kafka_password
613
615
 
614
616
  client = AdminClient(admin_config)
617
+ # Test the connection by trying to list topics with a timeout
618
+ # This will fail fast if the broker is not reachable
619
+ _ = client.list_topics(timeout=10) # 10 second timeout
615
620
  self.clients[cluster.name] = client # Store in dictionary
616
621
  except Exception as e:
617
622
  message = (