holmesgpt 0.12.0a0__py3-none-any.whl → 0.12.2a0__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 CHANGED
@@ -1,76 +1,8 @@
1
- import json
2
- import os
3
- import subprocess
4
- import sys
5
- from cachetools import cached # type: ignore
6
-
7
- # For relative imports to work in Python 3.6 - see https://stackoverflow.com/a/49375740
8
- this_path = os.path.dirname(os.path.realpath(__file__))
9
- sys.path.append(this_path)
10
-
11
1
  # This is patched by github actions during release
12
- __version__ = "0.12.0-alpha"
13
-
14
-
15
- def is_official_release() -> bool:
16
- return not __version__.startswith("0.0.0")
17
-
18
-
19
- @cached(cache=dict())
20
- def get_version() -> str:
21
- # the version string was patched by a release - return __version__ which will be correct
22
- if is_official_release():
23
- return __version__
24
-
25
- # we are running from an unreleased dev version
26
- try:
27
- # Get the latest git tag
28
- tag = (
29
- subprocess.check_output(
30
- ["git", "describe", "--tags"], stderr=subprocess.STDOUT, cwd=this_path
31
- )
32
- .decode()
33
- .strip()
34
- )
35
-
36
- # Get the current branch name
37
- branch = (
38
- subprocess.check_output(
39
- ["git", "rev-parse", "--abbrev-ref", "HEAD"],
40
- stderr=subprocess.STDOUT,
41
- cwd=this_path,
42
- )
43
- .decode()
44
- .strip()
45
- )
46
-
47
- # Check if there are uncommitted changes
48
- status = (
49
- subprocess.check_output(
50
- ["git", "status", "--porcelain"],
51
- stderr=subprocess.STDOUT,
52
- cwd=this_path,
53
- )
54
- .decode()
55
- .strip()
56
- )
57
- dirty = "-dirty" if status else ""
58
-
59
- return f"{tag}-{branch}{dirty}"
60
-
61
- except Exception:
62
- pass
63
-
64
- # we are running without git history, but we still might have git archival data (e.g. if we were pip installed)
65
- archival_file_path = os.path.join(this_path, ".git_archival.json")
66
- if os.path.exists(archival_file_path):
67
- try:
68
- with open(archival_file_path, "r") as f:
69
- archival_data = json.load(f)
70
- return f"dev-{archival_data['refs']}-{archival_data['hash-short']}"
71
- except Exception:
72
- pass
73
-
74
- return "dev-version"
2
+ __version__ = "0.12.2-alpha"
75
3
 
76
- return "unknown-version"
4
+ # Re-export version functions from version module for backward compatibility
5
+ from .version import (
6
+ get_version as get_version,
7
+ is_official_release as is_official_release,
8
+ )
@@ -5,7 +5,7 @@ from pydantic import BaseModel, ConfigDict
5
5
  from holmes.common.env_vars import ROBUSTA_API_ENDPOINT
6
6
 
7
7
  HOLMES_GET_INFO_URL = f"{ROBUSTA_API_ENDPOINT}/api/holmes/get_info"
8
- TIMEOUT = 0.3
8
+ TIMEOUT = 0.5
9
9
 
10
10
 
11
11
  class HolmesInfo(BaseModel):
holmes/common/env_vars.py CHANGED
@@ -49,3 +49,4 @@ KUBERNETES_LOGS_TIMEOUT_SECONDS = int(
49
49
  )
50
50
 
51
51
  TOOL_CALL_SAFEGUARDS_ENABLED = load_bool("TOOL_CALL_SAFEGUARDS_ENABLED", True)
52
+ IS_OPENSHIFT = load_bool("IS_OPENSHIFT", False)
@@ -0,0 +1,15 @@
1
+ from typing import Optional
2
+ import os
3
+
4
+ # NOTE: This one will be mounted if openshift is enabled in values.yaml
5
+ TOKEN_LOCATION = os.environ.get(
6
+ "TOKEN_LOCATION", "/var/run/secrets/kubernetes.io/serviceaccount/token"
7
+ )
8
+
9
+
10
+ def load_openshift_token() -> Optional[str]:
11
+ try:
12
+ with open(TOKEN_LOCATION, "r") as file:
13
+ return file.read()
14
+ except FileNotFoundError:
15
+ return None
holmes/config.py CHANGED
@@ -9,8 +9,6 @@ from typing import Any, List, Optional, Union
9
9
  import yaml # type: ignore
10
10
  from pydantic import BaseModel, ConfigDict, FilePath, SecretStr
11
11
 
12
- from holmes import get_version # type: ignore
13
- from holmes.clients.robusta_client import HolmesInfo, fetch_holmes_info
14
12
  from holmes.common.env_vars import ROBUSTA_AI, ROBUSTA_API_ENDPOINT, ROBUSTA_CONFIG_PATH
15
13
  from holmes.core.llm import LLM, DefaultLLM
16
14
  from holmes.core.runbooks import RunbookManager
@@ -116,26 +114,8 @@ class Config(RobustaBaseConfig):
116
114
 
117
115
  _server_tool_executor: Optional[ToolExecutor] = None
118
116
 
119
- _version: Optional[str] = None
120
- _holmes_info: Optional[HolmesInfo] = None
121
-
122
117
  _toolset_manager: Optional[ToolsetManager] = None
123
118
 
124
- @property
125
- def is_latest_version(self) -> bool:
126
- if (
127
- not self._holmes_info
128
- or not self._holmes_info.latest_version
129
- or not self._version
130
- ):
131
- # We couldn't resolve version, assume we are running the latest version
132
- return True
133
- if self._version.startswith("dev-"):
134
- # dev versions are considered to be the latest version
135
- return True
136
-
137
- return self._version.startswith(self._holmes_info.latest_version)
138
-
139
119
  @property
