holmesgpt 0.12.6__py3-none-any.whl → 0.13.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (125) hide show
  1. holmes/__init__.py +1 -1
  2. holmes/clients/robusta_client.py +19 -1
  3. holmes/common/env_vars.py +17 -0
  4. holmes/config.py +69 -9
  5. holmes/core/conversations.py +11 -0
  6. holmes/core/investigation.py +16 -3
  7. holmes/core/investigation_structured_output.py +12 -0
  8. holmes/core/llm.py +13 -1
  9. holmes/core/models.py +9 -1
  10. holmes/core/openai_formatting.py +72 -12
  11. holmes/core/prompt.py +13 -0
  12. holmes/core/supabase_dal.py +3 -0
  13. holmes/core/todo_manager.py +88 -0
  14. holmes/core/tool_calling_llm.py +230 -157
  15. holmes/core/tools.py +10 -1
  16. holmes/core/tools_utils/tool_executor.py +7 -2
  17. holmes/core/tools_utils/toolset_utils.py +7 -2
  18. holmes/core/toolset_manager.py +1 -5
  19. holmes/core/tracing.py +4 -3
  20. holmes/interactive.py +1 -0
  21. holmes/main.py +9 -2
  22. holmes/plugins/prompts/__init__.py +7 -1
  23. holmes/plugins/prompts/_current_date_time.jinja2 +1 -0
  24. holmes/plugins/prompts/_default_log_prompt.jinja2 +4 -2
  25. holmes/plugins/prompts/_fetch_logs.jinja2 +10 -1
  26. holmes/plugins/prompts/_general_instructions.jinja2 +14 -0
  27. holmes/plugins/prompts/_permission_errors.jinja2 +1 -1
  28. holmes/plugins/prompts/_toolsets_instructions.jinja2 +4 -4
  29. holmes/plugins/prompts/generic_ask.jinja2 +4 -3
  30. holmes/plugins/prompts/investigation_procedure.jinja2 +210 -0
  31. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +2 -0
  32. holmes/plugins/runbooks/CLAUDE.md +85 -0
  33. holmes/plugins/runbooks/README.md +24 -0
  34. holmes/plugins/toolsets/__init__.py +19 -6
  35. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +27 -0
  36. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +2 -2
  37. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +2 -1
  38. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -1
  39. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +2 -1
  40. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +2 -1
  41. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +3 -1
  42. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +2 -1
  43. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +2 -1
  44. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +2 -1
  45. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +2 -1
  46. holmes/plugins/toolsets/bash/argocd/__init__.py +65 -0
  47. holmes/plugins/toolsets/bash/argocd/constants.py +120 -0
  48. holmes/plugins/toolsets/bash/aws/__init__.py +66 -0
  49. holmes/plugins/toolsets/bash/aws/constants.py +529 -0
  50. holmes/plugins/toolsets/bash/azure/__init__.py +56 -0
  51. holmes/plugins/toolsets/bash/azure/constants.py +339 -0
  52. holmes/plugins/toolsets/bash/bash_instructions.jinja2 +6 -7
  53. holmes/plugins/toolsets/bash/bash_toolset.py +47 -13
  54. holmes/plugins/toolsets/bash/common/bash_command.py +131 -0
  55. holmes/plugins/toolsets/bash/common/stringify.py +14 -1
  56. holmes/plugins/toolsets/bash/common/validators.py +91 -0
  57. holmes/plugins/toolsets/bash/docker/__init__.py +59 -0
  58. holmes/plugins/toolsets/bash/docker/constants.py +255 -0
  59. holmes/plugins/toolsets/bash/helm/__init__.py +61 -0
  60. holmes/plugins/toolsets/bash/helm/constants.py +92 -0
  61. holmes/plugins/toolsets/bash/kubectl/__init__.py +80 -79
  62. holmes/plugins/toolsets/bash/kubectl/constants.py +0 -14
  63. holmes/plugins/toolsets/bash/kubectl/kubectl_describe.py +38 -56
  64. holmes/plugins/toolsets/bash/kubectl/kubectl_events.py +28 -76
  65. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +39 -99
  66. holmes/plugins/toolsets/bash/kubectl/kubectl_logs.py +34 -15
  67. holmes/plugins/toolsets/bash/kubectl/kubectl_run.py +1 -1
  68. holmes/plugins/toolsets/bash/kubectl/kubectl_top.py +38 -77
  69. holmes/plugins/toolsets/bash/parse_command.py +106 -32
  70. holmes/plugins/toolsets/bash/utilities/__init__.py +0 -0
  71. holmes/plugins/toolsets/bash/utilities/base64_util.py +12 -0
  72. holmes/plugins/toolsets/bash/utilities/cut.py +12 -0
  73. holmes/plugins/toolsets/bash/utilities/grep/__init__.py +10 -0
  74. holmes/plugins/toolsets/bash/utilities/head.py +12 -0
  75. holmes/plugins/toolsets/bash/utilities/jq.py +79 -0
  76. holmes/plugins/toolsets/bash/utilities/sed.py +164 -0
  77. holmes/plugins/toolsets/bash/utilities/sort.py +15 -0
  78. holmes/plugins/toolsets/bash/utilities/tail.py +12 -0
  79. holmes/plugins/toolsets/bash/utilities/tr.py +57 -0
  80. holmes/plugins/toolsets/bash/utilities/uniq.py +12 -0
  81. holmes/plugins/toolsets/bash/utilities/wc.py +12 -0
  82. holmes/plugins/toolsets/coralogix/api.py +6 -6
  83. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +7 -1
  84. holmes/plugins/toolsets/datadog/datadog_api.py +20 -8
  85. holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +8 -1
  86. holmes/plugins/toolsets/datadog/datadog_rds_instructions.jinja2 +82 -0
  87. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +12 -5
  88. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +20 -11
  89. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +735 -0
  90. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +18 -11
  91. holmes/plugins/toolsets/git.py +15 -15
  92. holmes/plugins/toolsets/grafana/grafana_api.py +12 -1
  93. holmes/plugins/toolsets/grafana/toolset_grafana.py +5 -1
  94. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +9 -4
  95. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +12 -5
  96. holmes/plugins/toolsets/internet/internet.py +2 -1
  97. holmes/plugins/toolsets/internet/notion.py +2 -1
  98. holmes/plugins/toolsets/investigator/__init__.py +0 -0
  99. holmes/plugins/toolsets/investigator/core_investigation.py +157 -0
  100. holmes/plugins/toolsets/investigator/investigator_instructions.jinja2 +253 -0
  101. holmes/plugins/toolsets/investigator/model.py +15 -0
  102. holmes/plugins/toolsets/kafka.py +14 -7
  103. holmes/plugins/toolsets/kubernetes_logs.py +454 -25
  104. holmes/plugins/toolsets/logging_utils/logging_api.py +115 -55
  105. holmes/plugins/toolsets/mcp/toolset_mcp.py +1 -1
  106. holmes/plugins/toolsets/newrelic.py +8 -3
  107. holmes/plugins/toolsets/opensearch/opensearch.py +8 -4
  108. holmes/plugins/toolsets/opensearch/opensearch_logs.py +9 -2
  109. holmes/plugins/toolsets/opensearch/opensearch_traces.py +6 -2
  110. holmes/plugins/toolsets/prometheus/prometheus.py +179 -44
  111. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +8 -2
  112. holmes/plugins/toolsets/robusta/robusta.py +4 -4
  113. holmes/plugins/toolsets/runbook/runbook_fetcher.py +6 -5
  114. holmes/plugins/toolsets/servicenow/servicenow.py +18 -3
  115. holmes/plugins/toolsets/utils.py +8 -1
  116. holmes/utils/console/logging.py +6 -1
  117. holmes/utils/llms.py +20 -0
  118. holmes/utils/stream.py +90 -0
  119. {holmesgpt-0.12.6.dist-info → holmesgpt-0.13.1.dist-info}/METADATA +47 -34
  120. {holmesgpt-0.12.6.dist-info → holmesgpt-0.13.1.dist-info}/RECORD +123 -91
  121. holmes/plugins/toolsets/bash/grep/__init__.py +0 -52
  122. holmes/utils/robusta.py +0 -9
  123. {holmesgpt-0.12.6.dist-info → holmesgpt-0.13.1.dist-info}/LICENSE.txt +0 -0
  124. {holmesgpt-0.12.6.dist-info → holmesgpt-0.13.1.dist-info}/WHEEL +0 -0
  125. {holmesgpt-0.12.6.dist-info → holmesgpt-0.13.1.dist-info}/entry_points.txt +0 -0
