holmesgpt 0.12.3__py3-none-any.whl → 0.12.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.

Potentially problematic release.


This version of holmesgpt might be problematic. Click here for more details.

Files changed (52) hide show
  1. holmes/__init__.py +1 -1
  2. holmes/config.py +75 -33
  3. holmes/core/config.py +5 -0
  4. holmes/core/conversations.py +17 -2
  5. holmes/core/investigation.py +1 -0
  6. holmes/core/llm.py +1 -2
  7. holmes/core/prompt.py +29 -4
  8. holmes/core/supabase_dal.py +49 -13
  9. holmes/core/tool_calling_llm.py +26 -1
  10. holmes/core/tools.py +2 -1
  11. holmes/core/tools_utils/tool_executor.py +1 -0
  12. holmes/core/toolset_manager.py +10 -3
  13. holmes/core/tracing.py +77 -10
  14. holmes/interactive.py +110 -20
  15. holmes/main.py +13 -18
  16. holmes/plugins/destinations/slack/plugin.py +19 -9
  17. holmes/plugins/prompts/_fetch_logs.jinja2 +11 -1
  18. holmes/plugins/prompts/_general_instructions.jinja2 +6 -37
  19. holmes/plugins/prompts/_permission_errors.jinja2 +6 -0
  20. holmes/plugins/prompts/_runbook_instructions.jinja2 +13 -5
  21. holmes/plugins/prompts/_toolsets_instructions.jinja2 +22 -14
  22. holmes/plugins/prompts/generic_ask.jinja2 +6 -0
  23. holmes/plugins/prompts/generic_ask_conversation.jinja2 +1 -0
  24. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +1 -0
  25. holmes/plugins/prompts/generic_investigation.jinja2 +1 -0
  26. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -2
  27. holmes/plugins/runbooks/__init__.py +20 -4
  28. holmes/plugins/toolsets/__init__.py +7 -9
  29. holmes/plugins/toolsets/aks-node-health.yaml +0 -8
  30. holmes/plugins/toolsets/argocd.yaml +4 -1
  31. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +1 -1
  32. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +2 -0
  33. holmes/plugins/toolsets/confluence.yaml +1 -1
  34. holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +54 -4
  35. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +150 -6
  36. holmes/plugins/toolsets/kubernetes.yaml +6 -0
  37. holmes/plugins/toolsets/prometheus/prometheus.py +2 -6
  38. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +2 -2
  39. holmes/plugins/toolsets/runbook/runbook_fetcher.py +65 -6
  40. holmes/plugins/toolsets/service_discovery.py +1 -1
  41. holmes/plugins/toolsets/slab.yaml +1 -1
  42. holmes/utils/colors.py +7 -0
  43. holmes/utils/console/consts.py +5 -0
  44. holmes/utils/console/result.py +2 -1
  45. holmes/utils/keygen_utils.py +6 -0
  46. holmes/version.py +2 -2
  47. holmesgpt-0.12.4.dist-info/METADATA +258 -0
  48. {holmesgpt-0.12.3.dist-info → holmesgpt-0.12.4.dist-info}/RECORD +51 -47
  49. holmesgpt-0.12.3.dist-info/METADATA +0 -400
  50. {holmesgpt-0.12.3.dist-info → holmesgpt-0.12.4.dist-info}/LICENSE.txt +0 -0
  51. {holmesgpt-0.12.3.dist-info → holmesgpt-0.12.4.dist-info}/WHEEL +0 -0
  52. {holmesgpt-0.12.3.dist-info → holmesgpt-0.12.4.dist-info}/entry_points.txt +0 -0
holmes/core/tools.py CHANGED
@@ -155,8 +155,9 @@ class Tool(ABC, BaseModel):
155
155
  else str(result)
156
156
  )
157
157
  show_hint = f"/show {tool_number}" if tool_number else "/show"
158
+ line_count = output_str.count("\n") + 1 if output_str else 0
158
159
  logging.info(
159
- f" [dim]Finished {tool_number_str}in {elapsed:.2f}s, output length: {len(output_str):,} characters - {show_hint} to view contents[/dim]"
160
+ f" [dim]Finished {tool_number_str}in {elapsed:.2f}s, output length: {len(output_str):,} characters ({line_count:,} lines) - {show_hint} to view contents[/dim]"
160
161
  )