140
120
  def toolset_manager(self) -> ToolsetManager:
141
121
  if not self._toolset_manager:
@@ -147,8 +127,6 @@ class Config(RobustaBaseConfig):
147
127
  return self._toolset_manager
148
128
 
149
129
  def model_post_init(self, __context: Any) -> None:
150
- self._version = get_version()
151
- self._holmes_info = fetch_holmes_info()
152
130
  self._model_list = parse_models_file(MODEL_LIST_FILE_LOCATION)
153
131
  if self._should_load_robusta_ai():
154
132
  logging.info("Loading Robusta AI model")
@@ -179,11 +157,6 @@ class Config(RobustaBaseConfig):
179
157
  if self._model_list:
180
158
  logging.info(f"loaded models: {list(self._model_list.keys())}")
181
159
 
182
- if not self.is_latest_version and self._holmes_info:
183
- logging.warning(
184
- f"You are running version {self._version} of holmes, but the latest version is {self._holmes_info.latest_version}. Please update.",
185
- )
186
-
187
160
  @classmethod
188
161
  def load_from_file(cls, config_file: Optional[Path], **kwargs) -> "Config":
189
162
  """
holmes/core/llm.py CHANGED
@@ -140,14 +140,11 @@ class DefaultLLM(LLM):
140
140
  https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json
141
141
  """
142
142
  model_name = self.model
143
- if model_name.startswith("openai/"):
144
- model_name = model_name[len("openai/") :] # Strip the 'openai/' prefix
145
- elif model_name.startswith("bedrock/"):
146
- model_name = model_name[len("bedrock/") :] # Strip the 'bedrock/' prefix
147
- elif model_name.startswith("vertex_ai/"):
148
- model_name = model_name[
149
- len("vertex_ai/") :
150
- ] # Strip the 'vertex_ai/' prefix
143
+ prefixes = ["openai/", "bedrock/", "vertex_ai/", "anthropic/"]
144
+
145
+ for prefix in prefixes:
146
+ if model_name.startswith(prefix):
147
+ return model_name[len(prefix) :]
151
148
 
152
149
  return model_name
153
150
 
@@ -182,6 +182,10 @@ class SupabaseDal:
182
182
  res = self.client.auth.sign_in_with_password(
183
183
  {"email": self.email, "password": self.password}
184
184
  )
185
+ if not res.session:
186
+ raise ValueError("Authentication failed: no session returned")
187
+ if not res.user:
188
+ raise ValueError("Authentication failed: no user returned")
185
189
  self.client.auth.set_session(
186
190
  res.session.access_token, res.session.refresh_token
187
191
  )
@@ -39,7 +39,7 @@ from holmes.utils.global_instructions import (
39
39
  )
40
40
  from holmes.utils.tags import format_tags_in_string, parse_messages_tags
41
41
  from holmes.core.tools_utils.tool_executor import ToolExecutor
42
- from holmes.core.tracing import DummySpan, SpanType
42
+ from holmes.core.tracing import DummySpan
43
43
 
44
44
 
45
45
  def format_tool_result_data(tool_result: StructuredToolResult) -> str:
@@ -249,6 +249,7 @@ class ToolCallingLLM:
249
249
  user_prompt: Optional[str] = None,
250
250
  sections: Optional[InputSectionsDataType] = None,
251
251
  trace_span=DummySpan(),
252
+ tool_number_offset: int = 0,
252
253
  ) -> LLMResult:
253
254
  perf_timing = PerformanceTiming("tool_calling_llm.call")
254
255
  tool_calls = [] # type: ignore
@@ -367,7 +368,7 @@ class ToolCallingLLM:
367
368
  tool_to_call=t,
368
369
  previous_tool_calls=tool_calls,
369
370
  trace_span=trace_span,
370
- tool_number=tool_index,
371
+ tool_number=tool_number_offset + tool_index,
371
372
  )
372
373
  )
373
374
 
@@ -383,6 +384,8 @@ class ToolCallingLLM:
383
384
  if tools_to_call:
384
385
  logging.info("")
385
386
 
387
+ raise Exception(f"Too many LLM calls - exceeded max_steps: {i}/{max_steps}")
388
+
386
389
  def _invoke_tool(
387
390
  self,
388
391
  tool_to_call: ChatCompletionMessageToolCall,
@@ -419,7 +422,7 @@ class ToolCallingLLM:
419
422
  tool_response = None
420
423
 
421
424
  # Create tool span if tracing is enabled
422
- tool_span = trace_span.start_span(name=tool_name, type=SpanType.TOOL)
425
+ tool_span = trace_span.start_span(name=tool_name, type="tool")
423
426
 
424
427
  try:
425
428
  tool_response = prevent_overly_repeated_tool_call(
@@ -448,6 +451,8 @@ class ToolCallingLLM:
448
451
  metadata={
449
452
  "status": tool_response.status.value,
450
453
  "error": tool_response.error,
454
+ "description": tool.get_parameterized_one_liner(tool_params),
455
+ "structured_tool_result": tool_response,
451
456
  },
452
457
  )
453
458
 
@@ -720,6 +725,10 @@ class ToolCallingLLM:
720
725
  "tool_calling_result", streaming_result_dict
721
726
  )
722
727
 
728
+ raise Exception(
729
+ f"Too many LLM calls - exceeded max_steps: {i}/{self.max_steps}"
730
+ )
731
+
723
732
 
724
733
  # TODO: consider getting rid of this entirely and moving templating into the cmds in holmes_cli.py
725
734
  class IssueInvestigator(ToolCallingLLM):
