holmesgpt 0.16.2a0__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 (162) hide show
  1. holmes/__init__.py +3 -5
  2. holmes/clients/robusta_client.py +4 -3
  3. holmes/common/env_vars.py +18 -2
  4. holmes/common/openshift.py +1 -1
  5. holmes/config.py +11 -6
  6. holmes/core/conversations.py +30 -13
  7. holmes/core/investigation.py +21 -25
  8. holmes/core/investigation_structured_output.py +3 -3
  9. holmes/core/issue.py +1 -1
  10. holmes/core/llm.py +50 -31
  11. holmes/core/models.py +19 -17
  12. holmes/core/openai_formatting.py +1 -1
  13. holmes/core/prompt.py +47 -2
  14. holmes/core/runbooks.py +1 -0
  15. holmes/core/safeguards.py +4 -2
  16. holmes/core/supabase_dal.py +4 -2
  17. holmes/core/tool_calling_llm.py +102 -141
  18. holmes/core/tools.py +19 -28
  19. holmes/core/tools_utils/token_counting.py +9 -2
  20. holmes/core/tools_utils/tool_context_window_limiter.py +13 -30
  21. holmes/core/tools_utils/tool_executor.py +0 -18
  22. holmes/core/tools_utils/toolset_utils.py +1 -0
  23. holmes/core/toolset_manager.py +37 -2
  24. holmes/core/tracing.py +13 -2
  25. holmes/core/transformers/__init__.py +1 -1
  26. holmes/core/transformers/base.py +1 -0
  27. holmes/core/transformers/llm_summarize.py +3 -2
  28. holmes/core/transformers/registry.py +2 -1
  29. holmes/core/transformers/transformer.py +1 -0
  30. holmes/core/truncation/compaction.py +37 -2
  31. holmes/core/truncation/input_context_window_limiter.py +3 -2
  32. holmes/interactive.py +52 -8
  33. holmes/main.py +17 -37
  34. holmes/plugins/interfaces.py +2 -1
  35. holmes/plugins/prompts/__init__.py +2 -1
  36. holmes/plugins/prompts/_fetch_logs.jinja2 +5 -5
  37. holmes/plugins/prompts/_runbook_instructions.jinja2 +2 -1
  38. holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
  39. holmes/plugins/prompts/conversation_history_compaction.jinja2 +2 -1
  40. holmes/plugins/prompts/generic_ask.jinja2 +0 -2
  41. holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -2
  42. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -2
  43. holmes/plugins/prompts/generic_investigation.jinja2 +0 -2
  44. holmes/plugins/prompts/investigation_procedure.jinja2 +2 -1
  45. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -2
  46. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -2
  47. holmes/plugins/runbooks/__init__.py +32 -3
  48. holmes/plugins/sources/github/__init__.py +4 -2
  49. holmes/plugins/sources/prometheus/models.py +1 -0
  50. holmes/plugins/toolsets/__init__.py +30 -26
  51. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +13 -12
  52. holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +3 -2
  53. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
  54. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +3 -2
  55. holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +3 -1
  56. holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +3 -1
  57. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +12 -12
  58. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +7 -7
  59. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +7 -7
  60. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -5
  61. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +3 -3
  62. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +7 -7
  63. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +6 -8
  64. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +3 -3
  65. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +3 -3
  66. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +3 -3
  67. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +3 -3
  68. holmes/plugins/toolsets/azure_sql/utils.py +0 -32
  69. holmes/plugins/toolsets/bash/argocd/__init__.py +3 -3
  70. holmes/plugins/toolsets/bash/aws/__init__.py +4 -4
  71. holmes/plugins/toolsets/bash/azure/__init__.py +4 -4
  72. holmes/plugins/toolsets/bash/bash_toolset.py +2 -3
  73. holmes/plugins/toolsets/bash/common/bash.py +19 -9
  74. holmes/plugins/toolsets/bash/common/bash_command.py +1 -1
  75. holmes/plugins/toolsets/bash/common/stringify.py +1 -1
  76. holmes/plugins/toolsets/bash/kubectl/__init__.py +2 -1
  77. holmes/plugins/toolsets/bash/kubectl/constants.py +0 -1
  78. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +3 -4
  79. holmes/plugins/toolsets/bash/parse_command.py +12 -13
  80. holmes/plugins/toolsets/connectivity_check.py +124 -0
  81. holmes/plugins/toolsets/coralogix/api.py +132 -119
  82. holmes/plugins/toolsets/coralogix/coralogix.jinja2 +14 -0
  83. holmes/plugins/toolsets/coralogix/toolset_coralogix.py +219 -0
  84. holmes/plugins/toolsets/coralogix/utils.py +15 -79
  85. holmes/plugins/toolsets/datadog/datadog_api.py +36 -3
  86. holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +34 -1
  87. holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +3 -3
  88. holmes/plugins/toolsets/datadog/datadog_models.py +59 -0
  89. holmes/plugins/toolsets/datadog/datadog_url_utils.py +213 -0
  90. holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +165 -28
  91. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +71 -28
  92. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +224 -375
  93. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +67 -36
  94. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +360 -343
  95. holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
  96. holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
  97. holmes/plugins/toolsets/git.py +7 -8
  98. holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
  99. holmes/plugins/toolsets/grafana/common.py +2 -30
  100. holmes/plugins/toolsets/grafana/grafana_tempo_api.py +2 -1
  101. holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +18 -2
  102. holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +92 -18
  103. holmes/plugins/toolsets/grafana/loki_api.py +4 -0
  104. holmes/plugins/toolsets/grafana/toolset_grafana.py +109 -25
  105. holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +22 -0
  106. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +201 -33
  107. holmes/plugins/toolsets/grafana/trace_parser.py +3 -2
  108. holmes/plugins/toolsets/internet/internet.py +10 -10
  109. holmes/plugins/toolsets/internet/notion.py +5 -6
  110. holmes/plugins/toolsets/investigator/core_investigation.py +3 -3
  111. holmes/plugins/toolsets/investigator/model.py +3 -1
  112. holmes/plugins/toolsets/json_filter_mixin.py +134 -0
  113. holmes/plugins/toolsets/kafka.py +12 -7
  114. holmes/plugins/toolsets/kubernetes.yaml +260 -30
  115. holmes/plugins/toolsets/kubernetes_logs.py +3 -3
  116. holmes/plugins/toolsets/logging_utils/logging_api.py +16 -6
  117. holmes/plugins/toolsets/mcp/toolset_mcp.py +88 -60
  118. holmes/plugins/toolsets/newrelic/new_relic_api.py +41 -1
  119. holmes/plugins/toolsets/newrelic/newrelic.jinja2 +24 -0
  120. holmes/plugins/toolsets/newrelic/newrelic.py +212 -55
  121. holmes/plugins/toolsets/prometheus/prometheus.py +358 -102
  122. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +11 -3
  123. holmes/plugins/toolsets/rabbitmq/api.py +23 -4
  124. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +5 -5
  125. holmes/plugins/toolsets/robusta/robusta.py +5 -5
  126. holmes/plugins/toolsets/runbook/runbook_fetcher.py +25 -6
  127. holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +1 -1
  128. holmes/plugins/toolsets/utils.py +1 -1
  129. holmes/utils/config_utils.py +1 -1
  130. holmes/utils/connection_utils.py +31 -0
  131. holmes/utils/console/result.py +10 -0
  132. holmes/utils/file_utils.py +2 -1
  133. holmes/utils/global_instructions.py +10 -26
  134. holmes/utils/holmes_status.py +4 -3
  135. holmes/utils/log.py +15 -0
  136. holmes/utils/markdown_utils.py +2 -3
  137. holmes/utils/memory_limit.py +58 -0
  138. holmes/utils/sentry_helper.py +23 -0
  139. holmes/utils/stream.py +12 -5
  140. holmes/utils/tags.py +4 -3
  141. holmes/version.py +3 -1
  142. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +12 -10
  143. holmesgpt-0.18.4.dist-info/RECORD +258 -0
  144. holmes/plugins/toolsets/aws.yaml +0 -80
  145. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -114
  146. holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
  147. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -736
  148. holmes/plugins/toolsets/grafana/grafana_api.py +0 -64
  149. holmes/plugins/toolsets/opensearch/__init__.py +0 -0
  150. holmes/plugins/toolsets/opensearch/opensearch.py +0 -250
  151. holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
  152. holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -215
  153. holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
  154. holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
  155. holmes/utils/keygen_utils.py +0 -6
  156. holmesgpt-0.16.2a0.dist-info/RECORD +0 -258
  157. holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_ppl_query_docs.jinja2 +0 -0
  158. holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist.py +2 -2
  159. /holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist_instructions.jinja2 +0 -0
  160. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/LICENSE +0 -0
  161. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
  162. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/entry_points.txt +0 -0