161
162
  return result
162
163
 
@@ -15,6 +15,7 @@ from holmes.core.tools_utils.toolset_utils import filter_out_default_logging_too
15
15
 
16
16
  class ToolExecutor:
17
17
  def __init__(self, toolsets: List[Toolset]):
18
+ # TODO: expose function for this instead of callers accessing directly
18
19
  self.toolsets = toolsets
19
20
 
20
21
  enabled_toolsets: list[Toolset] = list(
@@ -7,12 +7,13 @@ from typing import Any, List, Optional
7
7
  from benedict import benedict
8
8
  from pydantic import FilePath
9
9
 
10
+ from holmes.core.config import config_path_dir
10
11
  from holmes.core.supabase_dal import SupabaseDal
11
12
  from holmes.core.tools import Toolset, ToolsetStatusEnum, ToolsetTag, ToolsetType
12
13
  from holmes.plugins.toolsets import load_builtin_toolsets, load_toolsets_from_config
13
14
  from holmes.utils.definitions import CUSTOM_TOOLSET_LOCATION
14
15
 
15
- DEFAULT_TOOLSET_STATUS_LOCATION = os.path.expanduser("~/.holmes/toolsets_status.json")
16
+ DEFAULT_TOOLSET_STATUS_LOCATION = os.path.join(config_path_dir, "toolsets_status.json")
16
17
 
17
18
 
18
19
  class ToolsetManager:
@@ -25,11 +26,17 @@ class ToolsetManager:
25
26
  def __init__(
26
27
  self,
27
28
  toolsets: Optional[dict[str, dict[str, Any]]] = None,
29
+ mcp_servers: Optional[dict[str, dict[str, Any]]] = None,
28
30
  custom_toolsets: Optional[List[FilePath]] = None,
29
31
  custom_toolsets_from_cli: Optional[List[FilePath]] = None,
30
32
  toolset_status_location: Optional[FilePath] = None,
31
33
  ):
32
34
  self.toolsets = toolsets
35
+ self.toolsets = toolsets or {}
36
+ if mcp_servers is not None:
37
+ for _, mcp_server in mcp_servers.items():
38
+ mcp_server["type"] = ToolsetType.MCP.value
39
+ self.toolsets.update(mcp_servers or {})
33
40
  self.custom_toolsets = custom_toolsets
34
41
 
35
42
  if toolset_status_location is None:
@@ -254,7 +261,7 @@ class ToolsetManager:
254
261
  toolset.error = cached_status.get("error", None)
255
262
  toolset.enabled = cached_status.get("enabled", True)
256
263
  toolset.type = ToolsetType(
257
- cached_status.get("type", ToolsetType.BUILTIN)
264
+ cached_status.get("type", ToolsetType.BUILTIN.value)
258
265
  )
259
266
  toolset.path = cached_status.get("path", None)
260
267
  # check prerequisites for only enabled toolset when the toolset is loaded from cache. When the toolset is
@@ -354,7 +361,7 @@ class ToolsetManager:
354
361
  mcp_config: dict[str, dict[str, Any]] = parsed_yaml.get("mcp_servers", {})
355
362
 
356
363
  for server_config in mcp_config.values():
357
- server_config["type"] = ToolsetType.MCP
364
+ server_config["type"] = ToolsetType.MCP.value
358
365
 
359
366
  for toolset_config in toolsets_config.values():
360
367
  toolset_config["path"] = toolset_path
holmes/core/tracing.py CHANGED
@@ -1,12 +1,24 @@
1
1
  import os
2
2
  import logging
3
- from typing import Optional, Any, Union
3
+ import platform
4
+ import pwd
5
+ import socket
6
+ from datetime import datetime
7
+ from typing import Optional, Any, Union, Dict
8
+ from pathlib import Path
4
9
  from enum import Enum
5
10
 
11
+ BRAINTRUST_API_KEY = os.environ.get("BRAINTRUST_API_KEY")
12
+ BRAINTRUST_ORG = os.environ.get("BRAINTRUST_ORG", "robustadev")
13
+ BRAINTRUST_PROJECT = os.environ.get(
14
+ "BRAINTRUST_PROJECT", "HolmesGPT"
15
+ ) # only for evals - for CLI it's set differently
16
+
6
17
  try:
7
18
  import braintrust
8
19
  from braintrust import Span, SpanTypeAttribute
9
20
 
21
+ logging.info("Braintrust package imported successfully")
10
22
  BRAINTRUST_AVAILABLE = True
11
23
  except ImportError:
12
24
  BRAINTRUST_AVAILABLE = False
@@ -20,6 +32,56 @@ except ImportError:
20
32
  SpanTypeAttribute = Any
21
33
 
22
34
 
35
+ session_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
36
+
37
+
38
+ def readable_timestamp():
39
+ return session_timestamp
40
+
41
+
42
+ def get_active_branch_name():
43
+ try:
44
+ # First check if .git is a file (worktree case)
45
+ git_path = Path(".git")
46
+ if git_path.is_file():
47
+ # Read the worktree git directory path
48
+ with git_path.open("r") as f:
49
+ content = f.read().strip()
50
+ if content.startswith("gitdir:"):
51
+ worktree_git_dir = Path(content.split("gitdir:", 1)[1].strip())
52
+ head_file = worktree_git_dir / "HEAD"
53
+ else:
54
+ return "Unknown"
55
+ else:
56
+ # Regular .git directory
57
+ head_file = git_path / "HEAD"
58
+
59
+ with head_file.open("r") as f:
60
+ content = f.read().splitlines()
61
+ for line in content:
62
+ if line[0:4] == "ref:":
63
+ return line.partition("refs/heads/")[2]
64
+ except Exception:
65
+ pass
66
+
67
+ return "Unknown"
68
+
69
+
70
+ def get_machine_state_tags() -> Dict[str, str]:
71
+ return {
72
+ "username": pwd.getpwuid(os.getuid()).pw_name,
73
+ "branch": get_active_branch_name(),
74
+ "platform": platform.platform(),
75
+ "hostname": socket.gethostname(),
76
+ }
77
+
78
+
79
+ def get_experiment_name():
80
+ if os.environ.get("EXPERIMENT_ID"):
81
+ return os.environ.get("EXPERIMENT_ID")
82
+ return readable_timestamp() # should never happen in evals (we set EXPERIMENT_ID in conftest.py), but can happen with holmesgpt cli
83
+
84
+
23
85
  def _is_noop_span(span) -> bool:
24
86
  """Check if a span is a Braintrust NoopSpan (inactive span)."""
25
87
  return span is None or str(type(span)).endswith("_NoopSpan'>")
@@ -76,14 +138,16 @@ class DummyTracer:
76
138
  class BraintrustTracer:
77
139
  """Braintrust implementation of tracing."""
78
140
 
79
- def __init__(self, project: str = "HolmesGPT-CLI"):
141
+ def __init__(self, project: str):
80
142
  if not BRAINTRUST_AVAILABLE:
81
143
  raise ImportError("braintrust package is required for BraintrustTracer")
82
144
 
83
145
  self.project = project
84
146
 
85
147
  def start_experiment(
86
- self, experiment_name: Optional[str] = None, metadata: Optional[dict] = None
148
+ self,
149
+ experiment_name: Optional[str] = None,
150
+ additional_metadata: Optional[dict] = None,
87
151
  ):
88
152
  """Create and start a new Braintrust experiment.
89
153
 
@@ -97,10 +161,17 @@ class BraintrustTracer:
97
161
  if not os.environ.get("BRAINTRUST_API_KEY"):
98
162
  return None
99
163
 
164
+ if experiment_name is None:
165
+ experiment_name = get_experiment_name()
166
+
167
+ metadata = get_machine_state_tags()
168
+ if additional_metadata is not None:
169
+ metadata.update(additional_metadata)
170
+
100
171
  return braintrust.init(
101
172
  project=self.project,
102
173
  experiment=experiment_name,
103
- metadata=metadata or {},
174
+ metadata=metadata,
104
175
  update=True,
105
176
  )
106
177
 
@@ -143,7 +214,6 @@ class BraintrustTracer:
143
214
  logging.warning("BRAINTRUST_API_KEY not set, cannot get trace URL")
144
215
  return None
145
216
 
146
- # Get current experiment from Braintrust context
147
217
  current_experiment = braintrust.current_experiment()
148
218
  if not current_experiment:
149
219
  logging.warning("No current experiment found in Braintrust context")
@@ -156,10 +226,7 @@ class BraintrustTracer:
156
226
 
157
227
  current_span = braintrust.current_span()
158
228
  if not _is_noop_span(current_span):
159
- span_id = getattr(current_span, "span_id", None)
160
- id_attr = getattr(current_span, "id", None)
161
- if span_id and id_attr:
162
- return f"https://www.braintrust.dev/app/robustadev/p/{self.project}/experiments/{experiment_name}?c=&tg=false&r={id_attr}&s={span_id}"
229
+ current_span.link()
163
230
  else:
164
231
  logging.warning("No active span found in Braintrust context")
165
232
 
@@ -193,7 +260,7 @@ class TracingFactory:
193
260
  """Factory for creating tracer instances."""
194
261
 
195
262
  @staticmethod
196
- def create_tracer(trace_type: Optional[str], project: str):
263
+ def create_tracer(trace_type: Optional[str], project: str = BRAINTRUST_PROJECT):
197
264
  """Create a tracer instance based on the trace type.
198
265
 
199
266
  Args:
holmes/interactive.py CHANGED
@@ -19,17 +19,29 @@ from prompt_toolkit.key_binding import KeyBindings
19
19
  from prompt_toolkit.layout import Layout
20
20
  from prompt_toolkit.layout.containers import HSplit, Window
21
21
  from prompt_toolkit.layout.controls import FormattedTextControl
22
+ from prompt_toolkit.lexers import PygmentsLexer
22
23
  from prompt_toolkit.shortcuts.prompt import CompleteStyle
23
24
  from prompt_toolkit.styles import Style
24
25
  from prompt_toolkit.widgets import TextArea
26
+ from pygments.lexers import guess_lexer
25
27
  from rich.console import Console
26
28
  from rich.markdown import Markdown, Panel
27
29
 
30
+ from holmes.core.config import config_path_dir
28
31
  from holmes.core.prompt import build_initial_ask_messages
29
32
  from holmes.core.tool_calling_llm import ToolCallingLLM, ToolCallResult
30
33
  from holmes.core.tools import pretty_print_toolset_status
31
- from holmes.version import check_version_async
32
34
  from holmes.core.tracing import DummyTracer
35
+ from holmes.utils.colors import (
36
+ AI_COLOR,
37
+ ERROR_COLOR,
38
+ HELP_COLOR,
39
+ STATUS_COLOR,
40
+ TOOLS_COLOR,
41
+ USER_COLOR,
42
+ )
43
+ from holmes.utils.console.consts import agent_name
44
+ from holmes.version import check_version_async
33
45
 
34
46
 
35
47
  class SlashCommands(Enum):
@@ -139,14 +151,49 @@ class ConditionalExecutableCompleter(Completer):
139
151
  )
