holmesgpt 0.11.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 (183) hide show
  1. holmes/.git_archival.json +7 -0
  2. holmes/__init__.py +76 -0
  3. holmes/__init__.py.bak +76 -0
  4. holmes/clients/robusta_client.py +24 -0
  5. holmes/common/env_vars.py +47 -0
  6. holmes/config.py +526 -0
  7. holmes/core/__init__.py +0 -0
  8. holmes/core/conversations.py +578 -0
  9. holmes/core/investigation.py +152 -0
  10. holmes/core/investigation_structured_output.py +264 -0
  11. holmes/core/issue.py +54 -0
  12. holmes/core/llm.py +250 -0
  13. holmes/core/models.py +157 -0
  14. holmes/core/openai_formatting.py +51 -0
  15. holmes/core/performance_timing.py +72 -0
  16. holmes/core/prompt.py +42 -0
  17. holmes/core/resource_instruction.py +17 -0
  18. holmes/core/runbooks.py +26 -0
  19. holmes/core/safeguards.py +120 -0
  20. holmes/core/supabase_dal.py +540 -0
  21. holmes/core/tool_calling_llm.py +798 -0
  22. holmes/core/tools.py +566 -0
  23. holmes/core/tools_utils/__init__.py +0 -0
  24. holmes/core/tools_utils/tool_executor.py +65 -0
  25. holmes/core/tools_utils/toolset_utils.py +52 -0
  26. holmes/core/toolset_manager.py +418 -0
  27. holmes/interactive.py +229 -0
  28. holmes/main.py +1041 -0
  29. holmes/plugins/__init__.py +0 -0
  30. holmes/plugins/destinations/__init__.py +6 -0
  31. holmes/plugins/destinations/slack/__init__.py +2 -0
  32. holmes/plugins/destinations/slack/plugin.py +163 -0
  33. holmes/plugins/interfaces.py +32 -0
  34. holmes/plugins/prompts/__init__.py +48 -0
  35. holmes/plugins/prompts/_current_date_time.jinja2 +1 -0
  36. holmes/plugins/prompts/_default_log_prompt.jinja2 +11 -0
  37. holmes/plugins/prompts/_fetch_logs.jinja2 +36 -0
  38. holmes/plugins/prompts/_general_instructions.jinja2 +86 -0
  39. holmes/plugins/prompts/_global_instructions.jinja2 +12 -0
  40. holmes/plugins/prompts/_runbook_instructions.jinja2 +13 -0
  41. holmes/plugins/prompts/_toolsets_instructions.jinja2 +56 -0
  42. holmes/plugins/prompts/generic_ask.jinja2 +36 -0
  43. holmes/plugins/prompts/generic_ask_conversation.jinja2 +32 -0
  44. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +50 -0
  45. holmes/plugins/prompts/generic_investigation.jinja2 +42 -0
  46. holmes/plugins/prompts/generic_post_processing.jinja2 +13 -0
  47. holmes/plugins/prompts/generic_ticket.jinja2 +12 -0
  48. holmes/plugins/prompts/investigation_output_format.jinja2 +32 -0
  49. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +84 -0
  50. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +39 -0
  51. holmes/plugins/runbooks/README.md +22 -0
  52. holmes/plugins/runbooks/__init__.py +100 -0
  53. holmes/plugins/runbooks/catalog.json +14 -0
  54. holmes/plugins/runbooks/jira.yaml +12 -0
  55. holmes/plugins/runbooks/kube-prometheus-stack.yaml +10 -0
  56. holmes/plugins/runbooks/networking/dns_troubleshooting_instructions.md +66 -0
  57. holmes/plugins/runbooks/upgrade/upgrade_troubleshooting_instructions.md +44 -0
  58. holmes/plugins/sources/github/__init__.py +77 -0
  59. holmes/plugins/sources/jira/__init__.py +123 -0
  60. holmes/plugins/sources/opsgenie/__init__.py +93 -0
  61. holmes/plugins/sources/pagerduty/__init__.py +147 -0
  62. holmes/plugins/sources/prometheus/__init__.py +0 -0
  63. holmes/plugins/sources/prometheus/models.py +104 -0
  64. holmes/plugins/sources/prometheus/plugin.py +154 -0
  65. holmes/plugins/toolsets/__init__.py +171 -0
  66. holmes/plugins/toolsets/aks-node-health.yaml +65 -0
  67. holmes/plugins/toolsets/aks.yaml +86 -0
  68. holmes/plugins/toolsets/argocd.yaml +70 -0
  69. holmes/plugins/toolsets/atlas_mongodb/instructions.jinja2 +8 -0
  70. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +307 -0
  71. holmes/plugins/toolsets/aws.yaml +76 -0
  72. holmes/plugins/toolsets/azure_sql/__init__.py +0 -0
  73. holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +600 -0
  74. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +309 -0
  75. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +445 -0
  76. holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +251 -0
  77. holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +317 -0
  78. holmes/plugins/toolsets/azure_sql/azure_base_toolset.py +55 -0
  79. holmes/plugins/toolsets/azure_sql/azure_sql_instructions.jinja2 +137 -0
  80. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +183 -0
  81. holmes/plugins/toolsets/azure_sql/install.md +66 -0
  82. holmes/plugins/toolsets/azure_sql/tools/__init__.py +1 -0
  83. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +324 -0
  84. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +243 -0
  85. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +205 -0
  86. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +249 -0
  87. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +373 -0
  88. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +237 -0
  89. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +172 -0
  90. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +170 -0
  91. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +188 -0
  92. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +180 -0
  93. holmes/plugins/toolsets/azure_sql/utils.py +83 -0
  94. holmes/plugins/toolsets/bash/__init__.py +0 -0
  95. holmes/plugins/toolsets/bash/bash_instructions.jinja2 +14 -0
  96. holmes/plugins/toolsets/bash/bash_toolset.py +208 -0
  97. holmes/plugins/toolsets/bash/common/bash.py +52 -0
  98. holmes/plugins/toolsets/bash/common/config.py +14 -0
  99. holmes/plugins/toolsets/bash/common/stringify.py +25 -0
  100. holmes/plugins/toolsets/bash/common/validators.py +24 -0
  101. holmes/plugins/toolsets/bash/grep/__init__.py +52 -0
  102. holmes/plugins/toolsets/bash/kubectl/__init__.py +100 -0
  103. holmes/plugins/toolsets/bash/kubectl/constants.py +96 -0
  104. holmes/plugins/toolsets/bash/kubectl/kubectl_describe.py +66 -0
  105. holmes/plugins/toolsets/bash/kubectl/kubectl_events.py +88 -0
  106. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +108 -0
  107. holmes/plugins/toolsets/bash/kubectl/kubectl_logs.py +20 -0
  108. holmes/plugins/toolsets/bash/kubectl/kubectl_run.py +46 -0
  109. holmes/plugins/toolsets/bash/kubectl/kubectl_top.py +81 -0
  110. holmes/plugins/toolsets/bash/parse_command.py +103 -0
  111. holmes/plugins/toolsets/confluence.yaml +19 -0
  112. holmes/plugins/toolsets/consts.py +5 -0
  113. holmes/plugins/toolsets/coralogix/api.py +158 -0
  114. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +103 -0
  115. holmes/plugins/toolsets/coralogix/utils.py +181 -0
  116. holmes/plugins/toolsets/datadog.py +153 -0
  117. holmes/plugins/toolsets/docker.yaml +46 -0
  118. holmes/plugins/toolsets/git.py +756 -0
  119. holmes/plugins/toolsets/grafana/__init__.py +0 -0
  120. holmes/plugins/toolsets/grafana/base_grafana_toolset.py +54 -0
  121. holmes/plugins/toolsets/grafana/common.py +68 -0
  122. holmes/plugins/toolsets/grafana/grafana_api.py +31 -0
  123. holmes/plugins/toolsets/grafana/loki_api.py +89 -0
  124. holmes/plugins/toolsets/grafana/tempo_api.py +124 -0
  125. holmes/plugins/toolsets/grafana/toolset_grafana.py +102 -0
  126. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +102 -0
  127. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +10 -0
  128. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +299 -0
  129. holmes/plugins/toolsets/grafana/trace_parser.py +195 -0
  130. holmes/plugins/toolsets/helm.yaml +42 -0
  131. holmes/plugins/toolsets/internet/internet.py +275 -0
  132. holmes/plugins/toolsets/internet/notion.py +137 -0
  133. holmes/plugins/toolsets/kafka.py +638 -0
  134. holmes/plugins/toolsets/kubernetes.yaml +255 -0
  135. holmes/plugins/toolsets/kubernetes_logs.py +426 -0
  136. holmes/plugins/toolsets/kubernetes_logs.yaml +42 -0
  137. holmes/plugins/toolsets/logging_utils/__init__.py +0 -0
  138. holmes/plugins/toolsets/logging_utils/logging_api.py +217 -0
  139. holmes/plugins/toolsets/logging_utils/types.py +0 -0
  140. holmes/plugins/toolsets/mcp/toolset_mcp.py +135 -0
  141. holmes/plugins/toolsets/newrelic.py +222 -0
  142. holmes/plugins/toolsets/opensearch/__init__.py +0 -0
  143. holmes/plugins/toolsets/opensearch/opensearch.py +245 -0
  144. holmes/plugins/toolsets/opensearch/opensearch_logs.py +151 -0
  145. holmes/plugins/toolsets/opensearch/opensearch_traces.py +211 -0
  146. holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +12 -0
  147. holmes/plugins/toolsets/opensearch/opensearch_utils.py +166 -0
  148. holmes/plugins/toolsets/prometheus/prometheus.py +818 -0
  149. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +38 -0
  150. holmes/plugins/toolsets/rabbitmq/api.py +398 -0
  151. holmes/plugins/toolsets/rabbitmq/rabbitmq_instructions.jinja2 +37 -0
  152. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +222 -0
  153. holmes/plugins/toolsets/robusta/__init__.py +0 -0
  154. holmes/plugins/toolsets/robusta/robusta.py +235 -0
  155. holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +24 -0
  156. holmes/plugins/toolsets/runbook/__init__.py +0 -0
  157. holmes/plugins/toolsets/runbook/runbook_fetcher.py +78 -0
  158. holmes/plugins/toolsets/service_discovery.py +92 -0
  159. holmes/plugins/toolsets/servicenow/install.md +37 -0
  160. holmes/plugins/toolsets/servicenow/instructions.jinja2 +3 -0
  161. holmes/plugins/toolsets/servicenow/servicenow.py +198 -0
  162. holmes/plugins/toolsets/slab.yaml +20 -0
  163. holmes/plugins/toolsets/utils.py +137 -0
  164. holmes/plugins/utils.py +14 -0
  165. holmes/utils/__init__.py +0 -0
  166. holmes/utils/cache.py +84 -0
  167. holmes/utils/cert_utils.py +40 -0
  168. holmes/utils/default_toolset_installation_guide.jinja2 +44 -0
  169. holmes/utils/definitions.py +13 -0
  170. holmes/utils/env.py +53 -0
  171. holmes/utils/file_utils.py +56 -0
  172. holmes/utils/global_instructions.py +20 -0
  173. holmes/utils/holmes_status.py +22 -0
  174. holmes/utils/holmes_sync_toolsets.py +80 -0
  175. holmes/utils/markdown_utils.py +55 -0
  176. holmes/utils/pydantic_utils.py +54 -0
  177. holmes/utils/robusta.py +10 -0
  178. holmes/utils/tags.py +97 -0
  179. holmesgpt-0.11.5.dist-info/LICENSE.txt +21 -0
  180. holmesgpt-0.11.5.dist-info/METADATA +400 -0
  181. holmesgpt-0.11.5.dist-info/RECORD +183 -0
  182. holmesgpt-0.11.5.dist-info/WHEEL +4 -0
  183. holmesgpt-0.11.5.dist-info/entry_points.txt +3 -0