holmes/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # This is patched by github actions during release
2
- __version__ = "0.12.6"
2
+ __version__ = "0.13.1"
3
3
 
4
4
  # Re-export version functions from version module for backward compatibility
5
5
  from .version import (
@@ -1,4 +1,5 @@
1
- from typing import Optional
1
+ import logging
2
+ from typing import List, Optional
2
3
  import requests # type: ignore
3
4
  from functools import cache
4
5
  from pydantic import BaseModel, ConfigDict
@@ -13,6 +14,23 @@ class HolmesInfo(BaseModel):
13
14
  latest_version: Optional[str] = None
14
15
 
15
16
 
17
+ @cache
18
+ def fetch_robusta_models(account_id, token) -> Optional[List[str]]:
19
+ try:
20
+ session_request = {"session_token": token, "account_id": account_id}
21
+ resp = requests.post(
22
+ f"{ROBUSTA_API_ENDPOINT}/api/llm/models",
23
+ json=session_request,
24
+ timeout=10,
25
+ )
26
+ resp.raise_for_status()
27
+ response_json = resp.json()
28
+ return response_json.get("models")
29
+ except Exception:
30
+ logging.exception("Failed to fetch robusta models")
31
+ return None
32
+
33
+
16
34
  @cache
17
35
  def fetch_holmes_info() -> Optional[HolmesInfo]:
18
36
  try:
holmes/common/env_vars.py CHANGED
@@ -27,16 +27,19 @@ STORE_EMAIL = os.environ.get("STORE_EMAIL", "")
27
27
  STORE_PASSWORD = os.environ.get("STORE_PASSWORD", "")
28
28
  HOLMES_POST_PROCESSING_PROMPT = os.environ.get("HOLMES_POST_PROCESSING_PROMPT", "")
29
29
  ROBUSTA_AI = load_bool("ROBUSTA_AI", None)
30
+ LOAD_ALL_ROBUSTA_MODELS = load_bool("LOAD_ALL_ROBUSTA_MODELS", True)
30
31
  ROBUSTA_API_ENDPOINT = os.environ.get("ROBUSTA_API_ENDPOINT", "https://api.robusta.dev")
31
32
 
32
33
  LOG_PERFORMANCE = os.environ.get("LOG_PERFORMANCE", None)
33
34
 
34
35
 
35
36
  ENABLE_TELEMETRY = load_bool("ENABLE_TELEMETRY", False)
37
+ DEVELOPMENT_MODE = load_bool("DEVELOPMENT_MODE", False)
36
38
  SENTRY_DSN = os.environ.get("SENTRY_DSN", "")
37
39
  SENTRY_TRACES_SAMPLE_RATE = float(os.environ.get("SENTRY_TRACES_SAMPLE_RATE", "0.0"))
38
40
 
39
41
  THINKING = os.environ.get("THINKING", "")
42
+ REASONING_EFFORT = os.environ.get("REASONING_EFFORT", "").strip().lower()
40
43
  TEMPERATURE = float(os.environ.get("TEMPERATURE", "0.00000001"))
41
44
 
42
45
  STREAM_CHUNKS_PER_PARSE = int(
@@ -50,3 +53,17 @@ KUBERNETES_LOGS_TIMEOUT_SECONDS = int(
50
53
 
51
54
  TOOL_CALL_SAFEGUARDS_ENABLED = load_bool("TOOL_CALL_SAFEGUARDS_ENABLED", True)
52
55
  IS_OPENSHIFT = load_bool("IS_OPENSHIFT", False)
56
+
57
+ LLMS_WITH_STRICT_TOOL_CALLS = os.environ.get(
58
+ "LLMS_WITH_STRICT_TOOL_CALLS", "azure/gpt-4o, openai/*"
59
+ )
60
+ TOOL_SCHEMA_NO_PARAM_OBJECT_IF_NO_PARAMS = load_bool(
61
+ "TOOL_SCHEMA_NO_PARAM_OBJECT_IF_NO_PARAMS", False
62
+ )
63
+
64
+ MAX_OUTPUT_TOKEN_RESERVATION = int(
65
+ os.environ.get("MAX_OUTPUT_TOKEN_RESERVATION", 16384)
66
+ ) ## 16k
67
+
68
+ # When using the bash tool, setting BASH_TOOL_UNSAFE_ALLOW_ALL will skip any command validation and run any command requested by the LLM
69
+ BASH_TOOL_UNSAFE_ALLOW_ALL = load_bool("BASH_TOOL_UNSAFE_ALLOW_ALL", False)
holmes/config.py CHANGED
@@ -9,7 +9,15 @@ from typing import TYPE_CHECKING, Any, List, Optional, Union
9
9
  import yaml # type: ignore
10
10
  from pydantic import BaseModel, ConfigDict, FilePath, SecretStr
11
11
 
12
- from holmes.common.env_vars import ROBUSTA_AI, ROBUSTA_API_ENDPOINT, ROBUSTA_CONFIG_PATH
12
+
13
+ from holmes.clients.robusta_client import fetch_robusta_models
14
+ from holmes.core.llm import DefaultLLM
15
+ from holmes.common.env_vars import (
16
+ ROBUSTA_AI,
17
+ LOAD_ALL_ROBUSTA_MODELS,
18
+ ROBUSTA_API_ENDPOINT,
19
+ ROBUSTA_CONFIG_PATH,
20
+ )
13
21
  from holmes.core.tools_utils.tool_executor import ToolExecutor
14
22
  from holmes.core.toolset_manager import ToolsetManager
15
23
  from holmes.plugins.runbooks import (
@@ -22,7 +30,6 @@ from holmes.plugins.runbooks import (
22
30
  # Source plugin imports moved to their respective create methods to speed up startup
23
31
  if TYPE_CHECKING:
24
32
  from holmes.core.llm import LLM
25
- from holmes.core.supabase_dal import SupabaseDal
26
33
  from holmes.core.tool_calling_llm import IssueInvestigator, ToolCallingLLM
27
34
  from holmes.plugins.destinations.slack import SlackDestination
28
35
  from holmes.plugins.sources.github import GitHubSource
@@ -31,6 +38,7 @@ if TYPE_CHECKING:
31
38
  from holmes.plugins.sources.pagerduty import PagerDutySource
32
39
  from holmes.plugins.sources.prometheus.plugin import AlertManagerSource
33
40
 
41
+ from holmes.core.supabase_dal import SupabaseDal
34
42
  from holmes.core.config import config_path_dir
35
43
  from holmes.utils.definitions import RobustaConfig
36
44
  from holmes.utils.env import replace_env_vars_values
@@ -71,8 +79,11 @@ class Config(RobustaBaseConfig):
71
79
  api_key: Optional[SecretStr] = (
72
80
  None # if None, read from OPENAI_API_KEY or AZURE_OPENAI_ENDPOINT env var
73
81
  )
82
+ account_id: Optional[str] = None
83
+ session_token: Optional[SecretStr] = None
84
+
74
85
  model: Optional[str] = "gpt-4o"
75
- max_steps: int = 10
86
+ max_steps: int = 40
76
87
  cluster_name: Optional[str] = None
77
88
 
78
89
  alertmanager_url: Optional[str] = None
@@ -134,10 +145,51 @@ class Config(RobustaBaseConfig):
134
145
 
135
146
  def model_post_init(self, __context: Any) -> None:
136
147
  self._model_list = parse_models_file(MODEL_LIST_FILE_LOCATION)
137
- if self._should_load_robusta_ai():
138
- logging.info("Loading Robusta AI model")
148
+
149
+ if not self._should_load_robusta_ai():
150
+ return
151
+
152
+ self.configure_robusta_ai_model()
153
+
154
+ def configure_robusta_ai_model(self) -> None:
155
+ try:
156
+ if not self.cluster_name or not LOAD_ALL_ROBUSTA_MODELS:
157
+ self._load_default_robusta_config()
158
+ return
159
+
160
+ if not self.api_key:
161
+ dal = SupabaseDal(self.cluster_name)
162
+ self.load_robusta_api_key(dal)
163
+
164
+ if not self.account_id or not self.session_token:
165
+ self._load_default_robusta_config()
166
+ return
167
+
168
+ models = fetch_robusta_models(
169
+ self.account_id, self.session_token.get_secret_value()
170
+ )
171
+ if not models:
172
+ self._load_default_robusta_config()
173
+ return
174
+
175
+ for model in models:
176
+ logging.info(f"Loading Robusta AI model: {model}")
177
+ self._model_list[model] = {
178
+ "base_url": f"{ROBUSTA_API_ENDPOINT}/llm/{model}",
179
+ "is_robusta_model": True,
180
+ }
181
+
182
+ except Exception:
183
+ logging.exception("Failed to get all robusta models")
184
+ # fallback to default behavior
185
+ self._load_default_robusta_config()
186
+
187
+ def _load_default_robusta_config(self):
188
+ if self._should_load_robusta_ai() and self.api_key:
189
+ logging.info("Loading default Robusta AI model")
139
190
  self._model_list[ROBUSTA_AI_MODEL_NAME] = {
140
191
  "base_url": ROBUSTA_API_ENDPOINT,
192
+ "is_robusta_model": True,
141
193
  }
142
194
 
143
195
  def _should_load_robusta_ai(self) -> bool:
@@ -465,7 +517,7 @@ class Config(RobustaBaseConfig):
465
517
  return SlackDestination(self.slack_token.get_secret_value(), self.slack_channel)
466
518
 
467
519
  def _get_llm(self, model_key: Optional[str] = None, tracer=None) -> "LLM":
468
- api_key = self.api_key.get_secret_value() if self.api_key else None
520
+ api_key: Optional[str] = None
469
521
  model = self.model
470
522
  model_params = {}
471
523
  if self._model_list:
@@ -475,11 +527,12 @@ class Config(RobustaBaseConfig):
475
527
  if model_key
476
528
  else next(iter(self._model_list.values())).copy()
477
529
  )
478
- api_key = model_params.pop("api_key", api_key)
530
+ if model_params.get("is_robusta_model") and self.api_key:
531
+ api_key = self.api_key.get_secret_value()
532
+ else:
533
+ api_key = model_params.pop("api_key", api_key)
479
534
  model = model_params.pop("model", model)
480
535
 
481
- from holmes.core.llm import DefaultLLM
482
-
483
536
  return DefaultLLM(model, api_key, model_params, tracer) # type: ignore
484
537
 
485
538
  def get_models_list(self) -> List[str]:
@@ -488,6 +541,13 @@ class Config(RobustaBaseConfig):
488
541
 
489
542
  return json.dumps([self.model]) # type: ignore
490
543
 
544
+ def load_robusta_api_key(self, dal: SupabaseDal):
545
+ if ROBUSTA_AI:
546
+ account_id, token = dal.get_ai_credentials()
547
+ self.api_key = SecretStr(f"{account_id} {token}")
548
+ self.account_id = account_id
549
+ self.session_token = SecretStr(token)
550
+
491
551
 
492
552
  class TicketSource(BaseModel):
493
553
  config: Config
@@ -133,6 +133,7 @@ def build_issue_chat_messages(
133
133
  "issue": issue_chat_request.issue_type,
134
134
  "toolsets": ai.tool_executor.toolsets,
135
135
  "cluster_name": config.cluster_name,
136
+ "investigation_id": ai.investigation_id,
136
137
  },
137
138
  )
138
139
  messages = [
@@ -153,6 +154,7 @@ def build_issue_chat_messages(
153
154
  "issue": issue_chat_request.issue_type,
154
155
  "toolsets": ai.tool_executor.toolsets,
155
156
  "cluster_name": config.cluster_name,
157
+ "investigation_id": ai.investigation_id,
156
158
  }
157
159
  system_prompt_without_tools = load_and_render_prompt(
158
160
  template_path, template_context_without_tools
@@ -186,6 +188,7 @@ def build_issue_chat_messages(
186
188
  "issue": issue_chat_request.issue_type,
187
189
  "toolsets": ai.tool_executor.toolsets,
188
190
  "cluster_name": config.cluster_name,
191
+ "investigation_id": ai.investigation_id,
189
192
  }
190
193
  system_prompt_with_truncated_tools = load_and_render_prompt(
191
194
  template_path, truncated_template_context
@@ -227,6 +230,7 @@ def build_issue_chat_messages(
227
230
  "issue": issue_chat_request.issue_type,
228
231
  "toolsets": ai.tool_executor.toolsets,
229
232
  "cluster_name": config.cluster_name,
233
+ "investigation_id": ai.investigation_id,
230
234
  }
231
235
  system_prompt_without_tools = load_and_render_prompt(
232
236
  template_path, template_context_without_tools
@@ -250,6 +254,7 @@ def build_issue_chat_messages(
250
254
  "issue": issue_chat_request.issue_type,
251
255
  "toolsets": ai.tool_executor.toolsets,
252
256
  "cluster_name": config.cluster_name,
257
+ "investigation_id": ai.investigation_id,
253
258
  }
254
259
  system_prompt_with_truncated_tools = load_and_render_prompt(
255
260
  template_path, template_context
@@ -274,6 +279,7 @@ def add_or_update_system_prompt(
274
279
  context = {
275
280
  "toolsets": ai.tool_executor.toolsets,
276
281
  "cluster_name": config.cluster_name,
282
+ "investigation_id": ai.investigation_id,
277
283
  }
278
284
 
279
285
  system_prompt = load_and_render_prompt(template_path, context)
@@ -465,6 +471,7 @@ def build_workload_health_chat_messages(
465
471
  "resource": resource,
466
472
  "toolsets": ai.tool_executor.toolsets,
467
473
  "cluster_name": config.cluster_name,
474
+ "investigation_id": ai.investigation_id,
468
475
  },
469
476
  )
470
477
  messages = [
@@ -485,6 +492,7 @@ def build_workload_health_chat_messages(
485
492
  "resource": resource,
486
493
  "toolsets": ai.tool_executor.toolsets,
487
494
  "cluster_name": config.cluster_name,
495
+ "investigation_id": ai.investigation_id,
488
496
  }
489
497
  system_prompt_without_tools = load_and_render_prompt(
490
498
  template_path, template_context_without_tools
@@ -518,6 +526,7 @@ def build_workload_health_chat_messages(
518
526
  "resource": resource,
519
527
  "toolsets": ai.tool_executor.toolsets,
520
528
  "cluster_name": config.cluster_name,
529
+ "investigation_id": ai.investigation_id,
521
530
  }
522
531
  system_prompt_with_truncated_tools = load_and_render_prompt(
523
532
  template_path, truncated_template_context
@@ -559,6 +568,7 @@ def build_workload_health_chat_messages(
559
568
  "resource": resource,
560
569
  "toolsets": ai.tool_executor.toolsets,
561
570
  "cluster_name": config.cluster_name,
571
+ "investigation_id": ai.investigation_id,
562
572
  }
563
573
  system_prompt_without_tools = load_and_render_prompt(
564
574
  template_path, template_context_without_tools
@@ -582,6 +592,7 @@ def build_workload_health_chat_messages(
582
592
  "resource": resource,
583
593
  "toolsets": ai.tool_executor.toolsets,
584
594
  "cluster_name": config.cluster_name,
595
+ "investigation_id": ai.investigation_id,
585
596
  }
586
597
  system_prompt_with_truncated_tools = load_and_render_prompt(
587
598
  template_path, template_context
@@ -7,8 +7,9 @@ from holmes.core.investigation_structured_output import process_response_into_se
7
7
  from holmes.core.issue import Issue
8
8
  from holmes.core.models import InvestigateRequest, InvestigationResult
9
9
  from holmes.core.supabase_dal import SupabaseDal
10
+ from holmes.core.tracing import DummySpan, SpanType
10
11
  from holmes.utils.global_instructions import add_global_instructions_to_user_prompt
11
- from holmes.utils.robusta import load_robusta_api_key
12
+ from holmes.core.todo_manager import get_todo_manager
12
13
 
13
14
  from holmes.core.investigation_structured_output import (
14
15
  DEFAULT_SECTIONS,
@@ -24,8 +25,9 @@ def investigate_issues(
24
25
  dal: SupabaseDal,
25
26
  config: Config,
26
27
  model: Optional[str] = None,
28
+ trace_span=DummySpan(),
27
29
  ) -> InvestigationResult:
28
- load_robusta_api_key(dal=dal, config=config)
30
+ config.load_robusta_api_key(dal=dal)
29
31
  context = dal.get_issue_data(investigate_request.context.get("robusta_issue_id"))
30
32
 
31
33
  resource_instructions = dal.get_resource_instructions(
@@ -37,7 +39,12 @@ def investigate_issues(
37
39
  if context:
38
40
  raw_data["extra_context"] = context
39
41
 
42
+ # If config is not preinitilized
43
+ create_issue_investigator_span = trace_span.start_span(
44
+ "create_issue_investigator", SpanType.FUNCTION.value
45
+ )
40
46
  ai = config.create_issue_investigator(dal=dal, model=model)
47
+ create_issue_investigator_span.end()
41
48
 
42
49
  issue = Issue(
43
50
  id=context["id"] if context else "",
@@ -54,6 +61,7 @@ def investigate_issues(
54
61
  instructions=resource_instructions,
55
62
  global_instructions=global_instructions,
56
63
  sections=investigate_request.sections,
64
+ trace_span=trace_span,
57
65
  )
58
66
 
59
67
  (text_response, sections) = process_response_into_sections(investigation.result)
@@ -73,7 +81,7 @@ def get_investigation_context(
73
81
  config: Config,
74
82
  request_structured_output_from_llm: Optional[bool] = None,
75
83
  ):
76
- load_robusta_api_key(dal=dal, config=config)
84
+ config.load_robusta_api_key(dal=dal)
77
85
  ai = config.create_issue_investigator(dal=dal, model=investigate_request.model)
78
86
 
79
87
  raw_data = investigate_request.model_dump()
@@ -125,6 +133,9 @@ def get_investigation_context(
125
133
  else:
126
134
  logging.info("Structured output is disabled for this request")
127
135
 
136
+ todo_manager = get_todo_manager()
137
+ todo_context = todo_manager.format_tasks_for_prompt(ai.investigation_id)
138
+
128
139
  system_prompt = load_and_render_prompt(
129
140
  investigate_request.prompt_template,
130
141
  {
@@ -133,6 +144,8 @@ def get_investigation_context(
133
144
  "structured_output": request_structured_output_from_llm,
134
145
  "toolsets": ai.tool_executor.toolsets,
135
146
  "cluster_name": config.cluster_name,
147
+ "todo_list": todo_context,
148
+ "investigation_id": ai.investigation_id,
136
149
  },
137
150
  )
138
151
 
@@ -177,6 +177,18 @@ def pre_format_sections(response: Any) -> Any:
177
177
  # In that case it gets parsed once to get rid of the first level of marshalling
178
178
  with suppress(Exception):
179
179
  response = json.loads(response)
180
+
181
+ # Try to find any embedded code block with or without "json" label and parse it
182
+ # This has been seen a lot in newer bedrock models
183
+ # This is a more robust check for patterns like ```json\n{...}\n``` or ```\n{...}\n```
184
+ matches = re.findall(r"```(?:json)?\s*\n(.*?)\n```", response, re.DOTALL)
185
+ for block in matches:
186
+ with suppress(Exception):
187
+ parsed = json.loads(block)
188
+ if isinstance(parsed, dict):
189
+ logging.info("Extracted and parsed embedded JSON block successfully.")
190
+ return json.dumps(parsed)
191
+
180
192
  return response
181
193
 
182
194
 
holmes/core/llm.py CHANGED
@@ -11,6 +11,7 @@ from pydantic import BaseModel
11
11
  import litellm
12
12
  import os
13
13
  from holmes.common.env_vars import (
14
+ REASONING_EFFORT,
14
15
  THINKING,
15
16
  )
16
17
 
@@ -118,7 +119,9 @@ class DefaultLLM(LLM):
118
119
  "environment variable for proper functionality. For more information, refer to the documentation: "
119
120
  "https://docs.litellm.ai/docs/providers/watsonx#usage---models-in-deployment-spaces"
120
121
  )
121
- elif provider == "bedrock" and os.environ.get("AWS_PROFILE"):
122
+ elif provider == "bedrock" and (
123
+ os.environ.get("AWS_PROFILE") or os.environ.get("AWS_BEARER_TOKEN_BEDROCK")
124
+ ):
122
125
  model_requirements = {"keys_in_environment": True, "missing_keys": []}
123
126
  else:
124
127
  #
@@ -207,6 +210,8 @@ class DefaultLLM(LLM):
207
210
  stream: Optional[bool] = None,
208
211
  ) -> Union[ModelResponse, CustomStreamWrapper]:
209
212
  tools_args = {}
213
+ allowed_openai_params = None
214
+
210
215
  if tools and len(tools) > 0 and tool_choice == "auto":
211
216
  tools_args["tools"] = tools
212
217
  tools_args["tool_choice"] = tool_choice # type: ignore
@@ -217,6 +222,12 @@ class DefaultLLM(LLM):
217
222
  if self.args.get("thinking", None):
218
223
  litellm.modify_params = True
219
224
 
225
+ if REASONING_EFFORT:
226
+ self.args.setdefault("reasoning_effort", REASONING_EFFORT)
227
+ allowed_openai_params = [
228
+ "reasoning_effort"
229
+ ] # can be removed after next litelm version
230
+
220
231
  self.args.setdefault("temperature", temperature)
221
232
  # Get the litellm module to use (wrapped or unwrapped)
222
233
  litellm_to_use = self.tracer.wrap_llm(litellm) if self.tracer else litellm
@@ -227,6 +238,7 @@ class DefaultLLM(LLM):
227
238
  messages=messages,
228
239
  response_format=response_format,
229
240
  drop_params=drop_params,
241
+ allowed_openai_params=allowed_openai_params,
230
242
  stream=stream,
231
243
  **tools_args,
232
244
  **self.args,
holmes/core/models.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from holmes.core.investigation_structured_output import InputSectionsDataType
2
2
  from holmes.core.tool_calling_llm import ToolCallResult
3
3
  from typing import Optional, List, Dict, Any, Union
4
- from pydantic import BaseModel, model_validator
4
+ from pydantic import BaseModel, model_validator, Field
5
5
  from enum import Enum
6
6
 
7
7
 
@@ -89,6 +89,7 @@ class ConversationRequest(BaseModel):
89
89
  class ChatRequestBaseModel(BaseModel):
90
90
  conversation_history: Optional[list[dict]] = None
91
91
  model: Optional[str] = None
92
+ stream: bool = Field(default=False)
92
93
 
93
94
  # In our setup with litellm, the first message in conversation_history
94
95
  # should follow the structure [{"role": "system", "content": ...}],
@@ -150,6 +151,13 @@ class WorkloadHealthInvestigationResult(BaseModel):
150
151
  analysis: Optional[str] = None
151
152
  tools: Optional[List[ToolCallConversationResult]] = []
152
153
 
154
+ @model_validator(mode="before")
155
+ def check_analysis_and_result(cls, values):
156
+ if "result" in values and "analysis" not in values:
157
+ values["analysis"] = values["result"]
158
+ del values["result"]
159
+ return values
160
+
153
161
 
154
162
  class WorkloadHealthChatRequest(ChatRequestBaseModel):
155
163
  ask: str
@@ -1,33 +1,87 @@
1
1
  import re
2
+ from typing import Any, Optional
3
+
4
+ from holmes.common.env_vars import (
5
+ TOOL_SCHEMA_NO_PARAM_OBJECT_IF_NO_PARAMS,
6
+ LLMS_WITH_STRICT_TOOL_CALLS,
7
+ )
8
+ from holmes.utils.llms import model_matches_list
2
9
 
3
10
  # parses both simple types: "int", "array", "string"
4
11
  # but also arrays of those simpler types: "array[int]", "array[string]", etc.
5
12
  pattern = r"^(array\[(?P<inner_type>\w+)\])|(?P<simple_type>\w+)$"
6
13
 
14
+ LLMS_WITH_STRICT_TOOL_CALLS_LIST = [
15
+ llm.strip() for llm in LLMS_WITH_STRICT_TOOL_CALLS.split(",")
16
+ ]
17
+
7
18
 
8
- def type_to_open_ai_schema(type_value):
9
- match = re.match(pattern, type_value.strip())
19
+ def type_to_open_ai_schema(param_attributes: Any, strict_mode: bool) -> dict[str, Any]:
20
+ param_type = param_attributes.type.strip()
21
+ type_obj: Optional[dict[str, Any]] = None
10
22
 
11
- if not match:
12
- raise ValueError(f"Invalid type format: {type_value}")
23
+ if param_type == "object":
24
+ type_obj = {"type": "object"}
25
+ if strict_mode:
26
+ type_obj["additionalProperties"] = False
13
27
 
14
- if match.group("inner_type"):
15
- return {"type": "array", "items": {"type": match.group("inner_type")}}
28
+ # Use explicit properties if provided
29
+ if hasattr(param_attributes, "properties") and param_attributes.properties:
30
+ type_obj["properties"] = {
31
+ name: type_to_open_ai_schema(prop, strict_mode)
32
+ for name, prop in param_attributes.properties.items()
33
+ }
34
+ if strict_mode:
35
+ type_obj["required"] = list(param_attributes.properties.keys())
16
36
 
37
+ elif param_type == "array":
38
+ # Handle arrays with explicit item schemas
39
+ if hasattr(param_attributes, "items") and param_attributes.items:
40
+ items_schema = type_to_open_ai_schema(param_attributes.items, strict_mode)
41
+ type_obj = {"type": "array", "items": items_schema}
42
+ else:
43
+ # Fallback for arrays without explicit item schema
44
+ type_obj = {"type": "array", "items": {"type": "object"}}
45
+ if strict_mode:
46
+ type_obj["items"]["additionalProperties"] = False
17
47
  else:
18
- return {"type": match.group("simple_type")}
48
+ match = re.match(pattern, param_type)
49
+
50
+ if not match:
51
+ raise ValueError(f"Invalid type format: {param_type}")
52
+
53
+ if match.group("inner_type"):
54
+ inner_type = match.group("inner_type")
55
+ if inner_type == "object":
56
+ raise ValueError(
57
+ "object inner type must have schema. Use ToolParameter.items"
58
+ )
59
+ else:
60
+ type_obj = {"type": "array", "items": {"type": inner_type}}
61
+ else:
62
+ type_obj = {"type": match.group("simple_type")}
63
+
64
+ if strict_mode and type_obj and not param_attributes.required:
65
+ type_obj["type"] = [type_obj["type"], "null"]
66
+
67
+ return type_obj
19
68
 
20
69
 
21
70
  def format_tool_to_open_ai_standard(
22
- tool_name: str, tool_description: str, tool_parameters: dict
71
+ tool_name: str, tool_description: str, tool_parameters: dict, target_model: str
23
72
  ):
24
73
  tool_properties = {}
74
+
75
+ strict_mode = model_matches_list(target_model, LLMS_WITH_STRICT_TOOL_CALLS_LIST)
76
+
25
77
  for param_name, param_attributes in tool_parameters.items():
26
- tool_properties[param_name] = type_to_open_ai_schema(param_attributes.type)
78
+ tool_properties[param_name] = type_to_open_ai_schema(
79
+ param_attributes=param_attributes, strict_mode=strict_mode
80
+ )
27
81
  if param_attributes.description is not None:
28
82
  tool_properties[param_name]["description"] = param_attributes.description
29
83
 
30
- result = {
84
+ result: dict[str, Any] = {
31
85
  "type": "function",
32
86
  "function": {
33
87
  "name": tool_name,
@@ -37,15 +91,21 @@ def format_tool_to_open_ai_standard(
37
91
  "required": [
38
92
  param_name
39
93
  for param_name, param_attributes in tool_parameters.items()
40
- if param_attributes.required
94
+ if param_attributes.required or strict_mode
41
95
  ],
42
96
  "type": "object",
43
97
  },
44
98
  },
45
99
  }
46
100
 
101
+ if strict_mode and result["function"]:
102
+ result["function"]["strict"] = True
103
+ result["function"]["parameters"]["additionalProperties"] = False
104
+
47
105
  # gemini doesnt have parameters object if it is without params
48
- if tool_properties is None or tool_properties == {}:
106
+ if TOOL_SCHEMA_NO_PARAM_OBJECT_IF_NO_PARAMS and (
107
+ tool_properties is None or tool_properties == {}
108
+ ):
49
109
  result["function"].pop("parameters") # type: ignore
50
110
 
51
111
  return result
holmes/core/prompt.py CHANGED
@@ -25,11 +25,22 @@ def append_all_files_to_user_prompt(
25
25
  return user_prompt
26
26
 
27
27
 
28
+ def get_tasks_management_system_reminder() -> str:
29
+ return (
30
+ "\n\n<system-reminder>\nIMPORTANT: You have access to the TodoWrite tool. It creates a TodoList, in order to track progress. It's very important. You MUST use it:\n1. FIRST: Ask your self which sub problems you need to solve in order to answer the question."
31
+ "Do this, BEFORE any other tools\n2. "
32
+ "AFTER EVERY TOOL CALL: If required, update the TodoList\n3. "
33
+ "\n\nFAILURE TO UPDATE TodoList = INCOMPLETE INVESTIGATION\n\n"
34
+ "Example flow:\n- Think and divide to sub problems → create TodoList → Perform each task on the list → Update list → Verify your solution\n</system-reminder>"
35
+ )
36
+
37
+
28
38
  def build_initial_ask_messages(
29
39
  console: Console,
30
40
  initial_user_prompt: str,
31
41
  file_paths: Optional[List[Path]],
32
42
  tool_executor: Any, # ToolExecutor type
43
+ investigation_id: str,
33
44
  runbooks: Union[RunbookCatalog, Dict, None] = None,
34
45
  system_prompt_additions: Optional[str] = None,
35
46
  ) -> List[Dict]:
@@ -49,6 +60,7 @@ def build_initial_ask_messages(
49
60
  "toolsets": tool_executor.toolsets,
50
61
  "runbooks": runbooks or {},
51
62
  "system_prompt_additions": system_prompt_additions or "",
63
+ "investigation_id": investigation_id,
52
64
  }
53
65
  system_prompt_rendered = load_and_render_prompt(
54
66
  system_prompt_template, template_context
@@ -59,6 +71,7 @@ def build_initial_ask_messages(
59
71
  console, initial_user_prompt, file_paths
60
72
  )
61
73
 
74
+ user_prompt_with_files += get_tasks_management_system_reminder()
62
75
  messages = [
63
76
  {"role": "system", "content": system_prompt_rendered},
64
77
  {"role": "user", "content": user_prompt_with_files},
@@ -427,6 +427,9 @@ class SupabaseDal:
427
427
  return None
428
428
 
429
429
  def get_global_instructions_for_account(self) -> Optional[Instructions]:
430
+ if not self.enabled:
431
+ return None
432
+
430
433
  try:
431
434
  res = (
432
435
  self.client.table(RUNBOOKS_TABLE)