holmes/core/tools.py CHANGED
@@ -154,8 +154,9 @@ class Tool(ABC, BaseModel):
154
154
  if hasattr(result, "get_stringified_data")
155
155
  else str(result)
156
156
  )
157
+ show_hint = f"/show {tool_number}" if tool_number else "/show"
157
158
  logging.info(
158
- f" [dim]Finished {tool_number_str}in {elapsed:.2f}s, output length: {len(output_str):,} characters - /show to view contents[/dim]"
159
+ f" [dim]Finished {tool_number_str}in {elapsed:.2f}s, output length: {len(output_str):,} characters - {show_hint} to view contents[/dim]"
159
160
  )
160
161
  return result
161
162
 
@@ -290,7 +290,7 @@ class ToolsetManager:
290
290
  [toolset for toolset in all_toolsets_with_status if toolset.enabled]
291
291
  )
292
292
  logging.info(
293
- f"Using {num_available_toolsets} datasources (toolsets). To refresh: `holmes toolset refresh`"
293
+ f"Using {num_available_toolsets} datasources (toolsets). To refresh: use flag `--refresh-toolsets`"
294
294
  )
295
295
  return all_toolsets_with_status
296
296
 
holmes/core/tracing.py CHANGED
@@ -32,12 +32,13 @@ class SpanType(Enum):
32
32
  TOOL = "tool"
33
33
  TASK = "task"
34
34
  SCORE = "score"
35
+ EVAL = "eval"
35
36
 
36
37
 
37
38
  class DummySpan:
38
39
  """A no-op span implementation for when tracing is disabled."""
39
40
 
40
- def start_span(self, name: str, span_type: Optional[SpanType] = None, **kwargs):
41
+ def start_span(self, name: str, span_type=None, **kwargs):
41
42
  return DummySpan()
42
43
 
43
44
  def log(self, *args, **kwargs):
@@ -121,17 +122,17 @@ class BraintrustTracer:
121
122
  # Add span type to kwargs if provided
122
123
  kwargs = {}
123
124
  if span_type:
124
- kwargs["type"] = getattr(SpanTypeAttribute, span_type.name)
125
+ kwargs["type"] = span_type.value
125
126
 
126
127
  # Use current Braintrust context (experiment or parent span)
127
128
  current_span = braintrust.current_span()
128
129
  if not _is_noop_span(current_span):
129
- return current_span.start_span(name=name, **kwargs)
130
+ return current_span.start_span(name=name, **kwargs) # type: ignore
130
131
 
131
132
  # Fallback to current experiment
132
133
  current_experiment = braintrust.current_experiment()
133
134
  if current_experiment:
134
- return current_experiment.start_span(name=name, **kwargs)
135
+ return current_experiment.start_span(name=name, **kwargs) # type: ignore
135
136
 
136
137
  return DummySpan()
137
138
 
holmes/interactive.py CHANGED
@@ -6,15 +6,15 @@ import threading
6
6
  from collections import defaultdict
7
7
  from enum import Enum
8
8
  from pathlib import Path
9
- from typing import Optional, List, DefaultDict
9
+ from typing import DefaultDict, List, Optional
10
10
 
11
11
  import typer
12
12
  from prompt_toolkit import PromptSession
13
13
  from prompt_toolkit.application import Application
14
14
  from prompt_toolkit.completion import Completer, Completion, merge_completers
15
15
  from prompt_toolkit.completion.filesystem import ExecutableCompleter, PathCompleter
16
- from prompt_toolkit.history import InMemoryHistory, FileHistory
17
16
  from prompt_toolkit.document import Document
17
+ from prompt_toolkit.history import FileHistory, InMemoryHistory
18
18
  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
@@ -22,27 +22,26 @@ from prompt_toolkit.layout.controls import FormattedTextControl
22
22
  from prompt_toolkit.shortcuts.prompt import CompleteStyle
23
23
  from prompt_toolkit.styles import Style
24
24
  from prompt_toolkit.widgets import TextArea
25
-
26
25
  from rich.console import Console
27
26
  from rich.markdown import Markdown, Panel
28
27
 
29
28
  from holmes.core.prompt import build_initial_ask_messages
30
29
  from holmes.core.tool_calling_llm import ToolCallingLLM, ToolCallResult
31
30
  from holmes.core.tools import pretty_print_toolset_status
32
- from holmes.core.tracing import DummySpan
31
+ from holmes.version import check_version_async
32
+ from holmes.core.tracing import DummyTracer
33
33
 
34
34
 
35
35
  class SlashCommands(Enum):
36
36
  EXIT = ("/exit", "Exit interactive mode")
37
37
  HELP = ("/help", "Show help message with all commands")
38
- RESET = ("/reset", "Reset the conversation context")
38
+ CLEAR = ("/clear", "Clear screen and reset conversation context")
39
39
  TOOLS_CONFIG = ("/tools", "Show available toolsets and their status")
40
40
  TOGGLE_TOOL_OUTPUT = (
41
41
  "/auto",
42
42
  "Toggle auto-display of tool outputs after responses",
43
43
  )
44
44
  LAST_OUTPUT = ("/last", "Show all tool outputs from last response")
45
- CLEAR = ("/clear", "Clear the terminal screen")
46
45
  RUN = ("/run", "Run a bash command and optionally share with LLM")