holmes/core/llm.py ADDED
@@ -0,0 +1,250 @@
1
+ import json
2
+ import logging
3
+ from abc import abstractmethod
4
+ from typing import Any, Dict, List, Optional, Type, Union
5
+
6
+ from litellm.types.utils import ModelResponse
7
+ import sentry_sdk
8
+
9
+ from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper
10
+ from pydantic import BaseModel
11
+ import litellm
12
+ import os
13
+ from holmes.common.env_vars import (
14
+ THINKING,
15
+ TEMPERATURE,
16
+ )
17
+
18
+
19
+ def environ_get_safe_int(env_var, default="0"):
20
+ try:
21
+ return max(int(os.environ.get(env_var, default)), 0)
22
+ except ValueError:
23
+ return int(default)
24
+
25
+
26
+ OVERRIDE_MAX_OUTPUT_TOKEN = environ_get_safe_int("OVERRIDE_MAX_OUTPUT_TOKEN")
27
+ OVERRIDE_MAX_CONTENT_SIZE = environ_get_safe_int("OVERRIDE_MAX_CONTENT_SIZE")
28
+
29
+
30
+ class LLM:
31
+ @abstractmethod
32
+ def __init__(self):
33
+ self.model: str # type: ignore
34
+
35
+ @abstractmethod
36
+ def get_context_window_size(self) -> int:
37
+ pass
38
+
39
+ @abstractmethod
40
+ def get_maximum_output_token(self) -> int:
41
+ pass
42
+
43
+ @abstractmethod
44
+ def count_tokens_for_message(self, messages: list[dict]) -> int:
45
+ pass
46
+
47
+ @abstractmethod
48
+ def completion(
49
+ self,
50
+ messages: List[Dict[str, Any]],
51
+ tools: Optional[List[Dict[str, Any]]] = [],
52
+ tool_choice: Optional[Union[str, dict]] = None,
53
+ response_format: Optional[Union[dict, Type[BaseModel]]] = None,
54
+ temperature: Optional[float] = None,
55
+ drop_params: Optional[bool] = None,
56
+ stream: Optional[bool] = None,
57
+ ) -> Union[ModelResponse, CustomStreamWrapper]:
58
+ pass
59
+
60
+
61
+ class DefaultLLM(LLM):
62
+ model: str
63
+ api_key: Optional[str]
64
+ base_url: Optional[str]
65
+ args: Dict
66
+
67
+ def __init__(self, model: str, api_key: Optional[str] = None, args: Dict = {}):
68
+ self.model = model
69
+ self.api_key = api_key
70
+ self.args = args
71
+
72
+ if not args:
73
+ self.check_llm(self.model, self.api_key)
74
+
75
+ def check_llm(self, model: str, api_key: Optional[str]):
76
+ logging.debug(f"Checking LiteLLM model {model}")
77
+ # TODO: this WAS a hack to get around the fact that we can't pass in an api key to litellm.validate_environment
78
+ # so without this hack it always complains that the environment variable for the api key is missing
79
+ # to fix that, we always set an api key in the standard format that litellm expects (which is ${PROVIDER}_API_KEY)
80
+ # TODO: we can now handle this better - see https://github.com/BerriAI/litellm/issues/4375#issuecomment-2223684750
81
+ lookup = litellm.get_llm_provider(self.model)
82
+ if not lookup:
83
+ raise Exception(f"Unknown provider for model {model}")
84
+ provider = lookup[1]
85
+ if provider == "watsonx":
86
+ # NOTE: LiteLLM's validate_environment does not currently include checks for IBM WatsonX.
87
+ # The following WatsonX-specific variables are set based on documentation from:
88
+ # https://docs.litellm.ai/docs/providers/watsonx
89
+ # Required variables for WatsonX:
90
+ # - WATSONX_URL: Base URL of your WatsonX instance (required)
91
+ # - WATSONX_APIKEY or WATSONX_TOKEN: IBM Cloud API key or IAM auth token (one is required)
92
+ model_requirements = {"missing_keys": [], "keys_in_environment": True}
93
+ if api_key:
94
+ os.environ["WATSONX_APIKEY"] = api_key
95
+ if "WATSONX_URL" not in os.environ:
96
+ model_requirements["missing_keys"].append("WATSONX_URL") # type: ignore
97
+ model_requirements["keys_in_environment"] = False
98
+ if "WATSONX_APIKEY" not in os.environ and "WATSONX_TOKEN" not in os.environ:
99
+ model_requirements["missing_keys"].extend( # type: ignore
100
+ ["WATSONX_APIKEY", "WATSONX_TOKEN"]
101
+ )
102
+ model_requirements["keys_in_environment"] = False
103
+ # WATSONX_PROJECT_ID is required because we don't let user pass it to completion call directly
104
+ if "WATSONX_PROJECT_ID" not in os.environ:
105
+ model_requirements["missing_keys"].append("WATSONX_PROJECT_ID") # type: ignore
106
+ model_requirements["keys_in_environment"] = False
107
+ # https://docs.litellm.ai/docs/providers/watsonx#usage---models-in-deployment-spaces
108
+ # using custom watsonx deployments might require to set WATSONX_DEPLOYMENT_SPACE_ID env
109
+ if "watsonx/deployment/" in self.model:
110
+ logging.warning(
111
+ "Custom WatsonX deployment detected. You may need to set the WATSONX_DEPLOYMENT_SPACE_ID "
112
+ "environment variable for proper functionality. For more information, refer to the documentation: "
113
+ "https://docs.litellm.ai/docs/providers/watsonx#usage---models-in-deployment-spaces"
114
+ )
115
+ elif provider == "bedrock" and os.environ.get("AWS_PROFILE"):
116
+ model_requirements = {"keys_in_environment": True, "missing_keys": []}
117
+ else:
118
+ #
119
+ api_key_env_var = f"{provider.upper()}_API_KEY"
120
+ if api_key:
121
+ os.environ[api_key_env_var] = api_key
122
+ model_requirements = litellm.validate_environment(model=model)
123
+
124
+ if not model_requirements["keys_in_environment"]:
125
+ raise Exception(
126
+ f"model {model} requires the following environment variables: {model_requirements['missing_keys']}"
127
+ )
128
+
129
+ def _strip_model_prefix(self) -> str:
130
+ """
131
+ Helper function to strip 'openai/' prefix from model name if it exists.
132
+ model cost is taken from here which does not have the openai prefix
133
+ https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json
134
+ """
135
+ model_name = self.model
136
+ if model_name.startswith("openai/"):
137
+ model_name = model_name[len("openai/") :] # Strip the 'openai/' prefix
138
+ elif model_name.startswith("bedrock/"):
139
+ model_name = model_name[len("bedrock/") :] # Strip the 'bedrock/' prefix
140
+ elif model_name.startswith("vertex_ai/"):
141
+ model_name = model_name[
142
+ len("vertex_ai/") :
143
+ ] # Strip the 'vertex_ai/' prefix
144
+
145
+ return model_name
146
+
147
+ # this unfortunately does not seem to work for azure if the deployment name is not a well-known model name
148
+ # if not litellm.supports_function_calling(model=model):
149
+ # raise Exception(f"model {model} does not support function calling. You must use HolmesGPT with a model that supports function calling.")
150
+
151
+ def get_context_window_size(self) -> int:
152
+ if OVERRIDE_MAX_CONTENT_SIZE:
153
+ logging.debug(
154
+ f"Using override OVERRIDE_MAX_CONTENT_SIZE {OVERRIDE_MAX_CONTENT_SIZE}"
155
+ )
156
+ return OVERRIDE_MAX_CONTENT_SIZE
157
+
158
+ model_name = os.environ.get("MODEL_TYPE", self._strip_model_prefix())
159
+ try:
160
+ return litellm.model_cost[model_name]["max_input_tokens"]
161
+ except Exception:
162
+ logging.warning(
163
+ f"Couldn't find model's name {model_name} in litellm's model list, fallback to 128k tokens for max_input_tokens"
164
+ )
165
+ return 128000
166
+
167
+ @sentry_sdk.trace
168
+ def count_tokens_for_message(self, messages: list[dict]) -> int:
169
+ total_token_count = 0
170
+ for message in messages:
171
+ if "token_count" in message and message["token_count"]:
172
+ total_token_count += message["token_count"]
173
+ else:
174
+ # message can be counted by this method only if message contains a "content" key
175
+ if "content" in message:
176
+ if isinstance(message["content"], str):
177
+ message_to_count = [
178
+ {"type": "text", "text": message["content"]}
179
+ ]
180
+ elif isinstance(message["content"], list):
181
+ message_to_count = [
182
+ {"type": "text", "text": json.dumps(message["content"])}
183
+ ]
184
+ elif isinstance(message["content"], dict):
185
+ if "type" not in message["content"]:
186
+ message_to_count = [
187
+ {"type": "text", "text": json.dumps(message["content"])}
188
+ ]
189
+ token_count = litellm.token_counter(
190
+ model=self.model, messages=message_to_count
191
+ )
192
+ message["token_count"] = token_count
193
+ total_token_count += token_count
194
+ return total_token_count
195
+
196
+ def completion(
197
+ self,
198
+ messages: List[Dict[str, Any]],
199
+ tools: Optional[List[Dict[str, Any]]] = None,
200
+ tool_choice: Optional[Union[str, dict]] = None,
201
+ response_format: Optional[Union[dict, Type[BaseModel]]] = None,
202
+ temperature: Optional[float] = None,
203
+ drop_params: Optional[bool] = None,
204
+ stream: Optional[bool] = None,
205
+ ) -> Union[ModelResponse, CustomStreamWrapper]:
206
+ tools_args = {}
207
+ if tools and len(tools) > 0 and tool_choice == "auto":
208
+ tools_args["tools"] = tools
209
+ tools_args["tool_choice"] = tool_choice # type: ignore
210
+
211
+ if THINKING:
212
+ self.args.setdefault("thinking", json.loads(THINKING))
213
+
214
+ if self.args.get("thinking", None):
215
+ litellm.modify_params = True
216
+
217
+ result = litellm.completion(
218
+ model=self.model,
219
+ api_key=self.api_key,
220
+ messages=messages,
221
+ temperature=temperature or self.args.pop("temperature", TEMPERATURE),
222
+ response_format=response_format,
223
+ drop_params=drop_params,
224
+ stream=stream,
225
+ **tools_args,
226
+ **self.args,
227
+ )
228
+
229
+ if isinstance(result, ModelResponse):
230
+ return result
231
+ elif isinstance(result, CustomStreamWrapper):
232
+ return result
233
+ else:
234
+ raise Exception(f"Unexpected type returned by the LLM {type(result)}")
235
+
236
+ def get_maximum_output_token(self) -> int:
237
+ if OVERRIDE_MAX_OUTPUT_TOKEN:
238
+ logging.debug(
239
+ f"Using OVERRIDE_MAX_OUTPUT_TOKEN {OVERRIDE_MAX_OUTPUT_TOKEN}"
240
+ )
241
+ return OVERRIDE_MAX_OUTPUT_TOKEN
242
+
243
+ model_name = os.environ.get("MODEL_TYPE", self._strip_model_prefix())
244
+ try:
245
+ return litellm.model_cost[model_name]["max_output_tokens"]
246
+ except Exception:
247
+ logging.warning(
248
+ f"Couldn't find model's name {model_name} in litellm's model list, fallback to 4096 tokens for max_output_tokens"
249
+ )
250
+ return 4096
holmes/core/models.py ADDED
@@ -0,0 +1,157 @@
1
+ from holmes.core.investigation_structured_output import InputSectionsDataType
2
+ from holmes.core.tool_calling_llm import ToolCallResult
3
+ from typing import Optional, List, Dict, Any, Union
4
+ from pydantic import BaseModel, model_validator
5
+ from enum import Enum
6
+
7
+
8
+ class InvestigationResult(BaseModel):
9
+ analysis: Optional[str] = None
10
+ sections: Optional[Dict[str, Union[str, None]]] = None
11
+ tool_calls: List[ToolCallResult] = []
12
+ instructions: List[str] = []
13
+
14
+
15
+ class InvestigateRequest(BaseModel):
16
+ source: str # "prometheus" etc
17
+ title: str
18
+ description: str
19
+ subject: dict
20
+ context: Dict[str, Any]
21
+ source_instance_id: str = "ApiRequest"
22
+ include_tool_calls: bool = False
23
+ include_tool_call_results: bool = False
24
+ prompt_template: str = "builtin://generic_investigation.jinja2"
25
+ sections: Optional[InputSectionsDataType] = None
26
+ model: Optional[str] = None
27
+ # TODO in the future
28
+ # response_handler: ...
29
+
30
+
31
+ class ToolCallConversationResult(BaseModel):
32
+ name: str
33
+ description: str
34
+ output: str
35
+
36
+
37
+ class ConversationInvestigationResponse(BaseModel):
38
+ analysis: Optional[str] = None
39
+ tool_calls: List[ToolCallResult] = []
40
+
41
+
42
+ class ConversationInvestigationResult(BaseModel):
43
+ analysis: Optional[str] = None
44
+ tools: Optional[List[ToolCallConversationResult]] = []
45
+
46
+
47
+ class IssueInvestigationResult(BaseModel):
48
+ """
49
+ :var result: A dictionary containing the summary of the issue investigation.
50
+ :var tools: A list of dictionaries where each dictionary contains information
51
+ about the tool, its name, description and output.
52
+
53
+ It is based on the holmes investigation saved to Evidence table.
54
+ """
55
+
56
+ result: str
57
+ tools: Optional[List[ToolCallConversationResult]] = []
58
+
59
+
60
+ class HolmesConversationHistory(BaseModel):
61
+ ask: str
62
+ answer: ConversationInvestigationResult
63
+
64
+
65
+ # HolmesConversationIssueContext, ConversationType and ConversationRequest classes will be deprecated later
66
+ class HolmesConversationIssueContext(BaseModel):
67
+ investigation_result: IssueInvestigationResult
68
+ conversation_history: Optional[List[HolmesConversationHistory]] = []
69
+ issue_type: str
70
+ robusta_issue_id: Optional[str] = None
71
+ source: Optional[str] = None
72
+
73
+
74
+ class ConversationType(str, Enum):
75
+ ISSUE = "issue"
76
+
77
+
78
+ class ConversationRequest(BaseModel):
79
+ user_prompt: str
80
+ source: Optional[str] = None
81
+ resource: Optional[dict] = None
82
+ # ConversationType.ISSUE is default as we gonna deprecate this class and won't add new conversation types
83
+ conversation_type: Optional[ConversationType] = ConversationType.ISSUE
84
+ context: HolmesConversationIssueContext
85
+ include_tool_calls: bool = False
86
+ include_tool_call_results: bool = False
87
+
88
+
89
+ class ChatRequestBaseModel(BaseModel):
90
+ conversation_history: Optional[list[dict]] = None
91
+ model: Optional[str] = None
92
+
93
+ # In our setup with litellm, the first message in conversation_history
94
+ # should follow the structure [{"role": "system", "content": ...}],
95
+ # where the "role" field is expected to be "system".
96
+ @model_validator(mode="before")
97
+ def check_first_item_role(cls, values):
98
+ conversation_history = values.get("conversation_history")
99
+ if (
100
+ conversation_history
101
+ and isinstance(conversation_history, list)
102
+ and len(conversation_history) > 0
103
+ ):
104
+ first_item = conversation_history[0]
105
+ if not first_item.get("role") == "system":
106
+ raise ValueError(
107
+ "The first item in conversation_history must contain 'role': 'system'"
108
+ )
109
+ return values
110
+
111
+
112
+ class IssueChatRequest(ChatRequestBaseModel):
113
+ ask: str
114
+ investigation_result: IssueInvestigationResult
115
+ issue_type: str
116
+
117
+
118
+ class WorkloadHealthRequest(BaseModel):
119
+ ask: str
120
+ resource: dict
121
+ alert_history_since_hours: float = 24
122
+ alert_history: bool = True
123
+ stored_instrucitons: bool = True
124
+ instructions: Optional[List[str]] = []
125
+ include_tool_calls: bool = False
126
+ include_tool_call_results: bool = False
127
+ prompt_template: str = "builtin://kubernetes_workload_ask.jinja2"
128
+ model: Optional[str] = None
129
+
130
+
131
+ class ChatRequest(ChatRequestBaseModel):
132
+ ask: str
133
+
134
+
135
+ class FollowUpAction(BaseModel):
136
+ id: str
137
+ action_label: str
138
+ pre_action_notification_text: str
139
+ prompt: str
140
+
141
+
142
+ class ChatResponse(BaseModel):
143
+ analysis: str
144
+ conversation_history: list[dict]
145
+ tool_calls: Optional[List[ToolCallResult]] = []
146
+ follow_up_actions: Optional[List[FollowUpAction]] = []
147
+
148
+
149
+ class WorkloadHealthInvestigationResult(BaseModel):
150
+ analysis: Optional[str] = None
151
+ tools: Optional[List[ToolCallConversationResult]] = []
152
+
153
+
154
+ class WorkloadHealthChatRequest(ChatRequestBaseModel):
155
+ ask: str
156
+ workload_health_result: WorkloadHealthInvestigationResult
157
+ resource: dict
@@ -0,0 +1,51 @@
1
+ import re
2
+
3
+ # parses both simple types: "int", "array", "string"
4
+ # but also arrays of those simpler types: "array[int]", "array[string]", etc.
5
+ pattern = r"^(array\[(?P<inner_type>\w+)\])|(?P<simple_type>\w+)$"
6
+
7
+
8
+ def type_to_open_ai_schema(type_value):
9
+ match = re.match(pattern, type_value.strip())
10
+
11
+ if not match:
12
+ raise ValueError(f"Invalid type format: {type_value}")
13
+
14
+ if match.group("inner_type"):
15
+ return {"type": "array", "items": {"type": match.group("inner_type")}}
16
+
17
+ else:
18
+ return {"type": match.group("simple_type")}
19
+
20
+
21
+ def format_tool_to_open_ai_standard(
22
+ tool_name: str, tool_description: str, tool_parameters: dict
23
+ ):
24
+ tool_properties = {}
25
+ for param_name, param_attributes in tool_parameters.items():
26
+ tool_properties[param_name] = type_to_open_ai_schema(param_attributes.type)
27
+ if param_attributes.description is not None:
28
+ tool_properties[param_name]["description"] = param_attributes.description
29
+
30
+ result = {
31
+ "type": "function",
32
+ "function": {
33
+ "name": tool_name,
34
+ "description": tool_description,
35
+ "parameters": {
36
+ "properties": tool_properties,
37
+ "required": [
38
+ param_name
39
+ for param_name, param_attributes in tool_parameters.items()
40
+ if param_attributes.required
41
+ ],
42
+ "type": "object",
43
+ },
44
+ },
45
+ }
46
+
47
+ # gemini doesnt have parameters object if it is without params
48
+ if tool_properties is None or tool_properties == {}:
49
+ result["function"].pop("parameters") # type: ignore
50
+
51
+ return result
@@ -0,0 +1,72 @@
1
+ import time
2
+ import logging
3
+
4
+ from functools import wraps
5
+
6
+ from holmes.common.env_vars import (
7
+ LOG_PERFORMANCE,
8
+ )
9
+
10
+
11
+ class PerformanceTiming:
12
+ def __init__(self, name):
13
+ self.ended = False
14
+
15
+ self.name = name
16
+ self.start_time = time.time()
17
+ self.last_measure_time = self.start_time
18
+ self.last_measure_label = "Start"
19
+ self.timings = []
20
+
21
+ def measure(self, label):
22
+ if not LOG_PERFORMANCE:
23
+ return
24
+ if self.ended:
25
+ raise Exception("cannot measure a perf timing that is already ended")
26
+ current_time = time.time()
27
+
28
+ time_since_start = int((current_time - self.start_time) * 1000)
29
+ time_since_last = int((current_time - self.last_measure_time) * 1000)
30
+
31
+ self.timings.append((label, time_since_last, time_since_start))
32
+
33
+ self.last_measure_time = current_time
34
+ self.last_measure_label = label
35
+
36
+ def end(self, custom_message: str = ""):
37
+ if not LOG_PERFORMANCE:
38
+ return
39
+ self.ended = True
40
+ current_time = time.time()
41
+ time_since_start = int((current_time - self.start_time) * 1000)
42
+ message = f"{self.name} {custom_message} {time_since_start}ms"
43
+ logging.info(message)
44
+ if LOG_PERFORMANCE:
45
+ for label, time_since_last, time_since_start in self.timings:
46
+ logging.info(
47
+ f"\t{self.name}({label}) +{time_since_last}ms {time_since_start}ms"
48
+ )
49
+
50
+
51
+ def log_function_timing(label=None):
52
+ def decorator(func):
53
+ @wraps(func)
54
+ def function_timing_wrapper(*args, **kwargs):
55
+ start_time = time.perf_counter()
56
+ result = func(*args, **kwargs)
57
+ end_time = time.perf_counter()
58
+ total_time = int((end_time - start_time) * 1000)
59
+
60
+ function_identifier = (
61
+ f'"{label}: {func.__name__}()"' if label else f'"{func.__name__}()"'
62
+ )
63
+ logging.info(f"Function {function_identifier} took {total_time}ms")
64
+ return result
65
+
66
+ return function_timing_wrapper
67
+
68
+ if callable(label):
69
+ func = label
70
+ label = None
71
+ return decorator(func)
72
+ return decorator
holmes/core/prompt.py ADDED
@@ -0,0 +1,42 @@
1
+ from rich.console import Console
2
+ from typing import Optional, List, Dict
3
+ from pathlib import Path
4
+
5
+
6
+ def append_file_to_user_prompt(user_prompt: str, file_path: Path) -> str:
7
+ 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>"
9
+
10
+ return user_prompt
11
+
12
+
13
+ def append_all_files_to_user_prompt(
14
+ console: Console, user_prompt: str, file_paths: Optional[List[Path]]
15
+ ) -> str:
16
+ if not file_paths:
17
+ return user_prompt
18
+
19
+ for file_path in file_paths:
20
+ console.print(f"[bold yellow]Adding file {file_path} to context[/bold yellow]")
21
+ user_prompt = append_file_to_user_prompt(user_prompt, file_path)
22
+
23
+ return user_prompt
24
+
25
+
26
+ def build_initial_ask_messages(
27
+ console: Console,
28
+ system_prompt_rendered: str,
29
+ initial_user_prompt: str,
30
+ file_paths: Optional[List[Path]],
31
+ ) -> List[Dict]:
32
+ """Build the initial messages for the AI call."""
33
+ user_prompt_with_files = append_all_files_to_user_prompt(
34
+ console, initial_user_prompt, file_paths
35
+ )
36
+
37
+ messages = [
38
+ {"role": "system", "content": system_prompt_rendered},
39
+ {"role": "user", "content": user_prompt_with_files},
40
+ ]
41
+
42
+ return messages
@@ -0,0 +1,17 @@
1
+ from typing import List
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class ResourceInstructionDocument(BaseModel):
7
+ """Represents context necessary for an investigation in the form of a URL
8
+ It is expected that Holmes will use that URL to fetch additional context about an error.
9
+ This URL can for example be the location of a runbook
10
+ """
11
+
12
+ url: str
13
+
14
+
15
+ class ResourceInstructions(BaseModel):
16
+ instructions: List[str] = []
17
+ documents: List[ResourceInstructionDocument] = []
@@ -0,0 +1,26 @@
1
+ from typing import List
2
+ from holmes.core.issue import Issue
3
+ from holmes.plugins.runbooks import Runbook
4
+
5
+
6
+ # TODO: our default prompt has a lot of kubernetes specific stuff - see if we can get that into the runbook
7
+ class RunbookManager:
8
+ def __init__(self, runbooks: List[Runbook]):
9
+ self.runbooks = runbooks
10
+
11
+ def get_instructions_for_issue(self, issue: Issue) -> List[str]:
12
+ instructions = []
13
+ for runbook in self.runbooks:
14
+ if runbook.match.issue_id and not runbook.match.issue_id.match(issue.id):
15
+ continue
16
+ if runbook.match.issue_name and not runbook.match.issue_name.match(
17
+ issue.name
18
+ ):
19
+ continue
20
+ if runbook.match.source and not runbook.match.source.match(
21
+ issue.source_type
22
+ ):
23
+ continue
24
+ instructions.append(runbook.instructions)
25
+
26
+ return instructions