holmesgpt 0.12.3a1__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.
- holmes/__init__.py +1 -1
- holmes/config.py +75 -33
- holmes/core/config.py +5 -0
- holmes/core/conversations.py +17 -2
- holmes/core/investigation.py +1 -0
- holmes/core/llm.py +1 -2
- holmes/core/prompt.py +29 -4
- holmes/core/supabase_dal.py +49 -13
- holmes/core/tool_calling_llm.py +26 -1
- holmes/core/tools.py +2 -1
- holmes/core/tools_utils/tool_executor.py +1 -0
- holmes/core/toolset_manager.py +10 -3
- holmes/core/tracing.py +77 -10
- holmes/interactive.py +110 -20
- holmes/main.py +13 -18
- holmes/plugins/destinations/slack/plugin.py +19 -9
- holmes/plugins/prompts/_fetch_logs.jinja2 +11 -1
- holmes/plugins/prompts/_general_instructions.jinja2 +6 -37
- holmes/plugins/prompts/_permission_errors.jinja2 +6 -0
- holmes/plugins/prompts/_runbook_instructions.jinja2 +13 -5
- holmes/plugins/prompts/_toolsets_instructions.jinja2 +22 -14
- holmes/plugins/prompts/generic_ask.jinja2 +6 -0
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +1 -0
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +1 -0
- holmes/plugins/prompts/generic_investigation.jinja2 +1 -0
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -2
- holmes/plugins/runbooks/__init__.py +20 -4
- holmes/plugins/toolsets/__init__.py +7 -9
- holmes/plugins/toolsets/aks-node-health.yaml +0 -8
- holmes/plugins/toolsets/argocd.yaml +4 -1
- holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +1 -1
- holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +2 -0
- holmes/plugins/toolsets/confluence.yaml +1 -1
- holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +54 -4
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +150 -6
- holmes/plugins/toolsets/kubernetes.yaml +6 -0
- holmes/plugins/toolsets/prometheus/prometheus.py +2 -6
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +2 -2
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +65 -6
- holmes/plugins/toolsets/service_discovery.py +1 -1
- holmes/plugins/toolsets/slab.yaml +1 -1
- holmes/utils/colors.py +7 -0
- holmes/utils/console/consts.py +5 -0
- holmes/utils/console/result.py +2 -1
- holmes/utils/keygen_utils.py +6 -0
- holmes/version.py +2 -2
- holmesgpt-0.12.4.dist-info/METADATA +258 -0
- {holmesgpt-0.12.3a1.dist-info → holmesgpt-0.12.4.dist-info}/RECORD +51 -47
- holmesgpt-0.12.3a1.dist-info/METADATA +0 -400
- {holmesgpt-0.12.3a1.dist-info → holmesgpt-0.12.4.dist-info}/LICENSE.txt +0 -0
- {holmesgpt-0.12.3a1.dist-info → holmesgpt-0.12.4.dist-info}/WHEEL +0 -0
- {holmesgpt-0.12.3a1.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(
|
holmes/core/toolset_manager.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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.
|