holmesgpt 0.14.0a0__py3-none-any.whl → 0.14.1__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.

Files changed (82) hide show
  1. holmes/__init__.py +1 -1
  2. holmes/clients/robusta_client.py +15 -4
  3. holmes/common/env_vars.py +8 -1
  4. holmes/config.py +66 -139
  5. holmes/core/investigation.py +1 -2
  6. holmes/core/llm.py +295 -52
  7. holmes/core/models.py +2 -0
  8. holmes/core/safeguards.py +4 -4
  9. holmes/core/supabase_dal.py +14 -8
  10. holmes/core/tool_calling_llm.py +110 -102
  11. holmes/core/tools.py +260 -25
  12. holmes/core/tools_utils/data_types.py +81 -0
  13. holmes/core/tools_utils/tool_context_window_limiter.py +33 -0
  14. holmes/core/tools_utils/tool_executor.py +2 -2
  15. holmes/core/toolset_manager.py +150 -3
  16. holmes/core/transformers/__init__.py +23 -0
  17. holmes/core/transformers/base.py +62 -0
  18. holmes/core/transformers/llm_summarize.py +174 -0
  19. holmes/core/transformers/registry.py +122 -0
  20. holmes/core/transformers/transformer.py +31 -0
  21. holmes/main.py +5 -0
  22. holmes/plugins/prompts/_fetch_logs.jinja2 +10 -1
  23. holmes/plugins/toolsets/aks-node-health.yaml +46 -0
  24. holmes/plugins/toolsets/aks.yaml +64 -0
  25. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +17 -15
  26. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +8 -4
  27. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +7 -3
  28. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -3
  29. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +3 -3
  30. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +7 -3
  31. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +4 -4
  32. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +7 -3
  33. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +7 -3
  34. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +7 -3
  35. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +7 -3
  36. holmes/plugins/toolsets/bash/bash_toolset.py +6 -6
  37. holmes/plugins/toolsets/bash/common/bash.py +7 -7
  38. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +5 -3
  39. holmes/plugins/toolsets/datadog/datadog_api.py +490 -24
  40. holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +21 -10
  41. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +344 -205
  42. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +189 -17
  43. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +95 -30
  44. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +10 -10
  45. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +20 -20
  46. holmes/plugins/toolsets/git.py +21 -21
  47. holmes/plugins/toolsets/grafana/common.py +2 -2
  48. holmes/plugins/toolsets/grafana/toolset_grafana.py +4 -4
  49. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +5 -4
  50. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +123 -23
  51. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +165 -307
  52. holmes/plugins/toolsets/internet/internet.py +3 -3
  53. holmes/plugins/toolsets/internet/notion.py +3 -3
  54. holmes/plugins/toolsets/investigator/core_investigation.py +3 -3
  55. holmes/plugins/toolsets/kafka.py +18 -18
  56. holmes/plugins/toolsets/kubernetes.yaml +58 -0
  57. holmes/plugins/toolsets/kubernetes_logs.py +6 -6
  58. holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
  59. holmes/plugins/toolsets/logging_utils/logging_api.py +1 -1
  60. holmes/plugins/toolsets/mcp/toolset_mcp.py +4 -4
  61. holmes/plugins/toolsets/newrelic.py +5 -5
  62. holmes/plugins/toolsets/opensearch/opensearch.py +5 -5
  63. holmes/plugins/toolsets/opensearch/opensearch_logs.py +7 -7
  64. holmes/plugins/toolsets/opensearch/opensearch_traces.py +10 -10
  65. holmes/plugins/toolsets/prometheus/prometheus.py +841 -351
  66. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +39 -2
  67. holmes/plugins/toolsets/prometheus/utils.py +28 -0
  68. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +6 -4
  69. holmes/plugins/toolsets/robusta/robusta.py +10 -10
  70. holmes/plugins/toolsets/runbook/runbook_fetcher.py +4 -4
  71. holmes/plugins/toolsets/servicenow/servicenow.py +6 -6
  72. holmes/plugins/toolsets/utils.py +88 -0
  73. holmes/utils/config_utils.py +91 -0
  74. holmes/utils/env.py +7 -0
  75. holmes/utils/holmes_status.py +2 -1
  76. holmes/utils/sentry_helper.py +41 -0
  77. holmes/utils/stream.py +9 -0
  78. {holmesgpt-0.14.0a0.dist-info → holmesgpt-0.14.1.dist-info}/METADATA +10 -14
  79. {holmesgpt-0.14.0a0.dist-info → holmesgpt-0.14.1.dist-info}/RECORD +82 -72
  80. {holmesgpt-0.14.0a0.dist-info → holmesgpt-0.14.1.dist-info}/LICENSE.txt +0 -0
  81. {holmesgpt-0.14.0a0.dist-info → holmesgpt-0.14.1.dist-info}/WHEEL +0 -0
  82. {holmesgpt-0.14.0a0.dist-info → holmesgpt-0.14.1.dist-info}/entry_points.txt +0 -0
@@ -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 list(toolsets_by_name.values())
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 list(toolsets_by_name.values())
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.debug(
468
+ f"Starting fast_model injection. global_fast_model={self.global_fast_model}"
469
+ )
470
+
471
+ if not self.global_fast_model:
472
+ logger.debug("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
+ )
@@ -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,
@@ -11,6 +11,7 @@
11
11
  * IMPORTANT: ALWAYS inform the user about what logs you fetched. For example: "Here are pod logs for ..."
12
12
  * IMPORTANT: If logs commands have limits mention them. For example: "Showing last 100 lines of logs:"
13
13
  * IMPORTANT: If a filter was used, mention the filter. For example: "Logs filtered for 'error':"
14
+ * IMPORTANT: If a date range was used (even if just the default one and you didn't specify the parameter, mention the date range. For example: "Logs from last 1 hour..."
14
15
 
15
16
  {% if loki_ts and loki_ts.status == "enabled" -%}
16
17
  * For any logs, including for investigating kubernetes problems, use Loki
@@ -34,7 +35,15 @@ Tools to search and fetch logs from Coralogix.
34
35
  ### datadog/logs
35
36
  #### Datadog Logs Toolset
36
37
  Tools to search and fetch logs from Datadog.
37
- {% include '_default_log_prompt.jinja2' %}
38
+ * Use the tool `fetch_pod_logs` to access an application's logs.
39
+ * Do fetch application logs yourself and DO not ask users to do so
40
+ * If you have an alert/monitor try to figure out the time it fired
41
+ ** Then, use `start_time=-300` (5 minutes before `end_time`) and `end_time=<time monitor started firing>` when calling `fetch_pod_logs`.
42
+ ** If there are too many logs, or not enough, narrow or widen the timestamps
43
+ * If the user did not explicitly ask about a given timeframe, ignore the `start_time` and `end_time` so it will use the default.
44
+ * IMPORTANT: ALWAYS inform the user about the actual time period fetched (e.g., "Looking at logs from the last <X> days")
45
+ * IMPORTANT: If a limit was applied, ALWAYS tell the user how many logs were shown vs total (e.g., "Showing latest <Y> of <Z> logs")
46
+ * IMPORTANT: If any filters were applied, ALWAYS mention them explicitly
38
47
  {%- elif k8s_yaml_ts and k8s_yaml_ts.status == "enabled" -%}
39
48
  ### kubernetes/logs
40
49
  #### Kubernetes Logs Toolset