47
46
  SHELL = (
48
47
  "/shell",
@@ -722,9 +721,9 @@ def run_interactive_loop(
722
721
  show_tool_output: bool,
723
722
  tracer=None,
724
723
  ) -> None:
725
- # Initialize tracer - use DummySpan if no tracer provided
724
+ # Initialize tracer - use DummyTracer if no tracer provided
726
725
  if tracer is None:
727
- tracer = DummySpan()
726
+ tracer = DummyTracer()
728
727
 
729
728
  style = Style.from_dict(
730
729
  {
@@ -753,6 +752,23 @@ def run_interactive_loop(
753
752
  # Create custom key bindings for Ctrl+C behavior
754
753
  bindings = KeyBindings()
755
754
  status_message = ""
755
+ version_message = ""
756
+
757
+ def clear_version_message():
758
+ nonlocal version_message
759
+ version_message = ""
760
+ session.app.invalidate()
761
+
762
+ def on_version_check_complete(result):
763
+ """Callback when background version check completes"""
764
+ nonlocal version_message
765
+ if not result.is_latest and result.update_message:
766
+ version_message = result.update_message
767
+ session.app.invalidate()
768
+
769
+ # Auto-clear after 10 seconds
770
+ timer = threading.Timer(10, clear_version_message)
771
+ timer.start()
756
772
 
757
773
  @bindings.add("c-c")
758
774
  def _(event):
@@ -776,9 +792,19 @@ def run_interactive_loop(
776
792
  raise KeyboardInterrupt()
777
793
 
778
794
  def get_bottom_toolbar():
795
+ messages = []
796
+
797
+ # Ctrl-c status message (red background)
779
798
  if status_message:
780
- return [("bg:#ff0000 fg:#000000", status_message)]
781
- return None
799
+ messages.append(("bg:#ff0000 fg:#000000", status_message))
800
+
801
+ # Version message (yellow background)
802
+ if version_message:
803
+ if messages:
804
+ messages.append(("", " | "))
805
+ messages.append(("bg:#ffff00 fg:#000000", version_message))
806
+
807
+ return messages if messages else None
782
808
 
783
809
  session = PromptSession(
784
810
  completer=command_completer,
@@ -789,6 +815,9 @@ def run_interactive_loop(
789
815
  bottom_toolbar=get_bottom_toolbar,
790
816
  ) # type: ignore
791
817
 
818
+ # Start background version check
819
+ check_version_async(on_version_check_complete)
820
+
792
821
  input_prompt = [("class:prompt", "User: ")]
793
822
 
794
823
  console.print(WELCOME_BANNER)
@@ -833,9 +862,10 @@ def run_interactive_loop(
833
862
  for cmd, description in SLASH_COMMANDS_REFERENCE.items():
834
863
  console.print(f" [bold]{cmd}[/bold] - {description}")
835
864
  continue
836
- elif command == SlashCommands.RESET.value:
865
+ elif command == SlashCommands.CLEAR.command:
866
+ console.clear()
837
867
  console.print(
838
- f"[bold {STATUS_COLOR}]Context reset. You can now ask a new question.[/bold {STATUS_COLOR}]"
868
+ f"[bold {STATUS_COLOR}]Screen cleared and context reset. You can now ask a new question.[/bold {STATUS_COLOR}]"
839
869
  )
840
870
  messages = None
841
871
  last_response = None
@@ -854,9 +884,6 @@ def run_interactive_loop(
854
884
  elif command == SlashCommands.LAST_OUTPUT.command:
855
885
  handle_last_command(last_response, console, all_tool_calls_history)
856
886
  continue
857
- elif command == SlashCommands.CLEAR.command:
858
- console.clear()
859
- continue
860
887
  elif command == SlashCommands.CONTEXT.command:
861
888
  handle_context_command(messages, ai, console)
862
889
  continue
@@ -902,7 +929,10 @@ def run_interactive_loop(
902
929
  metadata={"type": "user_question"},
903
930
  )
904
931
  response = ai.call(
905
- messages, post_processing_prompt, trace_span=trace_span
932
+ messages,
933
+ post_processing_prompt,
934
+ trace_span=trace_span,
935
+ tool_number_offset=len(all_tool_calls_history),
906
936
  )
907
937
  trace_span.log(
908
938
  output=response.result,
holmes/main.py CHANGED
@@ -219,16 +219,20 @@ def ask(
219
219
  Ask any question and answer using available tools
220
220
  """
221
221
  console = init_logging(verbose) # type: ignore
222
-
223
222
  # Detect and read piped input
224
223
  piped_data = None
225
- if not sys.stdin.isatty():
224
+
225
+ # when attaching a pycharm debugger sys.stdin.isatty() returns false and sys.stdin.read() is stuck
226
+ running_from_pycharm = os.environ.get("PYCHARM_HOSTED", False)
227
+
228
+ if not sys.stdin.isatty() and not running_from_pycharm:
226
229
  piped_data = sys.stdin.read().strip()
227
230
  if interactive:
228
231
  console.print(
229
232
  "[bold yellow]Interactive mode disabled when reading piped input[/bold yellow]"
230
233
  )
231
234
  interactive = False
235
+
232
236
  config = Config.load_from_file(
233
237
  config_file,
234
238
  api_key=api_key,
@@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
9
9
  from urllib.parse import urljoin
10
10
 
11
11
  import requests # type: ignore
12
- from pydantic import BaseModel, field_validator
12
+ from pydantic import BaseModel, field_validator, Field, model_validator
13
13
  from requests import RequestException
14
14
 
15
15
  from holmes.core.tools import (
@@ -29,6 +29,8 @@ from holmes.plugins.toolsets.utils import (
29
29
  standard_start_datetime_tool_param_description,
30
30
  )
31
31
  from holmes.utils.cache import TTLCache
32
+ from holmes.common.env_vars import IS_OPENSHIFT
33
+ from holmes.common.openshift import load_openshift_token
32
34
 
33
35
  PROMETHEUS_RULES_CACHE_KEY = "cached_prometheus_rules"
34
36
  DEFAULT_TIME_SPAN_SECONDS = 3600
@@ -45,7 +47,7 @@ class PrometheusConfig(BaseModel):
45
47
  fetch_labels_with_labels_api: bool = False
46
48
  fetch_metadata_with_series_api: bool = False
47
49
  tool_calls_return_data: bool = True
48
- headers: Dict = {}
50
+ headers: Dict = Field(default_factory=dict)
49
51
  rules_cache_duration_seconds: Union[int, None] = 1800 # 30 minutes
50
52
  additional_labels: Optional[Dict[str, str]] = None
51
53
 
@@ -55,6 +57,23 @@ class PrometheusConfig(BaseModel):
55
57
  return v + "/"
56
58
  return v
57
59
 
60
+ @model_validator(mode="after")
61
+ def validate_prom_config(self):
62
+ # If openshift is enabled, and the user didn't configure auth headers, we will try to load the token from the service account.
63
+ if IS_OPENSHIFT:
64
+ if self.healthcheck == "-/healthy":
65
+ self.healthcheck = "api/v1/query?query=up"
66
+
67
+ if self.headers.get("Authorization"):
68
+ return self
69
+
70
+ openshift_token = load_openshift_token()
71
+ if openshift_token:
72
+ logging.info("Using openshift token for prometheus toolset auth")
73
+ self.headers["Authorization"] = f"Bearer {openshift_token}"
74
+
75
+ return self
76
+
58
77
 
59
78
  class BasePrometheusTool(Tool):
60
79
  toolset: "PrometheusToolset"
@@ -46,6 +46,7 @@ def init_logging(verbose_flags: Optional[List[bool]] = None):
46
46
 
47
47
  if verbosity == Verbosity.VERY_VERBOSE:
48
48
  logging.basicConfig(
49
+ force=True,
49
50
  level=logging.DEBUG,
50
51
  format="%(message)s",
51
52
  handlers=[
@@ -60,6 +61,7 @@ def init_logging(verbose_flags: Optional[List[bool]] = None):
60
61
  )
61
62
  elif verbosity == Verbosity.VERBOSE:
62
63
  logging.basicConfig(
64
+ force=True,
63
65
  level=logging.INFO,
64
66
  format="%(message)s",
65
67
  handlers=[
@@ -76,6 +78,7 @@ def init_logging(verbose_flags: Optional[List[bool]] = None):
76
78
  suppress_noisy_logs()
77
79
  else:
78
80
  logging.basicConfig(
81
+ force=True,
79
82
  level=logging.INFO,
80
83
  format="%(message)s",
81
84
  handlers=[
holmes/version.py ADDED
@@ -0,0 +1,178 @@
1
+ """
2
+ Centralized version management for Holmes.
3
+ Handles current version detection, latest version fetching, and comparison logic.
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ import threading
11
+ from typing import Optional, NamedTuple
12
+ from functools import cache
13
+ import requests # type: ignore
14
+ from pydantic import BaseModel, ConfigDict
15
+ from holmes.common.env_vars import ROBUSTA_API_ENDPOINT
16
+
17
+ # For relative imports to work in Python 3.6 - see https://stackoverflow.com/a/49375740
18
+ this_path = os.path.dirname(os.path.realpath(__file__))
19
+ sys.path.append(this_path)
20
+
21
+ # Version checking API constants
22
+ HOLMES_GET_INFO_URL = f"{ROBUSTA_API_ENDPOINT}/api/holmes/get_info"
23
+ TIMEOUT = 0.5
24
+
25
+
26
+ class HolmesInfo(BaseModel):
27
+ model_config = ConfigDict(extra="ignore")
28
+ latest_version: Optional[str] = None
29
+
30
+
31
+ class VersionCheckResult(NamedTuple):
32
+ """Result of version check with all relevant info"""
33
+
34
+ is_latest: bool
35
+ current_version: str
36
+ latest_version: Optional[str] = None
37
+ update_message: Optional[str] = None
38
+
39
+
40
+ def is_official_release() -> bool:
41
+ """Check if this is an official release (version was patched by CI/CD)"""
42
+ from holmes import __version__
43
+
44
+ return not __version__.startswith("0.0.0")
45
+
46
+
47
+ @cache
48
+ def get_version() -> str:
49
+ """
50
+ Get the current version of Holmes.
51
+ Returns the official version if patched by CI/CD, otherwise builds from git.
52
+ """
53
+ from holmes import __version__
54
+
55
+ # the version string was patched by a release - return __version__ which will be correct
56
+ if is_official_release():
57
+ return __version__
58
+
59
+ # we are running from an unreleased dev version
60
+ try:
61
+ # Get the latest git tag
62
+ tag = (
63
+ subprocess.check_output(
64
+ ["git", "describe", "--tags"], stderr=subprocess.STDOUT, cwd=this_path
65
+ )
66
+ .decode()
67
+ .strip()
68
+ )
69
+
70
+ # Get the current branch name
71
+ branch = (
72
+ subprocess.check_output(
73
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
74
+ stderr=subprocess.STDOUT,
75
+ cwd=this_path,
76
+ )
77
+ .decode()
78
+ .strip()
79
+ )
80
+
81
+ # Check if there are uncommitted changes
82
+ status = (
83
+ subprocess.check_output(
84
+ ["git", "status", "--porcelain"],
85
+ stderr=subprocess.STDOUT,
86
+ cwd=this_path,
87
+ )
88
+ .decode()
89
+ .strip()
90
+ )
91
+ dirty = "-dirty" if status else ""
92
+
93
+ return f"{tag}-{branch}{dirty}"
94
+
95
+ except Exception:
96
+ pass
97
+
98
+ # we are running without git history, but we still might have git archival data (e.g. if we were pip installed)
99
+ archival_file_path = os.path.join(this_path, ".git_archival.json")
100
+ if os.path.exists(archival_file_path):
101
+ try:
102
+ with open(archival_file_path, "r") as f:
103
+ archival_data = json.load(f)
104
+ return f"dev-{archival_data['refs']}-{archival_data['hash-short']}"
105
+ except Exception:
106
+ pass
107
+
108
+ return "dev-version"
109
+
110
+ return "unknown-version"
111
+
112
+
113
+ @cache
114
+ def fetch_holmes_info() -> Optional[HolmesInfo]:
115
+ """Fetch latest version information from Robusta API"""
116
+ try:
117
+ response = requests.get(HOLMES_GET_INFO_URL, timeout=TIMEOUT)
118
+ response.raise_for_status()
119
+ result = response.json()
120
+ return HolmesInfo(**result)
121
+ except Exception:
122
+ return None
123
+
124
+
125
+ def check_version() -> VersionCheckResult:
126
+ """
127
+ Centralized version checking logic.
128
+ Returns complete version check result with message.
129
+ """
130
+ current_version = get_version()
131
+ holmes_info = fetch_holmes_info()
132
+
133
+ # Default to latest if we can't determine
134
+ if not holmes_info or not holmes_info.latest_version or not current_version:
135
+ return VersionCheckResult(
136
+ is_latest=True, current_version=current_version or "unknown"
137
+ )
138
+
139
+ # Dev versions are considered latest
140
+ if current_version.startswith("dev-"):
141
+ return VersionCheckResult(
142
+ is_latest=True,
143
+ current_version=current_version,
144
+ latest_version=holmes_info.latest_version,
145
+ )
146
+
147
+ # Check if current version starts with latest version
148
+ is_latest = current_version.startswith(holmes_info.latest_version)
149
+
150
+ update_message = None
151
+ if not is_latest:
152
+ update_message = f"Update available: v{holmes_info.latest_version} (current: {current_version})"
153
+
154
+ return VersionCheckResult(
155
+ is_latest=is_latest,
156
+ current_version=current_version,
157
+ latest_version=holmes_info.latest_version,
158
+ update_message=update_message,
159
+ )
160
+
161
+
162
+ def check_version_async(callback):
163
+ """
164
+ Async version check for background use.
165
+ Calls callback with VersionCheckResult when complete.
166
+ """
167
+
168
+ def _check():
169
+ try:
170
+ result = check_version()
171
+ callback(result)
172
+ except Exception:
173
+ # Silent failure - call callback with "latest" result
174
+ callback(VersionCheckResult(is_latest=True, current_version="unknown"))
175
+
176
+ thread = threading.Thread(target=_check, daemon=True)
177
+ thread.start()
178
+ return thread
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: holmesgpt
3
- Version: 0.12.0a0
3
+ Version: 0.12.2a0
4
4
  Summary:
5
5
  Author: Natan Yellin
6
6
  Author-email: natan@robusta.dev
@@ -28,7 +28,7 @@ Requires-Dist: google-api-python-client (>=2.156.0,<3.0.0)
28
28
  Requires-Dist: humanize (>=4.9.0,<5.0.0)
29
29
  Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
30
30
  Requires-Dist: kubernetes (>=32.0.1,<33.0.0)
31
- Requires-Dist: litellm (==1.66.0)
31
+ Requires-Dist: litellm (==1.74.7)
32
32
  Requires-Dist: markdown (>=3.6,<4.0)
33
33
  Requires-Dist: markdownify (>=1.1.0,<2.0.0)
34
34
  Requires-Dist: mcp (==v1.9.0)
@@ -40,6 +40,7 @@ Requires-Dist: pydantic (>=2.7,<3.0)
40
40
  Requires-Dist: pydantic-settings (>=2.1.0,<3.0.0)
41
41
  Requires-Dist: pydash (>=8.0.1,<9.0.0)
42
42
  Requires-Dist: pyodbc (>=5.0.1,<6.0.0)
43
+ Requires-Dist: pytest-shared-session-scope (>=0.4.0,<0.5.0)
43
44
  Requires-Dist: python-benedict (>=0.33.1,<0.34.0)
44
45
  Requires-Dist: python_multipart (>=0.0.18,<0.0.19)
45
46
  Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
@@ -1,14 +1,15 @@
1
1
  holmes/.git_archival.json,sha256=PbwdO7rNhEJ4ALiO12DPPb81xNAIsVxCA0m8OrVoqsk,182
2
- holmes/__init__.py,sha256=YOPbAEB6arNwdwPT4mge8qNkAdjgv1yQdSZK3qwUefY,2178
3
- holmes/clients/robusta_client.py,sha256=b6zje8VF8aOpjXnluBcBDdf3Xb88yFXvKDcy2gV1DeM,672
4
- holmes/common/env_vars.py,sha256=ayfuw30HnfWmEoghgWlF3vOwiarl7uQns_yxVfs7N_w,1915
5
- holmes/config.py,sha256=0wRpKpBNCFAXqxrcAmfyPMQ5WYOCypjhoLeL4m4ZZic,21324
2
+ holmes/__init__.py,sha256=kzD9kWiOhuk8juWhaleYlfxpu82K3UT37Ik6P09Em-M,263
3
+ holmes/clients/robusta_client.py,sha256=u1ZvPBE7VaNVrPdtiTLDjI3Xrx6TWTnOWeIlky_aCHg,672
4
+ holmes/common/env_vars.py,sha256=6Pi3v9cumKKCnEoeJT5fGgmzzerM5oV0izHnM-nPsBA,1963
5
+ holmes/common/openshift.py,sha256=akbQ0GpnmuzXOqTcotpTDQSDKIROypS9mgPOprUgkCw,407
6
+ holmes/config.py,sha256=WblVSBedeDeG_xl-SkPGkxTUgdw4vXz2mOJg08oQQpU,20266
6
7
  holmes/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
8
  holmes/core/conversations.py,sha256=LXT3T-5hwl4xHXBhO4XyOKm6k9w58anQ3dw4F2aptqs,20769
8
9
  holmes/core/investigation.py,sha256=nSVkCfeliZjQT2PrgNPD3o1EwKe9je0Pq3F2bDNQiCU,5646
9
10
  holmes/core/investigation_structured_output.py,sha256=f9MDfhaI5OfaZ4J_UKqMFhWncoODuo6CBNMNWbdIkvE,9991
10
11
  holmes/core/issue.py,sha256=dbctGv8KHAXC1SeOMkEP-BudJ50u7kA8jLN5FN_d808,2426
11
- holmes/core/llm.py,sha256=cC0B8p-mB3iiL2u1FgtcbwBgDv_Xg94DmYSXdIlGNdg,10995
12
+ holmes/core/llm.py,sha256=P81ZlwDSzK4B1uIMAtJuchQdHUfh088KDz0Yvr-jHk4,10761
12
13
  holmes/core/models.py,sha256=Bfo-HxC4SjW1Y60fjwn8AAq2zyrTfM61x6OsWartmU8,5693
13
14
  holmes/core/openai_formatting.py,sha256=T5GguKhYiJbHx7mFTyJZZReV-s9LBX443BD_nJQZR2s,1677
14
15
  holmes/core/performance_timing.py,sha256=MTbTiiX2jjPmW7PuNA2eYON40eWsHPryR1ap_KlwZ_E,2217
@@ -16,16 +17,16 @@ holmes/core/prompt.py,sha256=pc2qoCw0xeJDjGwG0DHOtEUvKsnUAtR6API10ThdlkU,1244
16
17
  holmes/core/resource_instruction.py,sha256=rduue_t8iQi1jbWc3-k3jX867W1Fvc6Tah5uOJk35Mc,483
17
18
  holmes/core/runbooks.py,sha256=Oj5ICmiGgaq57t4erPzQDvHQ0rMGj1nhiiYhl8peH3Q,939
18
19
  holmes/core/safeguards.py,sha256=SAw-J9y3uAehJVZJYsFs4C62jzLV4p_C07F2jUuJHug,4895
19
- holmes/core/supabase_dal.py,sha256=spnBESlw5XK3BFjAFXUtKH069NHF0J-WCD_v--83ZdY,20031
20
- holmes/core/tool_calling_llm.py,sha256=8HwqWCa6H6WaWr49UHBQS-VGSrgQ9SnzcWBzwohLnPo,34547
21
- holmes/core/tools.py,sha256=gfs60sQ_4QQEKX785ejIoxgmrgBrSRolt6xLl2Px3GY,20203
20
+ holmes/core/supabase_dal.py,sha256=76QjQcQKlZb8Mr9RCfXq5OJOfkVgm1m6dn-Gtux_J24,20231
21
+ holmes/core/tool_calling_llm.py,sha256=SakvJ2nGgZquFMFkT66Iv0Kg3FnvsLxYTc3JnEGc6LA,34930
22
+ holmes/core/tools.py,sha256=RU9d1onms_1M3-knZQN9HTOrJCB15xhfn5_kRG22Fr4,20280
22
23
  holmes/core/tools_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
24
  holmes/core/tools_utils/tool_executor.py,sha256=ZGrzn8c8RelQa_t6ZR9siWBBzOCQ1fgKhug4JDqqgUM,2100
24
25
  holmes/core/tools_utils/toolset_utils.py,sha256=1r7nlET4e7CjzMl9MUc_pYOTtRU7YSL8AKp6fQF3_3o,2217
25
- holmes/core/toolset_manager.py,sha256=4oZGr8WinEIiFSVHkNSQ4Drj2k4h2ICMcC-i1aACddE,18166
26
- holmes/core/tracing.py,sha256=Ei0x4Kp9D7LG9FVX-CjicLwEAindIxMbioe2sDdgJXo,7087
27
- holmes/interactive.py,sha256=cK-9bg_wAxlMNvqrqVnLVQag2n8BLjerHvLc2W7Oh40,35315
28
- holmes/main.py,sha256=Wsue8fVfaccnJ8cFfgW25yPSGk1yoa-OjG44jVc2s4k,34687
26
+ holmes/core/toolset_manager.py,sha256=xc9-tv5jt5f-8qgDCGD4tn_mYcDHvX65UxIcnZfUGME,18171
27
+ holmes/core/tracing.py,sha256=PTWjonUIfZLINeLzZgPmKRYdwYwINZopLXnco-cwpgU,7088
28
+ holmes/interactive.py,sha256=g4CLH8lr2e5n_VSRfU5v-wzy0g0Vn7uIRXuQJ9GvG_8,36355
29
+ holmes/main.py,sha256=DbDMrI3BDtcMT7E1n3tll3-U8xW4ZuAllBjbHhyR0Uw,34888
29
30
  holmes/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
31
  holmes/plugins/destinations/__init__.py,sha256=vMYwTfA5nQ05us5Rzbaoe0R5C8Navo6ENVZhojtACTk,98
31
32
  holmes/plugins/destinations/slack/__init__.py,sha256=HVoDdTbdJJ1amXt12ZSMVcn3E04rcOrqk6BgNwvMbWY,56
@@ -151,7 +152,7 @@ holmes/plugins/toolsets/opensearch/opensearch_logs.py,sha256=TbBzLv7we91pTGqmsM1
151
152
  holmes/plugins/toolsets/opensearch/opensearch_traces.py,sha256=gXV2yXrAn0Tu3r5GMiSdsLAKx9myUrHWD0U-OYBXN0g,8497
152
153
  holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2,sha256=Xn8AW4XCMYV1VkBbF8nNB9fUpKQ1Vbm88iFczj-LQXo,1035
153
154
  holmes/plugins/toolsets/opensearch/opensearch_utils.py,sha256=mh9Wp22tOdJYmA9IaFS7tD3aEENljyeuPOsF-lEe5C0,5097
154
- holmes/plugins/toolsets/prometheus/prometheus.py,sha256=dOi0rSh_oJtZrlxJ5bOT2r0wqXmBA0zBA6--njXuSyE,30958
155
+ holmes/plugins/toolsets/prometheus/prometheus.py,sha256=H8YWHj1Q2C8SqabNYQdyoTGe0kZlEarELDQcSVmSkE0,31795
155
156
  holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2,sha256=hIR8Uo9QNVgsUdY94NVNKwpQvJiuGpkECGw8Eu-oVuY,2856
156
157
  holmes/plugins/toolsets/rabbitmq/api.py,sha256=-BtqF7hQWtl_OamnQ521vYHhR8E2n2wcPNYxfI9r4kQ,14307
157
158
  holmes/plugins/toolsets/rabbitmq/rabbitmq_instructions.jinja2,sha256=qetmtJUMkx9LIihr2fSJ2EV9h2J-b-ZdUAvMtopXZYY,3105
@@ -172,7 +173,7 @@ holmes/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
172
173
  holmes/utils/cache.py,sha256=aPc7zTdpBQ3JQR7ET4wvIwzXhx2PpPKiBBSTgbVNXlY,2219
173
174
  holmes/utils/cert_utils.py,sha256=5YAUOY3LjFqqFpYHnHLvSc70LCxEWf0spw1vZwLLvOw,1193
174
175
  holmes/utils/console/consts.py,sha256=klHl0_iXcw_mqtv-YUWBzx3oxi0maaHok_Eq_7ZZdb8,257
175
- holmes/utils/console/logging.py,sha256=nKB3NN0lq45Ux1hMMGetk3TMb9WosTZVliVivkQv0sE,3000
176
+ holmes/utils/console/logging.py,sha256=jnNWqHIJRb6mCOXN24YSi1w_oMxUcOkkXsZb42daFU0,3072
176
177
  holmes/utils/console/result.py,sha256=-n9QtaJtgagIYXu9XYF6foQ6poiCUcYZbK0CWbK9cJw,1334
177
178
  holmes/utils/default_toolset_installation_guide.jinja2,sha256=HEf7tX75HE3H4-YeLbJfa0WbiLEsI71usKrXHCO_sHo,992
178
179
  holmes/utils/definitions.py,sha256=WKVDFh1wfuo0UCV_1jXFjgP0gjGM3-U-UdxdVxmXaKM,287
@@ -185,8 +186,9 @@ holmes/utils/markdown_utils.py,sha256=_yDc_IRB5zkj9THUlZ6nzir44VfirTjPccC_DrFrBk
185
186
  holmes/utils/pydantic_utils.py,sha256=g0e0jLTa8Je8JKrhEP4N5sMxj0_hhPOqFZr0Vpd67sg,1649
186
187
  holmes/utils/robusta.py,sha256=4FZKv5DhDnvINuMlbyFRWCYdR4p7Pj4tyYIUkq1vHUU,363
187
188
  holmes/utils/tags.py,sha256=SU4EZMBtLlIb7OlHsSpguFaypczRzOcuHYxDSanV3sQ,3364
188
- holmesgpt-0.12.0a0.dist-info/LICENSE.txt,sha256=RdZMj8VXRQdVslr6PMYMbAEu5pOjOdjDqt3yAmWb9Ds,1072
189
- holmesgpt-0.12.0a0.dist-info/METADATA,sha256=V2xw8ymq8gN2kOfuHbOnzgR4K_ziLf77XaTYiSsBxt4,21643
190
- holmesgpt-0.12.0a0.dist-info/WHEEL,sha256=kLuE8m1WYU0Ig0_YEGrXyTtiJvKPpLpDEiChiNyei5Y,88
191
- holmesgpt-0.12.0a0.dist-info/entry_points.txt,sha256=JdzEyZhpaYr7Boo4uy4UZgzY1VsAEbzMgGmHZtx9KFY,42
192
- holmesgpt-0.12.0a0.dist-info/RECORD,,
189
+ holmes/version.py,sha256=g_ytiKxYSKlQqYwYDMYBjDWprAc9ZyS2sLrfPb3XDkg,5217
190
+ holmesgpt-0.12.2a0.dist-info/LICENSE.txt,sha256=RdZMj8VXRQdVslr6PMYMbAEu5pOjOdjDqt3yAmWb9Ds,1072
191
+ holmesgpt-0.12.2a0.dist-info/METADATA,sha256=yelkj0mNpwggsMASSTatSoLYutgfbWI47ORy2J_8xlQ,21703
192
+ holmesgpt-0.12.2a0.dist-info/WHEEL,sha256=kLuE8m1WYU0Ig0_YEGrXyTtiJvKPpLpDEiChiNyei5Y,88
193
+ holmesgpt-0.12.2a0.dist-info/entry_points.txt,sha256=JdzEyZhpaYr7Boo4uy4UZgzY1VsAEbzMgGmHZtx9KFY,42
194
+ holmesgpt-0.12.2a0.dist-info/RECORD,,