@@ -4,12 +4,9 @@ from typing import List, Optional
4
4
  import sentry_sdk
5
5
 
6
6
  from holmes.core.tools import (
7
- StructuredToolResult,
8
7
  Tool,
9
- StructuredToolResultStatus,
10
8
  Toolset,
11
9
  ToolsetStatusEnum,
12
- ToolInvokeContext,
13
10
  )
14
11
  from holmes.core.tools_utils.toolset_utils import filter_out_default_logging_toolset
15
12
 
@@ -47,21 +44,6 @@ class ToolExecutor:
47
44
  )
48
45
  self.tools_by_name[tool.name] = tool
49
46
 
50
- def invoke(
51
- self, tool_name: str, params: dict, context: ToolInvokeContext
52
- ) -> StructuredToolResult:
53
- """TODO: remove this function as it seems unused.
54
- We call tool_executor.get_tool_by_name() and then tool.invoke() directly instead of this invoke function
55
- """
56
- tool = self.get_tool_by_name(tool_name)
57
- if not tool:
58
- return StructuredToolResult(
59
- status=StructuredToolResultStatus.ERROR,
60
- error=f"Could not find tool named {tool_name}",
61
- )
62
-
63
- return tool.invoke(params, context)
64
-
65
47
  def get_tool_by_name(self, name: str) -> Optional[Tool]:
