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.
Files changed (188) hide show
  1. holmes/__init__.py +3 -5
  2. holmes/clients/robusta_client.py +20 -6
  3. holmes/common/env_vars.py +58 -3
  4. holmes/common/openshift.py +1 -1
  5. holmes/config.py +123 -148
  6. holmes/core/conversations.py +71 -15
  7. holmes/core/feedback.py +191 -0
  8. holmes/core/investigation.py +31 -39
  9. holmes/core/investigation_structured_output.py +3 -3
  10. holmes/core/issue.py +1 -1
  11. holmes/core/llm.py +508 -88
  12. holmes/core/models.py +108 -4
  13. holmes/core/openai_formatting.py +14 -1
  14. holmes/core/prompt.py +48 -3
  15. holmes/core/runbooks.py +1 -0
  16. holmes/core/safeguards.py +8 -6
  17. holmes/core/supabase_dal.py +295 -100
  18. holmes/core/tool_calling_llm.py +489 -428
  19. holmes/core/tools.py +325 -56
  20. holmes/core/tools_utils/token_counting.py +21 -0
  21. holmes/core/tools_utils/tool_context_window_limiter.py +40 -0
  22. holmes/core/tools_utils/tool_executor.py +0 -13
  23. holmes/core/tools_utils/toolset_utils.py +1 -0
  24. holmes/core/toolset_manager.py +191 -5
  25. holmes/core/tracing.py +19 -3
  26. holmes/core/transformers/__init__.py +23 -0
  27. holmes/core/transformers/base.py +63 -0
  28. holmes/core/transformers/llm_summarize.py +175 -0
  29. holmes/core/transformers/registry.py +123 -0
  30. holmes/core/transformers/transformer.py +32 -0
  31. holmes/core/truncation/compaction.py +94 -0
  32. holmes/core/truncation/dal_truncation_utils.py +23 -0
  33. holmes/core/truncation/input_context_window_limiter.py +219 -0
  34. holmes/interactive.py +228 -31
  35. holmes/main.py +23 -40
  36. holmes/plugins/interfaces.py +2 -1
  37. holmes/plugins/prompts/__init__.py +2 -1
  38. holmes/plugins/prompts/_fetch_logs.jinja2 +31 -6
  39. holmes/plugins/prompts/_general_instructions.jinja2 +1 -2
  40. holmes/plugins/prompts/_runbook_instructions.jinja2 +24 -12
  41. holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
  42. holmes/plugins/prompts/conversation_history_compaction.jinja2 +89 -0
  43. holmes/plugins/prompts/generic_ask.jinja2 +0 -4
  44. holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -1
  45. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -1
  46. holmes/plugins/prompts/generic_investigation.jinja2 +0 -1
  47. holmes/plugins/prompts/investigation_procedure.jinja2 +50 -1
  48. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -1
  49. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -1
  50. holmes/plugins/runbooks/__init__.py +145 -17
  51. holmes/plugins/runbooks/catalog.json +2 -0
  52. holmes/plugins/sources/github/__init__.py +4 -2
  53. holmes/plugins/sources/prometheus/models.py +1 -0
  54. holmes/plugins/toolsets/__init__.py +44 -27
  55. holmes/plugins/toolsets/aks-node-health.yaml +46 -0
  56. holmes/plugins/toolsets/aks.yaml +64 -0
  57. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +38 -47
  58. holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +3 -2
  59. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
  60. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +3 -2
  61. holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +3 -1
  62. holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +3 -1
  63. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +12 -13
  64. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +15 -12
  65. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +15 -12
  66. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +11 -11
  67. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +11 -9
  68. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +15 -12
  69. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +15 -15
  70. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +11 -8
  71. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +11 -8
  72. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +11 -8
  73. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +11 -8
  74. holmes/plugins/toolsets/azure_sql/utils.py +0 -32
  75. holmes/plugins/toolsets/bash/argocd/__init__.py +3 -3
  76. holmes/plugins/toolsets/bash/aws/__init__.py +4 -4
  77. holmes/plugins/toolsets/bash/azure/__init__.py +4 -4
  78. holmes/plugins/toolsets/bash/bash_toolset.py +11 -15
  79. holmes/plugins/toolsets/bash/common/bash.py +23 -13
  80. holmes/plugins/toolsets/bash/common/bash_command.py +1 -1
  81. holmes/plugins/toolsets/bash/common/stringify.py +1 -1
  82. holmes/plugins/toolsets/bash/kubectl/__init__.py +2 -1
  83. holmes/plugins/toolsets/bash/kubectl/constants.py +0 -1
  84. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +3 -4
  85. holmes/plugins/toolsets/bash/parse_command.py +12 -13
  86. holmes/plugins/toolsets/cilium.yaml +284 -0
  87. holmes/plugins/toolsets/connectivity_check.py +124 -0
  88. holmes/plugins/toolsets/coralogix/api.py +132 -119
  89. holmes/plugins/toolsets/coralogix/coralogix.jinja2 +14 -0
  90. holmes/plugins/toolsets/coralogix/toolset_coralogix.py +219 -0
  91. holmes/plugins/toolsets/coralogix/utils.py +15 -79
  92. holmes/plugins/toolsets/datadog/datadog_api.py +525 -26
  93. holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +55 -11
  94. holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +3 -3
  95. holmes/plugins/toolsets/datadog/datadog_models.py +59 -0
  96. holmes/plugins/toolsets/datadog/datadog_url_utils.py +213 -0
  97. holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +165 -28
  98. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +417 -241
  99. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +234 -214
  100. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +167 -79
  101. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +374 -363
  102. holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
  103. holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
  104. holmes/plugins/toolsets/elasticsearch/opensearch_ppl_query_docs.jinja2 +1616 -0
  105. holmes/plugins/toolsets/elasticsearch/opensearch_query_assist.py +78 -0
  106. holmes/plugins/toolsets/elasticsearch/opensearch_query_assist_instructions.jinja2 +223 -0
  107. holmes/plugins/toolsets/git.py +54 -50
  108. holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
  109. holmes/plugins/toolsets/grafana/common.py +13 -29
  110. holmes/plugins/toolsets/grafana/grafana_tempo_api.py +455 -0
  111. holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +25 -0
  112. holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +191 -0
  113. holmes/plugins/toolsets/grafana/loki_api.py +4 -0
  114. holmes/plugins/toolsets/grafana/toolset_grafana.py +293 -89
  115. holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +49 -0
  116. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
  117. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +820 -292
  118. holmes/plugins/toolsets/grafana/trace_parser.py +4 -3
  119. holmes/plugins/toolsets/internet/internet.py +15 -16
  120. holmes/plugins/toolsets/internet/notion.py +9 -11
  121. holmes/plugins/toolsets/investigator/core_investigation.py +44 -36
  122. holmes/plugins/toolsets/investigator/model.py +3 -1
  123. holmes/plugins/toolsets/json_filter_mixin.py +134 -0
  124. holmes/plugins/toolsets/kafka.py +36 -42
  125. holmes/plugins/toolsets/kubernetes.yaml +317 -113
  126. holmes/plugins/toolsets/kubernetes_logs.py +9 -9
  127. holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
  128. holmes/plugins/toolsets/logging_utils/logging_api.py +94 -8
  129. holmes/plugins/toolsets/mcp/toolset_mcp.py +218 -64
  130. holmes/plugins/toolsets/newrelic/new_relic_api.py +165 -0
  131. holmes/plugins/toolsets/newrelic/newrelic.jinja2 +65 -0
  132. holmes/plugins/toolsets/newrelic/newrelic.py +320 -0
  133. holmes/plugins/toolsets/openshift.yaml +283 -0
  134. holmes/plugins/toolsets/prometheus/prometheus.py +1202 -421
  135. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +54 -5
  136. holmes/plugins/toolsets/prometheus/utils.py +28 -0
  137. holmes/plugins/toolsets/rabbitmq/api.py +23 -4
  138. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +13 -14
  139. holmes/plugins/toolsets/robusta/robusta.py +239 -68
  140. holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +26 -9
  141. holmes/plugins/toolsets/runbook/runbook_fetcher.py +157 -27
  142. holmes/plugins/toolsets/service_discovery.py +1 -1
  143. holmes/plugins/toolsets/servicenow_tables/instructions.jinja2 +83 -0
  144. holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +426 -0
  145. holmes/plugins/toolsets/utils.py +88 -0
  146. holmes/utils/config_utils.py +91 -0
  147. holmes/utils/connection_utils.py +31 -0
  148. holmes/utils/console/result.py +10 -0
  149. holmes/utils/default_toolset_installation_guide.jinja2 +1 -22
  150. holmes/utils/env.py +7 -0
  151. holmes/utils/file_utils.py +2 -1
  152. holmes/utils/global_instructions.py +60 -11
  153. holmes/utils/holmes_status.py +6 -4
  154. holmes/utils/holmes_sync_toolsets.py +0 -2
  155. holmes/utils/krr_utils.py +188 -0
  156. holmes/utils/log.py +15 -0
  157. holmes/utils/markdown_utils.py +2 -3
  158. holmes/utils/memory_limit.py +58 -0
  159. holmes/utils/sentry_helper.py +64 -0
  160. holmes/utils/stream.py +69 -8
  161. holmes/utils/tags.py +4 -3
  162. holmes/version.py +37 -15
  163. holmesgpt-0.18.4.dist-info/LICENSE +178 -0
  164. {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +35 -31
  165. holmesgpt-0.18.4.dist-info/RECORD +258 -0
  166. holmes/core/performance_timing.py +0 -72
  167. holmes/plugins/toolsets/aws.yaml +0 -80
  168. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -112
  169. holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
  170. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -739
  171. holmes/plugins/toolsets/grafana/grafana_api.py +0 -42
  172. holmes/plugins/toolsets/grafana/tempo_api.py +0 -124
  173. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +0 -110
  174. holmes/plugins/toolsets/newrelic.py +0 -231
  175. holmes/plugins/toolsets/opensearch/opensearch.py +0 -257
  176. holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
  177. holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -218
  178. holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
  179. holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
  180. holmes/plugins/toolsets/servicenow/install.md +0 -37
  181. holmes/plugins/toolsets/servicenow/instructions.jinja2 +0 -3
  182. holmes/plugins/toolsets/servicenow/servicenow.py +0 -219
  183. holmes/utils/keygen_utils.py +0 -6
  184. holmesgpt-0.13.2.dist-info/LICENSE.txt +0 -21
  185. holmesgpt-0.13.2.dist-info/RECORD +0 -234
  186. /holmes/plugins/toolsets/{opensearch → newrelic}/__init__.py +0 -0
  187. {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
  188. {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.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 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
- builtin_toolsets = load_builtin_toolsets(dal)
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 list(toolsets_by_name.values())
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 list(toolsets_by_name.values())
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 toolset.status == ToolsetStatusEnum.ENABLED:
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 if .git is a file (worktree case)
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/robustadev/p/{self.project}/experiments/{experiment_name}"
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"