holmesgpt 0.16.2a0__py3-none-any.whl → 0.18.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. holmes/__init__.py +3 -5
  2. holmes/clients/robusta_client.py +4 -3
  3. holmes/common/env_vars.py +18 -2
  4. holmes/common/openshift.py +1 -1
  5. holmes/config.py +11 -6
  6. holmes/core/conversations.py +30 -13
  7. holmes/core/investigation.py +21 -25
  8. holmes/core/investigation_structured_output.py +3 -3
  9. holmes/core/issue.py +1 -1
  10. holmes/core/llm.py +50 -31
  11. holmes/core/models.py +19 -17
  12. holmes/core/openai_formatting.py +1 -1
  13. holmes/core/prompt.py +47 -2
  14. holmes/core/runbooks.py +1 -0
  15. holmes/core/safeguards.py +4 -2
  16. holmes/core/supabase_dal.py +4 -2
  17. holmes/core/tool_calling_llm.py +102 -141
  18. holmes/core/tools.py +19 -28
  19. holmes/core/tools_utils/token_counting.py +9 -2
  20. holmes/core/tools_utils/tool_context_window_limiter.py +13 -30
  21. holmes/core/tools_utils/tool_executor.py +0 -18
  22. holmes/core/tools_utils/toolset_utils.py +1 -0
  23. holmes/core/toolset_manager.py +37 -2
  24. holmes/core/tracing.py +13 -2
  25. holmes/core/transformers/__init__.py +1 -1
  26. holmes/core/transformers/base.py +1 -0
  27. holmes/core/transformers/llm_summarize.py +3 -2
  28. holmes/core/transformers/registry.py +2 -1
  29. holmes/core/transformers/transformer.py +1 -0
  30. holmes/core/truncation/compaction.py +37 -2
  31. holmes/core/truncation/input_context_window_limiter.py +3 -2
  32. holmes/interactive.py +52 -8
  33. holmes/main.py +17 -37
  34. holmes/plugins/interfaces.py +2 -1
  35. holmes/plugins/prompts/__init__.py +2 -1
  36. holmes/plugins/prompts/_fetch_logs.jinja2 +5 -5
  37. holmes/plugins/prompts/_runbook_instructions.jinja2 +2 -1
  38. holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
  39. holmes/plugins/prompts/conversation_history_compaction.jinja2 +2 -1
  40. holmes/plugins/prompts/generic_ask.jinja2 +0 -2
  41. holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -2
  42. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -2
  43. holmes/plugins/prompts/generic_investigation.jinja2 +0 -2
  44. holmes/plugins/prompts/investigation_procedure.jinja2 +2 -1
  45. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -2
  46. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -2
  47. holmes/plugins/runbooks/__init__.py +32 -3
  48. holmes/plugins/sources/github/__init__.py +4 -2
  49. holmes/plugins/sources/prometheus/models.py +1 -0
  50. holmes/plugins/toolsets/__init__.py +30 -26
  51. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +13 -12
  52. holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +3 -2
  53. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
  54. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +3 -2
  55. holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +3 -1
  56. holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +3 -1
  57. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +12 -12
  58. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +7 -7
  59. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +7 -7
  60. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -5
  61. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +3 -3
  62. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +7 -7
  63. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +6 -8
  64. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +3 -3
  65. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +3 -3
  66. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +3 -3
  67. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +3 -3
  68. holmes/plugins/toolsets/azure_sql/utils.py +0 -32
  69. holmes/plugins/toolsets/bash/argocd/__init__.py +3 -3
  70. holmes/plugins/toolsets/bash/aws/__init__.py +4 -4
  71. holmes/plugins/toolsets/bash/azure/__init__.py +4 -4
  72. holmes/plugins/toolsets/bash/bash_toolset.py +2 -3
  73. holmes/plugins/toolsets/bash/common/bash.py +19 -9
  74. holmes/plugins/toolsets/bash/common/bash_command.py +1 -1
  75. holmes/plugins/toolsets/bash/common/stringify.py +1 -1
  76. holmes/plugins/toolsets/bash/kubectl/__init__.py +2 -1
  77. holmes/plugins/toolsets/bash/kubectl/constants.py +0 -1
  78. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +3 -4
  79. holmes/plugins/toolsets/bash/parse_command.py +12 -13
  80. holmes/plugins/toolsets/connectivity_check.py +124 -0
  81. holmes/plugins/toolsets/coralogix/api.py +132 -119
  82. holmes/plugins/toolsets/coralogix/coralogix.jinja2 +14 -0
  83. holmes/plugins/toolsets/coralogix/toolset_coralogix.py +219 -0
  84. holmes/plugins/toolsets/coralogix/utils.py +15 -79
  85. holmes/plugins/toolsets/datadog/datadog_api.py +36 -3
  86. holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +34 -1
  87. holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +3 -3
  88. holmes/plugins/toolsets/datadog/datadog_models.py +59 -0
  89. holmes/plugins/toolsets/datadog/datadog_url_utils.py +213 -0
  90. holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +165 -28
  91. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +71 -28
  92. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +224 -375
  93. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +67 -36
  94. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +360 -343
  95. holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
  96. holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
  97. holmes/plugins/toolsets/git.py +7 -8
  98. holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
  99. holmes/plugins/toolsets/grafana/common.py +2 -30
  100. holmes/plugins/toolsets/grafana/grafana_tempo_api.py +2 -1
  101. holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +18 -2
  102. holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +92 -18
  103. holmes/plugins/toolsets/grafana/loki_api.py +4 -0
  104. holmes/plugins/toolsets/grafana/toolset_grafana.py +109 -25
  105. holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +22 -0
  106. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +201 -33
  107. holmes/plugins/toolsets/grafana/trace_parser.py +3 -2
  108. holmes/plugins/toolsets/internet/internet.py +10 -10
  109. holmes/plugins/toolsets/internet/notion.py +5 -6
  110. holmes/plugins/toolsets/investigator/core_investigation.py +3 -3
  111. holmes/plugins/toolsets/investigator/model.py +3 -1
  112. holmes/plugins/toolsets/json_filter_mixin.py +134 -0
  113. holmes/plugins/toolsets/kafka.py +12 -7
  114. holmes/plugins/toolsets/kubernetes.yaml +260 -30
  115. holmes/plugins/toolsets/kubernetes_logs.py +3 -3
  116. holmes/plugins/toolsets/logging_utils/logging_api.py +16 -6
  117. holmes/plugins/toolsets/mcp/toolset_mcp.py +88 -60
  118. holmes/plugins/toolsets/newrelic/new_relic_api.py +41 -1
  119. holmes/plugins/toolsets/newrelic/newrelic.jinja2 +24 -0
  120. holmes/plugins/toolsets/newrelic/newrelic.py +212 -55
  121. holmes/plugins/toolsets/prometheus/prometheus.py +358 -102
  122. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +11 -3
  123. holmes/plugins/toolsets/rabbitmq/api.py +23 -4
  124. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +5 -5
  125. holmes/plugins/toolsets/robusta/robusta.py +5 -5
  126. holmes/plugins/toolsets/runbook/runbook_fetcher.py +25 -6
  127. holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +1 -1
  128. holmes/plugins/toolsets/utils.py +1 -1
  129. holmes/utils/config_utils.py +1 -1
  130. holmes/utils/connection_utils.py +31 -0
  131. holmes/utils/console/result.py +10 -0
  132. holmes/utils/file_utils.py +2 -1
  133. holmes/utils/global_instructions.py +10 -26
  134. holmes/utils/holmes_status.py +4 -3
  135. holmes/utils/log.py +15 -0
  136. holmes/utils/markdown_utils.py +2 -3
  137. holmes/utils/memory_limit.py +58 -0
  138. holmes/utils/sentry_helper.py +23 -0
  139. holmes/utils/stream.py +12 -5
  140. holmes/utils/tags.py +4 -3
  141. holmes/version.py +3 -1
  142. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +12 -10
  143. holmesgpt-0.18.4.dist-info/RECORD +258 -0
  144. holmes/plugins/toolsets/aws.yaml +0 -80
  145. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -114
  146. holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
  147. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -736
  148. holmes/plugins/toolsets/grafana/grafana_api.py +0 -64
  149. holmes/plugins/toolsets/opensearch/__init__.py +0 -0
  150. holmes/plugins/toolsets/opensearch/opensearch.py +0 -250
  151. holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
  152. holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -215
  153. holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
  154. holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
  155. holmes/utils/keygen_utils.py +0 -6
  156. holmesgpt-0.16.2a0.dist-info/RECORD +0 -258
  157. holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_ppl_query_docs.jinja2 +0 -0
  158. holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist.py +2 -2
  159. /holmes/plugins/toolsets/{opensearch → elasticsearch}/opensearch_query_assist_instructions.jinja2 +0 -0
  160. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/LICENSE +0 -0
  161. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
  162. {holmesgpt-0.16.2a0.dist-info → holmesgpt-0.18.4.dist-info}/entry_points.txt +0 -0