66
48
  if name in self.tools_by_name:
67
49
  return self.tools_by_name[name]
@@ -1,4 +1,5 @@
1
1
  import logging
2
+
2
3
  from holmes.core.tools import Toolset, ToolsetStatusEnum
3
4
  from holmes.plugins.toolsets.logging_utils.logging_api import BasePodLoggingToolset
4
5
 
@@ -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, TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, Any, List, Optional, Union
6
6
 
7
7
  from benedict import benedict
8
8
  from pydantic import FilePath
@@ -18,6 +18,26 @@ if TYPE_CHECKING:
18
18
 
19
19
  DEFAULT_TOOLSET_STATUS_LOCATION = os.path.join(config_path_dir, "toolsets_status.json")
20
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
+
21
41
 
22
42
  class ToolsetManager:
23
43
  """
@@ -34,9 +54,11 @@ class ToolsetManager:
34
54
  custom_toolsets_from_cli: Optional[List[FilePath]] = None,
35
55
  toolset_status_location: Optional[FilePath] = None,
36
56
  global_fast_model: Optional[str] = None,
57
+ custom_runbook_catalogs: Optional[List[Union[str, FilePath]]] = None,
37
58
  ):
38
59
  self.toolsets = toolsets
39
60
  self.toolsets = toolsets or {}
61
+ self.custom_runbook_catalogs = custom_runbook_catalogs
40
62
  if mcp_servers is not None:
41
63
  for _, mcp_server in mcp_servers.items():
42
64
  mcp_server["type"] = ToolsetType.MCP.value
@@ -86,7 +108,15 @@ class ToolsetManager:
86
108
  3. custom toolset from config can override both built-in and add new custom toolsets # for backward compatibility
87
109
  """
88
110
  # Load built-in toolsets
89
- 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)
90
120
  toolsets_by_name: dict[str, Toolset] = {
91
121
  toolset.name: toolset for toolset in builtin_toolsets
92
122
  }
@@ -164,6 +194,10 @@ class ToolsetManager:
164
194
  builtin_toolsets_dict: dict[str, dict[str, Any]] = {}
165
195
  custom_toolsets_dict: dict[str, dict[str, Any]] = {}
166
196
  for toolset_name, toolset_config in toolsets.items():
197
+ toolset_name = handle_deprecated_toolset_name(
198
+ toolset_name, builtin_toolset_names
199
+ )
200
+
167
201
  if toolset_name in builtin_toolset_names:
168
202
  # build-in types was assigned when loaded
169
203
  builtin_toolsets_dict[toolset_name] = toolset_config
@@ -464,6 +498,7 @@ class ToolsetManager:
464
498
  IMPORTANT: This also forces recreation of transformer instances since they may already be created.
465
499
  """
466
500
  import logging
501
+
467
502
  from holmes.core.transformers import registry
468
503
 
469
504
  logger = logging.getLogger(__name__)
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
@@ -236,7 +247,7 @@ class BraintrustTracer:
236
247
  else:
237
248
  logging.warning("No active span found in Braintrust context")
238
249
 
239
- 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}"
240
251
 
241
252
  def wrap_llm(self, llm_module):
242
253
  """Wrap LiteLLM with Braintrust tracing if in active context, otherwise return unwrapped."""
@@ -6,8 +6,8 @@ before they are passed to the LLM for analysis.
6
6
  """
7
7
 
8
8
  from .base import BaseTransformer, TransformerError
9
- from .registry import TransformerRegistry, registry
10
9
  from .llm_summarize import LLMSummarizeTransformer
10
+ from .registry import TransformerRegistry, registry
11
11
  from .transformer import Transformer
12
12
 
13
13
  # Register built-in transformers
