holmesgpt 0.13.2__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 +20 -6
- holmes/common/env_vars.py +58 -3
- holmes/common/openshift.py +1 -1
- holmes/config.py +123 -148
- holmes/core/conversations.py +71 -15
- holmes/core/feedback.py +191 -0
- holmes/core/investigation.py +31 -39
- holmes/core/investigation_structured_output.py +3 -3
- holmes/core/issue.py +1 -1
- holmes/core/llm.py +508 -88
- holmes/core/models.py +108 -4
- holmes/core/openai_formatting.py +14 -1
- holmes/core/prompt.py +48 -3
- holmes/core/runbooks.py +1 -0
- holmes/core/safeguards.py +8 -6
- holmes/core/supabase_dal.py +295 -100
- holmes/core/tool_calling_llm.py +489 -428
- holmes/core/tools.py +325 -56
- holmes/core/tools_utils/token_counting.py +21 -0
- holmes/core/tools_utils/tool_context_window_limiter.py +40 -0
- holmes/core/tools_utils/tool_executor.py +0 -13
- holmes/core/tools_utils/toolset_utils.py +1 -0
- holmes/core/toolset_manager.py +191 -5
- holmes/core/tracing.py +19 -3
- holmes/core/transformers/__init__.py +23 -0
- holmes/core/transformers/base.py +63 -0
- holmes/core/transformers/llm_summarize.py +175 -0
- holmes/core/transformers/registry.py +123 -0
- holmes/core/transformers/transformer.py +32 -0
- holmes/core/truncation/compaction.py +94 -0
- holmes/core/truncation/dal_truncation_utils.py +23 -0
- holmes/core/truncation/input_context_window_limiter.py +219 -0
- holmes/interactive.py +228 -31
- holmes/main.py +23 -40
- holmes/plugins/interfaces.py +2 -1
- holmes/plugins/prompts/__init__.py +2 -1
- holmes/plugins/prompts/_fetch_logs.jinja2 +31 -6
- holmes/plugins/prompts/_general_instructions.jinja2 +1 -2
- holmes/plugins/prompts/_runbook_instructions.jinja2 +24 -12
- holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
- holmes/plugins/prompts/conversation_history_compaction.jinja2 +89 -0
- holmes/plugins/prompts/generic_ask.jinja2 +0 -4
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -1
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -1
- holmes/plugins/prompts/generic_investigation.jinja2 +0 -1
- holmes/plugins/prompts/investigation_procedure.jinja2 +50 -1
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -1
- holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -1
- holmes/plugins/runbooks/__init__.py +145 -17
- holmes/plugins/runbooks/catalog.json +2 -0
- holmes/plugins/sources/github/__init__.py +4 -2
- holmes/plugins/sources/prometheus/models.py +1 -0
- holmes/plugins/toolsets/__init__.py +44 -27
- holmes/plugins/toolsets/aks-node-health.yaml +46 -0
- holmes/plugins/toolsets/aks.yaml +64 -0
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +38 -47
- 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 -13
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +15 -12
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +15 -12
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +11 -11
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +11 -9
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +15 -12
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +15 -15
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +11 -8
- 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 +11 -15
- holmes/plugins/toolsets/bash/common/bash.py +23 -13
- 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/cilium.yaml +284 -0
- 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 +525 -26
- holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +55 -11
- 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 +417 -241
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +234 -214
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +167 -79
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +374 -363
- holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
- holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
- holmes/plugins/toolsets/elasticsearch/opensearch_ppl_query_docs.jinja2 +1616 -0
- holmes/plugins/toolsets/elasticsearch/opensearch_query_assist.py +78 -0
- holmes/plugins/toolsets/elasticsearch/opensearch_query_assist_instructions.jinja2 +223 -0
- holmes/plugins/toolsets/git.py +54 -50
- holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
- holmes/plugins/toolsets/grafana/common.py +13 -29
- holmes/plugins/toolsets/grafana/grafana_tempo_api.py +455 -0
- holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +25 -0
- holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +191 -0
- holmes/plugins/toolsets/grafana/loki_api.py +4 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +293 -89
- holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +49 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +820 -292
- holmes/plugins/toolsets/grafana/trace_parser.py +4 -3
- holmes/plugins/toolsets/internet/internet.py +15 -16
- holmes/plugins/toolsets/internet/notion.py +9 -11
- holmes/plugins/toolsets/investigator/core_investigation.py +44 -36
- holmes/plugins/toolsets/investigator/model.py +3 -1
- holmes/plugins/toolsets/json_filter_mixin.py +134 -0
- holmes/plugins/toolsets/kafka.py +36 -42
- holmes/plugins/toolsets/kubernetes.yaml +317 -113
- holmes/plugins/toolsets/kubernetes_logs.py +9 -9
- holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
- holmes/plugins/toolsets/logging_utils/logging_api.py +94 -8
- holmes/plugins/toolsets/mcp/toolset_mcp.py +218 -64
- holmes/plugins/toolsets/newrelic/new_relic_api.py +165 -0
- holmes/plugins/toolsets/newrelic/newrelic.jinja2 +65 -0
- holmes/plugins/toolsets/newrelic/newrelic.py +320 -0
- holmes/plugins/toolsets/openshift.yaml +283 -0
- holmes/plugins/toolsets/prometheus/prometheus.py +1202 -421
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +54 -5
- holmes/plugins/toolsets/prometheus/utils.py +28 -0
- holmes/plugins/toolsets/rabbitmq/api.py +23 -4
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +13 -14
- holmes/plugins/toolsets/robusta/robusta.py +239 -68
- holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +26 -9
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +157 -27
- 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/connection_utils.py +31 -0
- holmes/utils/console/result.py +10 -0
- holmes/utils/default_toolset_installation_guide.jinja2 +1 -22
- holmes/utils/env.py +7 -0
- holmes/utils/file_utils.py +2 -1
- holmes/utils/global_instructions.py +60 -11
- holmes/utils/holmes_status.py +6 -4
- holmes/utils/holmes_sync_toolsets.py +0 -2
- holmes/utils/krr_utils.py +188 -0
- 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 +64 -0
- holmes/utils/stream.py +69 -8
- holmes/utils/tags.py +4 -3
- holmes/version.py +37 -15
- holmesgpt-0.18.4.dist-info/LICENSE +178 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +35 -31
- holmesgpt-0.18.4.dist-info/RECORD +258 -0
- holmes/core/performance_timing.py +0 -72
- holmes/plugins/toolsets/aws.yaml +0 -80
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -112
- holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
- holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -739
- holmes/plugins/toolsets/grafana/grafana_api.py +0 -42
- 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/opensearch/opensearch.py +0 -257
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -218
- holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
- holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
- holmes/plugins/toolsets/servicenow/install.md +0 -37
- holmes/plugins/toolsets/servicenow/instructions.jinja2 +0 -3
- holmes/plugins/toolsets/servicenow/servicenow.py +0 -219
- holmes/utils/keygen_utils.py +0 -6
- holmesgpt-0.13.2.dist-info/LICENSE.txt +0 -21
- holmesgpt-0.13.2.dist-info/RECORD +0 -234
- /holmes/plugins/toolsets/{opensearch → newrelic}/__init__.py +0 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/entry_points.txt +0 -0
holmes/core/toolset_manager.py
CHANGED
|
@@ -2,7 +2,7 @@ import concurrent.futures
|
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
-
from typing import Any, List, Optional
|
|
5
|
+
from typing import TYPE_CHECKING, Any, List, Optional, Union
|
|
6
6
|
|
|
7
7
|
from benedict import benedict
|
|
8
8
|
from pydantic import FilePath
|
|
@@ -13,8 +13,31 @@ from holmes.core.tools import Toolset, ToolsetStatusEnum, ToolsetTag, ToolsetTyp
|
|
|
13
13
|
from holmes.plugins.toolsets import load_builtin_toolsets, load_toolsets_from_config
|
|
14
14
|
from holmes.utils.definitions import CUSTOM_TOOLSET_LOCATION
|
|
15
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
pass
|
|
18
|
+
|
|
16
19
|
DEFAULT_TOOLSET_STATUS_LOCATION = os.path.join(config_path_dir, "toolsets_status.json")
|
|
17
20
|
|
|
21
|
+
# Mapping of deprecated toolset names to their new names
|
|
22
|
+
DEPRECATED_TOOLSET_NAMES: dict[str, str] = {
|
|
23
|
+
"coralogix/logs": "coralogix",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def handle_deprecated_toolset_name(
|
|
28
|
+
toolset_name: str, builtin_toolset_names: list[str]
|
|
29
|
+
) -> str:
|
|
30
|
+
if toolset_name in DEPRECATED_TOOLSET_NAMES:
|
|
31
|
+
new_name = DEPRECATED_TOOLSET_NAMES[toolset_name]
|
|
32
|
+
if new_name in builtin_toolset_names:
|
|
33
|
+
logging.warning(
|
|
34
|
+
f"The toolset name '{toolset_name}' is deprecated. "
|
|
35
|
+
f"Please use '{new_name}' instead. "
|
|
36
|
+
"The old name will continue to work but may be removed in a future version."
|
|
37
|
+
)
|
|
38
|
+
return new_name
|
|
39
|
+
return toolset_name
|
|
40
|
+
|
|
18
41
|
|
|
19
42
|
class ToolsetManager:
|
|
20
43
|
"""
|
|
@@ -30,14 +53,18 @@ class ToolsetManager:
|
|
|
30
53
|
custom_toolsets: Optional[List[FilePath]] = None,
|
|
31
54
|
custom_toolsets_from_cli: Optional[List[FilePath]] = None,
|
|
32
55
|
toolset_status_location: Optional[FilePath] = None,
|
|
56
|
+
global_fast_model: Optional[str] = None,
|
|
57
|
+
custom_runbook_catalogs: Optional[List[Union[str, FilePath]]] = None,
|
|
33
58
|
):
|
|
34
59
|
self.toolsets = toolsets
|
|
35
60
|
self.toolsets = toolsets or {}
|
|
61
|
+
self.custom_runbook_catalogs = custom_runbook_catalogs
|
|
36
62
|
if mcp_servers is not None:
|
|
37
63
|
for _, mcp_server in mcp_servers.items():
|
|
38
64
|
mcp_server["type"] = ToolsetType.MCP.value
|
|
39
65
|
self.toolsets.update(mcp_servers or {})
|
|
40
66
|
self.custom_toolsets = custom_toolsets
|
|
67
|
+
self.global_fast_model = global_fast_model
|
|
41
68
|
|
|
42
69
|
if toolset_status_location is None:
|
|
43
70
|
toolset_status_location = FilePath(DEFAULT_TOOLSET_STATUS_LOCATION)
|
|
@@ -81,7 +108,15 @@ class ToolsetManager:
|
|
|
81
108
|
3. custom toolset from config can override both built-in and add new custom toolsets # for backward compatibility
|
|
82
109
|
"""
|
|
83
110
|
# Load built-in toolsets
|
|
84
|
-
|
|
111
|
+
# Extract search paths from custom catalog files
|
|
112
|
+
additional_search_paths = None
|
|
113
|
+
if self.custom_runbook_catalogs:
|
|
114
|
+
additional_search_paths = [
|
|
115
|
+
os.path.dirname(os.path.abspath(str(catalog_path)))
|
|
116
|
+
for catalog_path in self.custom_runbook_catalogs
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
builtin_toolsets = load_builtin_toolsets(dal, additional_search_paths)
|
|
85
120
|
toolsets_by_name: dict[str, Toolset] = {
|
|
86
121
|
toolset.name: toolset for toolset in builtin_toolsets
|
|
87
122
|
}
|
|
@@ -118,9 +153,13 @@ class ToolsetManager:
|
|
|
118
153
|
if any(tag in toolset_tags for tag in toolset.tags)
|
|
119
154
|
}
|
|
120
155
|
|
|
156
|
+
# Inject global fast_model into all toolsets
|
|
157
|
+
final_toolsets = list(toolsets_by_name.values())
|
|
158
|
+
self._inject_fast_model_into_transformers(final_toolsets)
|
|
159
|
+
|
|
121
160
|
# check_prerequisites against each enabled toolset
|
|
122
161
|
if not check_prerequisites:
|
|
123
|
-
return
|
|
162
|
+
return final_toolsets
|
|
124
163
|
|
|
125
164
|
enabled_toolsets: List[Toolset] = []
|
|
126
165
|
for _, toolset in toolsets_by_name.items():
|
|
@@ -130,7 +169,7 @@ class ToolsetManager:
|
|
|
130
169
|
toolset.status = ToolsetStatusEnum.DISABLED
|
|
131
170
|
self.check_toolset_prerequisites(enabled_toolsets)
|
|
132
171
|
|
|
133
|
-
return
|
|
172
|
+
return final_toolsets
|
|
134
173
|
|
|
135
174
|
@classmethod
|
|
136
175
|
def check_toolset_prerequisites(cls, toolsets: list[Toolset]):
|
|
@@ -155,6 +194,10 @@ class ToolsetManager:
|
|
|
155
194
|
builtin_toolsets_dict: dict[str, dict[str, Any]] = {}
|
|
156
195
|
custom_toolsets_dict: dict[str, dict[str, Any]] = {}
|
|
157
196
|
for toolset_name, toolset_config in toolsets.items():
|
|
197
|
+
toolset_name = handle_deprecated_toolset_name(
|
|
198
|
+
toolset_name, builtin_toolset_names
|
|
199
|
+
)
|
|
200
|
+
|
|
158
201
|
if toolset_name in builtin_toolset_names:
|
|
159
202
|
# build-in types was assigned when loaded
|
|
160
203
|
builtin_toolsets_dict[toolset_name] = toolset_config
|
|
@@ -266,7 +309,11 @@ class ToolsetManager:
|
|
|
266
309
|
toolset.path = cached_status.get("path", None)
|
|
267
310
|
# check prerequisites for only enabled toolset when the toolset is loaded from cache. When the toolset is
|
|
268
311
|
# not loaded from cache, the prerequisites are checked in the refresh_toolset_status method.
|
|
269
|
-
if toolset.enabled and
|
|
312
|
+
if toolset.enabled and (
|
|
313
|
+
toolset.status == ToolsetStatusEnum.ENABLED
|
|
314
|
+
or toolset.type == ToolsetType.MCP
|
|
315
|
+
):
|
|
316
|
+
# MCP servers need to reload their tools even if previously failed, so rerun prerequisites
|
|
270
317
|
enabled_toolsets_from_cache.append(toolset)
|
|
271
318
|
self.check_toolset_prerequisites(enabled_toolsets_from_cache)
|
|
272
319
|
|
|
@@ -276,6 +323,10 @@ class ToolsetManager:
|
|
|
276
323
|
list(toolsets_status_by_name.keys()),
|
|
277
324
|
check_conflict_default=True,
|
|
278
325
|
)
|
|
326
|
+
|
|
327
|
+
# Inject fast_model into CLI custom toolsets
|
|
328
|
+
self._inject_fast_model_into_transformers(custom_toolsets_from_cli)
|
|
329
|
+
|
|
279
330
|
# custom toolsets from cli as experimental toolset should not override custom toolsets from config
|
|
280
331
|
enabled_toolsets_from_cli: List[Toolset] = []
|
|
281
332
|
for custom_toolset_from_cli in custom_toolsets_from_cli:
|
|
@@ -438,3 +489,138 @@ class ToolsetManager:
|
|
|
438
489
|
else:
|
|
439
490
|
existing_toolsets_by_name[new_toolset.name] = new_toolset
|
|
440
491
|
existing_toolsets_by_name[new_toolset.name] = new_toolset
|
|
492
|
+
|
|
493
|
+
def _inject_fast_model_into_transformers(self, toolsets: List[Toolset]) -> None:
|
|
494
|
+
"""
|
|
495
|
+
Inject global fast_model setting into all llm_summarize transformers that don't already have fast_model.
|
|
496
|
+
This ensures --fast-model reaches all tools regardless of toolset-level transformer configuration.
|
|
497
|
+
|
|
498
|
+
IMPORTANT: This also forces recreation of transformer instances since they may already be created.
|
|
499
|
+
"""
|
|
500
|
+
import logging
|
|
501
|
+
|
|
502
|
+
from holmes.core.transformers import registry
|
|
503
|
+
|
|
504
|
+
logger = logging.getLogger(__name__)
|
|
505
|
+
|
|
506
|
+
logger.debug(
|
|
507
|
+
f"Starting fast_model injection. global_fast_model={self.global_fast_model}"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
if not self.global_fast_model:
|
|
511
|
+
logger.debug("No global_fast_model configured, skipping injection")
|
|
512
|
+
return
|
|
513
|
+
|
|
514
|
+
injected_count = 0
|
|
515
|
+
toolset_count = 0
|
|
516
|
+
|
|
517
|
+
for toolset in toolsets:
|
|
518
|
+
toolset_count += 1
|
|
519
|
+
toolset_injected = 0
|
|
520
|
+
logger.debug(
|
|
521
|
+
f"Processing toolset '{toolset.name}', has toolset transformers: {toolset.transformers is not None}"
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Inject into toolset-level transformers
|
|
525
|
+
if toolset.transformers:
|
|
526
|
+
logger.debug(
|
|
527
|
+
f"Toolset '{toolset.name}' has {len(toolset.transformers)} toolset-level transformers"
|
|
528
|
+
)
|
|
529
|
+
for transformer in toolset.transformers:
|
|
530
|
+
logger.debug(
|
|
531
|
+
f" Toolset transformer: name='{transformer.name}', config keys={list(transformer.config.keys())}"
|
|
532
|
+
)
|
|
533
|
+
if (
|
|
534
|
+
transformer.name == "llm_summarize"
|
|
535
|
+
and "fast_model" not in transformer.config
|
|
536
|
+
):
|
|
537
|
+
transformer.config["global_fast_model"] = self.global_fast_model
|
|
538
|
+
injected_count += 1
|
|
539
|
+
toolset_injected += 1
|
|
540
|
+
logger.info(
|
|
541
|
+
f" ✓ Injected global_fast_model into toolset '{toolset.name}' transformer"
|
|
542
|
+
)
|
|
543
|
+
elif transformer.name == "llm_summarize":
|
|
544
|
+
logger.debug(
|
|
545
|
+
f" - Toolset transformer already has fast_model: {transformer.config.get('fast_model')}"
|
|
546
|
+
)
|
|
547
|
+
else:
|
|
548
|
+
logger.debug(
|
|
549
|
+
f"Toolset '{toolset.name}' has no toolset-level transformers"
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
# Inject into tool-level transformers
|
|
553
|
+
if hasattr(toolset, "tools") and toolset.tools:
|
|
554
|
+
logger.debug(f"Toolset '{toolset.name}' has {len(toolset.tools)} tools")
|
|
555
|
+
for tool in toolset.tools:
|
|
556
|
+
logger.debug(
|
|
557
|
+
f" Processing tool '{tool.name}', has transformers: {tool.transformers is not None}"
|
|
558
|
+
)
|
|
559
|
+
if tool.transformers:
|
|
560
|
+
logger.debug(
|
|
561
|
+
f" Tool '{tool.name}' has {len(tool.transformers)} transformers"
|
|
562
|
+
)
|
|
563
|
+
tool_updated = False
|
|
564
|
+
for transformer in tool.transformers:
|
|
565
|
+
logger.debug(
|
|
566
|
+
f" Tool transformer: name='{transformer.name}', config keys={list(transformer.config.keys())}"
|
|
567
|
+
)
|
|
568
|
+
if (
|
|
569
|
+
transformer.name == "llm_summarize"
|
|
570
|
+
and "fast_model" not in transformer.config
|
|
571
|
+
):
|
|
572
|
+
transformer.config["global_fast_model"] = (
|
|
573
|
+
self.global_fast_model
|
|
574
|
+
)
|
|
575
|
+
injected_count += 1
|
|
576
|
+
toolset_injected += 1
|
|
577
|
+
tool_updated = True
|
|
578
|
+
logger.info(
|
|
579
|
+
f" ✓ Injected global_fast_model into tool '{tool.name}' transformer"
|
|
580
|
+
)
|
|
581
|
+
elif transformer.name == "llm_summarize":
|
|
582
|
+
logger.debug(
|
|
583
|
+
f" - Tool transformer already has fast_model: {transformer.config.get('fast_model')}"
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# CRITICAL: Force recreation of transformer instances if we updated the config
|
|
587
|
+
if tool_updated:
|
|
588
|
+
logger.info(
|
|
589
|
+
f" 🔄 Recreating transformer instances for tool '{tool.name}' after injection"
|
|
590
|
+
)
|
|
591
|
+
if tool.transformers:
|
|
592
|
+
tool._transformer_instances = []
|
|
593
|
+
for transformer in tool.transformers:
|
|
594
|
+
if not transformer:
|
|
595
|
+
continue
|
|
596
|
+
try:
|
|
597
|
+
# Create transformer instance with updated config
|
|
598
|
+
transformer_instance = (
|
|
599
|
+
registry.create_transformer(
|
|
600
|
+
transformer.name, transformer.config
|
|
601
|
+
)
|
|
602
|
+
)
|
|
603
|
+
tool._transformer_instances.append(
|
|
604
|
+
transformer_instance
|
|
605
|
+
)
|
|
606
|
+
logger.debug(
|
|
607
|
+
f" Recreated transformer '{transformer.name}' for tool '{tool.name}' with config: {transformer.config}"
|
|
608
|
+
)
|
|
609
|
+
except Exception as e:
|
|
610
|
+
logger.warning(
|
|
611
|
+
f" Failed to recreate transformer '{transformer.name}' for tool '{tool.name}': {e}"
|
|
612
|
+
)
|
|
613
|
+
continue
|
|
614
|
+
else:
|
|
615
|
+
logger.debug(f" Tool '{tool.name}' has no transformers")
|
|
616
|
+
else:
|
|
617
|
+
logger.debug(f"Toolset '{toolset.name}' has no tools")
|
|
618
|
+
|
|
619
|
+
if toolset_injected > 0:
|
|
620
|
+
logger.info(
|
|
621
|
+
f"Toolset '{toolset.name}': injected into {toolset_injected} transformers"
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
logger.info(
|
|
625
|
+
f"Fast_model injection complete: {injected_count} transformers updated across {toolset_count} toolsets"
|
|
626
|
+
)
|
holmes/core/tracing.py
CHANGED
|
@@ -41,7 +41,18 @@ def readable_timestamp():
|
|
|
41
41
|
|
|
42
42
|
def get_active_branch_name():
|
|
43
43
|
try:
|
|
44
|
-
# First check
|
|
44
|
+
# First check GitHub Actions environment variables (CI)
|
|
45
|
+
github_head_ref = os.environ.get("GITHUB_HEAD_REF") # Set for PRs
|
|
46
|
+
if github_head_ref:
|
|
47
|
+
return github_head_ref
|
|
48
|
+
|
|
49
|
+
github_ref = os.environ.get(
|
|
50
|
+
"GITHUB_REF", ""
|
|
51
|
+
) # Set for pushes: refs/heads/branch-name
|
|
52
|
+
if github_ref.startswith("refs/heads/"):
|
|
53
|
+
return github_ref.replace("refs/heads/", "")
|
|
54
|
+
|
|
55
|
+
# Check if .git is a file (worktree case)
|
|
45
56
|
git_path = Path(".git")
|
|
46
57
|
if git_path.is_file():
|
|
47
58
|
# Read the worktree git directory path
|
|
@@ -101,7 +112,7 @@ class SpanType(Enum):
|
|
|
101
112
|
class DummySpan:
|
|
102
113
|
"""A no-op span implementation for when tracing is disabled."""
|
|
103
114
|
|
|
104
|
-
def start_span(self, name: str, span_type=None, **kwargs):
|
|
115
|
+
def start_span(self, name: Optional[str] = None, span_type=None, **kwargs):
|
|
105
116
|
return DummySpan()
|
|
106
117
|
|
|
107
118
|
def log(self, *args, **kwargs):
|
|
@@ -110,6 +121,11 @@ class DummySpan:
|
|
|
110
121
|
def end(self):
|
|
111
122
|
pass
|
|
112
123
|
|
|
124
|
+
def set_attributes(
|
|
125
|
+
self, name: Optional[str] = None, type=None, span_attributes=None
|
|
126
|
+
) -> None:
|
|
127
|
+
pass
|
|
128
|
+
|
|
113
129
|
def __enter__(self):
|
|
114
130
|
return self
|
|
115
131
|
|
|
@@ -231,7 +247,7 @@ class BraintrustTracer:
|
|
|
231
247
|
else:
|
|
232
248
|
logging.warning("No active span found in Braintrust context")
|
|
233
249
|
|
|
234
|
-
return f"https://www.braintrust.dev/app/
|
|
250
|
+
return f"https://www.braintrust.dev/app/{BRAINTRUST_ORG}/p/{self.project}/experiments/{experiment_name}"
|
|
235
251
|
|
|
236
252
|
def wrap_llm(self, llm_module):
|
|
237
253
|
"""Wrap LiteLLM with Braintrust tracing if in active context, otherwise return unwrapped."""
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transformer system for processing tool outputs.
|
|
3
|
+
|
|
4
|
+
This module provides the infrastructure for transforming tool outputs
|
|
5
|
+
before they are passed to the LLM for analysis.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .base import BaseTransformer, TransformerError
|
|
9
|
+
from .llm_summarize import LLMSummarizeTransformer
|
|
10
|
+
from .registry import TransformerRegistry, registry
|
|
11
|
+
from .transformer import Transformer
|
|
12
|
+
|
|
13
|
+
# Register built-in transformers
|
|
14
|
+
registry.register(LLMSummarizeTransformer)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"BaseTransformer",
|
|
18
|
+
"TransformerError",
|
|
19
|
+
"TransformerRegistry",
|
|
20
|
+
"registry",
|
|
21
|
+
"LLMSummarizeTransformer",
|
|
22
|
+
"Transformer",
|
|
23
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base transformer abstract class for tool output transformation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__all__ = ["BaseTransformer", "TransformerError"]
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TransformerError(Exception):
|
|
13
|
+
"""Exception raised when transformer operations fail."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseTransformer(BaseModel, ABC):
|
|
19
|
+
"""
|
|
20
|
+
Abstract base class for all tool output transformers.
|
|
21
|
+
|
|
22
|
+
Transformers process tool outputs before they are returned to the LLM,
|
|
23
|
+
enabling operations like summarization, filtering, or format conversion.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def transform(self, input_text: str) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Transform the input text and return the transformed output.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
input_text: The raw tool output to transform
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The transformed output text
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
TransformerError: If transformation fails
|
|
39
|
+
"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def should_apply(self, input_text: str) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Determine whether this transformer should be applied to the input.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
input_text: The raw tool output to check
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if the transformer should be applied, False otherwise
|
|
52
|
+
"""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def name(self) -> str:
|
|
57
|
+
"""
|
|
58
|
+
Get the transformer name.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The transformer name (class name by default)
|
|
62
|
+
"""
|
|
63
|
+
return self.__class__.__name__
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM Summarize Transformer for fast model summarization of large tool outputs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import ClassVar, Optional
|
|
7
|
+
|
|
8
|
+
from pydantic import Field, PrivateAttr, StrictStr
|
|
9
|
+
|
|
10
|
+
from ..llm import LLM, DefaultLLM
|
|
11
|
+
from .base import BaseTransformer, TransformerError
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LLMSummarizeTransformer(BaseTransformer):
|
|
17
|
+
"""
|
|
18
|
+
Transformer that uses a fast LLM model to summarize large tool outputs.
|
|
19
|
+
|
|
20
|
+
This transformer applies summarization when:
|
|
21
|
+
1. A fast model is available
|
|
22
|
+
2. The input length exceeds the configured threshold
|
|
23
|
+
|
|
24
|
+
Configuration options:
|
|
25
|
+
- input_threshold: Minimum input length to trigger summarization (default: 1000)
|
|
26
|
+
- prompt: Custom prompt template for summarization (optional)
|
|
27
|
+
- fast_model: Fast model name for summarization (e.g., "gpt-4o-mini")
|
|
28
|
+
- api_key: API key for the fast model (optional, uses default if not provided)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
DEFAULT_PROMPT: ClassVar[str] = """Summarize this operational data focusing on:
|
|
32
|
+
- What needs attention or immediate action
|
|
33
|
+
- Group similar entries into a single line and description
|
|
34
|
+
- Make sure to mention outliers, errors, and non-standard patterns
|
|
35
|
+
- List normal/healthy patterns as aggregate descriptions
|
|
36
|
+
- When listing problematic entries, also try to use aggregate descriptions when possible
|
|
37
|
+
- When possible, mention exact keywords, IDs, or patterns so the user can filter/search the original data and drill down on the parts they care about (extraction over abstraction)"""
|
|
38
|
+
|
|
39
|
+
# Pydantic fields with validation
|
|
40
|
+
input_threshold: int = Field(
|
|
41
|
+
default=1000, ge=0, description="Minimum input length to trigger summarization"
|
|
42
|
+
)
|
|
43
|
+
prompt: Optional[StrictStr] = Field(
|
|
44
|
+
default=None,
|
|
45
|
+
min_length=1,
|
|
46
|
+
description="Custom prompt template for summarization",
|
|
47
|
+
)
|
|
48
|
+
fast_model: Optional[StrictStr] = Field(
|
|
49
|
+
default=None,
|
|
50
|
+
min_length=1,
|
|
51
|
+
description="Fast model name for summarization (e.g., 'gpt-4o-mini')",
|
|
52
|
+
)
|
|
53
|
+
global_fast_model: Optional[StrictStr] = Field(
|
|
54
|
+
default=None,
|
|
55
|
+
min_length=1,
|
|
56
|
+
description="Global fast model name fallback when fast_model is not set",
|
|
57
|
+
)
|
|
58
|
+
api_key: Optional[str] = Field(
|
|
59
|
+
default=None,
|
|
60
|
+
description="API key for the fast model (optional, uses default if not provided)",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Private attribute for the LLM instance (not serialized)
|
|
64
|
+
_fast_llm: Optional[LLM] = PrivateAttr(default=None)
|
|
65
|
+
|
|
66
|
+
def model_post_init(self, __context) -> None:
|
|
67
|
+
"""Initialize the fast LLM instance after model validation."""
|
|
68
|
+
logger = logging.getLogger(__name__)
|
|
69
|
+
|
|
70
|
+
self._fast_llm = None
|
|
71
|
+
|
|
72
|
+
# Determine which fast model to use: fast_model takes precedence over global_fast_model
|
|
73
|
+
effective_fast_model = self.fast_model or self.global_fast_model
|
|
74
|
+
|
|
75
|
+
logger.debug(
|
|
76
|
+
f"LLMSummarizeTransformer initialization: fast_model='{self.fast_model}', global_fast_model='{self.global_fast_model}', effective='{effective_fast_model}'"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Create fast LLM instance if a fast model is available
|
|
80
|
+
if effective_fast_model:
|
|
81
|
+
try:
|
|
82
|
+
self._fast_llm = DefaultLLM(effective_fast_model, self.api_key)
|
|
83
|
+
logger.info(
|
|
84
|
+
f"Created fast LLM instance with model: {effective_fast_model}"
|
|
85
|
+
)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.warning(f"Failed to create fast LLM instance: {e}")
|
|
88
|
+
self._fast_llm = None
|
|
89
|
+
else:
|
|
90
|
+
logger.debug(
|
|
91
|
+
"No fast model configured (neither fast_model nor global_fast_model)"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def should_apply(self, input_text: str) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Determine if summarization should be applied to the input.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
input_text: The tool output to check
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if summarization should be applied, False otherwise
|
|
103
|
+
"""
|
|
104
|
+
logger = logging.getLogger(__name__)
|
|
105
|
+
|
|
106
|
+
# Skip if no fast model is configured
|
|
107
|
+
if self._fast_llm is None:
|
|
108
|
+
logger.debug(
|
|
109
|
+
f"Skipping summarization: no fast model configured (fast_model='{self.fast_model}', global_fast_model='{self.global_fast_model}')"
|
|
110
|
+
)
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
# Check if input exceeds threshold
|
|
114
|
+
input_length = len(input_text)
|
|
115
|
+
|
|
116
|
+
if input_length <= self.input_threshold:
|
|
117
|
+
logger.debug(
|
|
118
|
+
f"Skipping summarization: input length {input_length} <= threshold {self.input_threshold}"
|
|
119
|
+
)
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
logger.debug(
|
|
123
|
+
f"Applying summarization: input length {input_length} > threshold {self.input_threshold}"
|
|
124
|
+
)
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
def transform(self, input_text: str) -> str:
|
|
128
|
+
"""
|
|
129
|
+
Transform the input text by summarizing it with the fast model.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
input_text: The tool output to summarize
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Summarized text
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
TransformerError: If summarization fails
|
|
139
|
+
"""
|
|
140
|
+
if self._fast_llm is None:
|
|
141
|
+
raise TransformerError("Cannot transform: no fast model configured")
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
# Get the prompt to use
|
|
145
|
+
prompt = self.prompt or self.DEFAULT_PROMPT
|
|
146
|
+
|
|
147
|
+
# Construct the full prompt with the content
|
|
148
|
+
full_prompt = f"{prompt}\n\nContent to summarize:\n{input_text}"
|
|
149
|
+
|
|
150
|
+
# Perform the summarization
|
|
151
|
+
logger.debug(f"Summarizing {len(input_text)} characters with fast model")
|
|
152
|
+
|
|
153
|
+
response = self._fast_llm.completion(
|
|
154
|
+
[{"role": "user", "content": full_prompt}]
|
|
155
|
+
)
|
|
156
|
+
summarized_text = response.choices[0].message.content # type: ignore
|
|
157
|
+
|
|
158
|
+
if not summarized_text or not summarized_text.strip():
|
|
159
|
+
raise TransformerError("Fast model returned empty summary")
|
|
160
|
+
|
|
161
|
+
logger.debug(
|
|
162
|
+
f"Summarization complete: {len(input_text)} -> {len(summarized_text)} characters"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return summarized_text.strip()
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
error_msg = f"Failed to summarize content with fast model: {e}"
|
|
169
|
+
logger.error(error_msg)
|
|
170
|
+
raise TransformerError(error_msg) from e
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def name(self) -> str:
|
|
174
|
+
"""Get the transformer name."""
|
|
175
|
+
return "llm_summarize"
|