holmesgpt 0.13.2__py3-none-any.whl → 0.16.2a0__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 +1 -1
- holmes/clients/robusta_client.py +17 -4
- holmes/common/env_vars.py +40 -1
- holmes/config.py +114 -144
- holmes/core/conversations.py +53 -14
- holmes/core/feedback.py +191 -0
- holmes/core/investigation.py +18 -22
- holmes/core/llm.py +489 -88
- holmes/core/models.py +103 -1
- holmes/core/openai_formatting.py +13 -0
- holmes/core/prompt.py +1 -1
- holmes/core/safeguards.py +4 -4
- holmes/core/supabase_dal.py +293 -100
- holmes/core/tool_calling_llm.py +423 -323
- holmes/core/tools.py +311 -33
- holmes/core/tools_utils/token_counting.py +14 -0
- holmes/core/tools_utils/tool_context_window_limiter.py +57 -0
- holmes/core/tools_utils/tool_executor.py +13 -8
- holmes/core/toolset_manager.py +155 -4
- holmes/core/tracing.py +6 -1
- holmes/core/transformers/__init__.py +23 -0
- holmes/core/transformers/base.py +62 -0
- holmes/core/transformers/llm_summarize.py +174 -0
- holmes/core/transformers/registry.py +122 -0
- holmes/core/transformers/transformer.py +31 -0
- holmes/core/truncation/compaction.py +59 -0
- holmes/core/truncation/dal_truncation_utils.py +23 -0
- holmes/core/truncation/input_context_window_limiter.py +218 -0
- holmes/interactive.py +177 -24
- holmes/main.py +7 -4
- holmes/plugins/prompts/_fetch_logs.jinja2 +26 -1
- holmes/plugins/prompts/_general_instructions.jinja2 +1 -2
- holmes/plugins/prompts/_runbook_instructions.jinja2 +23 -12
- holmes/plugins/prompts/conversation_history_compaction.jinja2 +88 -0
- holmes/plugins/prompts/generic_ask.jinja2 +2 -4
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +2 -1
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +2 -1
- holmes/plugins/prompts/generic_investigation.jinja2 +2 -1
- holmes/plugins/prompts/investigation_procedure.jinja2 +48 -0
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +2 -1
- holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +2 -1
- holmes/plugins/runbooks/__init__.py +117 -18
- holmes/plugins/runbooks/catalog.json +2 -0
- holmes/plugins/toolsets/__init__.py +21 -8
- holmes/plugins/toolsets/aks-node-health.yaml +46 -0
- holmes/plugins/toolsets/aks.yaml +64 -0
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +26 -36
- holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +0 -1
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +10 -7
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +9 -6
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +8 -6
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +8 -6
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +9 -6
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +9 -7
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +9 -6
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +9 -6
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +9 -6
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +9 -6
- holmes/plugins/toolsets/bash/bash_toolset.py +10 -13
- holmes/plugins/toolsets/bash/common/bash.py +7 -7
- holmes/plugins/toolsets/cilium.yaml +284 -0
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +5 -3
- holmes/plugins/toolsets/datadog/datadog_api.py +490 -24
- holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +21 -10
- holmes/plugins/toolsets/datadog/toolset_datadog_general.py +349 -216
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +190 -19
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +101 -44
- holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +13 -16
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +25 -31
- holmes/plugins/toolsets/git.py +51 -46
- holmes/plugins/toolsets/grafana/common.py +15 -3
- holmes/plugins/toolsets/grafana/grafana_api.py +46 -24
- holmes/plugins/toolsets/grafana/grafana_tempo_api.py +454 -0
- holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +9 -0
- holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +117 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +211 -91
- holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +27 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +653 -293
- holmes/plugins/toolsets/grafana/trace_parser.py +1 -1
- holmes/plugins/toolsets/internet/internet.py +6 -7
- holmes/plugins/toolsets/internet/notion.py +5 -6
- holmes/plugins/toolsets/investigator/core_investigation.py +42 -34
- holmes/plugins/toolsets/kafka.py +25 -36
- holmes/plugins/toolsets/kubernetes.yaml +58 -84
- holmes/plugins/toolsets/kubernetes_logs.py +6 -6
- holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
- holmes/plugins/toolsets/logging_utils/logging_api.py +80 -4
- holmes/plugins/toolsets/mcp/toolset_mcp.py +181 -55
- holmes/plugins/toolsets/newrelic/__init__.py +0 -0
- holmes/plugins/toolsets/newrelic/new_relic_api.py +125 -0
- holmes/plugins/toolsets/newrelic/newrelic.jinja2 +41 -0
- holmes/plugins/toolsets/newrelic/newrelic.py +163 -0
- holmes/plugins/toolsets/opensearch/opensearch.py +10 -17
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +7 -7
- holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2 +1616 -0
- holmes/plugins/toolsets/opensearch/opensearch_query_assist.py +78 -0
- holmes/plugins/toolsets/opensearch/opensearch_query_assist_instructions.jinja2 +223 -0
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +13 -16
- holmes/plugins/toolsets/openshift.yaml +283 -0
- holmes/plugins/toolsets/prometheus/prometheus.py +915 -390
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +43 -2
- holmes/plugins/toolsets/prometheus/utils.py +28 -0
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +9 -10
- holmes/plugins/toolsets/robusta/robusta.py +236 -65
- holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +26 -9
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +137 -26
- holmes/plugins/toolsets/service_discovery.py +1 -1
- holmes/plugins/toolsets/servicenow_tables/instructions.jinja2 +83 -0
- holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +426 -0
- holmes/plugins/toolsets/utils.py +88 -0
- holmes/utils/config_utils.py +91 -0
- holmes/utils/default_toolset_installation_guide.jinja2 +1 -22
- holmes/utils/env.py +7 -0
- holmes/utils/global_instructions.py +75 -10
- holmes/utils/holmes_status.py +2 -1
- holmes/utils/holmes_sync_toolsets.py +0 -2
- holmes/utils/krr_utils.py +188 -0
- holmes/utils/sentry_helper.py +41 -0
- holmes/utils/stream.py +61 -7
- holmes/version.py +34 -14
- holmesgpt-0.16.2a0.dist-info/LICENSE +178 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/METADATA +29 -27
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/RECORD +126 -102
- holmes/core/performance_timing.py +0 -72
- holmes/plugins/toolsets/grafana/tempo_api.py +0 -124
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +0 -110
- holmes/plugins/toolsets/newrelic.py +0 -231
- holmes/plugins/toolsets/servicenow/install.md +0 -37
- holmes/plugins/toolsets/servicenow/instructions.jinja2 +0 -3
- holmes/plugins/toolsets/servicenow/servicenow.py +0 -219
- holmesgpt-0.13.2.dist-info/LICENSE.txt +0 -21
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/WHEEL +0 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/entry_points.txt +0 -0
holmes/core/tools.py
CHANGED
|
@@ -8,40 +8,69 @@ import tempfile
|
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
from enum import Enum
|
|
11
|
-
from typing import
|
|
11
|
+
from typing import (
|
|
12
|
+
TYPE_CHECKING,
|
|
13
|
+
Any,
|
|
14
|
+
Callable,
|
|
15
|
+
Dict,
|
|
16
|
+
List,
|
|
17
|
+
Optional,
|
|
18
|
+
OrderedDict,
|
|
19
|
+
Tuple,
|
|
20
|
+
Union,
|
|
21
|
+
)
|
|
12
22
|
|
|
13
23
|
from jinja2 import Template
|
|
14
|
-
from pydantic import
|
|
24
|
+
from pydantic import (
|
|
25
|
+
BaseModel,
|
|
26
|
+
ConfigDict,
|
|
27
|
+
Field,
|
|
28
|
+
FilePath,
|
|
29
|
+
model_validator,
|
|
30
|
+
PrivateAttr,
|
|
31
|
+
)
|
|
15
32
|
from rich.console import Console
|
|
16
33
|
|
|
34
|
+
from holmes.core.llm import LLM
|
|
17
35
|
from holmes.core.openai_formatting import format_tool_to_open_ai_standard
|
|
18
36
|
from holmes.plugins.prompts import load_and_render_prompt
|
|
37
|
+
from holmes.core.transformers import (
|
|
38
|
+
registry,
|
|
39
|
+
TransformerError,
|
|
40
|
+
Transformer,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from holmes.core.transformers import BaseTransformer
|
|
45
|
+
from holmes.utils.config_utils import merge_transformers
|
|
19
46
|
import time
|
|
20
47
|
from rich.table import Table
|
|
21
48
|
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
22
50
|
|
|
23
|
-
|
|
51
|
+
|
|
52
|
+
class StructuredToolResultStatus(str, Enum):
|
|
24
53
|
SUCCESS = "success"
|
|
25
54
|
ERROR = "error"
|
|
26
55
|
NO_DATA = "no_data"
|
|
27
56
|
APPROVAL_REQUIRED = "approval_required"
|
|
28
57
|
|
|
29
58
|
def to_color(self) -> str:
|
|
30
|
-
if self ==
|
|
59
|
+
if self == StructuredToolResultStatus.SUCCESS:
|
|
31
60
|
return "green"
|
|
32
|
-
elif self ==
|
|
61
|
+
elif self == StructuredToolResultStatus.ERROR:
|
|
33
62
|
return "red"
|
|
34
|
-
elif self ==
|
|
63
|
+
elif self == StructuredToolResultStatus.APPROVAL_REQUIRED:
|
|
35
64
|
return "yellow"
|
|
36
65
|
else:
|
|
37
66
|
return "white"
|
|
38
67
|
|
|
39
68
|
def to_emoji(self) -> str:
|
|
40
|
-
if self ==
|
|
69
|
+
if self == StructuredToolResultStatus.SUCCESS:
|
|
41
70
|
return "✔"
|
|
42
|
-
elif self ==
|
|
71
|
+
elif self == StructuredToolResultStatus.ERROR:
|
|
43
72
|
return "❌"
|
|
44
|
-
elif self ==
|
|
73
|
+
elif self == StructuredToolResultStatus.APPROVAL_REQUIRED:
|
|
45
74
|
return "⚠️"
|
|
46
75
|
else:
|
|
47
76
|
return "⚪️"
|
|
@@ -49,7 +78,7 @@ class ToolResultStatus(str, Enum):
|
|
|
49
78
|
|
|
50
79
|
class StructuredToolResult(BaseModel):
|
|
51
80
|
schema_version: str = "robusta:v1.0.0"
|
|
52
|
-
status:
|
|
81
|
+
status: StructuredToolResultStatus
|
|
53
82
|
error: Optional[str] = None
|
|
54
83
|
return_code: Optional[int] = None
|
|
55
84
|
data: Optional[Any] = None
|
|
@@ -129,6 +158,16 @@ class ToolParameter(BaseModel):
|
|
|
129
158
|
required: bool = True
|
|
130
159
|
properties: Optional[Dict[str, "ToolParameter"]] = None # For object types
|
|
131
160
|
items: Optional["ToolParameter"] = None # For array item schemas
|
|
161
|
+
enum: Optional[List[str]] = None # For restricting to specific values
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class ToolInvokeContext(BaseModel):
|
|
165
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
166
|
+
|
|
167
|
+
tool_number: Optional[int] = None
|
|
168
|
+
user_approved: bool = False
|
|
169
|
+
llm: LLM
|
|
170
|
+
max_token_count: int
|
|
132
171
|
|
|
133
172
|
|
|
134
173
|
class Tool(ABC, BaseModel):
|
|
@@ -143,6 +182,48 @@ class Tool(ABC, BaseModel):
|
|
|
143
182
|
default=None,
|
|
144
183
|
description="The URL of the icon for the tool, if None will get toolset icon",
|
|
145
184
|
)
|
|
185
|
+
transformers: Optional[List[Transformer]] = None
|
|
186
|
+
|
|
187
|
+
# Private attribute to store initialized transformer instances for performance
|
|
188
|
+
_transformer_instances: Optional[List["BaseTransformer"]] = PrivateAttr(
|
|
189
|
+
default=None
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def model_post_init(self, __context) -> None:
|
|
193
|
+
"""Initialize transformer instances once during tool creation for better performance."""
|
|
194
|
+
logger.debug(
|
|
195
|
+
f"Tool '{self.name}' model_post_init: creating transformer instances"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if self.transformers:
|
|
199
|
+
logger.debug(
|
|
200
|
+
f"Tool '{self.name}' has {len(self.transformers)} transformers to initialize"
|
|
201
|
+
)
|
|
202
|
+
self._transformer_instances = []
|
|
203
|
+
for transformer in self.transformers:
|
|
204
|
+
if not transformer:
|
|
205
|
+
continue
|
|
206
|
+
logger.debug(
|
|
207
|
+
f" Initializing transformer '{transformer.name}' with config: {transformer.config}"
|
|
208
|
+
)
|
|
209
|
+
try:
|
|
210
|
+
# Create transformer instance once and cache it
|
|
211
|
+
transformer_instance = registry.create_transformer(
|
|
212
|
+
transformer.name, transformer.config
|
|
213
|
+
)
|
|
214
|
+
self._transformer_instances.append(transformer_instance)
|
|
215
|
+
logger.debug(
|
|
216
|
+
f"Initialized transformer '{transformer.name}' for tool '{self.name}'"
|
|
217
|
+
)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.warning(
|
|
220
|
+
f"Failed to initialize transformer '{transformer.name}' for tool '{self.name}': {e}"
|
|
221
|
+
)
|
|
222
|
+
# Continue with other transformers, don't fail the entire initialization
|
|
223
|
+
continue
|
|
224
|
+
else:
|
|
225
|
+
logger.debug(f"Tool '{self.name}' has no transformers")
|
|
226
|
+
self._transformer_instances = None
|
|
146
227
|
|
|
147
228
|
def get_openai_format(self, target_model: str):
|
|
148
229
|
return format_tool_to_open_ai_standard(
|
|
@@ -155,32 +236,123 @@ class Tool(ABC, BaseModel):
|
|
|
155
236
|
def invoke(
|
|
156
237
|
self,
|
|
157
238
|
params: Dict,
|
|
158
|
-
|
|
159
|
-
user_approved: bool = False,
|
|
239
|
+
context: ToolInvokeContext,
|
|
160
240
|
) -> StructuredToolResult:
|
|
161
|
-
tool_number_str = f"#{tool_number} " if tool_number else ""
|
|
162
|
-
|
|
241
|
+
tool_number_str = f"#{context.tool_number} " if context.tool_number else ""
|
|
242
|
+
logger.info(
|
|
163
243
|
f"Running tool {tool_number_str}[bold]{self.name}[/bold]: {self.get_parameterized_one_liner(params)}"
|
|
164
244
|
)
|
|
165
245
|
start_time = time.time()
|
|
166
|
-
result = self._invoke(params=params,
|
|
246
|
+
result = self._invoke(params=params, context=context)
|
|
167
247
|
result.icon_url = self.icon_url
|
|
248
|
+
|
|
249
|
+
# Apply transformers to the result
|
|
250
|
+
transformed_result = self._apply_transformers(result)
|
|
168
251
|
elapsed = time.time() - start_time
|
|
169
252
|
output_str = (
|
|
170
|
-
|
|
171
|
-
if hasattr(
|
|
172
|
-
else str(
|
|
253
|
+
transformed_result.get_stringified_data()
|
|
254
|
+
if hasattr(transformed_result, "get_stringified_data")
|
|
255
|
+
else str(transformed_result)
|
|
173
256
|
)
|
|
174
|
-
show_hint = f"/show {tool_number}" if tool_number else "/show"
|
|
257
|
+
show_hint = f"/show {context.tool_number}" if context.tool_number else "/show"
|
|
175
258
|
line_count = output_str.count("\n") + 1 if output_str else 0
|
|
176
|
-
|
|
259
|
+
logger.info(
|
|
177
260
|
f" [dim]Finished {tool_number_str}in {elapsed:.2f}s, output length: {len(output_str):,} characters ({line_count:,} lines) - {show_hint} to view contents[/dim]"
|
|
178
261
|
)
|
|
262
|
+
return transformed_result
|
|
263
|
+
|
|
264
|
+
def _apply_transformers(self, result: StructuredToolResult) -> StructuredToolResult:
|
|
265
|
+
"""
|
|
266
|
+
Apply configured transformers to the tool result.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
result: The original tool result
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
The tool result with transformed data, or original result if transformation fails
|
|
273
|
+
"""
|
|
274
|
+
if (
|
|
275
|
+
not self._transformer_instances
|
|
276
|
+
or result.status != StructuredToolResultStatus.SUCCESS
|
|
277
|
+
):
|
|
278
|
+
return result
|
|
279
|
+
|
|
280
|
+
# Get the output string to transform
|
|
281
|
+
original_data = result.get_stringified_data()
|
|
282
|
+
if not original_data:
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
transformed_data = original_data
|
|
286
|
+
transformers_applied = []
|
|
287
|
+
|
|
288
|
+
# Use cached transformer instances instead of creating new ones
|
|
289
|
+
for transformer_instance in self._transformer_instances:
|
|
290
|
+
try:
|
|
291
|
+
# Check if transformer should be applied
|
|
292
|
+
if not transformer_instance.should_apply(transformed_data):
|
|
293
|
+
logger.debug(
|
|
294
|
+
f"Transformer '{transformer_instance.name}' skipped for tool '{self.name}' (conditions not met)"
|
|
295
|
+
)
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
# Apply transformation
|
|
299
|
+
pre_transform_size = len(transformed_data)
|
|
300
|
+
transform_start_time = time.time()
|
|
301
|
+
original_data = transformed_data # Keep a copy for potential reversion
|
|
302
|
+
transformed_data = transformer_instance.transform(transformed_data)
|
|
303
|
+
transform_elapsed = time.time() - transform_start_time
|
|
304
|
+
|
|
305
|
+
# Check if this is llm_summarize and revert if summary is not smaller
|
|
306
|
+
post_transform_size = len(transformed_data)
|
|
307
|
+
if (
|
|
308
|
+
transformer_instance.name == "llm_summarize"
|
|
309
|
+
and post_transform_size >= pre_transform_size
|
|
310
|
+
):
|
|
311
|
+
# Revert to original data if summary is not smaller
|
|
312
|
+
transformed_data = original_data
|
|
313
|
+
logger.debug(
|
|
314
|
+
f"Transformer '{transformer_instance.name}' reverted for tool '{self.name}' "
|
|
315
|
+
f"(output size {post_transform_size:,} >= input size {pre_transform_size:,})"
|
|
316
|
+
)
|
|
317
|
+
continue # Don't mark as applied
|
|
318
|
+
|
|
319
|
+
transformers_applied.append(transformer_instance.name)
|
|
320
|
+
|
|
321
|
+
# Generic logging - transformers can override this with their own specific metrics
|
|
322
|
+
size_change = post_transform_size - pre_transform_size
|
|
323
|
+
logger.info(
|
|
324
|
+
f"Applied transformer '{transformer_instance.name}' to tool '{self.name}' output "
|
|
325
|
+
f"in {transform_elapsed:.2f}s (size: {pre_transform_size:,} → {post_transform_size:,} chars, "
|
|
326
|
+
f"change: {size_change:+,})"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
except TransformerError as e:
|
|
330
|
+
logger.warning(
|
|
331
|
+
f"Transformer '{transformer_instance.name}' failed for tool '{self.name}': {e}"
|
|
332
|
+
)
|
|
333
|
+
# Continue with other transformers, don't fail the entire chain
|
|
334
|
+
continue
|
|
335
|
+
except Exception as e:
|
|
336
|
+
logger.error(
|
|
337
|
+
f"Unexpected error applying transformer '{transformer_instance.name}' to tool '{self.name}': {e}"
|
|
338
|
+
)
|
|
339
|
+
# Continue with other transformers
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
# If any transformers were applied, update the result
|
|
343
|
+
if transformers_applied:
|
|
344
|
+
# Create a copy of the result with transformed data
|
|
345
|
+
result_dict = result.model_dump(exclude={"data"})
|
|
346
|
+
result_dict["data"] = transformed_data
|
|
347
|
+
return StructuredToolResult(**result_dict)
|
|
348
|
+
|
|
179
349
|
return result
|
|
180
350
|
|
|
181
351
|
@abstractmethod
|
|
182
352
|
def _invoke(
|
|
183
|
-
self,
|
|
353
|
+
self,
|
|
354
|
+
params: dict,
|
|
355
|
+
context: ToolInvokeContext,
|
|
184
356
|
) -> StructuredToolResult:
|
|
185
357
|
"""
|
|
186
358
|
params: the tool params
|
|
@@ -230,15 +402,19 @@ class YAMLTool(Tool, BaseModel):
|
|
|
230
402
|
context = {**params}
|
|
231
403
|
return context
|
|
232
404
|
|
|
233
|
-
def _get_status(
|
|
405
|
+
def _get_status(
|
|
406
|
+
self, return_code: int, raw_output: str
|
|
407
|
+
) -> StructuredToolResultStatus:
|
|
234
408
|
if return_code != 0:
|
|
235
|
-
return
|
|
409
|
+
return StructuredToolResultStatus.ERROR
|
|
236
410
|
if raw_output == "":
|
|
237
|
-
return
|
|
238
|
-
return
|
|
411
|
+
return StructuredToolResultStatus.NO_DATA
|
|
412
|
+
return StructuredToolResultStatus.SUCCESS
|
|
239
413
|
|
|
240
414
|
def _invoke(
|
|
241
|
-
self,
|
|
415
|
+
self,
|
|
416
|
+
params: dict,
|
|
417
|
+
context: ToolInvokeContext,
|
|
242
418
|
) -> StructuredToolResult:
|
|
243
419
|
if self.command is not None:
|
|
244
420
|
raw_output, return_code, invocation = self.__invoke_command(params)
|
|
@@ -246,7 +422,7 @@ class YAMLTool(Tool, BaseModel):
|
|
|
246
422
|
raw_output, return_code, invocation = self.__invoke_script(params) # type: ignore
|
|
247
423
|
|
|
248
424
|
if self.additional_instructions and return_code == 0:
|
|
249
|
-
|
|
425
|
+
logger.info(
|
|
250
426
|
f"Applying additional instructions: {self.additional_instructions}"
|
|
251
427
|
)
|
|
252
428
|
output_with_instructions = self.__apply_additional_instructions(raw_output)
|
|
@@ -281,7 +457,7 @@ class YAMLTool(Tool, BaseModel):
|
|
|
281
457
|
)
|
|
282
458
|
return result.stdout.strip()
|
|
283
459
|
except subprocess.CalledProcessError as e:
|
|
284
|
-
|
|
460
|
+
logger.error(
|
|
285
461
|
f"Failed to apply additional instructions: {self.additional_instructions}. "
|
|
286
462
|
f"Error: {e.stderr}"
|
|
287
463
|
)
|
|
@@ -316,7 +492,7 @@ class YAMLTool(Tool, BaseModel):
|
|
|
316
492
|
|
|
317
493
|
def __execute_subprocess(self, cmd) -> Tuple[str, int]:
|
|
318
494
|
try:
|
|
319
|
-
|
|
495
|
+
logger.debug(f"Running `{cmd}`")
|
|
320
496
|
result = subprocess.run(
|
|
321
497
|
cmd,
|
|
322
498
|
shell=True,
|
|
@@ -329,7 +505,7 @@ class YAMLTool(Tool, BaseModel):
|
|
|
329
505
|
|
|
330
506
|
return result.stdout.strip(), result.returncode
|
|
331
507
|
except Exception as e:
|
|
332
|
-
|
|
508
|
+
logger.error(
|
|
333
509
|
f"An unexpected error occurred while running '{cmd}': {e}",
|
|
334
510
|
exc_info=True,
|
|
335
511
|
)
|
|
@@ -381,6 +557,7 @@ class Toolset(BaseModel):
|
|
|
381
557
|
config: Optional[Any] = None
|
|
382
558
|
is_default: bool = False
|
|
383
559
|
llm_instructions: Optional[str] = None
|
|
560
|
+
transformers: Optional[List[Transformer]] = None
|
|
384
561
|
|
|
385
562
|
# warning! private attributes are not copied, which can lead to subtle bugs.
|
|
386
563
|
# e.g. l.extend([some_tool]) will reset these private attribute to None
|
|
@@ -406,13 +583,85 @@ class Toolset(BaseModel):
|
|
|
406
583
|
@model_validator(mode="before")
|
|
407
584
|
def preprocess_tools(cls, values):
|
|
408
585
|
additional_instructions = values.get("additional_instructions", "")
|
|
586
|
+
transformers = values.get("transformers", None)
|
|
409
587
|
tools_data = values.get("tools", [])
|
|
588
|
+
|
|
589
|
+
# Convert raw dict transformers to Transformer objects BEFORE merging
|
|
590
|
+
if transformers:
|
|
591
|
+
converted_transformers = []
|
|
592
|
+
for t in transformers:
|
|
593
|
+
if isinstance(t, dict):
|
|
594
|
+
try:
|
|
595
|
+
transformer_obj = Transformer(**t)
|
|
596
|
+
# Check if transformer is registered
|
|
597
|
+
from holmes.core.transformers import registry
|
|
598
|
+
|
|
599
|
+
if not registry.is_registered(transformer_obj.name):
|
|
600
|
+
logger.warning(
|
|
601
|
+
f"Invalid toolset transformer configuration: Transformer '{transformer_obj.name}' is not registered"
|
|
602
|
+
)
|
|
603
|
+
continue # Skip invalid transformer
|
|
604
|
+
converted_transformers.append(transformer_obj)
|
|
605
|
+
except Exception as e:
|
|
606
|
+
# Log warning and skip invalid transformer
|
|
607
|
+
logger.warning(
|
|
608
|
+
f"Invalid toolset transformer configuration: {e}"
|
|
609
|
+
)
|
|
610
|
+
continue
|
|
611
|
+
else:
|
|
612
|
+
# Already a Transformer object
|
|
613
|
+
converted_transformers.append(t)
|
|
614
|
+
transformers = converted_transformers if converted_transformers else None
|
|
615
|
+
|
|
410
616
|
tools = []
|
|
411
617
|
for tool in tools_data:
|
|
412
618
|
if isinstance(tool, dict):
|
|
413
619
|
tool["additional_instructions"] = additional_instructions
|
|
620
|
+
|
|
621
|
+
# Convert tool-level transformers to Transformer objects
|
|
622
|
+
tool_transformers = tool.get("transformers")
|
|
623
|
+
if tool_transformers:
|
|
624
|
+
converted_tool_transformers = []
|
|
625
|
+
for t in tool_transformers:
|
|
626
|
+
if isinstance(t, dict):
|
|
627
|
+
try:
|
|
628
|
+
transformer_obj = Transformer(**t)
|
|
629
|
+
# Check if transformer is registered
|
|
630
|
+
from holmes.core.transformers import registry
|
|
631
|
+
|
|
632
|
+
if not registry.is_registered(transformer_obj.name):
|
|
633
|
+
logger.warning(
|
|
634
|
+
f"Invalid tool transformer configuration: Transformer '{transformer_obj.name}' is not registered"
|
|
635
|
+
)
|
|
636
|
+
continue # Skip invalid transformer
|
|
637
|
+
converted_tool_transformers.append(transformer_obj)
|
|
638
|
+
except Exception as e:
|
|
639
|
+
# Log warning and skip invalid transformer
|
|
640
|
+
logger.warning(
|
|
641
|
+
f"Invalid tool transformer configuration: {e}"
|
|
642
|
+
)
|
|
643
|
+
continue
|
|
644
|
+
else:
|
|
645
|
+
# Already a Transformer object
|
|
646
|
+
converted_tool_transformers.append(t)
|
|
647
|
+
tool_transformers = (
|
|
648
|
+
converted_tool_transformers
|
|
649
|
+
if converted_tool_transformers
|
|
650
|
+
else None
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
# Merge toolset-level transformers with tool-level configs
|
|
654
|
+
tool["transformers"] = merge_transformers(
|
|
655
|
+
base_transformers=transformers,
|
|
656
|
+
override_transformers=tool_transformers,
|
|
657
|
+
)
|
|
414
658
|
if isinstance(tool, Tool):
|
|
415
659
|
tool.additional_instructions = additional_instructions
|
|
660
|
+
# Merge toolset-level transformers with tool-level configs
|
|
661
|
+
tool.transformers = merge_transformers( # type: ignore
|
|
662
|
+
base_transformers=transformers,
|
|
663
|
+
override_transformers=tool.transformers,
|
|
664
|
+
)
|
|
416
665
|
tools.append(tool)
|
|
417
666
|
values["tools"] = tools
|
|
418
667
|
|
|
@@ -434,7 +683,26 @@ class Toolset(BaseModel):
|
|
|
434
683
|
def check_prerequisites(self):
|
|
435
684
|
self.status = ToolsetStatusEnum.ENABLED
|
|
436
685
|
|
|
437
|
-
|
|
686
|
+
# Sort prerequisites by type to fail fast on missing env vars before
|
|
687
|
+
# running slow commands (e.g., ArgoCD checks that timeout):
|
|
688
|
+
# 1. Static checks (instant)
|
|
689
|
+
# 2. Environment variable checks (instant, often required by commands)
|
|
690
|
+
# 3. Callable checks (variable speed)
|
|
691
|
+
# 4. Command checks (slowest - may timeout or hang)
|
|
692
|
+
def prereq_priority(prereq):
|
|
693
|
+
if isinstance(prereq, StaticPrerequisite):
|
|
694
|
+
return 0
|
|
695
|
+
elif isinstance(prereq, ToolsetEnvironmentPrerequisite):
|
|
696
|
+
return 1
|
|
697
|
+
elif isinstance(prereq, CallablePrerequisite):
|
|
698
|
+
return 2
|
|
699
|
+
elif isinstance(prereq, ToolsetCommandPrerequisite):
|
|
700
|
+
return 3
|
|
701
|
+
return 4 # Unknown types go last
|
|
702
|
+
|
|
703
|
+
sorted_prereqs = sorted(self.prerequisites, key=prereq_priority)
|
|
704
|
+
|
|
705
|
+
for prereq in sorted_prereqs:
|
|
438
706
|
if isinstance(prereq, ToolsetCommandPrerequisite):
|
|
439
707
|
try:
|
|
440
708
|
command = self.interpolate_command(prereq.command)
|
|
@@ -482,11 +750,11 @@ class Toolset(BaseModel):
|
|
|
482
750
|
self.status == ToolsetStatusEnum.DISABLED
|
|
483
751
|
or self.status == ToolsetStatusEnum.FAILED
|
|
484
752
|
):
|
|
485
|
-
|
|
753
|
+
logger.info(f"❌ Toolset {self.name}: {self.error}")
|
|
486
754
|
# no point checking further prerequisites if one failed
|
|
487
755
|
return
|
|
488
756
|
|
|
489
|
-
|
|
757
|
+
logger.info(f"✅ Toolset {self.name}")
|
|
490
758
|
|
|
491
759
|
@abstractmethod
|
|
492
760
|
def get_example_config(self) -> Dict[str, Any]:
|
|
@@ -499,6 +767,16 @@ class Toolset(BaseModel):
|
|
|
499
767
|
context={"tool_names": tool_names, "config": self.config},
|
|
500
768
|
)
|
|
501
769
|
|
|
770
|
+
def _load_llm_instructions_from_file(self, file_dir: str, filename: str) -> None:
|
|
771
|
+
"""Helper method to load LLM instructions from a jinja2 template file.
|
|
772
|
+
|
|
773
|
+
Args:
|
|
774
|
+
file_dir: Directory where the template file is located (typically os.path.dirname(__file__))
|
|
775
|
+
filename: Name of the jinja2 template file (e.g., "toolset_grafana_dashboard.jinja2")
|
|
776
|
+
"""
|
|
777
|
+
template_file_path = os.path.abspath(os.path.join(file_dir, filename))
|
|
778
|
+
self._load_llm_instructions(jinja_template=f"file://{template_file_path}")
|
|
779
|
+
|
|
502
780
|
|
|
503
781
|
class YAMLToolset(Toolset):
|
|
504
782
|
tools: List[YAMLTool] # type: ignore
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from holmes.core.llm import LLM
|
|
2
|
+
from holmes.core.models import format_tool_result_data
|
|
3
|
+
from holmes.core.tools import StructuredToolResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def count_tool_response_tokens(
|
|
7
|
+
llm: LLM, structured_tool_result: StructuredToolResult
|
|
8
|
+
) -> int:
|
|
9
|
+
message = {
|
|
10
|
+
"role": "tool",
|
|
11
|
+
"content": format_tool_result_data(structured_tool_result),
|
|
12
|
+
}
|
|
13
|
+
tokens = llm.count_tokens([message])
|
|
14
|
+
return tokens.total_tokens
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from holmes.core.llm import LLM
|
|
4
|
+
from holmes.core.tools import StructuredToolResultStatus
|
|
5
|
+
from holmes.core.models import ToolCallResult
|
|
6
|
+
from holmes.utils import sentry_helper
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ToolCallSizeMetadata(BaseModel):
|
|
10
|
+
messages_token: int
|
|
11
|
+
max_tokens_allowed: int
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_pct_token_count(percent_of_total_context_window: float, llm: LLM) -> int:
|
|
15
|
+
context_window_size = llm.get_context_window_size()
|
|
16
|
+
|
|
17
|
+
if 0 < percent_of_total_context_window and percent_of_total_context_window <= 100:
|
|
18
|
+
return int(context_window_size * percent_of_total_context_window // 100)
|
|
19
|
+
else:
|
|
20
|
+
return context_window_size
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_tool_call_too_big(
|
|
24
|
+
tool_call_result: ToolCallResult, llm: LLM
|
|
25
|
+
) -> tuple[bool, Optional[ToolCallSizeMetadata]]:
|
|
26
|
+
if tool_call_result.result.status == StructuredToolResultStatus.SUCCESS:
|
|
27
|
+
message = tool_call_result.as_tool_call_message()
|
|
28
|
+
|
|
29
|
+
tokens = llm.count_tokens(messages=[message])
|
|
30
|
+
max_tokens_allowed = llm.get_max_token_count_for_single_tool()
|
|
31
|
+
return (
|
|
32
|
+
tokens.total_tokens > max_tokens_allowed,
|
|
33
|
+
ToolCallSizeMetadata(
|
|
34
|
+
messages_token=tokens.total_tokens,
|
|
35
|
+
max_tokens_allowed=max_tokens_allowed,
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
return False, None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def prevent_overly_big_tool_response(tool_call_result: ToolCallResult, llm: LLM):
|
|
42
|
+
tool_call_result_is_too_big, metadata = is_tool_call_too_big(
|
|
43
|
+
tool_call_result=tool_call_result, llm=llm
|
|
44
|
+
)
|
|
45
|
+
if tool_call_result_is_too_big and metadata:
|
|
46
|
+
relative_pct = (
|
|
47
|
+
(metadata.messages_token - metadata.max_tokens_allowed)
|
|
48
|
+
/ metadata.messages_token
|
|
49
|
+
) * 100
|
|
50
|
+
error_message = f"The tool call result is too large to return: {metadata.messages_token} tokens.\nThe maximum allowed tokens is {metadata.max_tokens_allowed} which is {format(relative_pct, '.1f')}% smaller.\nInstructions for the LLM: try to repeat the query but proactively narrow down the result so that the tool answer fits within the allowed number of tokens."
|
|
51
|
+
tool_call_result.result.status = StructuredToolResultStatus.ERROR
|
|
52
|
+
tool_call_result.result.data = None
|
|
53
|
+
tool_call_result.result.error = error_message
|
|
54
|
+
|
|
55
|
+
sentry_helper.capture_toolcall_contains_too_many_tokens(
|
|
56
|
+
tool_call_result, metadata.messages_token, metadata.max_tokens_allowed
|
|
57
|
+
)
|
|
@@ -6,9 +6,10 @@ import sentry_sdk
|
|
|
6
6
|
from holmes.core.tools import (
|
|
7
7
|
StructuredToolResult,
|
|
8
8
|
Tool,
|
|
9
|
-
|
|
9
|
+
StructuredToolResultStatus,
|
|
10
10
|
Toolset,
|
|
11
11
|
ToolsetStatusEnum,
|
|
12
|
+
ToolInvokeContext,
|
|
12
13
|
)
|
|
13
14
|
from holmes.core.tools_utils.toolset_utils import filter_out_default_logging_toolset
|
|
14
15
|
|
|
@@ -46,16 +47,20 @@ class ToolExecutor:
|
|
|
46
47
|
)
|
|
47
48
|
self.tools_by_name[tool.name] = tool
|
|
48
49
|
|
|
49
|
-
def invoke(
|
|
50
|
+
def invoke(
|
|
51
|
+
self, tool_name: str, params: dict, context: ToolInvokeContext
|
|
52
|
+
) -> StructuredToolResult:
|
|
53
|
+
"""TODO: remove this function as it seems unused.
|
|
54
|
+
We call tool_executor.get_tool_by_name() and then tool.invoke() directly instead of this invoke function
|
|
55
|
+
"""
|
|
50
56
|
tool = self.get_tool_by_name(tool_name)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
else StructuredToolResult(
|
|
55
|
-
status=ToolResultStatus.ERROR,
|
|
57
|
+
if not tool:
|
|
58
|
+
return StructuredToolResult(
|
|
59
|
+
status=StructuredToolResultStatus.ERROR,
|
|
56
60
|
error=f"Could not find tool named {tool_name}",
|
|
57
61
|
)
|
|
58
|
-
|
|
62
|
+
|
|
63
|
+
return tool.invoke(params, context)
|
|
59
64
|
|
|
60
65
|
def get_tool_by_name(self, name: str) -> Optional[Tool]:
|
|
61
66
|
if name in self.tools_by_name:
|