holmesgpt 0.12.3a1__py3-none-any.whl → 0.12.5__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 (53) hide show
  1. holmes/__init__.py +1 -1
  2. holmes/config.py +75 -33
  3. holmes/core/config.py +5 -0
  4. holmes/core/conversations.py +17 -2
  5. holmes/core/investigation.py +1 -0
  6. holmes/core/llm.py +1 -2
  7. holmes/core/prompt.py +29 -4
  8. holmes/core/supabase_dal.py +49 -13
  9. holmes/core/tool_calling_llm.py +26 -1
  10. holmes/core/tools.py +2 -1
  11. holmes/core/tools_utils/tool_executor.py +1 -0
  12. holmes/core/toolset_manager.py +10 -3
  13. holmes/core/tracing.py +78 -11
  14. holmes/interactive.py +110 -20
  15. holmes/main.py +13 -18
  16. holmes/plugins/destinations/slack/plugin.py +19 -9
  17. holmes/plugins/prompts/_ai_safety.jinja2 +43 -0
  18. holmes/plugins/prompts/_fetch_logs.jinja2 +11 -1
  19. holmes/plugins/prompts/_general_instructions.jinja2 +8 -37
  20. holmes/plugins/prompts/_permission_errors.jinja2 +6 -0
  21. holmes/plugins/prompts/_runbook_instructions.jinja2 +13 -5
  22. holmes/plugins/prompts/_toolsets_instructions.jinja2 +22 -14
  23. holmes/plugins/prompts/generic_ask.jinja2 +6 -0
  24. holmes/plugins/prompts/generic_ask_conversation.jinja2 +1 -0
  25. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +1 -0
  26. holmes/plugins/prompts/generic_investigation.jinja2 +1 -0
  27. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +2 -2
  28. holmes/plugins/runbooks/__init__.py +20 -4
  29. holmes/plugins/toolsets/__init__.py +7 -9
  30. holmes/plugins/toolsets/aks-node-health.yaml +0 -8
  31. holmes/plugins/toolsets/argocd.yaml +4 -1
  32. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +1 -1
  33. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +2 -0
  34. holmes/plugins/toolsets/confluence.yaml +1 -1
  35. holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +54 -4
  36. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +150 -6
  37. holmes/plugins/toolsets/kubernetes.yaml +13 -7
  38. holmes/plugins/toolsets/prometheus/prometheus.py +2 -6
  39. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +2 -2
  40. holmes/plugins/toolsets/runbook/runbook_fetcher.py +65 -6
  41. holmes/plugins/toolsets/service_discovery.py +1 -1
  42. holmes/plugins/toolsets/slab.yaml +1 -1
  43. holmes/utils/colors.py +7 -0
  44. holmes/utils/console/consts.py +5 -0
  45. holmes/utils/console/result.py +2 -1
  46. holmes/utils/keygen_utils.py +6 -0
  47. holmes/version.py +2 -2
  48. holmesgpt-0.12.5.dist-info/METADATA +258 -0
  49. {holmesgpt-0.12.3a1.dist-info → holmesgpt-0.12.5.dist-info}/RECORD +52 -47
  50. holmesgpt-0.12.3a1.dist-info/METADATA +0 -400
  51. {holmesgpt-0.12.3a1.dist-info → holmesgpt-0.12.5.dist-info}/LICENSE.txt +0 -0
  52. {holmesgpt-0.12.3a1.dist-info → holmesgpt-0.12.5.dist-info}/WHEEL +0 -0
  53. {holmesgpt-0.12.3a1.dist-info → holmesgpt-0.12.5.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.3-alpha.1"
2
+ __version__ = "0.12.5"
3
3
 
4
4
  # Re-export version functions from version module for backward compatibility
5
5
  from .version import (
holmes/config.py CHANGED
@@ -4,36 +4,40 @@ import os
4
4
  import os.path
5
5
  from enum import Enum
6
6
  from pathlib import Path
7
- from typing import Any, List, Optional, Union
7
+ from typing import TYPE_CHECKING, Any, List, Optional, Union
8
8
 
9
9
  import yaml # type: ignore
10
10
  from pydantic import BaseModel, ConfigDict, FilePath, SecretStr
11
11
 
12
12
  from holmes.common.env_vars import ROBUSTA_AI, ROBUSTA_API_ENDPOINT, ROBUSTA_CONFIG_PATH
13
- from holmes.core.llm import LLM, DefaultLLM
14
- from holmes.core.runbooks import RunbookManager
15
- from holmes.core.supabase_dal import SupabaseDal
16
- from holmes.core.tool_calling_llm import IssueInvestigator, ToolCallingLLM
17
13
  from holmes.core.tools_utils.tool_executor import ToolExecutor
18
14
  from holmes.core.toolset_manager import ToolsetManager
19
- from holmes.plugins.destinations.slack import SlackDestination
20
15
  from holmes.plugins.runbooks import (
21
16
  RunbookCatalog,
22
17
  load_builtin_runbooks,
23
18
  load_runbook_catalog,
24
19
  load_runbooks_from_file,
25
20
  )
26
- from holmes.plugins.sources.github import GitHubSource
27
- from holmes.plugins.sources.jira import JiraServiceManagementSource, JiraSource
28
- from holmes.plugins.sources.opsgenie import OpsGenieSource
29
- from holmes.plugins.sources.pagerduty import PagerDutySource
30
- from holmes.plugins.sources.prometheus.plugin import AlertManagerSource
21
+
22
+ # Source plugin imports moved to their respective create methods to speed up startup
23
+ if TYPE_CHECKING:
24
+ from holmes.core.llm import LLM
25
+ from holmes.core.supabase_dal import SupabaseDal
26
+ from holmes.core.tool_calling_llm import IssueInvestigator, ToolCallingLLM
27
+ from holmes.plugins.destinations.slack import SlackDestination
28
+ from holmes.plugins.sources.github import GitHubSource
29
+ from holmes.plugins.sources.jira import JiraServiceManagementSource, JiraSource
30
+ from holmes.plugins.sources.opsgenie import OpsGenieSource
31
+ from holmes.plugins.sources.pagerduty import PagerDutySource
32
+ from holmes.plugins.sources.prometheus.plugin import AlertManagerSource
33
+
34
+ from holmes.core.config import config_path_dir
31
35
  from holmes.utils.definitions import RobustaConfig
32
36
  from holmes.utils.env import replace_env_vars_values
33
37
  from holmes.utils.file_utils import load_yaml_file
34
38
  from holmes.utils.pydantic_utils import RobustaBaseConfig, load_model_from_file
35
39
 
36
- DEFAULT_CONFIG_LOCATION = os.path.expanduser("~/.holmes/config.yaml")
40
+ DEFAULT_CONFIG_LOCATION = os.path.join(config_path_dir, "config.yaml")
37
41
  MODEL_LIST_FILE_LOCATION = os.environ.get(
38
42
  "MODEL_LIST_FILE_LOCATION", "/etc/holmes/config/model_list.yaml"
39
43
  )
@@ -111,6 +115,7 @@ class Config(RobustaBaseConfig):
111
115
  should_try_robusta_ai: bool = False # if True, we will try to load the Robusta AI model, in cli we aren't trying to load it.
112
116
 
113
117
  toolsets: Optional[dict[str, dict[str, Any]]] = None
118
+ mcp_servers: Optional[dict[str, dict[str, Any]]] = None
114
119
 
115
120
  _server_tool_executor: Optional[ToolExecutor] = None
116
121
 
@@ -121,6 +126,7 @@ class Config(RobustaBaseConfig):
121
126
  if not self._toolset_manager:
122
127
  self._toolset_manager = ToolsetManager(
123
128
  toolsets=self.toolsets,
129
+ mcp_servers=self.mcp_servers,
124
130
  custom_toolsets=self.custom_toolsets,
125
131
  custom_toolsets_from_cli=self.custom_toolsets_from_cli,
126
132
  )
@@ -246,7 +252,7 @@ class Config(RobustaBaseConfig):
246
252
  return runbook_catalog
247
253
 
248
254
  def create_console_tool_executor(
249
- self, dal: Optional[SupabaseDal], refresh_status: bool = False
255
+ self, dal: Optional["SupabaseDal"], refresh_status: bool = False
250
256
  ) -> ToolExecutor:
251
257
  """
252
258
  Creates a ToolExecutor instance configured for CLI usage. This executor manages the available tools
@@ -262,7 +268,7 @@ class Config(RobustaBaseConfig):
262
268
  )
263
269
  return ToolExecutor(cli_toolsets)
264
270
 
265
- def create_tool_executor(self, dal: Optional[SupabaseDal]) -> ToolExecutor:
271
+ def create_tool_executor(self, dal: Optional["SupabaseDal"]) -> ToolExecutor:
266
272
  """
267
273
  Creates ToolExecutor for the server endpoints
268
274
  """
@@ -282,53 +288,73 @@ class Config(RobustaBaseConfig):
282
288
 
283
289
  def create_console_toolcalling_llm(
284
290
  self,
285
- dal: Optional[SupabaseDal] = None,
291
+ dal: Optional["SupabaseDal"] = None,
286
292
  refresh_toolsets: bool = False,
287
293
  tracer=None,
288
- ) -> ToolCallingLLM:
294
+ ) -> "ToolCallingLLM":
289
295
  tool_executor = self.create_console_tool_executor(dal, refresh_toolsets)
296
+ from holmes.core.tool_calling_llm import ToolCallingLLM
297
+
290
298
  return ToolCallingLLM(
291
299
  tool_executor, self.max_steps, self._get_llm(tracer=tracer)
292
300
  )
293
301
 
294
302
  def create_toolcalling_llm(
295
303
  self,
296
- dal: Optional[SupabaseDal] = None,
304
+ dal: Optional["SupabaseDal"] = None,
297
305
  model: Optional[str] = None,
298
306
  tracer=None,
299
- ) -> ToolCallingLLM:
307
+ ) -> "ToolCallingLLM":
300
308
  tool_executor = self.create_tool_executor(dal)
309
+ from holmes.core.tool_calling_llm import ToolCallingLLM
310
+
301
311
  return ToolCallingLLM(
302
312
  tool_executor, self.max_steps, self._get_llm(model, tracer)
303
313
  )
304
314
 
305
315
  def create_issue_investigator(
306
316
  self,
307
- dal: Optional[SupabaseDal] = None,
317
+ dal: Optional["SupabaseDal"] = None,
308
318
  model: Optional[str] = None,
309
319
  tracer=None,
310
- ) -> IssueInvestigator:
320
+ ) -> "IssueInvestigator":
311
321
  all_runbooks = load_builtin_runbooks()
312
322
  for runbook_path in self.custom_runbooks:
313
323
  all_runbooks.extend(load_runbooks_from_file(runbook_path))
314
324
 
325
+ from holmes.core.runbooks import RunbookManager
326
+
315
327
  runbook_manager = RunbookManager(all_runbooks)
316
328
  tool_executor = self.create_tool_executor(dal)
329
+ from holmes.core.tool_calling_llm import IssueInvestigator
330
+
317
331
  return IssueInvestigator(
318
- tool_executor, runbook_manager, self.max_steps, self._get_llm(model, tracer)
332
+ tool_executor=tool_executor,
333
+ runbook_manager=runbook_manager,
334
+ max_steps=self.max_steps,
335
+ llm=self._get_llm(model, tracer),
336
+ cluster_name=self.cluster_name,
319
337
  )
320
338
 
321
339
  def create_console_issue_investigator(
322
- self, dal: Optional[SupabaseDal] = None
323
- ) -> IssueInvestigator:
340
+ self, dal: Optional["SupabaseDal"] = None
341
+ ) -> "IssueInvestigator":
324
342
  all_runbooks = load_builtin_runbooks()
325
343
  for runbook_path in self.custom_runbooks:
326
344
  all_runbooks.extend(load_runbooks_from_file(runbook_path))
327
345
 
346
+ from holmes.core.runbooks import RunbookManager
347
+
328
348
  runbook_manager = RunbookManager(all_runbooks)
329
349
  tool_executor = self.create_console_tool_executor(dal=dal)
350
+ from holmes.core.tool_calling_llm import IssueInvestigator
351
+
330
352
  return IssueInvestigator(
331
- tool_executor, runbook_manager, self.max_steps, self._get_llm()
353
+ tool_executor=tool_executor,
354
+ runbook_manager=runbook_manager,
355
+ max_steps=self.max_steps,
356
+ llm=self._get_llm(),
357
+ cluster_name=self.cluster_name,
332
358
  )
333
359
 
334
360
  def validate_jira_config(self):
@@ -343,7 +369,9 @@ class Config(RobustaBaseConfig):
343
369
  if self.jira_api_key is None:
344
370
  raise ValueError("--jira-api-key must be specified")
345
371
 
346
- def create_jira_source(self) -> JiraSource:
372
+ def create_jira_source(self) -> "JiraSource":
373
+ from holmes.plugins.sources.jira import JiraSource
374
+
347
375
  self.validate_jira_config()
348
376
 
349
377
  return JiraSource(
@@ -353,7 +381,9 @@ class Config(RobustaBaseConfig):
353
381
  jql_query=self.jira_query, # type: ignore
354
382
  )
355
383
 
356
- def create_jira_service_management_source(self) -> JiraServiceManagementSource:
384
+ def create_jira_service_management_source(self) -> "JiraServiceManagementSource":
385
+ from holmes.plugins.sources.jira import JiraServiceManagementSource
386
+
357
387
  self.validate_jira_config()
358
388
 
359
389
  return JiraServiceManagementSource(
@@ -363,7 +393,9 @@ class Config(RobustaBaseConfig):
363
393
  jql_query=self.jira_query, # type: ignore
364
394
  )
365
395
 
366
- def create_github_source(self) -> GitHubSource:
396
+ def create_github_source(self) -> "GitHubSource":
397
+ from holmes.plugins.sources.github import GitHubSource
398
+
367
399
  if not self.github_url or not (
368
400
  self.github_url.startswith("http://")
369
401
  or self.github_url.startswith("https://")
@@ -384,7 +416,9 @@ class Config(RobustaBaseConfig):
384
416
  query=self.github_query,
385
417
  )
386
418
 
387
- def create_pagerduty_source(self) -> PagerDutySource:
419
+ def create_pagerduty_source(self) -> "PagerDutySource":
420
+ from holmes.plugins.sources.pagerduty import PagerDutySource
421
+
388
422
  if self.pagerduty_api_key is None:
389
423
  raise ValueError("--pagerduty-api-key must be specified")
390
424
 
@@ -394,7 +428,9 @@ class Config(RobustaBaseConfig):
394
428
  incident_key=self.pagerduty_incident_key,
395
429
  )
396
430
 
397
- def create_opsgenie_source(self) -> OpsGenieSource:
431
+ def create_opsgenie_source(self) -> "OpsGenieSource":
432
+ from holmes.plugins.sources.opsgenie import OpsGenieSource
433
+
398
434
  if self.opsgenie_api_key is None:
399
435
  raise ValueError("--opsgenie-api-key must be specified")
400
436
 
@@ -408,7 +444,9 @@ class Config(RobustaBaseConfig):
408
444
  ),
409
445
  )
410
446
 
411
- def create_alertmanager_source(self) -> AlertManagerSource:
447
+ def create_alertmanager_source(self) -> "AlertManagerSource":
448
+ from holmes.plugins.sources.prometheus.plugin import AlertManagerSource
449
+
412
450
  return AlertManagerSource(
413
451
  url=self.alertmanager_url, # type: ignore
414
452
  username=self.alertmanager_username,
@@ -417,14 +455,16 @@ class Config(RobustaBaseConfig):
417
455
  filepath=self.alertmanager_file,
418
456
  )
419
457
 
420
- def create_slack_destination(self):
458
+ def create_slack_destination(self) -> "SlackDestination":
459
+ from holmes.plugins.destinations.slack import SlackDestination
460
+
421
461
  if self.slack_token is None:
422
462
  raise ValueError("--slack-token must be specified")
423
463
  if self.slack_channel is None:
424
464
  raise ValueError("--slack-channel must be specified")
425
465
  return SlackDestination(self.slack_token.get_secret_value(), self.slack_channel)
426
466
 
427
- def _get_llm(self, model_key: Optional[str] = None, tracer=None) -> LLM:
467
+ def _get_llm(self, model_key: Optional[str] = None, tracer=None) -> "LLM":
428
468
  api_key = self.api_key.get_secret_value() if self.api_key else None
429
469
  model = self.model
430
470
  model_params = {}
@@ -438,6 +478,8 @@ class Config(RobustaBaseConfig):
438
478
  api_key = model_params.pop("api_key", api_key)
439
479
  model = model_params.pop("model", model)
440
480
 
481
+ from holmes.core.llm import DefaultLLM
482
+
441
483
  return DefaultLLM(model, api_key, model_params, tracer) # type: ignore
442
484
 
443
485
  def get_models_list(self) -> List[str]:
@@ -450,7 +492,7 @@ class Config(RobustaBaseConfig):
450
492
  class TicketSource(BaseModel):
451
493
  config: Config
452
494
  output_instructions: list[str]
453
- source: Union[JiraServiceManagementSource, PagerDutySource]
495
+ source: Union["JiraServiceManagementSource", "PagerDutySource"]
454
496
 
455
497
  model_config = ConfigDict(arbitrary_types_allowed=True)
456
498
 
holmes/core/config.py ADDED
@@ -0,0 +1,5 @@
1
+ import os
2
+
3
+ config_path_dir: str = os.environ.get(
4
+ "HOLMES_CONFIGPATH_DIR", os.path.expanduser("~/.holmes")
5
+ )
@@ -2,6 +2,7 @@ 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
7
  ToolCallConversationResult,
7
8
  IssueChatRequest,
@@ -60,6 +61,7 @@ def truncate_tool_messages(conversation_history: list, tool_size: int) -> None:
60
61
  def build_issue_chat_messages(
61
62
  issue_chat_request: IssueChatRequest,
62
63
  ai: ToolCallingLLM,
64
+ config: Config,
63
65
  global_instructions: Optional[Instructions] = None,
64
66
  ):
65
67
  """
@@ -130,6 +132,7 @@ def build_issue_chat_messages(
130
132
  "tools_called_for_investigation": tools_for_investigation,
131
133
  "issue": issue_chat_request.issue_type,
132
134
  "toolsets": ai.tool_executor.toolsets,
135
+ "cluster_name": config.cluster_name,
133
136
  },
134
137
  )
135
138
  messages = [
@@ -149,6 +152,7 @@ def build_issue_chat_messages(
149
152
  "tools_called_for_investigation": None,
150
153
  "issue": issue_chat_request.issue_type,
151
154
  "toolsets": ai.tool_executor.toolsets,
155
+ "cluster_name": config.cluster_name,
152
156
  }
153
157
  system_prompt_without_tools = load_and_render_prompt(
154
158
  template_path, template_context_without_tools
@@ -181,6 +185,7 @@ def build_issue_chat_messages(
181
185
  "tools_called_for_investigation": truncated_investigation_result_tool_calls,
182
186
  "issue": issue_chat_request.issue_type,
183
187
  "toolsets": ai.tool_executor.toolsets,
188
+ "cluster_name": config.cluster_name,
184
189
  }
185
190
  system_prompt_with_truncated_tools = load_and_render_prompt(
186
191
  template_path, truncated_template_context
@@ -221,6 +226,7 @@ def build_issue_chat_messages(
221
226
  "tools_called_for_investigation": None,
222
227
  "issue": issue_chat_request.issue_type,
223
228
  "toolsets": ai.tool_executor.toolsets,
229
+ "cluster_name": config.cluster_name,
224
230
  }
225
231
  system_prompt_without_tools = load_and_render_prompt(
226
232
  template_path, template_context_without_tools
@@ -243,6 +249,7 @@ def build_issue_chat_messages(
243
249
  "tools_called_for_investigation": truncated_investigation_result_tool_calls,
244
250
  "issue": issue_chat_request.issue_type,
245
251
  "toolsets": ai.tool_executor.toolsets,
252
+ "cluster_name": config.cluster_name,
246
253
  }
247
254
  system_prompt_with_truncated_tools = load_and_render_prompt(
248
255
  template_path, template_context
@@ -255,7 +262,7 @@ def build_issue_chat_messages(
255
262
 
256
263
 
257
264
  def add_or_update_system_prompt(
258
- conversation_history: List[Dict[str, str]], ai: ToolCallingLLM
265
+ conversation_history: List[Dict[str, str]], ai: ToolCallingLLM, config: Config
259
266
  ):
260
267
  """Either add the system prompt or replace an existing system prompt.
261
268
  As a 'defensive' measure, this code will only replace an existing system prompt if it is the
@@ -266,6 +273,7 @@ def add_or_update_system_prompt(
266
273
  template_path = "builtin://generic_ask_conversation.jinja2"
267
274
  context = {
268
275
  "toolsets": ai.tool_executor.toolsets,
276
+ "cluster_name": config.cluster_name,
269
277
  }
270
278
 
271
279
  system_prompt = load_and_render_prompt(template_path, context)
@@ -293,6 +301,7 @@ def build_chat_messages(
293
301
  ask: str,
294
302
  conversation_history: Optional[List[Dict[str, str]]],
295
303
  ai: ToolCallingLLM,
304
+ config: Config,
296
305
  global_instructions: Optional[Instructions] = None,
297
306
  ) -> List[dict]:
298
307
  """
@@ -349,7 +358,7 @@ def build_chat_messages(
349
358
  conversation_history = conversation_history.copy()
350
359
 
351
360
  conversation_history = add_or_update_system_prompt(
352
- conversation_history=conversation_history, ai=ai
361
+ conversation_history=conversation_history, ai=ai, config=config
353
362
  )
354
363
 
355
364
  ask = add_global_instructions_to_user_prompt(ask, global_instructions)
@@ -382,6 +391,7 @@ def build_chat_messages(
382
391
  def build_workload_health_chat_messages(
383
392
  workload_health_chat_request: WorkloadHealthChatRequest,
384
393
  ai: ToolCallingLLM,
394
+ config: Config,
385
395
  global_instructions: Optional[Instructions] = None,
386
396
  ):
387
397
  """
@@ -454,6 +464,7 @@ def build_workload_health_chat_messages(
454
464
  "tools_called_for_workload": tools_for_workload,
455
465
  "resource": resource,
456
466
  "toolsets": ai.tool_executor.toolsets,
467
+ "cluster_name": config.cluster_name,
457
468
  },
458
469
  )
459
470
  messages = [
@@ -473,6 +484,7 @@ def build_workload_health_chat_messages(
473
484
  "tools_called_for_workload": None,
474
485
  "resource": resource,
475
486
  "toolsets": ai.tool_executor.toolsets,
487
+ "cluster_name": config.cluster_name,
476
488
  }
477
489
  system_prompt_without_tools = load_and_render_prompt(
478
490
  template_path, template_context_without_tools
@@ -505,6 +517,7 @@ def build_workload_health_chat_messages(
505
517
  "tools_called_for_workload": truncated_workload_result_tool_calls,
506
518
  "resource": resource,
507
519
  "toolsets": ai.tool_executor.toolsets,
520
+ "cluster_name": config.cluster_name,
508
521
  }
509
522
  system_prompt_with_truncated_tools = load_and_render_prompt(
510
523
  template_path, truncated_template_context
@@ -545,6 +558,7 @@ def build_workload_health_chat_messages(
545
558
  "tools_called_for_workload": None,
546
559
  "resource": resource,
547
560
  "toolsets": ai.tool_executor.toolsets,
561
+ "cluster_name": config.cluster_name,
548
562
  }
549
563
  system_prompt_without_tools = load_and_render_prompt(
550
564
  template_path, template_context_without_tools
@@ -567,6 +581,7 @@ def build_workload_health_chat_messages(
567
581
  "tools_called_for_workload": truncated_workload_result_tool_calls,
568
582
  "resource": resource,
569
583
  "toolsets": ai.tool_executor.toolsets,
584
+ "cluster_name": config.cluster_name,
570
585
  }
571
586
  system_prompt_with_truncated_tools = load_and_render_prompt(
572
587
  template_path, template_context
@@ -132,6 +132,7 @@ def get_investigation_context(
132
132
  "sections": sections,
133
133
  "structured_output": request_structured_output_from_llm,
134
134
  "toolsets": ai.tool_executor.toolsets,
135
+ "cluster_name": config.cluster_name,
135
136
  },
136
137
  )
137
138
 
holmes/core/llm.py CHANGED
@@ -12,7 +12,6 @@ import litellm
12
12
  import os
13
13
  from holmes.common.env_vars import (
14
14
  THINKING,
15
- TEMPERATURE,
16
15
  )
17
16
 
18
17
 
@@ -218,6 +217,7 @@ class DefaultLLM(LLM):
218
217
  if self.args.get("thinking", None):
219
218
  litellm.modify_params = True
220
219
 
220
+ self.args.setdefault("temperature", temperature)
221
221
  # Get the litellm module to use (wrapped or unwrapped)
222
222
  litellm_to_use = self.tracer.wrap_llm(litellm) if self.tracer else litellm
223
223
 
@@ -225,7 +225,6 @@ class DefaultLLM(LLM):
225
225
  model=self.model,
226
226
  api_key=self.api_key,
227
227
  messages=messages,
228
- temperature=temperature or self.args.pop("temperature", TEMPERATURE),
229
228
  response_format=response_format,
230
229
  drop_params=drop_params,
231
230
  stream=stream,
holmes/core/prompt.py CHANGED
@@ -1,11 +1,13 @@
1
1
  from rich.console import Console
2
- from typing import Optional, List, Dict
2
+ from typing import Optional, List, Dict, Any, Union
3
3
  from pathlib import Path
4
+ from holmes.plugins.prompts import load_and_render_prompt
5
+ from holmes.plugins.runbooks import RunbookCatalog
4
6
 
5
7
 
6
8
  def append_file_to_user_prompt(user_prompt: str, file_path: Path) -> str:
7
9
  with file_path.open("r") as f:
8
- user_prompt += f"\n\n<attached-file path='{file_path.absolute()}>'\n{f.read()}\n</attached-file>"
10
+ user_prompt += f"\n\n<attached-file path='{file_path.absolute()}'>\n{f.read()}\n</attached-file>"
9
11
 
10
12
  return user_prompt
11
13
 
@@ -25,11 +27,34 @@ def append_all_files_to_user_prompt(
25
27
 
26
28
  def build_initial_ask_messages(
27
29
  console: Console,
28
- system_prompt_rendered: str,
29
30
  initial_user_prompt: str,
30
31
  file_paths: Optional[List[Path]],
32
+ tool_executor: Any, # ToolExecutor type
33
+ runbooks: Union[RunbookCatalog, Dict, None] = None,
34
+ system_prompt_additions: Optional[str] = None,
31
35
  ) -> List[Dict]:
32
- """Build the initial messages for the AI call."""
36
+ """Build the initial messages for the AI call.
37
+
38
+ Args:
39
+ console: Rich console for output
40
+ initial_user_prompt: The user's prompt
41
+ file_paths: Optional list of files to include
42
+ tool_executor: The tool executor with available toolsets
43
+ runbooks: Optional runbook catalog
44
+ system_prompt_additions: Optional additional system prompt content
45
+ """
46
+ # Load and render system prompt internally
47
+ system_prompt_template = "builtin://generic_ask.jinja2"
48
+ template_context = {
49
+ "toolsets": tool_executor.toolsets,
50
+ "runbooks": runbooks or {},
51
+ "system_prompt_additions": system_prompt_additions or "",
52
+ }
53
+ system_prompt_rendered = load_and_render_prompt(
54
+ system_prompt_template, template_context
55
+ )
56
+
57
+ # Append files to user prompt
33
58
  user_prompt_with_files = append_all_files_to_user_prompt(
34
59
  console, initial_user_prompt, file_paths
35
60
  )
@@ -7,6 +7,7 @@ import threading
7
7
  from datetime import datetime, timedelta
8
8
  from typing import Dict, List, Optional, Tuple
9
9
  from uuid import uuid4
10
+ import gzip
10
11
 
11
12
  import yaml # type: ignore
12
13
  from cachetools import TTLCache # type: ignore
@@ -291,6 +292,52 @@ class SupabaseDal:
291
292
 
292
293
  return changes_data
293
294
 
295
+ def unzip_evidence_file(self, data):
296
+ try:
297
+ evidence_list = json.loads(data.get("data", "[]"))
298
+ if not evidence_list:
299
+ return data
300
+
301
+ evidence = evidence_list[0]
302
+ raw_data = evidence.get("data")
303
+
304
+ if evidence.get("type") != "gz" or not raw_data:
305
+ return data
306
+
307
+ # Strip "b'...'" or 'b"..."' markers if present
308
+ if raw_data.startswith("b'") and raw_data.endswith("'"):
309
+ raw_data = raw_data[2:-1]
310
+ elif raw_data.startswith('b"') and raw_data.endswith('"'):
311
+ raw_data = raw_data[2:-1]
312
+
313
+ gz_bytes = base64.b64decode(raw_data)
314
+ decompressed = gzip.decompress(gz_bytes).decode("utf-8")
315
+
316
+ evidence["data"] = decompressed
317
+ data["data"] = json.dumps([evidence])
318
+ return data
319
+
320
+ except Exception:
321
+ logging.exception(f"Unknown issue unzipping gz finding: {data}")
322
+ return data
323
+
324
+ def extract_relevant_issues(self, evidence):
325
+ enrichment_blacklist = {"text_file", "graph", "ai_analysis", "holmes"}
326
+ data = [
327
+ enrich
328
+ for enrich in evidence.data
329
+ if enrich.get("enrichment_type") not in enrichment_blacklist
330
+ ]
331
+
332
+ unzipped_files = [
333
+ self.unzip_evidence_file(enrich)
334
+ for enrich in evidence.data
335
+ if enrich.get("enrichment_type") == "text_file"
336
+ ]
337
+
338
+ data.extend(unzipped_files)
339
+ return data
340
+
294
341
  def get_issue_data(self, issue_id: Optional[str]) -> Optional[Dict]:
295
342
  # TODO this could be done in a single atomic SELECT, but there is no
296
343
  # foreign key relation between Issues and Evidence.
@@ -320,12 +367,7 @@ class SupabaseDal:
320
367
  .filter("issue_id", "eq", issue_id)
321
368
  .execute()
322
369
  )
323
- enrichment_blacklist = {"text_file", "graph", "ai_analysis", "holmes"}
324
- data = [
325
- enrich
326
- for enrich in evidence.data
327
- if enrich.get("enrichment_type") not in enrichment_blacklist
328
- ]
370
+ data = self.extract_relevant_issues(evidence)
329
371
 
330
372
  issue_data["evidence"] = data
331
373
 
@@ -470,13 +512,7 @@ class SupabaseDal:
470
512
  .execute()
471
513
  )
472
514
 
473
- enrichment_blacklist = {"text_file", "graph", "ai_analysis", "holmes"}
474
- data = [
475
- evidence.get("data")
476
- for evidence in res.data
477
- if evidence.get("enrichment_type") not in enrichment_blacklist
478
- ]
479
- return data
515
+ return self.extract_relevant_issues(res)
480
516
 
481
517
  except Exception:
482
518
  logging.exception("failed to fetch workload issues data", exc_info=True)
@@ -15,7 +15,11 @@ from pydantic import BaseModel
15
15
  from pydantic_core import from_json
16
16
  from rich.console import Console
17
17
 
18
- from holmes.common.env_vars import ROBUSTA_API_ENDPOINT, STREAM_CHUNKS_PER_PARSE
18
+ from holmes.common.env_vars import (
19
+ ROBUSTA_API_ENDPOINT,
20
+ STREAM_CHUNKS_PER_PARSE,
21
+ TEMPERATURE,
22
+ )
19
23
  from holmes.core.investigation_structured_output import (
20
24
  DEFAULT_SECTIONS,
21
25
  REQUEST_STRUCTURED_OUTPUT_FROM_LLM,
@@ -40,6 +44,7 @@ from holmes.utils.global_instructions import (
40
44
  from holmes.utils.tags import format_tags_in_string, parse_messages_tags
41
45
  from holmes.core.tools_utils.tool_executor import ToolExecutor
42
46
  from holmes.core.tracing import DummySpan
47
+ from holmes.utils.colors import AI_COLOR
43
48
 
44
49
 
45
50
  def format_tool_result_data(tool_result: StructuredToolResult) -> str:
@@ -285,6 +290,7 @@ class ToolCallingLLM:
285
290
  messages=parse_messages_tags(messages),
286
291
  tools=tools,
287
292
  tool_choice=tool_choice,
293
+ temperature=TEMPERATURE,
288
294
  response_format=response_format,
289
295
  drop_params=True,
290
296
  )
@@ -328,6 +334,15 @@ class ToolCallingLLM:
328
334
 
329
335
  tools_to_call = getattr(response_message, "tool_calls", None)
330
336
  text_response = response_message.content
337
+
338
+ if (
339
+ hasattr(response_message, "reasoning_content")
340
+ and response_message.reasoning_content
341
+ ):
342
+ logging.debug(
343
+ f"[bold {AI_COLOR}]AI (reasoning) 🤔:[/bold {AI_COLOR}] {response_message.reasoning_content}\n"
344
+ )
345
+
331
346
  if not tools_to_call:
332
347
  # For chatty models post process and summarize the result
333
348
  # this only works for calls where user prompt is explicitly passed through
@@ -357,6 +372,11 @@ class ToolCallingLLM:
357
372
  messages=messages,
358
373
  )
359
374
 
375
+ if text_response and text_response.strip():
376
+ logging.info(f"[bold {AI_COLOR}]AI:[/bold {AI_COLOR}] {text_response}")
377
+ logging.info(
378
+ f"The AI requested [bold]{len(tools_to_call) if tools_to_call else 0}[/bold] tool call(s)."
379
+ )
360
380
  perf_timing.measure("pre-tool-calls")
361
381
  with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor:
362
382
  futures = []
@@ -610,6 +630,7 @@ class ToolCallingLLM:
610
630
  "messages": parse_messages_tags(messages), # type: ignore
611
631
  "tools": tools,
612
632
  "tool_choice": tool_choice,
633
+ "temperature": TEMPERATURE,
613
634
  "response_format": response_format,
614
635
  "stream": True,
615
636
  "drop_param": True,
@@ -634,6 +655,7 @@ class ToolCallingLLM:
634
655
  messages=parse_messages_tags(messages), # type: ignore
635
656
  tools=tools,
636
657
  tool_choice=tool_choice,
658
+ temperature=TEMPERATURE,
637
659
  response_format=response_format,
638
660
  stream=False,
639
661
  drop_params=True,
@@ -745,9 +767,11 @@ class IssueInvestigator(ToolCallingLLM):
745
767
  runbook_manager: RunbookManager,
746
768
  max_steps: int,
747
769
  llm: LLM,
770
+ cluster_name: Optional[str],
748
771
  ):
749
772
  super().__init__(tool_executor, max_steps, llm)
750
773
  self.runbook_manager = runbook_manager
774
+ self.cluster_name = cluster_name
751
775
 
752
776
  def investigate(
753
777
  self,
@@ -806,6 +830,7 @@ class IssueInvestigator(ToolCallingLLM):
806
830
  "sections": sections,
807
831
  "structured_output": request_structured_output_from_llm,
808
832
  "toolsets": self.tool_executor.toolsets,
833
+ "cluster_name": self.cluster_name,
809
834
  },
810
835
  )
811
836