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.
- holmes/.git_archival.json +7 -0
- holmes/__init__.py +76 -0
- holmes/__init__.py.bak +76 -0
- holmes/clients/robusta_client.py +24 -0
- holmes/common/env_vars.py +47 -0
- holmes/config.py +526 -0
- holmes/core/__init__.py +0 -0
- holmes/core/conversations.py +578 -0
- holmes/core/investigation.py +152 -0
- holmes/core/investigation_structured_output.py +264 -0
- holmes/core/issue.py +54 -0
- holmes/core/llm.py +250 -0
- holmes/core/models.py +157 -0
- holmes/core/openai_formatting.py +51 -0
- holmes/core/performance_timing.py +72 -0
- holmes/core/prompt.py +42 -0
- holmes/core/resource_instruction.py +17 -0
- holmes/core/runbooks.py +26 -0
- holmes/core/safeguards.py +120 -0
- holmes/core/supabase_dal.py +540 -0
- holmes/core/tool_calling_llm.py +798 -0
- holmes/core/tools.py +566 -0
- holmes/core/tools_utils/__init__.py +0 -0
- holmes/core/tools_utils/tool_executor.py +65 -0
- holmes/core/tools_utils/toolset_utils.py +52 -0
- holmes/core/toolset_manager.py +418 -0
- holmes/interactive.py +229 -0
- holmes/main.py +1041 -0
- holmes/plugins/__init__.py +0 -0
- holmes/plugins/destinations/__init__.py +6 -0
- holmes/plugins/destinations/slack/__init__.py +2 -0
- holmes/plugins/destinations/slack/plugin.py +163 -0
- holmes/plugins/interfaces.py +32 -0
- holmes/plugins/prompts/__init__.py +48 -0
- holmes/plugins/prompts/_current_date_time.jinja2 +1 -0
- holmes/plugins/prompts/_default_log_prompt.jinja2 +11 -0
- holmes/plugins/prompts/_fetch_logs.jinja2 +36 -0
- holmes/plugins/prompts/_general_instructions.jinja2 +86 -0
- holmes/plugins/prompts/_global_instructions.jinja2 +12 -0
- holmes/plugins/prompts/_runbook_instructions.jinja2 +13 -0
- holmes/plugins/prompts/_toolsets_instructions.jinja2 +56 -0
- holmes/plugins/prompts/generic_ask.jinja2 +36 -0
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +32 -0
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +50 -0
- holmes/plugins/prompts/generic_investigation.jinja2 +42 -0
- holmes/plugins/prompts/generic_post_processing.jinja2 +13 -0
- holmes/plugins/prompts/generic_ticket.jinja2 +12 -0
- holmes/plugins/prompts/investigation_output_format.jinja2 +32 -0
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +84 -0
- holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +39 -0
- holmes/plugins/runbooks/README.md +22 -0
- holmes/plugins/runbooks/__init__.py +100 -0
- holmes/plugins/runbooks/catalog.json +14 -0
- holmes/plugins/runbooks/jira.yaml +12 -0
- holmes/plugins/runbooks/kube-prometheus-stack.yaml +10 -0
- holmes/plugins/runbooks/networking/dns_troubleshooting_instructions.md +66 -0
- holmes/plugins/runbooks/upgrade/upgrade_troubleshooting_instructions.md +44 -0
- holmes/plugins/sources/github/__init__.py +77 -0
- holmes/plugins/sources/jira/__init__.py +123 -0
- holmes/plugins/sources/opsgenie/__init__.py +93 -0
- holmes/plugins/sources/pagerduty/__init__.py +147 -0
- holmes/plugins/sources/prometheus/__init__.py +0 -0
- holmes/plugins/sources/prometheus/models.py +104 -0
- holmes/plugins/sources/prometheus/plugin.py +154 -0
- holmes/plugins/toolsets/__init__.py +171 -0
- holmes/plugins/toolsets/aks-node-health.yaml +65 -0
- holmes/plugins/toolsets/aks.yaml +86 -0
- holmes/plugins/toolsets/argocd.yaml +70 -0
- holmes/plugins/toolsets/atlas_mongodb/instructions.jinja2 +8 -0
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +307 -0
- holmes/plugins/toolsets/aws.yaml +76 -0
- holmes/plugins/toolsets/azure_sql/__init__.py +0 -0
- holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +600 -0
- holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +309 -0
- holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +445 -0
- holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +251 -0
- holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +317 -0
- holmes/plugins/toolsets/azure_sql/azure_base_toolset.py +55 -0
- holmes/plugins/toolsets/azure_sql/azure_sql_instructions.jinja2 +137 -0
- holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +183 -0
- holmes/plugins/toolsets/azure_sql/install.md +66 -0
- holmes/plugins/toolsets/azure_sql/tools/__init__.py +1 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +324 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +243 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +205 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +249 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +373 -0
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +237 -0
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +172 -0
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +170 -0
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +188 -0
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +180 -0
- holmes/plugins/toolsets/azure_sql/utils.py +83 -0
- holmes/plugins/toolsets/bash/__init__.py +0 -0
- holmes/plugins/toolsets/bash/bash_instructions.jinja2 +14 -0
- holmes/plugins/toolsets/bash/bash_toolset.py +208 -0
- holmes/plugins/toolsets/bash/common/bash.py +52 -0
- holmes/plugins/toolsets/bash/common/config.py +14 -0
- holmes/plugins/toolsets/bash/common/stringify.py +25 -0
- holmes/plugins/toolsets/bash/common/validators.py +24 -0
- holmes/plugins/toolsets/bash/grep/__init__.py +52 -0
- holmes/plugins/toolsets/bash/kubectl/__init__.py +100 -0
- holmes/plugins/toolsets/bash/kubectl/constants.py +96 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_describe.py +66 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_events.py +88 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +108 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_logs.py +20 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_run.py +46 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_top.py +81 -0
- holmes/plugins/toolsets/bash/parse_command.py +103 -0
- holmes/plugins/toolsets/confluence.yaml +19 -0
- holmes/plugins/toolsets/consts.py +5 -0
- holmes/plugins/toolsets/coralogix/api.py +158 -0
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +103 -0
- holmes/plugins/toolsets/coralogix/utils.py +181 -0
- holmes/plugins/toolsets/datadog.py +153 -0
- holmes/plugins/toolsets/docker.yaml +46 -0
- holmes/plugins/toolsets/git.py +756 -0
- holmes/plugins/toolsets/grafana/__init__.py +0 -0
- holmes/plugins/toolsets/grafana/base_grafana_toolset.py +54 -0
- holmes/plugins/toolsets/grafana/common.py +68 -0
- holmes/plugins/toolsets/grafana/grafana_api.py +31 -0
- holmes/plugins/toolsets/grafana/loki_api.py +89 -0
- holmes/plugins/toolsets/grafana/tempo_api.py +124 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +102 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +102 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +10 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +299 -0
- holmes/plugins/toolsets/grafana/trace_parser.py +195 -0
- holmes/plugins/toolsets/helm.yaml +42 -0
- holmes/plugins/toolsets/internet/internet.py +275 -0
- holmes/plugins/toolsets/internet/notion.py +137 -0
- holmes/plugins/toolsets/kafka.py +638 -0
- holmes/plugins/toolsets/kubernetes.yaml +255 -0
- holmes/plugins/toolsets/kubernetes_logs.py +426 -0
- holmes/plugins/toolsets/kubernetes_logs.yaml +42 -0
- holmes/plugins/toolsets/logging_utils/__init__.py +0 -0
- holmes/plugins/toolsets/logging_utils/logging_api.py +217 -0
- holmes/plugins/toolsets/logging_utils/types.py +0 -0
- holmes/plugins/toolsets/mcp/toolset_mcp.py +135 -0
- holmes/plugins/toolsets/newrelic.py +222 -0
- holmes/plugins/toolsets/opensearch/__init__.py +0 -0
- holmes/plugins/toolsets/opensearch/opensearch.py +245 -0
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +151 -0
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +211 -0
- holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +12 -0
- holmes/plugins/toolsets/opensearch/opensearch_utils.py +166 -0
- holmes/plugins/toolsets/prometheus/prometheus.py +818 -0
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +38 -0
- holmes/plugins/toolsets/rabbitmq/api.py +398 -0
- holmes/plugins/toolsets/rabbitmq/rabbitmq_instructions.jinja2 +37 -0
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +222 -0
- holmes/plugins/toolsets/robusta/__init__.py +0 -0
- holmes/plugins/toolsets/robusta/robusta.py +235 -0
- holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +24 -0
- holmes/plugins/toolsets/runbook/__init__.py +0 -0
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +78 -0
- holmes/plugins/toolsets/service_discovery.py +92 -0
- holmes/plugins/toolsets/servicenow/install.md +37 -0
- holmes/plugins/toolsets/servicenow/instructions.jinja2 +3 -0
- holmes/plugins/toolsets/servicenow/servicenow.py +198 -0
- holmes/plugins/toolsets/slab.yaml +20 -0
- holmes/plugins/toolsets/utils.py +137 -0
- holmes/plugins/utils.py +14 -0
- holmes/utils/__init__.py +0 -0
- holmes/utils/cache.py +84 -0
- holmes/utils/cert_utils.py +40 -0
- holmes/utils/default_toolset_installation_guide.jinja2 +44 -0
- holmes/utils/definitions.py +13 -0
- holmes/utils/env.py +53 -0
- holmes/utils/file_utils.py +56 -0
- holmes/utils/global_instructions.py +20 -0
- holmes/utils/holmes_status.py +22 -0
- holmes/utils/holmes_sync_toolsets.py +80 -0
- holmes/utils/markdown_utils.py +55 -0
- holmes/utils/pydantic_utils.py +54 -0
- holmes/utils/robusta.py +10 -0
- holmes/utils/tags.py +97 -0
- holmesgpt-0.11.5.dist-info/LICENSE.txt +21 -0
- holmesgpt-0.11.5.dist-info/METADATA +400 -0
- holmesgpt-0.11.5.dist-info/RECORD +183 -0
- holmesgpt-0.11.5.dist-info/WHEEL +4 -0
- 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] = []
|
holmes/core/runbooks.py
ADDED
|
@@ -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
|