140
152
 
141
153
 
142
- USER_COLOR = "#DEFCC0" # light green
143
- AI_COLOR = "#00FFFF" # cyan
144
- TOOLS_COLOR = "magenta"
145
- HELP_COLOR = "cyan" # same as AI_COLOR for now
146
- ERROR_COLOR = "red"
147
- STATUS_COLOR = "yellow"
154
+ class ShowCommandCompleter(Completer):
155
+ """Completer that provides suggestions for /show command based on tool call history"""
156
+
157
+ def __init__(self):
158
+ self.tool_calls_history = []
148
159
 
149
- WELCOME_BANNER = f"[bold {HELP_COLOR}]Welcome to HolmesGPT:[/bold {HELP_COLOR}] Type '{SlashCommands.EXIT.command}' to exit, '{SlashCommands.HELP.command}' for commands."
160
+ def update_history(self, tool_calls_history: List[ToolCallResult]):
161
+ """Update the tool calls history for completion suggestions"""
162
+ self.tool_calls_history = tool_calls_history
163
+
164
+ def get_completions(self, document, complete_event):
165
+ text = document.text_before_cursor
166
+
167
+ # Only provide completion if the line starts with /show
168
+ if text.startswith("/show "):
169
+ # Extract the argument part after "/show "
170
+ show_part = text[6:] # Remove "/show "
171
+
172
+ # Don't complete if there are already multiple words
173
+ words = show_part.split()
174
+ if len(words) > 1:
175
+ return
176
+
177
+ # Provide completions based on available tool calls
178
+ if self.tool_calls_history:
179
+ for i, tool_call in enumerate(self.tool_calls_history):
180
+ tool_index = str(i + 1) # 1-based index
181
+ tool_description = tool_call.description
182
+
183
+ # Complete tool index numbers (show all if empty, or filter by what user typed)
184
+ if (
185
+ not show_part
186
+ or tool_index.startswith(show_part)
187
+ or show_part.lower() in tool_description.lower()
188
+ ):
189
+ yield Completion(
190
+ tool_index,
191
+ start_position=-len(show_part),
192
+ display=f"{tool_index} - {tool_description}",
193
+ )
194
+
195
+
196
+ WELCOME_BANNER = f"[bold {HELP_COLOR}]Welcome to {agent_name}:[/bold {HELP_COLOR}] Type '{SlashCommands.EXIT.command}' to exit, '{SlashCommands.HELP.command}' for commands."
150
197
 