holmes/__init__.py CHANGED
@@ -1,8 +1,6 @@
1
1
  # This is patched by github actions during release
2
- __version__ = "0.16.2-alpha"
2
+ __version__ = "0.18.4"
3
3
 
4
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
+ from .version import get_version as get_version
6
+ from .version import is_official_release as is_official_release
@@ -1,8 +1,10 @@
1
1
  import logging
2
- from typing import Optional, Dict, Any
3
- import requests # type: ignore
4
2
  from functools import cache
3
+ from typing import Any, Dict, Optional
4
+
5
+ import requests # type: ignore
5
6
  from pydantic import BaseModel, ConfigDict
7
+
6
8
  from holmes.common.env_vars import ROBUSTA_API_ENDPOINT
7
9
 
8
10
  HOLMES_GET_INFO_URL = f"{ROBUSTA_API_ENDPOINT}/api/holmes/get_info"
@@ -25,7 +27,6 @@ class RobustaModelsResponse(BaseModel):
25
27
  models: Dict[str, RobustaModel]
26
28
 
27
29
 
28
- @cache
29
30
  def fetch_robusta_models(
30
31
  account_id: str, token: str
31
32
  ) -> Optional[RobustaModelsResponse]:
holmes/common/env_vars.py CHANGED
@@ -1,5 +1,6 @@
1
- import os
2
1
  import json