@@ -5,6 +5,7 @@ Base transformer abstract class for tool output transformation.
5
5
  __all__ = ["BaseTransformer", "TransformerError"]
6
6
 
7
7
  from abc import ABC, abstractmethod
8
+
8
9
  from pydantic import BaseModel
9
10
 
10
11
 
@@ -3,11 +3,12 @@ LLM Summarize Transformer for fast model summarization of large tool outputs.
3
3
  """
4
4
 
5
5
  import logging
6
- from typing import Optional, ClassVar
6
+ from typing import ClassVar, Optional
7
+
7
8
  from pydantic import Field, PrivateAttr, StrictStr
8
9
 
10
+ from ..llm import LLM, DefaultLLM
9
11
  from .base import BaseTransformer, TransformerError
10
- from ..llm import DefaultLLM, LLM
11
12
 
12
13
  logger = logging.getLogger(__name__)
13
14
 
@@ -2,7 +2,8 @@
2
2
  Transformer registry for managing available transformers.
3
3
  """
4
4
 
5
- from typing import Dict, Type, Optional, Any, List
5
+ from typing import Any, Dict, List, Optional, Type
6
+
6
7
  from .base import BaseTransformer, TransformerError
7
8
 
8
9
 
@@ -4,6 +4,7 @@ Configuration class for tool transformers.
4
4
 
5
5
  import logging
6
6
  from typing import Any, Dict
7
+
7
8
  from pydantic import BaseModel, Field, model_validator
8
9
 
9
10
  from .registry import registry
@@ -1,8 +1,11 @@
1
1
  import logging
2
2
  from typing import Optional
3
+
4
+ import litellm
5
+ from litellm.types.utils import ModelResponse
6
+
3
7
  from holmes.core.llm import LLM
4
8
  from holmes.plugins.prompts import load_and_render_prompt
5
- from litellm.types.utils import ModelResponse
6
9
 
7
10
 