151
198
 
152
199
  def format_tool_call_output(
@@ -185,6 +232,28 @@ def build_modal_title(tool_call: ToolCallResult, wrap_status: str) -> str:
185
232
  return f"{tool_call.description} (exit: q, nav: ↑↓/j/k/g/G/d/u/f/b/space, wrap: w [{wrap_status}])"
186
233
 
187
234
 
235
+ def detect_lexer(content: str) -> Optional[PygmentsLexer]:
236
+ """
237
+ Detect appropriate lexer for content using Pygments' built-in detection.
238
+
239
+ Args:
240
+ content: String content to analyze
241
+
242
+ Returns:
243
+ PygmentsLexer instance if content type is detected, None otherwise
244
+ """
245
+ if not content.strip():
246
+ return None
247
+
248
+ try:
249
+ # Use Pygments' built-in lexer guessing
250
+ lexer = guess_lexer(content)
251
+ return PygmentsLexer(lexer.__class__)
252
+ except Exception:
253
+ # If detection fails, return None for no syntax highlighting
254
+ return None
255
+
256
+
188
257
  def handle_show_command(
189
258
  show_arg: str, all_tool_calls_history: List[ToolCallResult], console: Console
190
259
  ) -> None:
@@ -246,6 +315,9 @@ def show_tool_output_modal(tool_call: ToolCallResult, console: Console) -> None:
246
315
  output = tool_call.result.get_stringified_data()
247
316
  title = build_modal_title(tool_call, "off") # Word wrap starts disabled
248
317
 
318
+ # Detect appropriate syntax highlighting
319
+ lexer = detect_lexer(output)
320
+
249
321
  # Create text area with the output
250
322
  text_area = TextArea(
251
323
  text=output,
@@ -253,6 +325,7 @@ def show_tool_output_modal(tool_call: ToolCallResult, console: Console) -> None:
253
325
  scrollbar=True,
254
326
  line_numbers=False,
255
327
  wrap_lines=False, # Disable word wrap by default
328
+ lexer=lexer,
256
329
  )
257
330
 
258
331
  # Create header
@@ -275,15 +348,18 @@ def show_tool_output_modal(tool_call: ToolCallResult, console: Console) -> None:
275
348
  # Create key bindings
276
349
  bindings = KeyBindings()
277
350
 
278
- # Exit commands
351
+ # Track exit state to prevent double exits
352
+ exited = False
353
+
354
+ # Exit commands (q, escape, or ctrl+c to exit)
279
355
  @bindings.add("q")
280
356
  @bindings.add("escape")
281
- def _(event):
282
- event.app.exit()
283
-
284
357
  @bindings.add("c-c")
285
358
  def _(event):
286
- event.app.exit()
359
+ nonlocal exited
360
+ if not exited:
361
+ exited = True
362
+ event.app.exit()
287
363
 
288
364
  # Vim/less-like navigation
289
365
  @bindings.add("j")
@@ -596,7 +672,7 @@ def handle_shell_command(
596
672
  Formatted user input string if user chooses to share, None otherwise
597
673
  """
598
674
  console.print(
599
- f"[bold {STATUS_COLOR}]Starting interactive shell. Type 'exit' to return to HolmesGPT.[/bold {STATUS_COLOR}]"
675
+ f"[bold {STATUS_COLOR}]Starting interactive shell. Type 'exit' to return to {agent_name}.[/bold {STATUS_COLOR}]"
600
676
  )
601
677
  console.print(
602
678
  "[dim]Shell session will be recorded and can be shared with LLM when you exit.[/dim]"
@@ -714,12 +790,14 @@ def display_recent_tool_outputs(
714
790
  def run_interactive_loop(
715
791
  ai: ToolCallingLLM,
716
792
  console: Console,
717
- system_prompt_rendered: str,
718
793
  initial_user_input: Optional[str],
719
794
  include_files: Optional[List[Path]],
720
795
  post_processing_prompt: Optional[str],
721
796
  show_tool_output: bool,
722
797
  tracer=None,
798
+ runbooks=None,
799
+ system_prompt_additions: Optional[str] = None,
800
+ check_version: bool = True,
723
801
  ) -> None:
724
802
  # Initialize tracer - use DummyTracer if no tracer provided
725
803
  if tracer is None:
@@ -733,17 +811,19 @@ def run_interactive_loop(
733
811
  }
734
812
  )
735
813
 
736
- # Create merged completer with slash commands, conditional executables, and smart paths
814
+ # Create merged completer with slash commands, conditional executables, show command, and smart paths
737
815
  slash_completer = SlashCommandCompleter()
738
816
  executable_completer = ConditionalExecutableCompleter()
817
+ show_completer = ShowCommandCompleter()
739
818
  path_completer = SmartPathCompleter()
740
819
 
741
820
  command_completer = merge_completers(
742
- [slash_completer, executable_completer, path_completer]
821
+ [slash_completer, executable_completer, show_completer, path_completer]
743
822
  )
744
823
 
745
824
  # Use file-based history
746
- history_file = os.path.expanduser("~/.holmes/history")
825
+ history_file = os.path.join(config_path_dir, "history")
826
+
747
827
  os.makedirs(os.path.dirname(history_file), exist_ok=True)
748
828
  history = FileHistory(history_file)
749
829
  if initial_user_input:
@@ -816,7 +896,8 @@ def run_interactive_loop(
816
896
  ) # type: ignore
817
897
 
818
898
  # Start background version check
819
- check_version_async(on_version_check_complete)
899
+ if check_version:
900
+ check_version_async(on_version_check_complete)
820
901
 
821
902
  input_prompt = [("class:prompt", "User: ")]
822
903
 
@@ -870,6 +951,8 @@ def run_interactive_loop(
870
951
  messages = None
871
952
  last_response = None
872
953
  all_tool_calls_history.clear()
954
+ # Reset the show completer history
955
+ show_completer.update_history([])
873
956
  continue
874
957
  elif command == SlashCommands.TOOLS_CONFIG.command:
875
958
  pretty_print_toolset_status(ai.tool_executor.toolsets, console)
@@ -915,7 +998,12 @@ def run_interactive_loop(
915
998
 
916
999
  if messages is None:
917
1000
  messages = build_initial_ask_messages(
918
- console, system_prompt_rendered, user_input, include_files
1001
+ console,
1002
+ user_input,
1003
+ include_files,
1004
+ ai.tool_executor,
1005
+ runbooks,
1006
+ system_prompt_additions,
919
1007
  )
920
1008
  else:
921
1009
  messages.append({"role": "user", "content": user_input})
@@ -944,6 +1032,8 @@ def run_interactive_loop(
944
1032
 
945
1033
  if response.tool_calls:
946
1034
  all_tool_calls_history.extend(response.tool_calls)
1035
+ # Update the show completer with the latest tool call history
1036
+ show_completer.update_history(all_tool_calls_history)
947
1037
 
948
1038
  if show_tool_output and response.tool_calls:
949
1039
  display_recent_tool_outputs(
holmes/main.py CHANGED
@@ -3,6 +3,7 @@ import os
3
3
  import sys
4
4
 
5
5
  from holmes.utils.cert_utils import add_custom_certificate
6
+ from holmes.utils.colors import USER_COLOR
6
7
 
7
8
  ADDITIONAL_CERTIFICATE: str = os.environ.get("CERTIFICATE", "")
8
9
  if add_custom_certificate(ADDITIONAL_CERTIFICATE):
@@ -16,7 +17,6 @@ import json
16
17
  import logging
17
18
  import socket
18
19
  import uuid
19
- from datetime import datetime
20
20
  from pathlib import Path
21
21
  from typing import List, Optional
22
22
 
@@ -180,10 +180,6 @@ def ask(
180
180
  destination: Optional[DestinationType] = opt_destination,
181
181
  slack_token: Optional[str] = opt_slack_token,
182
182
  slack_channel: Optional[str] = opt_slack_channel,
183
- # advanced options for this command
184
- system_prompt: Optional[str] = typer.Option(
185
- "builtin://generic_ask.jinja2", help=system_prompt_help
186
- ),
187
183
  show_tool_output: bool = typer.Option(
188
184
  False,
189
185
  "--show-tool-output",
@@ -214,6 +210,11 @@ def ask(
214
210
  "--trace",
215
211
  help="Enable tracing to the specified provider (e.g., 'braintrust')",
216
212
  ),
213
+ system_prompt_additions: Optional[str] = typer.Option(
214
+ None,
215
+ "--system-prompt-additions",
216
+ help="Additional content to append to the system prompt",
217
+ ),
217
218
  ):
218
219
  """
219
220
  Ask any question and answer using available tools
@@ -245,22 +246,13 @@ def ask(
245
246
 
246
247
  # Create tracer if trace option is provided
247
248
  tracer = TracingFactory.create_tracer(trace, project="HolmesGPT-CLI")
248
- experiment_name = f"holmes-ask-{datetime.now().strftime('%Y%m%d_%H%M%S')}"
249
- tracer.start_experiment(
250
- experiment_name=experiment_name, metadata={"prompt": prompt or "holmes-ask"}
251
- )
249
+ tracer.start_experiment()
252
250
 
253
251
  ai = config.create_console_toolcalling_llm(
254
252
  dal=None, # type: ignore
255
253
  refresh_toolsets=refresh_toolsets, # flag to refresh the toolset status
256
254
  tracer=tracer,
257
255
  )
258
- template_context = {
259
- "toolsets": ai.tool_executor.toolsets,
260
- "runbooks": config.get_runbook_catalog(),
261
- }
262
-
263
- system_prompt_rendered = load_and_render_prompt(system_prompt, template_context) # type: ignore
264
256
 
265
257
  if prompt_file and prompt:
266
258
  raise typer.BadParameter(
@@ -289,26 +281,29 @@ def ask(
289
281
  prompt = f"Here's some piped output:\n\n{piped_data}\n\nWhat can you tell me about this output?"
290
282
 
291
283
  if echo_request and not interactive and prompt:
292
- console.print("[bold yellow]User:[/bold yellow] " + prompt)
284
+ console.print(f"[bold {USER_COLOR}]User:[/bold {USER_COLOR}] {prompt}")
293
285
 
294
286
  if interactive:
295
287
  run_interactive_loop(
296
288
  ai,
297
289
  console,
298
- system_prompt_rendered,
299
290
  prompt,
300
291
  include_file,
301
292
  post_processing_prompt,
302
293
  show_tool_output,
303
294
  tracer,
295
+ config.get_runbook_catalog(),
296
+ system_prompt_additions,
304
297
  )
305
298
  return
306
299
 
307
300
  messages = build_initial_ask_messages(
308
301
  console,
309
- system_prompt_rendered,
310
302
  prompt, # type: ignore
311
303
  include_file,
304
+ ai.tool_executor,
305
+ config.get_runbook_catalog(),
306
+ system_prompt_additions,
312
307
  )
313
308
 
314
309
  with tracer.start_trace(
@@ -85,10 +85,13 @@ class SlackDestination(DestinationPlugin):
85
85
  text = "*AI used info from alert and the following tools:*"
86
86
  for tool in result.tool_calls:
87
87
  file_response = self.client.files_upload_v2(
88
- content=tool.result, title=f"{tool.description}"
88
+ content=tool.result.get_stringified_data(), title=f"{tool.description}"
89
89
  )
90
- permalink = file_response["file"]["permalink"]
91
- text += f"\n• `<{permalink}|{tool.description}>`"
90
+ if file_response and "file" in file_response:
91
+ permalink = file_response["file"]["permalink"]
92
+ text += f"\n• `<{permalink}|{tool.description}>`"
93
+ else:
94
+ text += f"\n• {tool.description} (file upload failed)"
92
95
 
93
96
  self.client.chat_postMessage(
94
97
  channel=self.channel,
@@ -108,10 +111,13 @@ class SlackDestination(DestinationPlugin):
108
111
 
109
112
  text = "*🐞 DEBUG: messages with OpenAI*"
110
113
  file_response = self.client.files_upload_v2(
111
- content=result.prompt, title="ai-prompt"
114
+ content=str(result.prompt) if result.prompt else "", title="ai-prompt"
112
115
  )
113
- permalink = file_response["file"]["permalink"]
114
- text += f"\n`<{permalink}|ai-prompt>`"
116
+ if file_response and "file" in file_response:
117
+ permalink = file_response["file"]["permalink"]
118
+ text += f"\n`<{permalink}|ai-prompt>`"
119
+ else:
120
+ text += "\nai-prompt (file upload failed)"
115
121
 
116
122
  self.client.chat_postMessage(
117
123
  channel=self.channel,
@@ -132,9 +138,13 @@ class SlackDestination(DestinationPlugin):
132
138
  filename = f"{issue.name}"
133
139
  issue_json = issue.model_dump_json()
134
140
  file_response = self.client.files_upload_v2(content=issue_json, title=filename)
135
- permalink = file_response["file"]["permalink"]
136
- text = issue.presentation_all_metadata
137
- text += f"\n<{permalink}|{filename}>\n"
141
+ if file_response and "file" in file_response:
142
+ permalink = file_response["file"]["permalink"]
143
+ text = issue.presentation_all_metadata
144
+ text += f"\n<{permalink}|{filename}>\n"
145
+ else:
146
+ text = issue.presentation_all_metadata
147
+ text += f"\n{filename} (file upload failed)\n"
138
148
 
139
149
  self.client.chat_postMessage(
140
150
  channel=self.channel,
@@ -5,7 +5,7 @@
5
5
  {%- set opensearch_ts = toolsets | selectattr("name", "equalto", "opensearch/logs") | first -%}
6
6
  {%- set datadog_ts = toolsets | selectattr("name", "equalto", "datadog/logs") | first -%}
7
7
 
8
- # Logs
8
+ ## Logs
9
9
  {% if loki_ts and loki_ts.status == "enabled" -%}
10
10
  * For any logs, including for investigating kubernetes problems, use Loki
11
11
  * Use the tool fetch_loki_logs_for_resource to get the logs of any kubernetes pod or node
@@ -17,12 +17,22 @@
17
17
  ** If there are too many logs, or not enough, narrow or widen the timestamps
18
18
  * If you are not provided with time information. Ignore start_timestamp and end_timestamp. Loki will default to the latest logs.
19
19
  {%- elif coralogix_ts and coralogix_ts.status == "enabled" -%}
20
+ ### coralogix/logs
21
+ #### Coralogix Logs Toolset
22
+ Tools to search and fetch logs from Coralogix.
20
23
  {% include '_default_log_prompt.jinja2' %}
21
24
  {%- elif k8s_base_ts and k8s_base_ts.status == "enabled" -%}
22
25
  {% include '_default_log_prompt.jinja2' %}
23
26
  {%- elif datadog_ts and datadog_ts.status == "enabled" -%}
27
+ * If you are not sure what is the pod namespace, search for the logs only by pod name.
28
+ ### datadog/logs
29
+ #### Datadog Logs Toolset
30
+ Tools to search and fetch logs from Datadog.
24
31
  {% include '_default_log_prompt.jinja2' %}
25
32
  {%- elif k8s_yaml_ts and k8s_yaml_ts.status == "enabled" -%}
33
+ ### kubernetes/logs
34
+ #### Kubernetes Logs Toolset
35
+ Tools to search and fetch logs from Kubernetes.
26
36
  * if the user wants to find a specific term in a pod's logs, use kubectl_logs_grep
27
37
  * use both kubectl_previous_logs and kubectl_logs when reading logs. Treat the output of both as a single unified logs stream
28
38
  * if a pod has multiple containers, make sure you fetch the logs for either all or relevant containers using one of the containers log functions like kubectl_logs_all_containers, kubectl_logs_all_containers_grep or any other.