2
+ import os
3
+ import platform
3
4
  from typing import Optional
4
5
 
5
6
  # Recommended models for different providers
@@ -35,7 +36,6 @@ STORE_URL = os.environ.get("STORE_URL", "")
35
36
  STORE_API_KEY = os.environ.get("STORE_API_KEY", "")
36
37
  STORE_EMAIL = os.environ.get("STORE_EMAIL", "")
37
38
  STORE_PASSWORD = os.environ.get("STORE_PASSWORD", "")
38
- HOLMES_POST_PROCESSING_PROMPT = os.environ.get("HOLMES_POST_PROCESSING_PROMPT", "")
39
39
  ROBUSTA_AI = load_bool("ROBUSTA_AI", None)
40
40
  LOAD_ALL_ROBUSTA_MODELS = load_bool("LOAD_ALL_ROBUSTA_MODELS", True)
41
41
  ROBUSTA_API_ENDPOINT = os.environ.get("ROBUSTA_API_ENDPOINT", "https://api.robusta.dev")
@@ -53,6 +53,15 @@ THINKING = os.environ.get("THINKING", "")
53
53
  REASONING_EFFORT = os.environ.get("REASONING_EFFORT", "").strip().lower()
54
54
  TEMPERATURE = float(os.environ.get("TEMPERATURE", "0.00000001"))
55
55
 
56
+ # Set default memory limit based on CPU architecture
57
+ # ARM architectures typically need more memory
58
+ _default_memory_limit = (
59
+ 1500 if platform.machine().lower() in ("arm64", "aarch64", "arm") else 800
60
+ )
61
+ TOOL_MEMORY_LIMIT_MB = int(
62
+ os.environ.get("TOOL_MEMORY_LIMIT_MB", _default_memory_limit)
63
+ )
64
+
56
65
  STREAM_CHUNKS_PER_PARSE = int(
57
66
  os.environ.get("STREAM_CHUNKS_PER_PARSE", 80)
58
67
  ) # Empirical value with 6~ parsing calls. Consider using larger value if LLM response is long as to reduce markdown to section calls.
@@ -113,3 +122,10 @@ RESET_REPEATED_TOOL_CALL_CHECK_AFTER_COMPACTION = load_bool(
113
122
  )
114
123
 
115
124
  SSE_READ_TIMEOUT = float(os.environ.get("SSE_READ_TIMEOUT", "120"))
125
+
126
+ LLM_REQUEST_TIMEOUT = float(os.environ.get("LLM_REQUEST_TIMEOUT", "600"))
127
+
128
+ ENABLE_CONNECTION_KEEPALIVE = load_bool("ENABLE_CONNECTION_KEEPALIVE", False)
129
+ KEEPALIVE_IDLE = int(os.environ.get("KEEPALIVE_IDLE", 2))
130
+ KEEPALIVE_INTVL = int(os.environ.get("KEEPALIVE_INTVL", 2))
131
+ KEEPALIVE_CNT = int(os.environ.get("KEEPALIVE_CNT", 5))
@@ -1,5 +1,5 @@
1
- from typing import Optional
2
1
  import os
2
+ from typing import Optional
3
3
 
4
4
  # NOTE: This one will be mounted if openshift is enabled in values.yaml
5
5
  TOKEN_LOCATION = os.environ.get(
holmes/config.py CHANGED
@@ -84,6 +84,7 @@ class Config(RobustaBaseConfig):
84
84
  opsgenie_query: Optional[str] = None
85
85
 
86
86
  custom_runbooks: List[FilePath] = []
87
+ custom_runbook_catalogs: List[Union[str, FilePath]] = []
87
88
 
88
89
  # custom_toolsets is passed from config file, and be used to override built-in toolsets, provides 'stable' customized toolset.
89
90
  # The status of custom toolsets can be cached.
@@ -114,6 +115,7 @@ class Config(RobustaBaseConfig):
114
115
  custom_toolsets=self.custom_toolsets,
115
116
  custom_toolsets_from_cli=self.custom_toolsets_from_cli,
116
117
  global_fast_model=self.fast_model,
118
+ custom_runbook_catalogs=self.custom_runbook_catalogs,
117
119
  )
118
120
  return self._toolset_manager
119
121
 
@@ -224,8 +226,9 @@ class Config(RobustaBaseConfig):
224
226
  return None
225
227
 
226
228
  def get_runbook_catalog(self) -> Optional[RunbookCatalog]:
227
- # TODO(mainred): besides the built-in runbooks, we need to allow the user to bring their own runbooks
228
- runbook_catalog = load_runbook_catalog(dal=self.dal)
229
+ runbook_catalog = load_runbook_catalog(
230
+ dal=self.dal, custom_catalog_paths=self.custom_runbook_catalogs
231
+ )
229
232
  return runbook_catalog
230
233
 
231
234
  def create_console_tool_executor(
@@ -285,12 +288,15 @@ class Config(RobustaBaseConfig):
285
288
  dal: Optional["SupabaseDal"] = None,
286
289
  refresh_toolsets: bool = False,
287
290
  tracer=None,
291
+ model_name: Optional[str] = None,
288
292
  ) -> "ToolCallingLLM":
289
293
  tool_executor = self.create_console_tool_executor(dal, refresh_toolsets)
290
294
  from holmes.core.tool_calling_llm import ToolCallingLLM
291
295
 
292
296
  return ToolCallingLLM(
293
- tool_executor, self.max_steps, self._get_llm(tracer=tracer)
297
+ tool_executor,
298
+ self.max_steps,
299
+ self._get_llm(tracer=tracer, model_key=model_name),
294
300
  )
295
301
 
296
302
  def create_agui_toolcalling_llm(
@@ -344,7 +350,7 @@ class Config(RobustaBaseConfig):
344
350
  )
345
351
 
346
352
  def create_console_issue_investigator(
347
- self, dal: Optional["SupabaseDal"] = None
353
+ self, dal: Optional["SupabaseDal"] = None, model_name: Optional[str] = None
348
354
  ) -> "IssueInvestigator":
349
355
  all_runbooks = load_builtin_runbooks()
350
356
  for runbook_path in self.custom_runbooks:
@@ -360,7 +366,7 @@ class Config(RobustaBaseConfig):
360
366
  tool_executor=tool_executor,
361
367
  runbook_manager=runbook_manager,
362
368
  max_steps=self.max_steps,
363
- llm=self._get_llm(),
369
+ llm=self._get_llm(model_key=model_name),
364
370
  cluster_name=self.cluster_name,
365
371
  )
366
372
 
@@ -478,7 +484,6 @@ class Config(RobustaBaseConfig):
478
484
  model_params = model_entry.model_dump(exclude_none=True)
479
485
  api_base = self.api_base
480
486
  api_version = self.api_version
481
-
482
487
  is_robusta_model = model_params.pop("is_robusta_model", False)
483
488
  sentry_sdk.set_tag("is_robusta_model", is_robusta_model)
484
489
  if is_robusta_model:
@@ -1,18 +1,20 @@
1
1
  from typing import Dict, List, Optional
2
2
 
3
3
  import sentry_sdk
4
+
4
5
  from holmes.config import Config
5
6
  from holmes.core.models import (
6
- ToolCallConversationResult,
7
7
  IssueChatRequest,
8
+ ToolCallConversationResult,
8
9
  WorkloadHealthChatRequest,
9
10
  )
10
- from holmes.plugins.prompts import load_and_render_prompt
11
+ from holmes.core.prompt import generate_user_prompt
11
12
  from holmes.core.tool_calling_llm import ToolCallingLLM
13
+ from holmes.plugins.prompts import load_and_render_prompt
12
14
  from holmes.plugins.runbooks import RunbookCatalog
13
15
  from holmes.utils.global_instructions import (
14
16
  Instructions,
15
- add_runbooks_to_user_prompt,
17
+ generate_runbooks_args,
16
18
  )
17
19
 
18
20
  DEFAULT_TOOL_SIZE = 10000