8
11
  def strip_system_prompt(
@@ -16,9 +19,26 @@ def strip_system_prompt(
16
19
  return conversation_history[:], None
17
20
 
18
21
 
22
+ def find_last_user_prompt(conversation_history: list[dict]) -> Optional[dict]:
23
+ if not conversation_history:
24
+ return None
25
+ last_user_prompt: Optional[dict] = None
26
+ for message in conversation_history:
27
+ if message.get("role") == "user":
28
+ last_user_prompt = message
29
+ return last_user_prompt
30
+
31
+
19
32
  def compact_conversation_history(
20
33
  original_conversation_history: list[dict], llm: LLM
21
34
  ) -> list[dict]:
35
+ """
36
+ The compacted conversation history contains:
37
+ 1. Original system prompt, uncompacted (if present)
38
+ 2. Last user prompt, uncompacted (if present)
39
+ 3. Compacted conversation history (role=assistant)
40
+ 4. Compaction message (role=system)
41
+ """
22
42
  conversation_history, system_prompt_message = strip_system_prompt(
23
43
  original_conversation_history
24
44
  )
@@ -27,7 +47,16 @@ def compact_conversation_history(
27
47
  )
28
48
  conversation_history.append({"role": "user", "content": compaction_instructions})
29
49
 
30
- response: ModelResponse = llm.completion(conversation_history) # type: ignore
50
+ # Set modify_params to handle providers like Anthropic that require tools
51
+ # when conversation history contains tool calls
52
+ original_modify_params = litellm.modify_params
53
+ try:
54
+ litellm.modify_params = True # necessary when using anthropic
55
+ response: ModelResponse = llm.completion(
56
+ messages=conversation_history, drop_params=True
57
+ ) # type: ignore
58
+ finally:
59
+ litellm.modify_params = original_modify_params
31
60
  response_message = None
32
61
  if (
33
62
  response
@@ -45,11 +74,17 @@ def compact_conversation_history(
45
74
  compacted_conversation_history: list[dict] = []
46
75
  if system_prompt_message:
47
76
  compacted_conversation_history.append(system_prompt_message)
77
+
78
+ last_user_prompt = find_last_user_prompt(original_conversation_history)
79
+ if last_user_prompt:
80
+ compacted_conversation_history.append(last_user_prompt)
81
+
48
82
  compacted_conversation_history.append(
49
83
  response_message.model_dump(
50
84
  exclude_defaults=True, exclude_unset=True, exclude_none=True
51
85
  )
52
86
  )
87
+
53
88
  compacted_conversation_history.append(
54
89
  {
55
90
  "role": "system",
@@ -1,7 +1,9 @@
1
1
  import logging
2
2
  from typing import Any, Optional
3
- from pydantic import BaseModel
3
+
4
4
  import sentry_sdk
5
+ from pydantic import BaseModel
6
+
5
7
  from holmes.common.env_vars import (
6
8
  ENABLE_CONVERSATION_HISTORY_COMPACTION,
7
9
  MAX_OUTPUT_TOKEN_RESERVATION,
@@ -16,7 +18,6 @@ from holmes.core.truncation.compaction import compact_conversation_history
16
18
  from holmes.utils import sentry_helper
17
19
  from holmes.utils.stream import StreamEvents, StreamMessage
18
20
 
19
-
20
21
  TRUNCATION_NOTICE = "\n\n[TRUNCATED]"
21
22
 
22
23
 
holmes/interactive.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import os
3
+ import re
3
4
  import subprocess
4
5
  import tempfile
5
6
  import threading
@@ -37,7 +38,7 @@ from holmes.core.feedback import (
37
38
  UserFeedback,
38
39
  )
39
40
  from holmes.core.prompt import build_initial_ask_messages
40
- from holmes.core.tool_calling_llm import ToolCallingLLM, ToolCallResult
41
+ from holmes.core.tool_calling_llm import LLMResult, ToolCallingLLM, ToolCallResult
41
42
  from holmes.core.tools import StructuredToolResult, pretty_print_toolset_status
42
43
  from holmes.core.tracing import DummyTracer
43
44
  from holmes.utils.colors import (
@@ -49,8 +50,8 @@ from holmes.utils.colors import (
49
50
  USER_COLOR,
50
51
  )
51
52
  from holmes.utils.console.consts import agent_name
53
+ from holmes.utils.file_utils import write_json_file
52
54
  from holmes.version import check_version_async
53
- import re
54
55
 
55
56
 
56
57
  class SlashCommands(Enum):
@@ -952,18 +953,51 @@ def display_recent_tool_outputs(
952
953
  )
953
954
 
954
955
 
956
+ def save_conversation_to_file(
957
+ json_output_file: str,
958
+ messages: List,
959
+ all_tool_calls_history: List[ToolCallResult],
960
+ console: Console,
961
+ ) -> None:
962
+ """Save the current conversation to a JSON file."""
963
+ try:
964
+ # Create LLMResult-like structure for consistency with non-interactive mode
965
+ conversation_result = LLMResult(
966
+ messages=messages,
967
+ tool_calls=all_tool_calls_history,
968
+ result=None, # No single result in interactive mode
969
+ total_cost=0.0, # TODO: Could aggregate costs from all responses if needed
970
+ total_tokens=0,
971
+ prompt_tokens=0,
972
+ completion_tokens=0,
973
+ metadata={
974
+ "session_type": "interactive",
975
+ "total_turns": len([m for m in messages if m.get("role") == "user"]),
976
+ },
977
+ )
978
+ write_json_file(json_output_file, conversation_result.model_dump())
979
+ console.print(
980
+ f"[bold {STATUS_COLOR}]Conversation saved to {json_output_file}[/bold {STATUS_COLOR}]"
981
+ )
982
+ except Exception as e:
983
+ logging.error(f"Failed to save conversation: {e}", exc_info=e)
984
+ console.print(
985
+ f"[bold {ERROR_COLOR}]Failed to save conversation: {e}[/bold {ERROR_COLOR}]"
986
+ )
987
+
988
+
955
989
  def run_interactive_loop(
956
990
  ai: ToolCallingLLM,
957
991
  console: Console,
958
992
  initial_user_input: Optional[str],
959
993
  include_files: Optional[List[Path]],
960
- post_processing_prompt: Optional[str],
961
994
  show_tool_output: bool,
962
995
  tracer=None,
963
996
  runbooks=None,
964
997
  system_prompt_additions: Optional[str] = None,
965
998
  check_version: bool = True,
966
999
  feedback_callback: Optional[FeedbackCallback] = None,
1000
+ json_output_file: Optional[str] = None,
967
1001
  ) -> None:
968
1002
  # Initialize tracer - use DummyTracer if no tracer provided
969
1003
  if tracer is None:
@@ -1134,6 +1168,9 @@ def run_interactive_loop(
1134
1168
  continue
1135
1169
 
1136
1170
  if command == SlashCommands.EXIT.command:
1171
+ console.print(
1172
+ f"[bold {STATUS_COLOR}]Exiting interactive mode.[/bold {STATUS_COLOR}]"
1173
+ )
1137
1174
  return
1138
1175
  elif command == SlashCommands.HELP.command:
1139
1176
  console.print(
@@ -1230,7 +1267,6 @@ def run_interactive_loop(
1230
1267
  )
1231
1268
  response = ai.call(
1232
1269
  messages,
1233
- post_processing_prompt,
1234
1270
  trace_span=trace_span,
1235
1271
  tool_number_offset=len(all_tool_calls_history),
1236
1272
  )
@@ -1263,9 +1299,21 @@ def run_interactive_loop(
1263
1299
  )
1264
1300
 
1265
1301
  console.print("")
1302
+
1303
+ # Save conversation after each AI response
1304
+ if json_output_file and messages:
1305
+ save_conversation_to_file(
1306
+ json_output_file, messages, all_tool_calls_history, console
1307
+ )
1266
1308
  except typer.Abort:
1309
+ console.print(
1310
+ f"[bold {STATUS_COLOR}]Exiting interactive mode.[/bold {STATUS_COLOR}]"
1311
+ )
1267
1312
  break
1268
1313
  except EOFError: # Handle Ctrl+D
1314
+ console.print(
1315
+ f"[bold {STATUS_COLOR}]Exiting interactive mode.[/bold {STATUS_COLOR}]"
1316
+ )
1269
1317
  break
1270
1318
  except Exception as e:
1271
1319
  logging.error("An error occurred during interactive mode:", exc_info=e)
@@ -1275,7 +1323,3 @@ def run_interactive_loop(
1275
1323
  trace_url = tracer.get_trace_url()
1276
1324
  if trace_url:
1277
1325
  console.print(f"🔍 View trace: {trace_url}")
1278
-
1279
- console.print(
1280
- f"[bold {STATUS_COLOR}]Exiting interactive mode.[/bold {STATUS_COLOR}]"
1281
- )
holmes/main.py CHANGED
@@ -10,6 +10,7 @@ if add_custom_certificate(ADDITIONAL_CERTIFICATE):
10
10
  # DO NOT ADD ANY IMPORTS OR CODE ABOVE THIS LINE
11
11
  # IMPORTING ABOVE MIGHT INITIALIZE AN HTTPS CLIENT THAT DOESN'T TRUST THE CUSTOM CERTIFICATE
12
12
  import sys
13
+ from holmes.utils.colors import USER_COLOR
13
14
  import json
14
15
  import logging
15
16
  import socket
@@ -28,7 +29,7 @@ from holmes.config import (
28
29
  SourceFactory,
29
30
  SupportedTicketSources,
30
31
  )
31
- from holmes.core.prompt import build_initial_ask_messages
32
+ from holmes.core.prompt import build_initial_ask_messages, generate_user_prompt
32
33
  from holmes.core.resource_instruction import ResourceInstructionDocument
33
34
  from holmes.core.tools import pretty_print_toolset_status
34
35
  from holmes.core.tracing import SpanType, TracingFactory
@@ -41,7 +42,6 @@ from holmes.utils.console.consts import system_prompt_help
41
42
  from holmes.utils.console.logging import init_logging
42
43
  from holmes.utils.console.result import handle_result
43
44
  from holmes.utils.file_utils import write_json_file
44
- from holmes.utils.colors import USER_COLOR
45
45
 
46
46
  app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
47
47
  investigate_app = typer.Typer(
@@ -137,13 +137,6 @@ opt_json_output_file: Optional[str] = typer.Option(
137
137
  envvar="HOLMES_JSON_OUTPUT_FILE",
138
138
  )
139
139
 
140
- opt_post_processing_prompt: Optional[str] = typer.Option(
141
- None,
142
- "--post-processing-prompt",
143
- help="Adds a prompt for post processing. (Preferable for chatty ai models)",
144
- envvar="HOLMES_POST_PROCESSING_PROMPT",
145
- )
146
-
147
140
  opt_documents: Optional[str] = typer.Option(
148
141
  None,
149
142
  "--documents",
@@ -201,7 +194,6 @@ def ask(
201
194
  ),
202
195
  json_output_file: Optional[str] = opt_json_output_file,
203
196
  echo_request: bool = opt_echo_request,
204
- post_processing_prompt: Optional[str] = opt_post_processing_prompt,
205
197
  interactive: bool = typer.Option(
206
198
  True,
207
199
  "--interactive/--no-interactive",
@@ -261,6 +253,7 @@ def ask(
261
253
  dal=None, # type: ignore
262
254
  refresh_toolsets=refresh_toolsets, # flag to refresh the toolset status
263
255
  tracer=tracer,
256
+ model_name=model,
264
257
  )
265
258
 
266
259
  if prompt_file and prompt:
@@ -298,11 +291,11 @@ def ask(
298
291
  console,
299
292
  prompt,
300
293
  include_file,
301
- post_processing_prompt,
302
294
  show_tool_output,
303
295
  tracer,
304
296
  config.get_runbook_catalog(),
305
297
  system_prompt_additions,
298
+ json_output_file=json_output_file,
306
299
  )
307
300
  return
308
301
 
@@ -319,7 +312,7 @@ def ask(
319
312
  f'holmes ask "{prompt}"', span_type=SpanType.TASK
320
313
  ) as trace_span:
321
314
  trace_span.log(input=prompt, metadata={"type": "user_question"})
322
- response = ai.call(messages, post_processing_prompt, trace_span=trace_span)
315
+ response = ai.call(messages, trace_span=trace_span)
323
316
  trace_span.log(
324
317
  output=response.result,
325
318
  )
@@ -345,6 +338,7 @@ def ask(
345
338
  issue,
346
339
  show_tool_output,
347
340
  False, # type: ignore
341
+ log_costs,
348
342
  )
349
343
 
350
344
  if trace_url:
@@ -390,7 +384,6 @@ def alertmanager(
390
384
  system_prompt: Optional[str] = typer.Option(
391
385
  "builtin://generic_investigation.jinja2", help=system_prompt_help
392
386
  ),
393
- post_processing_prompt: Optional[str] = opt_post_processing_prompt,
394
387
  ):
395
388
  """
396
389
  Investigate a Prometheus/Alertmanager alert
@@ -413,7 +406,7 @@ def alertmanager(
413
406
  custom_runbooks=custom_runbooks,
414
407
  )
415
408
 
416
- ai = config.create_console_issue_investigator() # type: ignore
409
+ ai = config.create_console_issue_investigator(model_name=model) # type: ignore
417
410
 
418
411
  source = config.create_alertmanager_source()
419
412
 
@@ -446,8 +439,6 @@ def alertmanager(
446
439
  issue=issue,
447
440
  prompt=system_prompt, # type: ignore
448
441
  console=console,
449
- instructions=None,
450
- post_processing_prompt=post_processing_prompt,
451
442
  )
452
443
  results.append({"issue": issue.model_dump(), "result": result.model_dump()})
453
444
  handle_result(result, console, destination, config, issue, False, True) # type: ignore
@@ -524,7 +515,6 @@ def jira(
524
515
  system_prompt: Optional[str] = typer.Option(
525
516
  "builtin://generic_investigation.jinja2", help=system_prompt_help
526
517
  ),
527
- post_processing_prompt: Optional[str] = opt_post_processing_prompt,
528
518
  ):
529
519
  """
530
520
  Investigate a Jira ticket
@@ -542,7 +532,7 @@ def jira(
542
532
  custom_toolsets_from_cli=custom_toolsets,
543
533
  custom_runbooks=custom_runbooks,
544
534
  )
545
- ai = config.create_console_issue_investigator() # type: ignore
535
+ ai = config.create_console_issue_investigator(model_name=model) # type: ignore
546
536
  source = config.create_jira_source()
547
537
  try:
548
538
  issues = source.fetch_issues()
@@ -563,8 +553,6 @@ def jira(
563
553
  issue=issue,
564
554
  prompt=system_prompt, # type: ignore
565
555
  console=console,
566
- instructions=None,
567
- post_processing_prompt=post_processing_prompt,
568
556
  )
569
557
 
570
558
  console.print(Rule())
@@ -617,7 +605,7 @@ def ticket(
617
605
  system_prompt: Optional[str] = typer.Option(
618
606
  "builtin://generic_ticket.jinja2", help=system_prompt_help
619
607
  ),
620
- post_processing_prompt: Optional[str] = opt_post_processing_prompt,
608
+ model: Optional[str] = opt_model,
621
609
  ):
622
610
  """
623
611
  Fetch and print a Jira ticket from the specified source.
@@ -658,7 +646,7 @@ def ticket(
658
646
  },
659
647
  )
660
648
 
661
- ai = ticket_source.config.create_console_issue_investigator()
649
+ ai = ticket_source.config.create_console_issue_investigator(model_name=model)
662
650
  console.print(
663
651
  f"[bold yellow]Analyzing ticket: {issue_to_investigate.name}...[/bold yellow]"
664
652
  )
@@ -667,7 +655,8 @@ def ticket(
667
655
  + f" for issue '{issue_to_investigate.name}' with description:'{issue_to_investigate.description}'"
668
656
  )
669
657
 
670
- result = ai.prompt_call(system_prompt, prompt, post_processing_prompt)
658
+ ticket_user_prompt = generate_user_prompt(prompt, context={})
659
+ result = ai.prompt_call(system_prompt, ticket_user_prompt)
671
660
 
672
661
  console.print(Rule())
673
662
  console.print(
@@ -688,14 +677,14 @@ def github(
688
677
  ),
689
678
  github_owner: Optional[str] = typer.Option(
690
679
  None,
691
- help="The GitHub repository Owner, eg: if the repository url is https://github.com/robusta-dev/holmesgpt, the owner is robusta-dev",
680
+ help="The GitHub repository Owner, eg: if the repository url is https://github.com/HolmesGPT/holmesgpt, the owner is HolmesGPT",
692
681
  ),
693
682
  github_pat: str = typer.Option(
694
683
  None,
695
684
  ),
696
685
  github_repository: Optional[str] = typer.Option(
697
686
  None,
698
- help="The GitHub repository name, eg: if the repository url is https://github.com/robusta-dev/holmesgpt, the repository name is holmesgpt",
687
+ help="The GitHub repository name, eg: if the repository url is https://github.com/HolmesGPT/holmesgpt, the repository name is holmesgpt",
699
688
  ),
700
689
  update: Optional[bool] = typer.Option(False, help="Update GitHub with AI results"),
701
690
  github_query: Optional[str] = typer.Option(
@@ -714,7 +703,6 @@ def github(
714
703
  system_prompt: Optional[str] = typer.Option(
715
704
  "builtin://generic_investigation.jinja2", help=system_prompt_help
716
705
  ),
717
- post_processing_prompt: Optional[str] = opt_post_processing_prompt,
718
706
  ):
719
707
  """
720
708
  Investigate a GitHub issue
@@ -733,7 +721,7 @@ def github(
733
721
  custom_toolsets_from_cli=custom_toolsets,
734
722
  custom_runbooks=custom_runbooks,
735
723
  )
736
- ai = config.create_console_issue_investigator()
724
+ ai = config.create_console_issue_investigator(model_name=model)
737
725
  source = config.create_github_source()
738
726
  try:
739
727
  issues = source.fetch_issues()
@@ -753,8 +741,6 @@ def github(
753
741
  issue=issue,
754
742
  prompt=system_prompt, # type: ignore
755
743
  console=console,
756
- instructions=None,
757
- post_processing_prompt=post_processing_prompt,
758
744
  )
759
745
 
760
746
  console.print(Rule())
@@ -800,7 +786,6 @@ def pagerduty(
800
786
  system_prompt: Optional[str] = typer.Option(
801
787
  "builtin://generic_investigation.jinja2", help=system_prompt_help
802
788
  ),
803
- post_processing_prompt: Optional[str] = opt_post_processing_prompt,
804
789
  ):
805
790
  """
806
791
  Investigate a PagerDuty incident
@@ -817,7 +802,7 @@ def pagerduty(
817
802
  custom_toolsets_from_cli=custom_toolsets,
818
803
  custom_runbooks=custom_runbooks,
819
804
  )
820
- ai = config.create_console_issue_investigator()
805
+ ai = config.create_console_issue_investigator(model_name=model)
821
806
  source = config.create_pagerduty_source()
822
807
  try:
823
808
  issues = source.fetch_issues()
@@ -839,8 +824,6 @@ def pagerduty(
839
824
  issue=issue,
840
825
  prompt=system_prompt, # type: ignore
841
826
  console=console,
842
- instructions=None,
843
- post_processing_prompt=post_processing_prompt,
844
827
  )
845
828
 
846
829
  console.print(Rule())
@@ -885,7 +868,6 @@ def opsgenie(
885
868
  system_prompt: Optional[str] = typer.Option(
886
869
  "builtin://generic_investigation.jinja2", help=system_prompt_help
887
870
  ),
888
- post_processing_prompt: Optional[str] = opt_post_processing_prompt,
889
871
  documents: Optional[str] = opt_documents,
890
872
  ):
891
873
  """
@@ -903,7 +885,7 @@ def opsgenie(
903
885
  custom_toolsets_from_cli=custom_toolsets,
904
886
  custom_runbooks=custom_runbooks,
905
887
  )
906
- ai = config.create_console_issue_investigator()
888
+ ai = config.create_console_issue_investigator(model_name=model)
907
889
  source = config.create_opsgenie_source()
908
890
  try:
909
891
  issues = source.fetch_issues()
@@ -922,8 +904,6 @@ def opsgenie(
922
904
  issue=issue,
923
905
  prompt=system_prompt, # type: ignore
924
906
  console=console,
925
- instructions=None,
926
- post_processing_prompt=post_processing_prompt,
927
907
  )
928
908
 
929
909
  console.print(Rule())
@@ -1,4 +1,5 @@
1
- from typing import List, Iterable
1
+ from typing import Iterable, List
2
+
2
3
  from holmes.core.issue import Issue
3
4
  from holmes.core.tool_calling_llm import LLMResult
4
5