holmesgpt 0.13.2__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 (188) hide show
  1. holmes/__init__.py +3 -5
  2. holmes/clients/robusta_client.py +20 -6
  3. holmes/common/env_vars.py +58 -3
  4. holmes/common/openshift.py +1 -1
  5. holmes/config.py +123 -148
  6. holmes/core/conversations.py +71 -15
  7. holmes/core/feedback.py +191 -0
  8. holmes/core/investigation.py +31 -39
  9. holmes/core/investigation_structured_output.py +3 -3
  10. holmes/core/issue.py +1 -1
  11. holmes/core/llm.py +508 -88
  12. holmes/core/models.py +108 -4
  13. holmes/core/openai_formatting.py +14 -1
  14. holmes/core/prompt.py +48 -3
  15. holmes/core/runbooks.py +1 -0
  16. holmes/core/safeguards.py +8 -6
  17. holmes/core/supabase_dal.py +295 -100
  18. holmes/core/tool_calling_llm.py +489 -428
  19. holmes/core/tools.py +325 -56
  20. holmes/core/tools_utils/token_counting.py +21 -0
  21. holmes/core/tools_utils/tool_context_window_limiter.py +40 -0
  22. holmes/core/tools_utils/tool_executor.py +0 -13
  23. holmes/core/tools_utils/toolset_utils.py +1 -0
  24. holmes/core/toolset_manager.py +191 -5
  25. holmes/core/tracing.py +19 -3
  26. holmes/core/transformers/__init__.py +23 -0
  27. holmes/core/transformers/base.py +63 -0
  28. holmes/core/transformers/llm_summarize.py +175 -0
  29. holmes/core/transformers/registry.py +123 -0
  30. holmes/core/transformers/transformer.py +32 -0
  31. holmes/core/truncation/compaction.py +94 -0
  32. holmes/core/truncation/dal_truncation_utils.py +23 -0
  33. holmes/core/truncation/input_context_window_limiter.py +219 -0
  34. holmes/interactive.py +228 -31
  35. holmes/main.py +23 -40
  36. holmes/plugins/interfaces.py +2 -1
  37. holmes/plugins/prompts/__init__.py +2 -1
  38. holmes/plugins/prompts/_fetch_logs.jinja2 +31 -6
  39. holmes/plugins/prompts/_general_instructions.jinja2 +1 -2
  40. holmes/plugins/prompts/_runbook_instructions.jinja2 +24 -12
  41. holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
  42. holmes/plugins/prompts/conversation_history_compaction.jinja2 +89 -0
  43. holmes/plugins/prompts/generic_ask.jinja2 +0 -4
  44. holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -1
  45. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -1
  46. holmes/plugins/prompts/generic_investigation.jinja2 +0 -1
  47. holmes/plugins/prompts/investigation_procedure.jinja2 +50 -1
  48. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -1
  49. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -1
  50. holmes/plugins/runbooks/__init__.py +145 -17
  51. holmes/plugins/runbooks/catalog.json +2 -0
  52. holmes/plugins/sources/github/__init__.py +4 -2
  53. holmes/plugins/sources/prometheus/models.py +1 -0
  54. holmes/plugins/toolsets/__init__.py +44 -27
  55. holmes/plugins/toolsets/aks-node-health.yaml +46 -0
  56. holmes/plugins/toolsets/aks.yaml +64 -0
  57. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +38 -47
  58. holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +3 -2
  59. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
  60. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +3 -2
  61. holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +3 -1
  62. holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +3 -1
  63. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +12 -13
  64. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +15 -12
  65. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +15 -12
  66. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +11 -11
  67. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +11 -9
  68. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +15 -12
  69. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +15 -15
  70. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +11 -8
  71. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +11 -8
  72. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +11 -8
  73. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +11 -8
  74. holmes/plugins/toolsets/azure_sql/utils.py +0 -32
  75. holmes/plugins/toolsets/bash/argocd/__init__.py +3 -3
  76. holmes/plugins/toolsets/bash/aws/__init__.py +4 -4
  77. holmes/plugins/toolsets/bash/azure/__init__.py +4 -4
  78. holmes/plugins/toolsets/bash/bash_toolset.py +11 -15
  79. holmes/plugins/toolsets/bash/common/bash.py +23 -13
  80. holmes/plugins/toolsets/bash/common/bash_command.py +1 -1
  81. holmes/plugins/toolsets/bash/common/stringify.py +1 -1
  82. holmes/plugins/toolsets/bash/kubectl/__init__.py +2 -1
  83. holmes/plugins/toolsets/bash/kubectl/constants.py +0 -1
  84. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +3 -4
  85. holmes/plugins/toolsets/bash/parse_command.py +12 -13
  86. holmes/plugins/toolsets/cilium.yaml +284 -0
  87. holmes/plugins/toolsets/connectivity_check.py +124 -0
  88. holmes/plugins/toolsets/coralogix/api.py +132 -119
  89. holmes/plugins/toolsets/coralogix/coralogix.jinja2 +14 -0
  90. holmes/plugins/toolsets/coralogix/toolset_coralogix.py +219 -0
  91. holmes/plugins/toolsets/coralogix/utils.py +15 -79
  92. holmes/plugins/toolsets/datadog/datadog_api.py +525 -26
  93. holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +55 -11
  94. holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +3 -3
  95. holmes/plugins/toolsets/datadog/datadog_models.py +59 -0
  96. holmes/plugins/toolsets/datadog/datadog_url_utils.py +213 -0
  97. holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +165 -28
  98. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +417 -241
  99. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +234 -214
  100. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +167 -79
  101. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +374 -363
  102. holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
  103. holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
  104. holmes/plugins/toolsets/elasticsearch/opensearch_ppl_query_docs.jinja2 +1616 -0
  105. holmes/plugins/toolsets/elasticsearch/opensearch_query_assist.py +78 -0
  106. holmes/plugins/toolsets/elasticsearch/opensearch_query_assist_instructions.jinja2 +223 -0
  107. holmes/plugins/toolsets/git.py +54 -50
  108. holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
  109. holmes/plugins/toolsets/grafana/common.py +13 -29
  110. holmes/plugins/toolsets/grafana/grafana_tempo_api.py +455 -0
  111. holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +25 -0
  112. holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +191 -0
  113. holmes/plugins/toolsets/grafana/loki_api.py +4 -0
  114. holmes/plugins/toolsets/grafana/toolset_grafana.py +293 -89
  115. holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +49 -0
  116. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
  117. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +820 -292
  118. holmes/plugins/toolsets/grafana/trace_parser.py +4 -3
  119. holmes/plugins/toolsets/internet/internet.py +15 -16
  120. holmes/plugins/toolsets/internet/notion.py +9 -11
  121. holmes/plugins/toolsets/investigator/core_investigation.py +44 -36
  122. holmes/plugins/toolsets/investigator/model.py +3 -1
  123. holmes/plugins/toolsets/json_filter_mixin.py +134 -0
  124. holmes/plugins/toolsets/kafka.py +36 -42
  125. holmes/plugins/toolsets/kubernetes.yaml +317 -113
  126. holmes/plugins/toolsets/kubernetes_logs.py +9 -9
  127. holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
  128. holmes/plugins/toolsets/logging_utils/logging_api.py +94 -8
  129. holmes/plugins/toolsets/mcp/toolset_mcp.py +218 -64
  130. holmes/plugins/toolsets/newrelic/new_relic_api.py +165 -0
  131. holmes/plugins/toolsets/newrelic/newrelic.jinja2 +65 -0
  132. holmes/plugins/toolsets/newrelic/newrelic.py +320 -0
  133. holmes/plugins/toolsets/openshift.yaml +283 -0
  134. holmes/plugins/toolsets/prometheus/prometheus.py +1202 -421
  135. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +54 -5
  136. holmes/plugins/toolsets/prometheus/utils.py +28 -0
  137. holmes/plugins/toolsets/rabbitmq/api.py +23 -4
  138. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +13 -14
  139. holmes/plugins/toolsets/robusta/robusta.py +239 -68
  140. holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +26 -9
  141. holmes/plugins/toolsets/runbook/runbook_fetcher.py +157 -27
  142. holmes/plugins/toolsets/service_discovery.py +1 -1
  143. holmes/plugins/toolsets/servicenow_tables/instructions.jinja2 +83 -0
  144. holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +426 -0
  145. holmes/plugins/toolsets/utils.py +88 -0
  146. holmes/utils/config_utils.py +91 -0
  147. holmes/utils/connection_utils.py +31 -0
  148. holmes/utils/console/result.py +10 -0
  149. holmes/utils/default_toolset_installation_guide.jinja2 +1 -22
  150. holmes/utils/env.py +7 -0
  151. holmes/utils/file_utils.py +2 -1
  152. holmes/utils/global_instructions.py +60 -11
  153. holmes/utils/holmes_status.py +6 -4
  154. holmes/utils/holmes_sync_toolsets.py +0 -2
  155. holmes/utils/krr_utils.py +188 -0
  156. holmes/utils/log.py +15 -0
  157. holmes/utils/markdown_utils.py +2 -3
  158. holmes/utils/memory_limit.py +58 -0
  159. holmes/utils/sentry_helper.py +64 -0
  160. holmes/utils/stream.py +69 -8
  161. holmes/utils/tags.py +4 -3
  162. holmes/version.py +37 -15
  163. holmesgpt-0.18.4.dist-info/LICENSE +178 -0
  164. {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +35 -31
  165. holmesgpt-0.18.4.dist-info/RECORD +258 -0
  166. holmes/core/performance_timing.py +0 -72
  167. holmes/plugins/toolsets/aws.yaml +0 -80
  168. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -112
  169. holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
  170. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -739
  171. holmes/plugins/toolsets/grafana/grafana_api.py +0 -42
  172. holmes/plugins/toolsets/grafana/tempo_api.py +0 -124
  173. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +0 -110
  174. holmes/plugins/toolsets/newrelic.py +0 -231
  175. holmes/plugins/toolsets/opensearch/opensearch.py +0 -257
  176. holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
  177. holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -218
  178. holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
  179. holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
  180. holmes/plugins/toolsets/servicenow/install.md +0 -37
  181. holmes/plugins/toolsets/servicenow/instructions.jinja2 +0 -3
  182. holmes/plugins/toolsets/servicenow/servicenow.py +0 -219
  183. holmes/utils/keygen_utils.py +0 -6
  184. holmesgpt-0.13.2.dist-info/LICENSE.txt +0 -21
  185. holmesgpt-0.13.2.dist-info/RECORD +0 -234
  186. /holmes/plugins/toolsets/{opensearch → newrelic}/__init__.py +0 -0
  187. {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
  188. {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/entry_points.txt +0 -0
@@ -4,15 +4,17 @@ import sentry_sdk
4
4
 
5
5
  from holmes.config import Config
6
6
  from holmes.core.models import (
7
- ToolCallConversationResult,
8
7
  IssueChatRequest,
8
+ ToolCallConversationResult,
9
9
  WorkloadHealthChatRequest,
10
10
  )
11
- from holmes.plugins.prompts import load_and_render_prompt
11
+ from holmes.core.prompt import generate_user_prompt
12
12
  from holmes.core.tool_calling_llm import ToolCallingLLM
13
+ from holmes.plugins.prompts import load_and_render_prompt
14
+ from holmes.plugins.runbooks import RunbookCatalog
13
15
  from holmes.utils.global_instructions import (
14
16
  Instructions,
15
- add_global_instructions_to_user_prompt,
17
+ generate_runbooks_args,
16
18
  )
17
19
 
18
20
  DEFAULT_TOOL_SIZE = 10000
@@ -26,7 +28,8 @@ def calculate_tool_size(
26
28
  return DEFAULT_TOOL_SIZE
27
29
 
28
30
  context_window = ai.llm.get_context_window_size()
29
- message_size_without_tools = ai.llm.count_tokens_for_message(messages_without_tools)
31
+ tokens = ai.llm.count_tokens(messages_without_tools)
32
+ message_size_without_tools = tokens.total_tokens
30
33
  maximum_output_token = ai.llm.get_maximum_output_token()
31
34
 
32
35
  tool_size = min(
@@ -63,6 +66,7 @@ def build_issue_chat_messages(
63
66
  ai: ToolCallingLLM,
64
67
  config: Config,
65
68
  global_instructions: Optional[Instructions] = None,
69
+ runbooks: Optional[RunbookCatalog] = None,
66
70
  ):
67
71
  """
68
72
  This function generates a list of messages for issue conversation and ensures that the message sequence adheres to the model's context window limitations
@@ -119,8 +123,13 @@ def build_issue_chat_messages(
119
123
  tools_for_investigation = issue_chat_request.investigation_result.tools
120
124
 
121
125
  if not conversation_history or len(conversation_history) == 0:
122
- user_prompt = add_global_instructions_to_user_prompt(
123
- user_prompt, global_instructions
126
+ runbooks_ctx = generate_runbooks_args(
127
+ runbook_catalog=runbooks,
128
+ global_instructions=global_instructions,
129
+ )
130
+ user_prompt = generate_user_prompt(
131
+ user_prompt,
132
+ runbooks_ctx,
124
133
  )
125
134
 
126
135
  number_of_tools_for_investigation = len(tools_for_investigation) # type: ignore
@@ -133,6 +142,7 @@ def build_issue_chat_messages(
133
142
  "issue": issue_chat_request.issue_type,
134
143
  "toolsets": ai.tool_executor.toolsets,
135
144
  "cluster_name": config.cluster_name,
145
+ "runbooks_enabled": True if runbooks else False,
136
146
  },
137
147
  )
138
148
  messages = [
@@ -153,6 +163,7 @@ def build_issue_chat_messages(
153
163
  "issue": issue_chat_request.issue_type,
154
164
  "toolsets": ai.tool_executor.toolsets,
155
165
  "cluster_name": config.cluster_name,
166
+ "runbooks_enabled": True if runbooks else False,
156
167
  }
157
168
  system_prompt_without_tools = load_and_render_prompt(
158
169
  template_path, template_context_without_tools
@@ -186,6 +197,7 @@ def build_issue_chat_messages(
186
197
  "issue": issue_chat_request.issue_type,
187
198
  "toolsets": ai.tool_executor.toolsets,
188
199
  "cluster_name": config.cluster_name,
200
+ "runbooks_enabled": True if runbooks else False,
189
201
  }
190
202
  system_prompt_with_truncated_tools = load_and_render_prompt(
191
203
  template_path, truncated_template_context
@@ -201,8 +213,13 @@ def build_issue_chat_messages(
201
213
  },
202
214
  ]
203
215
 
204
- user_prompt = add_global_instructions_to_user_prompt(
205
- user_prompt, global_instructions
216
+ runbooks_ctx = generate_runbooks_args(
217
+ runbook_catalog=runbooks,
218
+ global_instructions=global_instructions,
219
+ )
220
+ user_prompt = generate_user_prompt(
221
+ user_prompt,
222
+ runbooks_ctx,
206
223
  )
207
224
 
208
225
  conversation_history.append(
@@ -227,6 +244,7 @@ def build_issue_chat_messages(
227
244
  "issue": issue_chat_request.issue_type,
228
245
  "toolsets": ai.tool_executor.toolsets,
229
246
  "cluster_name": config.cluster_name,
247
+ "runbooks_enabled": True if runbooks else False,
230
248
  }
231
249
  system_prompt_without_tools = load_and_render_prompt(
232
250
  template_path, template_context_without_tools
@@ -250,6 +268,7 @@ def build_issue_chat_messages(
250
268
  "issue": issue_chat_request.issue_type,
251
269
  "toolsets": ai.tool_executor.toolsets,
252
270
  "cluster_name": config.cluster_name,
271
+ "runbooks_enabled": True if runbooks else False,
253
272
  }
254
273
  system_prompt_with_truncated_tools = load_and_render_prompt(
255
274
  template_path, template_context
@@ -262,7 +281,11 @@ def build_issue_chat_messages(
262
281
 
263
282
 
264
283
  def add_or_update_system_prompt(
265
- conversation_history: List[Dict[str, str]], ai: ToolCallingLLM, config: Config
284
+ conversation_history: List[Dict[str, str]],
285
+ ai: ToolCallingLLM,
286
+ config: Config,
287
+ additional_system_prompt: Optional[str] = None,
288
+ runbooks: Optional[RunbookCatalog] = None,
266
289
  ):
267
290
  """Either add the system prompt or replace an existing system prompt.
268
291
  As a 'defensive' measure, this code will only replace an existing system prompt if it is the
@@ -274,9 +297,12 @@ def add_or_update_system_prompt(
274
297
  context = {
275
298
  "toolsets": ai.tool_executor.toolsets,
276
299
  "cluster_name": config.cluster_name,
300
+ "runbooks_enabled": True if runbooks else False,
277
301
  }
278
302
 
279
303
  system_prompt = load_and_render_prompt(template_path, context)
304
+ if additional_system_prompt:
305
+ system_prompt = system_prompt + "\n" + additional_system_prompt
280
306
 
281
307
  if not conversation_history or len(conversation_history) == 0:
282
308
  conversation_history.append({"role": "system", "content": system_prompt})
@@ -303,6 +329,8 @@ def build_chat_messages(
303
329
  ai: ToolCallingLLM,
304
330
  config: Config,
305
331
  global_instructions: Optional[Instructions] = None,
332
+ additional_system_prompt: Optional[str] = None,
333
+ runbooks: Optional[RunbookCatalog] = None,
306
334
  ) -> List[dict]:
307
335
  """
308
336
  This function generates a list of messages for general chat conversation and ensures that the message sequence adheres to the model's context window limitations
@@ -358,10 +386,21 @@ def build_chat_messages(
358
386
  conversation_history = conversation_history.copy()
359
387
 
360
388
  conversation_history = add_or_update_system_prompt(
361
- conversation_history=conversation_history, ai=ai, config=config
389
+ conversation_history=conversation_history,
390
+ ai=ai,
391
+ config=config,
392
+ additional_system_prompt=additional_system_prompt,
393
+ runbooks=runbooks,
362
394
  )
363
395
 
364
- ask = add_global_instructions_to_user_prompt(ask, global_instructions)
396
+ runbooks_ctx = generate_runbooks_args(
397
+ runbook_catalog=runbooks,
398
+ global_instructions=global_instructions,
399
+ )
400
+ ask = generate_user_prompt(
401
+ ask,
402
+ runbooks_ctx,
403
+ )
365
404
 
366
405
  conversation_history.append( # type: ignore
367
406
  {
@@ -369,6 +408,7 @@ def build_chat_messages(
369
408
  "content": ask,
370
409
  },
371
410
  )
411
+
372
412
  number_of_tools = len(
373
413
  [message for message in conversation_history if message.get("role") == "tool"] # type: ignore
374
414
  )
@@ -393,6 +433,7 @@ def build_workload_health_chat_messages(
393
433
  ai: ToolCallingLLM,
394
434
  config: Config,
395
435
  global_instructions: Optional[Instructions] = None,
436
+ runbooks: Optional[RunbookCatalog] = None,
396
437
  ):
397
438
  """
398
439
  This function generates a list of messages for workload health conversation and ensures that the message sequence adheres to the model's context window limitations
@@ -451,8 +492,13 @@ def build_workload_health_chat_messages(
451
492
  resource = workload_health_chat_request.resource
452
493
 
453
494
  if not conversation_history or len(conversation_history) == 0:
454
- user_prompt = add_global_instructions_to_user_prompt(
455
- user_prompt, global_instructions
495
+ runbooks_ctx = generate_runbooks_args(
496
+ runbook_catalog=runbooks,
497
+ global_instructions=global_instructions,
498
+ )
499
+ user_prompt = generate_user_prompt(
500
+ user_prompt,
501
+ runbooks_ctx,
456
502
  )
457
503
 
458
504
  number_of_tools_for_workload = len(tools_for_workload) # type: ignore
@@ -465,6 +511,7 @@ def build_workload_health_chat_messages(
465
511
  "resource": resource,
466
512
  "toolsets": ai.tool_executor.toolsets,
467
513
  "cluster_name": config.cluster_name,
514
+ "runbooks_enabled": True if runbooks else False,
468
515
  },
469
516
  )
470
517
  messages = [
@@ -485,6 +532,7 @@ def build_workload_health_chat_messages(
485
532
  "resource": resource,
486
533
  "toolsets": ai.tool_executor.toolsets,
487
534
  "cluster_name": config.cluster_name,
535
+ "runbooks_enabled": True if runbooks else False,
488
536
  }
489
537
  system_prompt_without_tools = load_and_render_prompt(
490
538
  template_path, template_context_without_tools
@@ -518,6 +566,7 @@ def build_workload_health_chat_messages(
518
566
  "resource": resource,
519
567
  "toolsets": ai.tool_executor.toolsets,
520
568
  "cluster_name": config.cluster_name,
569
+ "runbooks_enabled": True if runbooks else False,
521
570
  }
522
571
  system_prompt_with_truncated_tools = load_and_render_prompt(
523
572
  template_path, truncated_template_context
@@ -533,8 +582,13 @@ def build_workload_health_chat_messages(
533
582
  },
534
583
  ]
535
584
 
536
- user_prompt = add_global_instructions_to_user_prompt(
537
- user_prompt, global_instructions
585
+ runbooks_ctx = generate_runbooks_args(
586
+ runbook_catalog=runbooks,
587
+ global_instructions=global_instructions,
588
+ )
589
+ user_prompt = generate_user_prompt(
590
+ user_prompt,
591
+ runbooks_ctx,
538
592
  )
539
593
 
540
594
  conversation_history.append(
@@ -559,6 +613,7 @@ def build_workload_health_chat_messages(
559
613
  "resource": resource,
560
614
  "toolsets": ai.tool_executor.toolsets,
561
615
  "cluster_name": config.cluster_name,
616
+ "runbooks_enabled": True if runbooks else False,
562
617
  }
563
618
  system_prompt_without_tools = load_and_render_prompt(
564
619
  template_path, template_context_without_tools
@@ -582,6 +637,7 @@ def build_workload_health_chat_messages(
582
637
  "resource": resource,
583
638
  "toolsets": ai.tool_executor.toolsets,
584
639
  "cluster_name": config.cluster_name,
640
+ "runbooks_enabled": True if runbooks else False,
585
641
  }
586
642
  system_prompt_with_truncated_tools = load_and_render_prompt(
587
643
  template_path, template_context
@@ -0,0 +1,191 @@
1
+ import os
2
+ from abc import ABC, abstractmethod
3
+ from typing import Callable, Optional
4
+
5
+ from .llm import LLM
6
+
7
+ DEFAULT_PRIVACY_NOTICE_BANNER = "Your feedback will be used to improve Holmesgpt's performance. Please avoid sharing sensitive personal information. By continuing, you consent to this data usage."
8
+ PRIVACY_NOTICE_BANNER = os.environ.get(
9
+ "PRIVACY_NOTICE_BANNER", DEFAULT_PRIVACY_NOTICE_BANNER
10
+ )
11
+
12
+
13
+ class FeedbackInfoBase(ABC):
14
+ """Abstract base class for all feedback-related classes that must implement to_dict()."""
15
+
16
+ @abstractmethod
17
+ def to_dict(self) -> dict:
18
+ """Convert to dictionary representation. Must be implemented by all subclasses."""
19
+ pass
20
+
21
+
22
+ class FeedbackLLM(FeedbackInfoBase):
23
+ """Class to represent a LLM in the feedback."""
24
+
25
+ def __init__(self, model: str, max_context_size: int):
26
+ self.model = model
27
+ self.max_context_size = max_context_size
28
+
29
+ def update_from_llm(self, llm: LLM):
30
+ self.model = llm.model
31
+ self.max_context_size = llm.get_context_window_size()
32
+
33
+ def to_dict(self) -> dict:
34
+ """Convert to dictionary representation."""
35
+ return self.__dict__
36
+
37
+
38
+ # TODO: extend the FeedbackLLMResponse to include each tool call results details used for evaluate the overall response.
39
+ # Currenlty tool call details in plan:
40
+ # - toolcall parameter and success/failure, toolcall truncation size
41
+ # - Holmes plan (todo list)
42
+ # - Holmes intermediate output
43
+ class FeedbackLLMResponse(FeedbackInfoBase):
44
+ """Class to represent a LLM response in the feedback"""
45
+
46
+ def __init__(self, user_ask: str, response: str):
47
+ self.user_ask = user_ask
48
+ self.response = response
49
+
50
+ def to_dict(self) -> dict:
51
+ """Convert to dictionary representation."""
52
+ return self.__dict__
53
+
54
+
55
+ class FeedbackMetadata(FeedbackInfoBase):
56
+ """Class to store feedback metadata."""
57
+
58
+ def __init__(self):
59
+ # In iteration mode, there can be multiple ask and response pairs.
60
+ self.llm_responses = []
61
+ self.llm = FeedbackLLM("", 0)
62
+
63
+ def add_llm_response(self, user_ask: str, response: str) -> None:
64
+ """Add a LLM response to the metadata."""
65
+ llm_response = FeedbackLLMResponse(user_ask, response)
66
+ self.llm_responses.append(llm_response)
67
+
68
+ def update_llm(self, llm: LLM) -> None:
69
+ """Update the LLM information in the metadata."""
70
+ self.llm.update_from_llm(llm)
71
+
72
+ def to_dict(self) -> dict:
73
+ """Convert to dictionary representation."""
74
+ return {
75
+ "llm_responses": [resp.to_dict() for resp in self.llm_responses],
76
+ "llm": self.llm.to_dict(),
77
+ }
78
+
79
+
80
+ class UserFeedback(FeedbackInfoBase):
81
+ """Class to store user rate and comment to the AI response."""
82
+
83
+ def __init__(self, is_positive: bool, comment: Optional[str]):
84
+ self.is_positive = is_positive
85
+ self.comment = comment
86
+
87
+ @property
88
+ def rating_text(self) -> str:
89
+ """Return human-readable rating text."""
90
+ return "useful" if self.is_positive else "not useful"
91
+
92
+ @property
93
+ def rating_emoji(self) -> str:
94
+ """Return emoji representation of the rating."""
95
+ return "👍" if self.is_positive else "👎"
96
+
97
+ def __str__(self) -> str:
98
+ """Return string representation of the feedback."""
99
+ if self.comment:
100
+ return f"Rating: {self.rating_text}. Comment: {self.comment}"
101
+ else:
102
+ return f"Rating: {self.rating_text}. No additional comment."
103
+
104
+ def to_dict(self) -> dict:
105
+ """Convert to dictionary representation."""
106
+ return {
107
+ "is_positive": self.is_positive,
108
+ "comment": self.comment,
109
+ }
110
+
111
+
112
+ class Feedback(FeedbackInfoBase):
113
+ """Class to store overall feedback data used to evaluate the AI response."""
114
+
115
+ def __init__(self):
116
+ self.metadata = FeedbackMetadata()
117
+ self.user_feedback: Optional[UserFeedback] = None
118
+
119
+ def set_user_feedback(self, user_feedback: UserFeedback) -> None:
120
+ """Set the user feedback."""
121
+ self.user_feedback = user_feedback
122
+
123
+ def to_dict(self) -> dict:
124
+ """Convert to dictionary representation."""
125
+ return {
126
+ "metadata": self.metadata.to_dict(),
127
+ "user_feedback": self.user_feedback.to_dict()
128
+ if self.user_feedback
129
+ else None,
130
+ }
131
+
132
+
133
+ FeedbackCallback = Callable[[Feedback], None]
134
+
135
+
136
+ def feedback_callback_example(feedback: Feedback) -> None:
137
+ """
138
+ Example implementation of a feedback callback function.
139
+
140
+ This function demonstrates how to process feedback data using to_dict() methods
141
+ and could be used for:
142
+ - Logging feedback to files or databases
143
+ - Sending feedback to analytics services
144
+ - Training data collection
145
+ - User satisfaction monitoring
146
+
147
+ Args:
148
+ feedback: Feedback object containing user feedback and metadata
149
+ """
150
+ print("\n=== Feedback Received ===")
151
+
152
+ # Convert entire feedback to dict first - this is the main data structure
153
+ feedback_dict = feedback.to_dict()
154
+ print(f"Complete feedback dictionary keys: {list(feedback_dict.keys())}")
155
+
156
+ # How to check user feedback using to_dict()
157
+ print("\n1. Checking User Feedback:")
158
+ user_feedback_dict = (
159
+ feedback.user_feedback.to_dict() if feedback.user_feedback else None
160
+ )
161
+ if user_feedback_dict:
162
+ print(f" User feedback dict: {user_feedback_dict}")
163
+ print(f" Is positive: {user_feedback_dict['is_positive']}")
164
+ print(f" Comment: {user_feedback_dict['comment'] or 'None'}")
165
+ # You can also access properties through the object:
166
+ print(f" Rating emoji: {feedback.user_feedback.rating_emoji}") # type: ignore
167
+ print(f" Rating text: {feedback.user_feedback.rating_text}") # type: ignore
168
+ else:
169
+ print(" No user feedback provided (user_feedback is None)")
170
+
171
+ # How to check LLM information using to_dict()
172
+ print("\n2. Checking LLM Information:")
173
+ metadata_dict = feedback.metadata.to_dict()
174
+ llm_dict = metadata_dict["llm"]
175
+ print(f" LLM dict: {llm_dict}")
176
+ print(f" Model: {llm_dict['model']}")
177
+ print(f" Max context size: {llm_dict['max_context_size']}")
178
+
179
+ # How to check ask and response pairs using to_dict()
180
+ print("\n3. Checking Ask and Response History:")
181
+ llm_responses_dict = metadata_dict["llm_responses"]
182
+ print(f" Number of exchanges: {len(llm_responses_dict)}")
183
+
184
+ for i, response_dict in enumerate(llm_responses_dict, 1):
185
+ print(f" Exchange {i} dict: {list(response_dict.keys())}")
186
+ user_ask = response_dict["user_ask"]
187
+ ai_response = response_dict["response"]
188
+ print(f" User ask: {user_ask}")
189
+ print(f" AI response: {ai_response}")
190
+
191
+ print("=== End Feedback ===\n")
@@ -1,22 +1,22 @@
1
1
  import logging
2
2
  from typing import Optional
3
3
 
4
- from holmes.common.env_vars import HOLMES_POST_PROCESSING_PROMPT
5
4
  from holmes.config import Config
6
- from holmes.core.investigation_structured_output import process_response_into_sections
7
- from holmes.core.issue import Issue
8
- from holmes.core.models import InvestigateRequest, InvestigationResult
9
- from holmes.core.supabase_dal import SupabaseDal
10
- from holmes.core.tracing import DummySpan, SpanType
11
- from holmes.utils.global_instructions import add_global_instructions_to_user_prompt
12
-
13
5
  from holmes.core.investigation_structured_output import (
14
6
  DEFAULT_SECTIONS,
15
7
  REQUEST_STRUCTURED_OUTPUT_FROM_LLM,
16
8
  get_output_format_for_investigation,
9
+ process_response_into_sections,
17
10
  )
18
-
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
19
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
20
20
 
21
21
 
22
22
  def investigate_issues(
@@ -25,13 +25,10 @@ def investigate_issues(
25
25
  config: Config,
26
26
  model: Optional[str] = None,
27
27
  trace_span=DummySpan(),
28
+ runbooks: Optional[RunbookCatalog] = None,
28
29
  ) -> InvestigationResult:
29
- config.load_robusta_api_key(dal=dal)
30
30
  context = dal.get_issue_data(investigate_request.context.get("robusta_issue_id"))
31
31
 
32
- resource_instructions = dal.get_resource_instructions(
33
- "alert", investigate_request.context.get("issue_type")
34
- )
35
32
  global_instructions = dal.get_global_instructions_for_account()
36
33
 
37
34
  raw_data = investigate_request.model_dump()
@@ -56,21 +53,25 @@ def investigate_issues(
56
53
  investigation = ai.investigate(
57
54
  issue,
58
55
  prompt=investigate_request.prompt_template,
59
- post_processing_prompt=HOLMES_POST_PROCESSING_PROMPT,
60
- instructions=resource_instructions,
61
56
  global_instructions=global_instructions,
62
57
  sections=investigate_request.sections,
63
58
  trace_span=trace_span,
59
+ runbooks=runbooks,
64
60
  )
65
61
 
66
62
  (text_response, sections) = process_response_into_sections(investigation.result)
67
63
 
64
+ if sections is None:
65
+ sentry_helper.capture_sections_none(content=investigation.result)
66
+
68
67
  logging.debug(f"text response: {text_response}")
69
68
  return InvestigationResult(
70
69
  analysis=text_response,
71
70
  sections=sections,
72
71
  tool_calls=investigation.tool_calls or [],
72
+ num_llm_calls=investigation.num_llm_calls,
73
73
  instructions=investigation.instructions,
74
+ metadata=investigation.metadata,
74
75
  )
75
76
 
76
77
 
@@ -80,7 +81,6 @@ def get_investigation_context(
80
81
  config: Config,
81
82
  request_structured_output_from_llm: Optional[bool] = None,
82
83
  ):
83
- config.load_robusta_api_key(dal=dal)
84
84
  ai = config.create_issue_investigator(dal=dal, model=investigate_request.model)
85
85
 
86
86
  raw_data = investigate_request.model_dump()
@@ -96,18 +96,7 @@ def get_investigation_context(
96
96
  raw=raw_data,
97
97
  )
98
98
 
99
- runbooks = ai.runbook_manager.get_instructions_for_issue(issue)
100
-
101
- instructions = dal.get_resource_instructions(
102
- "alert", investigate_request.context.get("issue_type")
103
- )
104
- if instructions is not None and instructions.instructions:
105
- runbooks.extend(instructions.instructions)
106
- if instructions is not None and len(instructions.documents) > 0:
107
- docPrompts = []
108
- for document in instructions.documents:
109
- docPrompts.append(f"* fetch information from this URL: {document.url}\n")
110
- runbooks.extend(docPrompts)
99
+ issue_instructions = ai.runbook_manager.get_instructions_for_issue(issue)
111
100
 
112
101
  # This section is about setting vars to request the LLM to return structured output.
113
102
  # It does not mean that Holmes will not return structured sections for investigation as it is
@@ -132,6 +121,7 @@ def get_investigation_context(
132
121
  else:
133
122
  logging.info("Structured output is disabled for this request")
134
123
 
124
+ runbook_catalog = config.get_runbook_catalog()
135
125
  system_prompt = load_and_render_prompt(
136
126
  investigate_request.prompt_template,
137
127
  {
@@ -140,21 +130,23 @@ def get_investigation_context(
140
130
  "structured_output": request_structured_output_from_llm,
141
131
  "toolsets": ai.tool_executor.toolsets,
142
132
  "cluster_name": config.cluster_name,
133
+ "runbooks_enabled": True if runbook_catalog else False,
143
134
  },
144
135
  )
145
-
146
- user_prompt = ""
147
- if runbooks:
148
- for runbook_str in runbooks:
149
- user_prompt += f"* {runbook_str}\n"
150
-
151
- user_prompt = f'My instructions to check \n"""{user_prompt}"""'
136
+ base_user = ""
152
137
 
153
138
  global_instructions = dal.get_global_instructions_for_account()
154
- user_prompt = add_global_instructions_to_user_prompt(
155
- user_prompt, global_instructions
139
+ runbooks_ctx = generate_runbooks_args(
140
+ runbook_catalog=runbook_catalog,
141
+ global_instructions=global_instructions,
142
+ issue_instructions=issue_instructions,
156
143
  )
157
144
 
158
- user_prompt = f"{user_prompt}\n This is context from the issue {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
+ )
159
151
 
160
- return ai, system_prompt, user_prompt, response_format, sections, runbooks
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):