@@ -121,11 +123,14 @@ def build_issue_chat_messages(
121
123
  tools_for_investigation = issue_chat_request.investigation_result.tools
122
124
 
123
125
  if not conversation_history or len(conversation_history) == 0:
124
- user_prompt = add_runbooks_to_user_prompt(
125
- user_prompt=user_prompt,
126
+ runbooks_ctx = generate_runbooks_args(
126
127
  runbook_catalog=runbooks,
127
128
  global_instructions=global_instructions,
128
129
  )
130
+ user_prompt = generate_user_prompt(
131
+ user_prompt,
132
+ runbooks_ctx,
133
+ )
129
134
 
130
135
  number_of_tools_for_investigation = len(tools_for_investigation) # type: ignore
131
136
  if number_of_tools_for_investigation == 0:
@@ -208,11 +213,14 @@ def build_issue_chat_messages(
208
213
  },
209
214
  ]
210
215
 
211
- user_prompt = add_runbooks_to_user_prompt(
212
- user_prompt=user_prompt,
216
+ runbooks_ctx = generate_runbooks_args(
213
217
  runbook_catalog=runbooks,
214
218
  global_instructions=global_instructions,
215
219
  )
220
+ user_prompt = generate_user_prompt(
221
+ user_prompt,
222
+ runbooks_ctx,
223
+ )
216
224
 
217
225
  conversation_history.append(
218
226
  {
@@ -385,11 +393,14 @@ def build_chat_messages(
385
393
  runbooks=runbooks,
386
394
  )
387
395
 
388
- ask = add_runbooks_to_user_prompt(
389
- user_prompt=ask,
396
+ runbooks_ctx = generate_runbooks_args(
390
397
  runbook_catalog=runbooks,
391
398
  global_instructions=global_instructions,
392
399
  )
400
+ ask = generate_user_prompt(
401
+ ask,
402
+ runbooks_ctx,
403
+ )
393
404
 
394
405
  conversation_history.append( # type: ignore
395
406
  {
@@ -481,11 +492,14 @@ def build_workload_health_chat_messages(
481
492
  resource = workload_health_chat_request.resource
482
493
 
483
494
  if not conversation_history or len(conversation_history) == 0:
484
- user_prompt = add_runbooks_to_user_prompt(
485
- user_prompt=user_prompt,
495
+ runbooks_ctx = generate_runbooks_args(
486
496
  runbook_catalog=runbooks,
487
497
  global_instructions=global_instructions,
488
498
  )
499
+ user_prompt = generate_user_prompt(
500
+ user_prompt,
501
+ runbooks_ctx,
502
+ )
489
503
 
490
504
  number_of_tools_for_workload = len(tools_for_workload) # type: ignore
491
505
  if number_of_tools_for_workload == 0:
@@ -568,11 +582,14 @@ def build_workload_health_chat_messages(
568
582
  },
569
583
  ]
570
584
 
571
- user_prompt = add_runbooks_to_user_prompt(
572
- user_prompt=user_prompt,
585
+ runbooks_ctx = generate_runbooks_args(
573
586
  runbook_catalog=runbooks,
574
587
  global_instructions=global_instructions,
575
588
  )
589
+ user_prompt = generate_user_prompt(
590
+ user_prompt,
591
+ runbooks_ctx,
592
+ )
576
593
 
577
594
  conversation_history.append(
578
595
  {
@@ -1,24 +1,22 @@
1
1
  import logging
2
2
  from typing import Optional
3
3
 
4
-
5
- from holmes.common.env_vars import HOLMES_POST_PROCESSING_PROMPT
6
4
  from holmes.config import Config
7
- from holmes.core.investigation_structured_output import process_response_into_sections
8
- from holmes.core.issue import Issue
9
- from holmes.core.models import InvestigateRequest, InvestigationResult
10
- from holmes.core.supabase_dal import SupabaseDal
11
- from holmes.core.tracing import DummySpan, SpanType
12
- from holmes.plugins.runbooks import RunbookCatalog
13
- from holmes.utils.global_instructions import add_runbooks_to_user_prompt
14
-
15
5
  from holmes.core.investigation_structured_output import (
16
6
  DEFAULT_SECTIONS,
17
7
  REQUEST_STRUCTURED_OUTPUT_FROM_LLM,
18
8
  get_output_format_for_investigation,
9
+ process_response_into_sections,
19
10
  )
20
-
11
+ from holmes.core.issue import Issue
12
+ from holmes.core.models import InvestigateRequest, InvestigationResult
13
+ from holmes.core.prompt import generate_user_prompt
14
+ from holmes.core.supabase_dal import SupabaseDal
15
+ from holmes.core.tracing import DummySpan, SpanType
21
16
  from holmes.plugins.prompts import load_and_render_prompt
17
+ from holmes.plugins.runbooks import RunbookCatalog
18
+ from holmes.utils import sentry_helper
19
+ from holmes.utils.global_instructions import generate_runbooks_args
22
20
 
23
21
 
24
22
  def investigate_issues(
@@ -31,9 +29,6 @@ def investigate_issues(
31
29
  ) -> InvestigationResult:
32
30
  context = dal.get_issue_data(investigate_request.context.get("robusta_issue_id"))
33
31
 
34
- resource_instructions = dal.get_resource_instructions(
35
- "alert", investigate_request.context.get("issue_type")
36
- )
37
32
  global_instructions = dal.get_global_instructions_for_account()
38
33
 
39
34
  raw_data = investigate_request.model_dump()
@@ -58,8 +53,6 @@ def investigate_issues(
58
53
  investigation = ai.investigate(
59
54
  issue,
60
55
  prompt=investigate_request.prompt_template,
61
- post_processing_prompt=HOLMES_POST_PROCESSING_PROMPT,
62
- instructions=resource_instructions,
63
56
  global_instructions=global_instructions,
64
57
  sections=investigate_request.sections,
65
58
  trace_span=trace_span,
@@ -68,11 +61,15 @@ def investigate_issues(
68
61
 
69
62
  (text_response, sections) = process_response_into_sections(investigation.result)
70
63
 
64
+ if sections is None:
65
+ sentry_helper.capture_sections_none(content=investigation.result)
66
+
71
67
  logging.debug(f"text response: {text_response}")
72
68
  return InvestigationResult(
73
69
  analysis=text_response,
74
70
  sections=sections,
75
71
  tool_calls=investigation.tool_calls or [],
72
+ num_llm_calls=investigation.num_llm_calls,
76
73
  instructions=investigation.instructions,
77
74
  metadata=investigation.metadata,
78
75
  )
@@ -101,10 +98,6 @@ def get_investigation_context(
101
98
 
102
99
  issue_instructions = ai.runbook_manager.get_instructions_for_issue(issue)
103
100
 
104
- resource_instructions = dal.get_resource_instructions(
105
- "alert", investigate_request.context.get("issue_type")
106
- )
107
-
108
101
  # This section is about setting vars to request the LLM to return structured output.
109
102
  # It does not mean that Holmes will not return structured sections for investigation as it is
110
103
  # capable of splitting the markdown into sections
@@ -140,17 +133,20 @@ def get_investigation_context(
140
133
  "runbooks_enabled": True if runbook_catalog else False,
141
134
  },
142
135
  )
143
- user_prompt = ""
136
+ base_user = ""
144
137
 
145
138
  global_instructions = dal.get_global_instructions_for_account()
146
- user_prompt = add_runbooks_to_user_prompt(
147
- user_prompt=user_prompt,
139
+ runbooks_ctx = generate_runbooks_args(
148
140
  runbook_catalog=runbook_catalog,
149
141
  global_instructions=global_instructions,
150
142
  issue_instructions=issue_instructions,
151
- resource_instructions=resource_instructions,
152
143
  )
153
144
 
154
- user_prompt = f"{user_prompt}\n #This is context from the issue:\n{issue.raw}"
145
+ base_user = f"{base_user}\n #This is context from the issue:\n{issue.raw}"
146
+
147
+ user_prompt = generate_user_prompt(
148
+ base_user,
149
+ runbooks_ctx,
150
+ )
155
151
 
156
152
  return ai, system_prompt, user_prompt, response_format, sections, issue_instructions
@@ -1,10 +1,10 @@
1
- import logging
2
- from typing import Any, Dict, Optional, Tuple
3
1
  import json
2
+ import logging
4
3
  import re
5
4
  from contextlib import suppress
6
- from holmes.common.env_vars import load_bool
5
+ from typing import Any, Dict, Optional, Tuple
7
6
 
7
+ from holmes.common.env_vars import load_bool
8
8
 
9
9
  REQUEST_STRUCTURED_OUTPUT_FROM_LLM = load_bool(
10
10
  "REQUEST_STRUCTURED_OUTPUT_FROM_LLM", True
holmes/core/issue.py CHANGED
@@ -1,7 +1,7 @@
1
- from strenum import StrEnum
2
1
  from typing import Optional
3
2
 
4
3
  from pydantic import BaseModel, ConfigDict
4
+ from strenum import StrEnum
5
5
 
6
6
 
7
7
  class IssueStatus(StrEnum):
holmes/core/llm.py CHANGED
@@ -1,14 +1,15 @@
1
1
  import json
2
2
  import logging
3
3
  import os
4
+ import threading
4
5
  from abc import abstractmethod
5
6
  from math import floor
6
7
  from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union
7
8
 
8
9
  import litellm
10
+ import sentry_sdk
9
11
  from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper
10
12
  from litellm.types.utils import ModelResponse, TextCompletionResponse
11
- import sentry_sdk
12
13
  from pydantic import BaseModel, ConfigDict, SecretStr
13
14
  from typing_extensions import Self
14
15
 
@@ -17,15 +18,15 @@ from holmes.clients.robusta_client import (
17
18
  RobustaModelsResponse,
18
19
  fetch_robusta_models,
19
20
  )
20
-
21
21
  from holmes.common.env_vars import (
22
+ EXTRA_HEADERS,
22
23
  FALLBACK_CONTEXT_WINDOW_SIZE,
24
+ LLM_REQUEST_TIMEOUT,
23
25
  LOAD_ALL_ROBUSTA_MODELS,
24
26
  REASONING_EFFORT,
25
27
  ROBUSTA_AI,
26
28
  ROBUSTA_API_ENDPOINT,
27
29
  THINKING,
28
- EXTRA_HEADERS,
29
30
  TOOL_MAX_ALLOCATED_CONTEXT_WINDOW_PCT,
30
31
  TOOL_MAX_ALLOCATED_CONTEXT_WINDOW_TOKENS,
31
32
  )
@@ -396,6 +397,12 @@ class DefaultLLM(LLM):
396
397
  "reasoning_effort"
397
398
  ] # can be removed after next litelm version
398
399
 
400
+ existing_allowed = self.args.pop("allowed_openai_params", None)
401
+ if existing_allowed:
402
+ if allowed_openai_params is None:
403
+ allowed_openai_params = []
404
+ allowed_openai_params.extend(existing_allowed)
405
+
399
406
  self.args.setdefault("temperature", temperature)
400
407
 
401
408
  self._add_cache_control_to_last_message(messages)
@@ -414,6 +421,7 @@ class DefaultLLM(LLM):
414
421
  drop_params=drop_params,
415
422
  allowed_openai_params=allowed_openai_params,
416
423
  stream=stream,
424
+ timeout=LLM_REQUEST_TIMEOUT,
417
425
  **tools_args,
418
426
  **self.args,
419
427
  )
@@ -524,6 +532,7 @@ class LLMModelRegistry:
524
532
  self._llms: dict[str, ModelEntry] = {}
525
533
  self._default_robusta_model = None
526
534
  self.dal = dal
535
+ self._lock = threading.RLock()
527
536
 
528
537
  self._init_models()
529
538
 
@@ -549,6 +558,9 @@ class LLMModelRegistry:
549
558
 
550
559
  def _should_load_config_model(self) -> bool:
551
560
  if self.config.model is not None:
561
+ if self._llms and self.config.model in self._llms:
562
+ # model already loaded from file
563
+ return False
552
564
  return True
553
565
 
554
566
  # backward compatibility - in the past config.model was set by default to gpt-4o.
@@ -628,39 +640,46 @@ class LLMModelRegistry:
628
640
  return True
629
641
 
630
642
  def get_model_params(self, model_key: Optional[str] = None) -> ModelEntry:
631
- if not self._llms:
632
- raise Exception("No llm models were loaded")
633
-
634
- if model_key:
635
- model_params = self._llms.get(model_key)
636
- if model_params is not None:
637
- logging.info(f"Using selected model: {model_key}")
638
- return model_params.copy()
639
-
640
- logging.error(f"Couldn't find model: {model_key} in model list")
641
-
642
- if self._default_robusta_model:
643
- model_params = self._llms.get(self._default_robusta_model)
644
- if model_params is not None:
645
- logging.info(
646
- f"Using default Robusta AI model: {self._default_robusta_model}"
643
+ with self._lock:
644
+ if not self._llms:
645
+ raise Exception("No llm models were loaded")
646
+
647
+ if model_key:
648
+ model_params = self._llms.get(model_key)
649
+ if model_params:
650
+ logging.info(f"Using selected model: {model_key}")
651
+ return model_params.model_copy()
652
+
653
+ if model_key.startswith("Robusta/"):
654
+ logging.warning("Resyncing Registry and Robusta models.")
655
+ self._init_models()
656
+ model_params = self._llms.get(model_key)
657
+ if model_params:
658
+ logging.info(f"Using selected model: {model_key}")
659
+ return model_params.model_copy()
660
+
661
+ logging.error(f"Couldn't find model: {model_key} in model list")
662
+
663
+ if self._default_robusta_model:
664
+ model_params = self._llms.get(self._default_robusta_model)
665
+ if model_params is not None:
666
+ logging.info(
667
+ f"Using default Robusta AI model: {self._default_robusta_model}"
668
+ )
669
+ return model_params.model_copy()
670
+
671
+ logging.error(
672
+ f"Couldn't find default Robusta AI model: {self._default_robusta_model} in model list"
647
673
  )
648
- return model_params.copy()
649
-
650
- logging.error(
651
- f"Couldn't find default Robusta AI model: {self._default_robusta_model} in model list"
652
- )
653
-
654
- model_key, first_model_params = next(iter(self._llms.items()))
655
- logging.debug(f"Using first available model: {model_key}")
656
- return first_model_params.copy()
657
674
 
658
- def get_llm(self, name: str) -> LLM: # TODO: fix logic
659
- return self._llms[name] # type: ignore
675
+ model_key, first_model_params = next(iter(self._llms.items()))
676
+ logging.debug(f"Using first available model: {model_key}")
677
+ return first_model_params.model_copy()
660
678
 
661
679
  @property
662
680
  def models(self) -> dict[str, ModelEntry]:
663
- return self._llms
681
+ with self._lock:
682
+ return self._llms
664
683
 
665
684
  def _parse_models_file(self, path: str) -> dict[str, ModelEntry]:
666
685
  models = load_yaml_file(path, raise_error=False, warn_not_found=False)
holmes/core/models.py CHANGED
@@ -1,9 +1,10 @@
1
1
  import json
2
- from holmes.core.investigation_structured_output import InputSectionsDataType
3
- from typing import Optional, List, Dict, Any, Union
4
- from pydantic import BaseModel, model_validator, Field
5
2
  from enum import Enum
3
+ from typing import Any, Dict, List, Optional, Union
4
+
5
+ from pydantic import BaseModel, Field, model_validator
6
6
 
7
+ from holmes.core.investigation_structured_output import InputSectionsDataType
7
8
  from holmes.core.tools import StructuredToolResult, StructuredToolResultStatus
8
9
 
9
10
 
@@ -32,7 +33,11 @@ class ToolCallResult(BaseModel):
32
33
  "tool_call_id": self.tool_call_id,
33
34
  "role": "tool",
34
35
  "name": self.tool_name,
35
- "content": format_tool_result_data(self.result),
36
+ "content": format_tool_result_data(
37
+ tool_result=self.result,
38
+ tool_call_id=self.tool_call_id,
39
+ tool_name=self.tool_name,
40
+ ),
36
41
  }
37
42
 
38
43
  def as_tool_result_response(self):
@@ -60,20 +65,16 @@ class ToolCallResult(BaseModel):
60
65
  }
61
66
 
62
67
 
63
- def format_tool_result_data(tool_result: StructuredToolResult) -> str:
64
- tool_response = tool_result.data
65
- if isinstance(tool_result.data, str):
66
- tool_response = tool_result.data
67
- else:
68
- try:
69
- if isinstance(tool_result.data, BaseModel):
70
- tool_response = tool_result.data.model_dump_json(indent=2)
71
- else:
72
- tool_response = json.dumps(tool_result.data, indent=2)
73
- except Exception:
74
- tool_response = str(tool_result.data)
68
+ def format_tool_result_data(
69
+ tool_result: StructuredToolResult, tool_call_id: str, tool_name: str
70
+ ) -> str:
71
+ tool_call_metadata = {"tool_name": tool_name, "tool_call_id": tool_call_id}
72
+ tool_response = f"tool_call_metadata={json.dumps(tool_call_metadata)}"
73
+
75
74
  if tool_result.status == StructuredToolResultStatus.ERROR:
76
- tool_response = f"{tool_result.error or 'Tool execution failed'}:\n\n{tool_result.data or ''}".strip()
75
+ tool_response += f"{tool_result.error or 'Tool execution failed'}:\n\n"
76
+
77
+ tool_response += tool_result.get_stringified_data()
77
78
 
78
79
  if tool_result.params:
79
80
  tool_response = (
@@ -87,6 +88,7 @@ class InvestigationResult(BaseModel):
87
88
  analysis: Optional[str] = None
88
89
  sections: Optional[Dict[str, Union[str, None]]] = None
89
90
  tool_calls: List[ToolCallResult] = []
91
+ num_llm_calls: Optional[int] = None # Number of LLM API calls (turns)
90
92
  instructions: List[str] = []
91
93
  metadata: Optional[Dict[Any, Any]] = None
92
94
 
@@ -2,8 +2,8 @@ import re
2
2
  from typing import Any, Optional
3
3
 
4
4
  from holmes.common.env_vars import (
5
- TOOL_SCHEMA_NO_PARAM_OBJECT_IF_NO_PARAMS,
6
5
  LLMS_WITH_STRICT_TOOL_CALLS,
6
+ TOOL_SCHEMA_NO_PARAM_OBJECT_IF_NO_PARAMS,
7
7
  )
8
8
  from holmes.utils.llms import model_matches_list
9
9
 
holmes/core/prompt.py CHANGED
@@ -1,8 +1,11 @@
1
- from rich.console import Console
2
- from typing import Optional, List, Dict, Any, Union
3
1
  from pathlib import Path
2
+ from typing import Any, Dict, List, Optional, Union
3
+
4
+ from rich.console import Console
5
+
4
6
  from holmes.plugins.prompts import load_and_render_prompt
5
7
  from holmes.plugins.runbooks import RunbookCatalog
8
+ from holmes.utils.global_instructions import generate_runbooks_args
6
9
 
7
10
 
8
11
  def append_file_to_user_prompt(user_prompt: str, file_path: Path) -> str:
@@ -35,6 +38,39 @@ def get_tasks_management_system_reminder() -> str:
35
38
  )
36
39
 
37
40
 
41
+ def _has_content(value: Optional[str]) -> bool:
42
+ """
43
+ Check if the value is a non-empty string and not None.
44
+ """
45
+ return bool(value and isinstance(value, str) and value.strip())
46
+
47
+
48
+ def _should_enable_runbooks(context: Dict[str, str]) -> bool:
49
+ return any(
50
+ (
51
+ _has_content(context.get("runbook_catalog")),
52
+ _has_content(context.get("custom_instructions")),
53
+ _has_content(context.get("global_instructions")),
54
+ )
55
+ )
56
+
57
+
58
+ def generate_user_prompt(
59
+ user_prompt: str,
60
+ context: Dict[str, str],
61
+ ) -> str:
62
+ runbooks_enabled = _should_enable_runbooks(context)
63
+
64
+ return load_and_render_prompt(
65
+ "builtin://base_user_prompt.jinja2",
66
+ context={
67
+ "user_prompt": user_prompt,
68
+ "runbooks_enabled": runbooks_enabled,
69
+ **context,
70
+ },
71
+ )
72
+
73
+
38
74
  def build_initial_ask_messages(
39
75
  console: Console,
40
76
  initial_user_prompt: str,
@@ -70,6 +106,15 @@ def build_initial_ask_messages(
70
106
  )
71
107
 
72
108
  user_prompt_with_files += get_tasks_management_system_reminder()
109
+
110
+ runbooks_ctx = generate_runbooks_args(
111
+ runbook_catalog=runbooks, # type: ignore
112
+ )
113
+ user_prompt_with_files = generate_user_prompt(
114
+ user_prompt_with_files,
115
+ runbooks_ctx,
116
+ )
117
+
73
118
  messages = [
74
119
  {"role": "system", "content": system_prompt_rendered},
75
120
  {"role": "user", "content": user_prompt_with_files},