holmesgpt 0.13.3a0__py3-none-any.whl → 0.14.1a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of holmesgpt might be problematic. Click here for more details.
- holmes/__init__.py +1 -1
- holmes/clients/robusta_client.py +10 -2
- holmes/common/env_vars.py +8 -1
- holmes/config.py +66 -139
- holmes/core/investigation.py +1 -2
- holmes/core/llm.py +256 -51
- holmes/core/models.py +2 -0
- holmes/core/safeguards.py +4 -4
- holmes/core/supabase_dal.py +14 -8
- holmes/core/tool_calling_llm.py +193 -176
- holmes/core/tools.py +260 -25
- holmes/core/tools_utils/data_types.py +81 -0
- holmes/core/tools_utils/tool_context_window_limiter.py +33 -0
- holmes/core/tools_utils/tool_executor.py +2 -2
- holmes/core/toolset_manager.py +150 -3
- 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/main.py +5 -0
- holmes/plugins/toolsets/aks-node-health.yaml +46 -0
- holmes/plugins/toolsets/aks.yaml +64 -0
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +17 -15
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +8 -4
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +7 -3
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -3
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +3 -3
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +7 -3
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +4 -4
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +7 -3
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +7 -3
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +7 -3
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +7 -3
- holmes/plugins/toolsets/bash/bash_toolset.py +6 -6
- holmes/plugins/toolsets/bash/common/bash.py +7 -7
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +5 -3
- holmes/plugins/toolsets/datadog/toolset_datadog_general.py +16 -17
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +9 -10
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +21 -22
- holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +8 -8
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +18 -19
- holmes/plugins/toolsets/git.py +22 -22
- holmes/plugins/toolsets/grafana/common.py +14 -2
- holmes/plugins/toolsets/grafana/grafana_tempo_api.py +473 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +4 -4
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +3 -3
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +662 -290
- holmes/plugins/toolsets/grafana/trace_parser.py +1 -1
- holmes/plugins/toolsets/internet/internet.py +3 -3
- holmes/plugins/toolsets/internet/notion.py +3 -3
- holmes/plugins/toolsets/investigator/core_investigation.py +3 -3
- holmes/plugins/toolsets/kafka.py +18 -18
- holmes/plugins/toolsets/kubernetes.yaml +58 -0
- holmes/plugins/toolsets/kubernetes_logs.py +6 -6
- holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
- holmes/plugins/toolsets/mcp/toolset_mcp.py +4 -4
- holmes/plugins/toolsets/newrelic.py +8 -8
- holmes/plugins/toolsets/opensearch/opensearch.py +5 -5
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +7 -7
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +10 -10
- holmes/plugins/toolsets/prometheus/prometheus.py +172 -39
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +25 -0
- holmes/plugins/toolsets/prometheus/utils.py +28 -0
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +6 -4
- holmes/plugins/toolsets/robusta/robusta.py +10 -10
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +4 -4
- holmes/plugins/toolsets/servicenow/servicenow.py +6 -6
- holmes/plugins/toolsets/utils.py +88 -0
- holmes/utils/config_utils.py +91 -0
- holmes/utils/env.py +7 -0
- holmes/utils/holmes_status.py +2 -1
- holmes/utils/sentry_helper.py +41 -0
- holmes/utils/stream.py +9 -0
- {holmesgpt-0.13.3a0.dist-info → holmesgpt-0.14.1a0.dist-info}/METADATA +10 -14
- {holmesgpt-0.13.3a0.dist-info → holmesgpt-0.14.1a0.dist-info}/RECORD +81 -71
- holmes/plugins/toolsets/grafana/tempo_api.py +0 -124
- {holmesgpt-0.13.3a0.dist-info → holmesgpt-0.14.1a0.dist-info}/LICENSE.txt +0 -0
- {holmesgpt-0.13.3a0.dist-info → holmesgpt-0.14.1a0.dist-info}/WHEEL +0 -0
- {holmesgpt-0.13.3a0.dist-info → holmesgpt-0.14.1a0.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 Any, List, Optional, TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
from benedict import benedict
|
|
8
8
|
from pydantic import FilePath
|
|
@@ -13,6 +13,9 @@ 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
|
|
|
18
21
|
|
|
@@ -30,6 +33,7 @@ class ToolsetManager:
|
|
|
30
33
|
custom_toolsets: Optional[List[FilePath]] = None,
|
|
31
34
|
custom_toolsets_from_cli: Optional[List[FilePath]] = None,
|
|
32
35
|
toolset_status_location: Optional[FilePath] = None,
|
|
36
|
+
global_fast_model: Optional[str] = None,
|
|
33
37
|
):
|
|
34
38
|
self.toolsets = toolsets
|
|
35
39
|
self.toolsets = toolsets or {}
|
|
@@ -38,6 +42,7 @@ class ToolsetManager:
|
|
|
38
42
|
mcp_server["type"] = ToolsetType.MCP.value
|
|
39
43
|
self.toolsets.update(mcp_servers or {})
|
|
40
44
|
self.custom_toolsets = custom_toolsets
|
|
45
|
+
self.global_fast_model = global_fast_model
|
|
41
46
|
|
|
42
47
|
if toolset_status_location is None:
|
|
43
48
|
toolset_status_location = FilePath(DEFAULT_TOOLSET_STATUS_LOCATION)
|
|
@@ -118,9 +123,13 @@ class ToolsetManager:
|
|
|
118
123
|
if any(tag in toolset_tags for tag in toolset.tags)
|
|
119
124
|
}
|
|
120
125
|
|
|
126
|
+
# Inject global fast_model into all toolsets
|
|
127
|
+
final_toolsets = list(toolsets_by_name.values())
|
|
128
|
+
self._inject_fast_model_into_transformers(final_toolsets)
|
|
129
|
+
|
|
121
130
|
# check_prerequisites against each enabled toolset
|
|
122
131
|
if not check_prerequisites:
|
|
123
|
-
return
|
|
132
|
+
return final_toolsets
|
|
124
133
|
|
|
125
134
|
enabled_toolsets: List[Toolset] = []
|
|
126
135
|
for _, toolset in toolsets_by_name.items():
|
|
@@ -130,7 +139,7 @@ class ToolsetManager:
|
|
|
130
139
|
toolset.status = ToolsetStatusEnum.DISABLED
|
|
131
140
|
self.check_toolset_prerequisites(enabled_toolsets)
|
|
132
141
|
|
|
133
|
-
return
|
|
142
|
+
return final_toolsets
|
|
134
143
|
|
|
135
144
|
@classmethod
|
|
136
145
|
def check_toolset_prerequisites(cls, toolsets: list[Toolset]):
|
|
@@ -276,6 +285,10 @@ class ToolsetManager:
|
|
|
276
285
|
list(toolsets_status_by_name.keys()),
|
|
277
286
|
check_conflict_default=True,
|
|
278
287
|
)
|
|
288
|
+
|
|
289
|
+
# Inject fast_model into CLI custom toolsets
|
|
290
|
+
self._inject_fast_model_into_transformers(custom_toolsets_from_cli)
|
|
291
|
+
|
|
279
292
|
# custom toolsets from cli as experimental toolset should not override custom toolsets from config
|
|
280
293
|
enabled_toolsets_from_cli: List[Toolset] = []
|
|
281
294
|
for custom_toolset_from_cli in custom_toolsets_from_cli:
|
|
@@ -438,3 +451,137 @@ class ToolsetManager:
|
|
|
438
451
|
else:
|
|
439
452
|
existing_toolsets_by_name[new_toolset.name] = new_toolset
|
|
440
453
|
existing_toolsets_by_name[new_toolset.name] = new_toolset
|
|
454
|
+
|
|
455
|
+
def _inject_fast_model_into_transformers(self, toolsets: List[Toolset]) -> None:
|
|
456
|
+
"""
|
|
457
|
+
Inject global fast_model setting into all llm_summarize transformers that don't already have fast_model.
|
|
458
|
+
This ensures --fast-model reaches all tools regardless of toolset-level transformer configuration.
|
|
459
|
+
|
|
460
|
+
IMPORTANT: This also forces recreation of transformer instances since they may already be created.
|
|
461
|
+
"""
|
|
462
|
+
import logging
|
|
463
|
+
from holmes.core.transformers import registry
|
|
464
|
+
|
|
465
|
+
logger = logging.getLogger(__name__)
|
|
466
|
+
|
|
467
|
+
logger.info(
|
|
468
|
+
f"Starting fast_model injection. global_fast_model={self.global_fast_model}"
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
if not self.global_fast_model:
|
|
472
|
+
logger.info("No global_fast_model configured, skipping injection")
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
injected_count = 0
|
|
476
|
+
toolset_count = 0
|
|
477
|
+
|
|
478
|
+
for toolset in toolsets:
|
|
479
|
+
toolset_count += 1
|
|
480
|
+
toolset_injected = 0
|
|
481
|
+
logger.debug(
|
|
482
|
+
f"Processing toolset '{toolset.name}', has toolset transformers: {toolset.transformers is not None}"
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Inject into toolset-level transformers
|
|
486
|
+
if toolset.transformers:
|
|
487
|
+
logger.debug(
|
|
488
|
+
f"Toolset '{toolset.name}' has {len(toolset.transformers)} toolset-level transformers"
|
|
489
|
+
)
|
|
490
|
+
for transformer in toolset.transformers:
|
|
491
|
+
logger.debug(
|
|
492
|
+
f" Toolset transformer: name='{transformer.name}', config keys={list(transformer.config.keys())}"
|
|
493
|
+
)
|
|
494
|
+
if (
|
|
495
|
+
transformer.name == "llm_summarize"
|
|
496
|
+
and "fast_model" not in transformer.config
|
|
497
|
+
):
|
|
498
|
+
transformer.config["global_fast_model"] = self.global_fast_model
|
|
499
|
+
injected_count += 1
|
|
500
|
+
toolset_injected += 1
|
|
501
|
+
logger.info(
|
|
502
|
+
f" ✓ Injected global_fast_model into toolset '{toolset.name}' transformer"
|
|
503
|
+
)
|
|
504
|
+
elif transformer.name == "llm_summarize":
|
|
505
|
+
logger.debug(
|
|
506
|
+
f" - Toolset transformer already has fast_model: {transformer.config.get('fast_model')}"
|
|
507
|
+
)
|
|
508
|
+
else:
|
|
509
|
+
logger.debug(
|
|
510
|
+
f"Toolset '{toolset.name}' has no toolset-level transformers"
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
# Inject into tool-level transformers
|
|
514
|
+
if hasattr(toolset, "tools") and toolset.tools:
|
|
515
|
+
logger.debug(f"Toolset '{toolset.name}' has {len(toolset.tools)} tools")
|
|
516
|
+
for tool in toolset.tools:
|
|
517
|
+
logger.debug(
|
|
518
|
+
f" Processing tool '{tool.name}', has transformers: {tool.transformers is not None}"
|
|
519
|
+
)
|
|
520
|
+
if tool.transformers:
|
|
521
|
+
logger.debug(
|
|
522
|
+
f" Tool '{tool.name}' has {len(tool.transformers)} transformers"
|
|
523
|
+
)
|
|
524
|
+
tool_updated = False
|
|
525
|
+
for transformer in tool.transformers:
|
|
526
|
+
logger.debug(
|
|
527
|
+
f" Tool transformer: name='{transformer.name}', config keys={list(transformer.config.keys())}"
|
|
528
|
+
)
|
|
529
|
+
if (
|
|
530
|
+
transformer.name == "llm_summarize"
|
|
531
|
+
and "fast_model" not in transformer.config
|
|
532
|
+
):
|
|
533
|
+
transformer.config["global_fast_model"] = (
|
|
534
|
+
self.global_fast_model
|
|
535
|
+
)
|
|
536
|
+
injected_count += 1
|
|
537
|
+
toolset_injected += 1
|
|
538
|
+
tool_updated = True
|
|
539
|
+
logger.info(
|
|
540
|
+
f" ✓ Injected global_fast_model into tool '{tool.name}' transformer"
|
|
541
|
+
)
|
|
542
|
+
elif transformer.name == "llm_summarize":
|
|
543
|
+
logger.debug(
|
|
544
|
+
f" - Tool transformer already has fast_model: {transformer.config.get('fast_model')}"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# CRITICAL: Force recreation of transformer instances if we updated the config
|
|
548
|
+
if tool_updated:
|
|
549
|
+
logger.info(
|
|
550
|
+
f" 🔄 Recreating transformer instances for tool '{tool.name}' after injection"
|
|
551
|
+
)
|
|
552
|
+
if tool.transformers:
|
|
553
|
+
tool._transformer_instances = []
|
|
554
|
+
for transformer in tool.transformers:
|
|
555
|
+
if not transformer:
|
|
556
|
+
continue
|
|
557
|
+
try:
|
|
558
|
+
# Create transformer instance with updated config
|
|
559
|
+
transformer_instance = (
|
|
560
|
+
registry.create_transformer(
|
|
561
|
+
transformer.name, transformer.config
|
|
562
|
+
)
|
|
563
|
+
)
|
|
564
|
+
tool._transformer_instances.append(
|
|
565
|
+
transformer_instance
|
|
566
|
+
)
|
|
567
|
+
logger.debug(
|
|
568
|
+
f" Recreated transformer '{transformer.name}' for tool '{tool.name}' with config: {transformer.config}"
|
|
569
|
+
)
|
|
570
|
+
except Exception as e:
|
|
571
|
+
logger.warning(
|
|
572
|
+
f" Failed to recreate transformer '{transformer.name}' for tool '{tool.name}': {e}"
|
|
573
|
+
)
|
|
574
|
+
continue
|
|
575
|
+
else:
|
|
576
|
+
logger.debug(f" Tool '{tool.name}' has no transformers")
|
|
577
|
+
else:
|
|
578
|
+
logger.debug(f"Toolset '{toolset.name}' has no tools")
|
|
579
|
+
|
|
580
|
+
if toolset_injected > 0:
|
|
581
|
+
logger.info(
|
|
582
|
+
f"Toolset '{toolset.name}': injected into {toolset_injected} transformers"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
logger.info(
|
|
586
|
+
f"Fast_model injection complete: {injected_count} transformers updated across {toolset_count} toolsets"
|
|
587
|
+
)
|
holmes/core/tracing.py
CHANGED
|
@@ -101,7 +101,7 @@ class SpanType(Enum):
|
|
|
101
101
|
class DummySpan:
|
|
102
102
|
"""A no-op span implementation for when tracing is disabled."""
|
|
103
103
|
|
|
104
|
-
def start_span(self, name: str, span_type=None, **kwargs):
|
|
104
|
+
def start_span(self, name: Optional[str] = None, span_type=None, **kwargs):
|
|
105
105
|
return DummySpan()
|
|
106
106
|
|
|
107
107
|
def log(self, *args, **kwargs):
|
|
@@ -110,6 +110,11 @@ class DummySpan:
|
|
|
110
110
|
def end(self):
|
|
111
111
|
pass
|
|
112
112
|
|
|
113
|
+
def set_attributes(
|
|
114
|
+
self, name: Optional[str] = None, type=None, span_attributes=None
|
|
115
|
+
) -> None:
|
|
116
|
+
pass
|
|
117
|
+
|
|
113
118
|
def __enter__(self):
|
|
114
119
|
return self
|
|
115
120
|
|
|
@@ -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 .registry import TransformerRegistry, registry
|
|
10
|
+
from .llm_summarize import LLMSummarizeTransformer
|
|
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,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base transformer abstract class for tool output transformation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__all__ = ["BaseTransformer", "TransformerError"]
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TransformerError(Exception):
|
|
12
|
+
"""Exception raised when transformer operations fail."""
|
|
13
|
+
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseTransformer(BaseModel, ABC):
|
|
18
|
+
"""
|
|
19
|
+
Abstract base class for all tool output transformers.
|
|
20
|
+
|
|
21
|
+
Transformers process tool outputs before they are returned to the LLM,
|
|
22
|
+
enabling operations like summarization, filtering, or format conversion.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def transform(self, input_text: str) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Transform the input text and return the transformed output.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
input_text: The raw tool output to transform
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The transformed output text
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
TransformerError: If transformation fails
|
|
38
|
+
"""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def should_apply(self, input_text: str) -> bool:
|
|
43
|
+
"""
|
|
44
|
+
Determine whether this transformer should be applied to the input.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
input_text: The raw tool output to check
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
True if the transformer should be applied, False otherwise
|
|
51
|
+
"""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def name(self) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Get the transformer name.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The transformer name (class name by default)
|
|
61
|
+
"""
|
|
62
|
+
return self.__class__.__name__
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM Summarize Transformer for fast model summarization of large tool outputs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Optional, ClassVar
|
|
7
|
+
from pydantic import Field, PrivateAttr, StrictStr
|
|
8
|
+
|
|
9
|
+
from .base import BaseTransformer, TransformerError
|
|
10
|
+
from ..llm import DefaultLLM, LLM
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LLMSummarizeTransformer(BaseTransformer):
|
|
16
|
+
"""
|
|
17
|
+
Transformer that uses a fast LLM model to summarize large tool outputs.
|
|
18
|
+
|
|
19
|
+
This transformer applies summarization when:
|
|
20
|
+
1. A fast model is available
|
|
21
|
+
2. The input length exceeds the configured threshold
|
|
22
|
+
|
|
23
|
+
Configuration options:
|
|
24
|
+
- input_threshold: Minimum input length to trigger summarization (default: 1000)
|
|
25
|
+
- prompt: Custom prompt template for summarization (optional)
|
|
26
|
+
- fast_model: Fast model name for summarization (e.g., "gpt-4o-mini")
|
|
27
|
+
- api_key: API key for the fast model (optional, uses default if not provided)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
DEFAULT_PROMPT: ClassVar[str] = """Summarize this operational data focusing on:
|
|
31
|
+
- What needs attention or immediate action
|
|
32
|
+
- Group similar entries into a single line and description
|
|
33
|
+
- Make sure to mention outliers, errors, and non-standard patterns
|
|
34
|
+
- List normal/healthy patterns as aggregate descriptions
|
|
35
|
+
- When listing problematic entries, also try to use aggregate descriptions when possible
|
|
36
|
+
- 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)"""
|
|
37
|
+
|
|
38
|
+
# Pydantic fields with validation
|
|
39
|
+
input_threshold: int = Field(
|
|
40
|
+
default=1000, ge=0, description="Minimum input length to trigger summarization"
|
|
41
|
+
)
|
|
42
|
+
prompt: Optional[StrictStr] = Field(
|
|
43
|
+
default=None,
|
|
44
|
+
min_length=1,
|
|
45
|
+
description="Custom prompt template for summarization",
|
|
46
|
+
)
|
|
47
|
+
fast_model: Optional[StrictStr] = Field(
|
|
48
|
+
default=None,
|
|
49
|
+
min_length=1,
|
|
50
|
+
description="Fast model name for summarization (e.g., 'gpt-4o-mini')",
|
|
51
|
+
)
|
|
52
|
+
global_fast_model: Optional[StrictStr] = Field(
|
|
53
|
+
default=None,
|
|
54
|
+
min_length=1,
|
|
55
|
+
description="Global fast model name fallback when fast_model is not set",
|
|
56
|
+
)
|
|
57
|
+
api_key: Optional[str] = Field(
|
|
58
|
+
default=None,
|
|
59
|
+
description="API key for the fast model (optional, uses default if not provided)",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Private attribute for the LLM instance (not serialized)
|
|
63
|
+
_fast_llm: Optional[LLM] = PrivateAttr(default=None)
|
|
64
|
+
|
|
65
|
+
def model_post_init(self, __context) -> None:
|
|
66
|
+
"""Initialize the fast LLM instance after model validation."""
|
|
67
|
+
logger = logging.getLogger(__name__)
|
|
68
|
+
|
|
69
|
+
self._fast_llm = None
|
|
70
|
+
|
|
71
|
+
# Determine which fast model to use: fast_model takes precedence over global_fast_model
|
|
72
|
+
effective_fast_model = self.fast_model or self.global_fast_model
|
|
73
|
+
|
|
74
|
+
logger.debug(
|
|
75
|
+
f"LLMSummarizeTransformer initialization: fast_model='{self.fast_model}', global_fast_model='{self.global_fast_model}', effective='{effective_fast_model}'"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Create fast LLM instance if a fast model is available
|
|
79
|
+
if effective_fast_model:
|
|
80
|
+
try:
|
|
81
|
+
self._fast_llm = DefaultLLM(effective_fast_model, self.api_key)
|
|
82
|
+
logger.info(
|
|
83
|
+
f"Created fast LLM instance with model: {effective_fast_model}"
|
|
84
|
+
)
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.warning(f"Failed to create fast LLM instance: {e}")
|
|
87
|
+
self._fast_llm = None
|
|
88
|
+
else:
|
|
89
|
+
logger.debug(
|
|
90
|
+
"No fast model configured (neither fast_model nor global_fast_model)"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def should_apply(self, input_text: str) -> bool:
|
|
94
|
+
"""
|
|
95
|
+
Determine if summarization should be applied to the input.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
input_text: The tool output to check
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True if summarization should be applied, False otherwise
|
|
102
|
+
"""
|
|
103
|
+
logger = logging.getLogger(__name__)
|
|
104
|
+
|
|
105
|
+
# Skip if no fast model is configured
|
|
106
|
+
if self._fast_llm is None:
|
|
107
|
+
logger.debug(
|
|
108
|
+
f"Skipping summarization: no fast model configured (fast_model='{self.fast_model}', global_fast_model='{self.global_fast_model}')"
|
|
109
|
+
)
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
# Check if input exceeds threshold
|
|
113
|
+
input_length = len(input_text)
|
|
114
|
+
|
|
115
|
+
if input_length <= self.input_threshold:
|
|
116
|
+
logger.debug(
|
|
117
|
+
f"Skipping summarization: input length {input_length} <= threshold {self.input_threshold}"
|
|
118
|
+
)
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
logger.debug(
|
|
122
|
+
f"Applying summarization: input length {input_length} > threshold {self.input_threshold}"
|
|
123
|
+
)
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
def transform(self, input_text: str) -> str:
|
|
127
|
+
"""
|
|
128
|
+
Transform the input text by summarizing it with the fast model.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
input_text: The tool output to summarize
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Summarized text
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
TransformerError: If summarization fails
|
|
138
|
+
"""
|
|
139
|
+
if self._fast_llm is None:
|
|
140
|
+
raise TransformerError("Cannot transform: no fast model configured")
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
# Get the prompt to use
|
|
144
|
+
prompt = self.prompt or self.DEFAULT_PROMPT
|
|
145
|
+
|
|
146
|
+
# Construct the full prompt with the content
|
|
147
|
+
full_prompt = f"{prompt}\n\nContent to summarize:\n{input_text}"
|
|
148
|
+
|
|
149
|
+
# Perform the summarization
|
|
150
|
+
logger.debug(f"Summarizing {len(input_text)} characters with fast model")
|
|
151
|
+
|
|
152
|
+
response = self._fast_llm.completion(
|
|
153
|
+
[{"role": "user", "content": full_prompt}]
|
|
154
|
+
)
|
|
155
|
+
summarized_text = response.choices[0].message.content # type: ignore
|
|
156
|
+
|
|
157
|
+
if not summarized_text or not summarized_text.strip():
|
|
158
|
+
raise TransformerError("Fast model returned empty summary")
|
|
159
|
+
|
|
160
|
+
logger.debug(
|
|
161
|
+
f"Summarization complete: {len(input_text)} -> {len(summarized_text)} characters"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return summarized_text.strip()
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
error_msg = f"Failed to summarize content with fast model: {e}"
|
|
168
|
+
logger.error(error_msg)
|
|
169
|
+
raise TransformerError(error_msg) from e
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def name(self) -> str:
|
|
173
|
+
"""Get the transformer name."""
|
|
174
|
+
return "llm_summarize"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transformer registry for managing available transformers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Type, Optional, Any, List
|
|
6
|
+
from .base import BaseTransformer, TransformerError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TransformerRegistry:
|
|
10
|
+
"""
|
|
11
|
+
Registry for managing transformer types and creating transformer instances.
|
|
12
|
+
|
|
13
|
+
This registry provides a centralized way to register transformer classes
|
|
14
|
+
and create instances based on configuration.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self._transformers: Dict[str, Type[BaseTransformer]] = {}
|
|
19
|
+
|
|
20
|
+
def register(self, transformer_class: Type[BaseTransformer]) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Register a transformer class, using the transformer's name property.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
transformer_class: The transformer class to register
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ValueError: If name is already registered or transformer_class is invalid
|
|
29
|
+
"""
|
|
30
|
+
if not issubclass(transformer_class, BaseTransformer):
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"Transformer class must inherit from BaseTransformer, got {transformer_class}"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Get name from the transformer class
|
|
36
|
+
try:
|
|
37
|
+
temp_instance = transformer_class()
|
|
38
|
+
name = temp_instance.name
|
|
39
|
+
except Exception:
|
|
40
|
+
# Fallback to class name if instantiation fails
|
|
41
|
+
name = transformer_class.__name__
|
|
42
|
+
|
|
43
|
+
if name in self._transformers:
|
|
44
|
+
raise ValueError(f"Transformer '{name}' is already registered")
|
|
45
|
+
|
|
46
|
+
self._transformers[name] = transformer_class
|
|
47
|
+
|
|
48
|
+
def unregister(self, name: str) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Unregister a transformer by name.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
name: The name of the transformer to unregister
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
KeyError: If transformer name is not registered
|
|
57
|
+
"""
|
|
58
|
+
if name not in self._transformers:
|
|
59
|
+
raise KeyError(f"Transformer '{name}' is not registered")
|
|
60
|
+
|
|
61
|
+
del self._transformers[name]
|
|
62
|
+
|
|
63
|
+
def create_transformer(
|
|
64
|
+
self, name: str, config: Optional[Dict[str, Any]] = None
|
|
65
|
+
) -> BaseTransformer:
|
|
66
|
+
"""
|
|
67
|
+
Create a transformer instance by name.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
name: The name of the transformer to create
|
|
71
|
+
config: Optional configuration for the transformer
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
A new transformer instance
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
KeyError: If transformer name is not registered
|
|
78
|
+
TransformerError: If transformer creation fails
|
|
79
|
+
"""
|
|
80
|
+
if name not in self._transformers:
|
|
81
|
+
raise KeyError(f"Transformer '{name}' is not registered")
|
|
82
|
+
|
|
83
|
+
transformer_class = self._transformers[name]
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
# Handle both old-style dict config and new Pydantic models
|
|
87
|
+
if config is None:
|
|
88
|
+
return transformer_class()
|
|
89
|
+
else:
|
|
90
|
+
# For Pydantic models, pass config as keyword arguments
|
|
91
|
+
return transformer_class(**config)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise TransformerError(f"Failed to create transformer '{name}': {e}") from e
|
|
94
|
+
|
|
95
|
+
def is_registered(self, name: str) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
Check if a transformer is registered.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
name: The name to check
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if the transformer is registered, False otherwise
|
|
104
|
+
"""
|
|
105
|
+
return name in self._transformers
|
|
106
|
+
|
|
107
|
+
def list_transformers(self) -> List[str]:
|
|
108
|
+
"""
|
|
109
|
+
Get a list of all registered transformer names.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
List of registered transformer names
|
|
113
|
+
"""
|
|
114
|
+
return list(self._transformers.keys())
|
|
115
|
+
|
|
116
|
+
def clear(self) -> None:
|
|
117
|
+
"""Clear all registered transformers."""
|
|
118
|
+
self._transformers.clear()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Global transformer registry instance
|
|
122
|
+
registry = TransformerRegistry()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration class for tool transformers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
from pydantic import BaseModel, Field, model_validator
|
|
8
|
+
|
|
9
|
+
from .registry import registry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Transformer(BaseModel):
|
|
13
|
+
"""
|
|
14
|
+
Configuration for a tool transformer.
|
|
15
|
+
|
|
16
|
+
Each transformer config specifies a transformer type and its parameters.
|
|
17
|
+
This replaces the previous dict-based configuration with proper type safety.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
name: str = Field(description="Name of the transformer (e.g., 'llm_summarize')")
|
|
21
|
+
config: Dict[str, Any] = Field(
|
|
22
|
+
default_factory=dict, description="Configuration parameters for the transformer"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@model_validator(mode="after")
|
|
26
|
+
def validate_transformer(self):
|
|
27
|
+
"""Validate that the transformer name is known to the registry."""
|
|
28
|
+
if not registry.is_registered(self.name):
|
|
29
|
+
# Log warning but don't fail validation - allows for graceful degradation
|
|
30
|
+
logging.warning(f"Transformer '{self.name}' is not registered")
|
|
31
|
+
return self
|
holmes/main.py
CHANGED
|
@@ -76,6 +76,9 @@ opt_api_key: Optional[str] = typer.Option(
|
|
|
76
76
|
help="API key to use for the LLM (if not given, uses environment variables OPENAI_API_KEY or AZURE_API_KEY)",
|
|
77
77
|
)
|
|
78
78
|
opt_model: Optional[str] = typer.Option(None, help="Model to use for the LLM")
|
|
79
|
+
opt_fast_model: Optional[str] = typer.Option(
|
|
80
|
+
None, help="Optional fast model for summarization tasks"
|
|
81
|
+
)
|
|
79
82
|
opt_config_file: Optional[Path] = typer.Option(
|
|
80
83
|
DEFAULT_CONFIG_LOCATION, # type: ignore
|
|
81
84
|
"--config",
|
|
@@ -177,6 +180,7 @@ def ask(
|
|
|
177
180
|
# common options
|
|
178
181
|
api_key: Optional[str] = opt_api_key,
|
|
179
182
|
model: Optional[str] = opt_model,
|
|
183
|
+
fast_model: Optional[str] = opt_fast_model,
|
|
180
184
|
config_file: Optional[Path] = opt_config_file,
|
|
181
185
|
custom_toolsets: Optional[List[Path]] = opt_custom_toolsets,
|
|
182
186
|
max_steps: Optional[int] = opt_max_steps,
|
|
@@ -244,6 +248,7 @@ def ask(
|
|
|
244
248
|
config_file,
|
|
245
249
|
api_key=api_key,
|
|
246
250
|
model=model,
|
|
251
|
+
fast_model=fast_model,
|
|
247
252
|
max_steps=max_steps,
|
|
248
253
|
custom_toolsets_from_cli=custom_toolsets,
|
|
249
254
|
slack_token=slack_token,
|