holmesgpt 0.16.2a0__py3-none-any.whl → 0.18.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- holmes/__init__.py +3 -5
- holmes/clients/robusta_client.py +4 -3
- holmes/common/env_vars.py +18 -2
- holmes/common/openshift.py +1 -1
- holmes/config.py +11 -6
- holmes/core/conversations.py +30 -13
- holmes/core/investigation.py +21 -25
- holmes/core/investigation_structured_output.py +3 -3
- holmes/core/issue.py +1 -1
- holmes/core/llm.py +50 -31
- holmes/core/models.py +19 -17
- holmes/core/openai_formatting.py +1 -1
- holmes/core/prompt.py +47 -2
- holmes/core/runbooks.py +1 -0
- holmes/core/safeguards.py +4 -2
- holmes/core/supabase_dal.py +4 -2
- holmes/core/tool_calling_llm.py +102 -141
- holmes/core/tools.py +19 -28
- holmes/core/tools_utils/token_counting.py +9 -2
- holmes/core/tools_utils/tool_context_window_limiter.py +13 -30
- holmes/core/tools_utils/tool_executor.py +0 -18
- holmes/core/tools_utils/toolset_utils.py +1 -0
- holmes/core/toolset_manager.py +37 -2
- holmes/core/tracing.py +13 -2
- holmes/core/transformers/__init__.py +1 -1
- holmes/core/transformers/base.py +1 -0
- holmes/core/transformers/llm_summarize.py +3 -2
- holmes/core/transformers/registry.py +2 -1
- holmes/core/transformers/transformer.py +1 -0
- holmes/core/truncation/compaction.py +37 -2
- holmes/core/truncation/input_context_window_limiter.py +3 -2
- holmes/interactive.py +52 -8
- holmes/main.py +17 -37
- holmes/plugins/interfaces.py +2 -1
- holmes/plugins/prompts/__init__.py +2 -1
- holmes/plugins/prompts/_fetch_logs.jinja2 +5 -5
- holmes/plugins/prompts/_runbook_instructions.jinja2 +2 -1
- holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
- holmes/plugins/prompts/conversation_history_compaction.jinja2 +2 -1
- holmes/plugins/prompts/generic_ask.jinja2 +0 -2
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -2
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -2
- holmes/plugins/prompts/generic_investigation.jinja2 +0 -2
- holmes/plugins/prompts/investigation_procedure.jinja2 +2 -1
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -2
- holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -2
- holmes/plugins/runbooks/__init__.py +32 -3
- holmes/plugins/sources/github/__init__.py +4 -2
- holmes/plugins/sources/prometheus/models.py +1 -0
- holmes/plugins/toolsets/__init__.py +30 -26
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +13 -12
- holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +3 -2
- holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
- holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +3 -2
- holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +3 -1
- holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +3 -1
- holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +12 -12
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +7 -7
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +7 -7
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -5
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +3 -3
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +7 -7
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +6 -8
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +3 -3
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +3 -3
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +3 -3
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +3 -3
- holmes/plugins/toolsets/azure_sql/utils.py +0 -32
- holmes/plugins/toolsets/bash/argocd/__init__.py +3 -3
- holmes/plugins/toolsets/bash/aws/__init__.py +4 -4
- holmes/plugins/toolsets/bash/azure/__init__.py +4 -4
- holmes/plugins/toolsets/bash/bash_toolset.py +2 -3
- holmes/plugins/toolsets/bash/common/bash.py +19 -9
- holmes/plugins/toolsets/bash/common/bash_command.py +1 -1
- holmes/plugins/toolsets/bash/common/stringify.py +1 -1
- holmes/plugins/toolsets/bash/kubectl/__init__.py +2 -1
- holmes/plugins/toolsets/bash/kubectl/constants.py +0 -1
- holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +3 -4
- holmes/plugins/toolsets/bash/parse_command.py +12 -13
- holmes/plugins/toolsets/connectivity_check.py +124 -0
- holmes/plugins/toolsets/coralogix/api.py +132 -119
- holmes/plugins/toolsets/coralogix/coralogix.jinja2 +14 -0
- holmes/plugins/toolsets/coralogix/toolset_coralogix.py +219 -0
- holmes/plugins/toolsets/coralogix/utils.py +15 -79
- holmes/plugins/toolsets/datadog/datadog_api.py +36 -3
- holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +34 -1
- holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +3 -3
- holmes/plugins/toolsets/datadog/datadog_models.py +59 -0
- holmes/plugins/toolsets/datadog/datadog_url_utils.py +213 -0
- holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +165 -28
- holmes/plugins/toolsets/datadog/toolset_datadog_general.py +71 -28
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +224 -375
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +67 -36
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +360 -343
- holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
- holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
- holmes/plugins/toolsets/git.py +7 -8
- holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
- holmes/plugins/toolsets/grafana/common.py +2 -30
- holmes/plugins/toolsets/grafana/grafana_tempo_api.py +2 -1
- holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +18 -2
- holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +92 -18
- holmes/plugins/toolsets/grafana/loki_api.py +4 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +109 -25
- holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +22 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +201 -33
- holmes/plugins/toolsets/grafana/trace_parser.py +3 -2
- holmes/plugins/toolsets/internet/internet.py +10 -10
- holmes/plugins/toolsets/internet/notion.py +5 -6
- holmes/plugins/toolsets/investigator/core_investigation.py +3 -3
- holmes/plugins/toolsets/investigator/model.py +3 -1
- holmes/plugins/toolsets/json_filter_mixin.py +134 -0
- holmes/plugins/toolsets/kafka.py +12 -7
- holmes/plugins/toolsets/kubernetes.yaml +260 -30
- holmes/plugins/toolsets/kubernetes_logs.py +3 -3
- holmes/plugins/toolsets/logging_utils/logging_api.py +16 -6
- holmes/plugins/toolsets/mcp/toolset_mcp.py +88 -60
- holmes/plugins/toolsets/newrelic/new_relic_api.py +41 -1
- holmes/plugins/toolsets/newrelic/newrelic.jinja2 +24 -0
- holmes/plugins/toolsets/newrelic/newrelic.py +212 -55
- holmes/plugins/toolsets/prometheus/prometheus.py +358 -102
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +11 -3
- holmes/plugins/toolsets/rabbitmq/api.py +23 -4
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +5 -5
- holmes/plugins/toolsets/robusta/robusta.py +5 -5
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +25 -6
- holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +1 -1
- holmes/plugins/toolsets/utils.py +1 -1
- holmes/utils/config_utils.py +1 -1
- holmes/utils/connection_utils.py +31 -0
- holmes/utils/console/result.py +10 -0
- holmes/utils/file_utils.py +2 -1
- holmes/utils/global_instructions.py +10 -26
- holmes/utils/holmes_status.py +4 -3
- holmes/utils/log.py +15 -0
- holmes/utils/markdown_utils.py +2 -3
- holmes/utils/memory_limit.py +58 -0
- holmes/utils/sentry_helper.py +23 -0
- holmes/utils/stream.py +12 -5
- holmes/utils/tags.py +4 -3
- holmes/version.py +3 -1
- {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +12 -10
- holmesgpt-0.18.4.dist-info/RECORD +258 -0
- holmes/plugins/toolsets/aws.yaml +0 -80
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -114
- holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
- holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -736
- holmes/plugins/toolsets/grafana/grafana_api.py +0 -64
- holmes/plugins/toolsets/opensearch/__init__.py +0 -0
- holmes/plugins/toolsets/opensearch/opensearch.py +0 -250
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -215
- holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
- holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
- holmes/utils/keygen_utils.py +0 -6
- holmesgpt-0.16.2a0.dist-info/RECORD +0 -258
- holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_ppl_query_docs.jinja2 +0 -0
- holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist.py +2 -2
- /holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist_instructions.jinja2 +0 -0
- {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/LICENSE +0 -0
- {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
- {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/entry_points.txt +0 -0
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import os
|
|
2
|
-
|
|
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
|
|
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
|
|
51
|
-
"""
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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"
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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
|
holmes/plugins/toolsets/kafka.py
CHANGED
|
@@ -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=
|
|
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=
|
|
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=
|
|
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 = (
|