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/config.py
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import os.path
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
import yaml # type: ignore
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, FilePath, SecretStr
|
|
11
|
+
|
|
12
|
+
from holmes import get_version # type: ignore
|
|
13
|
+
from holmes.clients.robusta_client import HolmesInfo, fetch_holmes_info
|
|
14
|
+
from holmes.common.env_vars import ROBUSTA_AI, ROBUSTA_API_ENDPOINT, ROBUSTA_CONFIG_PATH
|
|
15
|
+
from holmes.core.llm import LLM, DefaultLLM
|
|
16
|
+
from holmes.core.runbooks import RunbookManager
|
|
17
|
+
from holmes.core.supabase_dal import SupabaseDal
|
|
18
|
+
from holmes.core.tool_calling_llm import IssueInvestigator, ToolCallingLLM
|
|
19
|
+
from holmes.core.tools_utils.tool_executor import ToolExecutor
|
|
20
|
+
from holmes.core.toolset_manager import ToolsetManager
|
|
21
|
+
from holmes.plugins.destinations.slack import SlackDestination
|
|
22
|
+
from holmes.plugins.runbooks import (
|
|
23
|
+
RunbookCatalog,
|
|
24
|
+
load_builtin_runbooks,
|
|
25
|
+
load_runbook_catalog,
|
|
26
|
+
load_runbooks_from_file,
|
|
27
|
+
)
|
|
28
|
+
from holmes.plugins.sources.github import GitHubSource
|
|
29
|
+
from holmes.plugins.sources.jira import JiraServiceManagementSource, JiraSource
|
|
30
|
+
from holmes.plugins.sources.opsgenie import OpsGenieSource
|
|
31
|
+
from holmes.plugins.sources.pagerduty import PagerDutySource
|
|
32
|
+
from holmes.plugins.sources.prometheus.plugin import AlertManagerSource
|
|
33
|
+
from holmes.utils.definitions import RobustaConfig
|
|
34
|
+
from holmes.utils.env import replace_env_vars_values
|
|
35
|
+
from holmes.utils.file_utils import load_yaml_file
|
|
36
|
+
from holmes.utils.pydantic_utils import RobustaBaseConfig, load_model_from_file
|
|
37
|
+
|
|
38
|
+
DEFAULT_CONFIG_LOCATION = os.path.expanduser("~/.holmes/config.yaml")
|
|
39
|
+
MODEL_LIST_FILE_LOCATION = os.environ.get(
|
|
40
|
+
"MODEL_LIST_FILE_LOCATION", "/etc/holmes/config/model_list.yaml"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SupportedTicketSources(str, Enum):
|
|
45
|
+
JIRA_SERVICE_MANAGEMENT = "jira-service-management"
|
|
46
|
+
PAGERDUTY = "pagerduty"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_old_toolset_config(
|
|
50
|
+
toolsets: Union[dict[str, dict[str, Any]], List[dict[str, Any]]],
|
|
51
|
+
) -> bool:
|
|
52
|
+
# old config is a list of toolsets
|
|
53
|
+
if isinstance(toolsets, list):
|
|
54
|
+
return True
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_models_file(path: str):
|
|
59
|
+
models = load_yaml_file(path, raise_error=False, warn_not_found=False)
|
|
60
|
+
|
|
61
|
+
for model, params in models.items():
|
|
62
|
+
params = replace_env_vars_values(params)
|
|
63
|
+
|
|
64
|
+
return models
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Config(RobustaBaseConfig):
|
|
68
|
+
api_key: Optional[SecretStr] = (
|
|
69
|
+
None # if None, read from OPENAI_API_KEY or AZURE_OPENAI_ENDPOINT env var
|
|
70
|
+
)
|
|
71
|
+
model: Optional[str] = "gpt-4o"
|
|
72
|
+
max_steps: int = 10
|
|
73
|
+
cluster_name: Optional[str] = None
|
|
74
|
+
|
|
75
|
+
alertmanager_url: Optional[str] = None
|
|
76
|
+
alertmanager_username: Optional[str] = None
|
|
77
|
+
alertmanager_password: Optional[str] = None
|
|
78
|
+
alertmanager_alertname: Optional[str] = None
|
|
79
|
+
alertmanager_label: Optional[List[str]] = []
|
|
80
|
+
alertmanager_file: Optional[FilePath] = None
|
|
81
|
+
|
|
82
|
+
jira_url: Optional[str] = None
|
|
83
|
+
jira_username: Optional[str] = None
|
|
84
|
+
jira_api_key: Optional[SecretStr] = None
|
|
85
|
+
jira_query: Optional[str] = ""
|
|
86
|
+
|
|
87
|
+
github_url: Optional[str] = None
|
|
88
|
+
github_owner: Optional[str] = None
|
|
89
|
+
github_pat: Optional[SecretStr] = None
|
|
90
|
+
github_repository: Optional[str] = None
|
|
91
|
+
github_query: str = ""
|
|
92
|
+
|
|
93
|
+
slack_token: Optional[SecretStr] = None
|
|
94
|
+
slack_channel: Optional[str] = None
|
|
95
|
+
|
|
96
|
+
pagerduty_api_key: Optional[SecretStr] = None
|
|
97
|
+
pagerduty_user_email: Optional[str] = None
|
|
98
|
+
pagerduty_incident_key: Optional[str] = None
|
|
99
|
+
|
|
100
|
+
opsgenie_api_key: Optional[SecretStr] = None
|
|
101
|
+
opsgenie_team_integration_key: Optional[SecretStr] = None
|
|
102
|
+
opsgenie_query: Optional[str] = None
|
|
103
|
+
|
|
104
|
+
custom_runbooks: List[FilePath] = []
|
|
105
|
+
|
|
106
|
+
# custom_toolsets is passed from config file, and be used to override built-in toolsets, provides 'stable' customized toolset.
|
|
107
|
+
# The status of custom toolsets can be cached.
|
|
108
|
+
custom_toolsets: Optional[List[FilePath]] = None
|
|
109
|
+
# custom_toolsets_from_cli is passed from CLI option `--custom-toolsets` as 'experimental' custom toolsets.
|
|
110
|
+
# The status of toolset here won't be cached, so the toolset from cli will always be loaded when specified in the CLI.
|
|
111
|
+
custom_toolsets_from_cli: Optional[List[FilePath]] = None
|
|
112
|
+
|
|
113
|
+
toolsets: Optional[dict[str, dict[str, Any]]] = None
|
|
114
|
+
|
|
115
|
+
_server_tool_executor: Optional[ToolExecutor] = None
|
|
116
|
+
|
|
117
|
+
_version: Optional[str] = None
|
|
118
|
+
_holmes_info: Optional[HolmesInfo] = None
|
|
119
|
+
|
|
120
|
+
_toolset_manager: Optional[ToolsetManager] = None
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def is_latest_version(self) -> bool:
|
|
124
|
+
if (
|
|
125
|
+
not self._holmes_info
|
|
126
|
+
or not self._holmes_info.latest_version
|
|
127
|
+
or not self._version
|
|
128
|
+
):
|
|
129
|
+
# We couldn't resolve version, assume we are running the latest version
|
|
130
|
+
return True
|
|
131
|
+
if self._version.startswith("dev-"):
|
|
132
|
+
# dev versions are considered to be the latest version
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
return self._version.startswith(self._holmes_info.latest_version)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def toolset_manager(self) -> ToolsetManager:
|
|
139
|
+
if not self._toolset_manager:
|
|
140
|
+
self._toolset_manager = ToolsetManager(
|
|
141
|
+
toolsets=self.toolsets,
|
|
142
|
+
custom_toolsets=self.custom_toolsets,
|
|
143
|
+
custom_toolsets_from_cli=self.custom_toolsets_from_cli,
|
|
144
|
+
)
|
|
145
|
+
return self._toolset_manager
|
|
146
|
+
|
|
147
|
+
def model_post_init(self, __context: Any) -> None:
|
|
148
|
+
self._version = get_version()
|
|
149
|
+
self._holmes_info = fetch_holmes_info()
|
|
150
|
+
self._model_list = parse_models_file(MODEL_LIST_FILE_LOCATION)
|
|
151
|
+
if ROBUSTA_AI:
|
|
152
|
+
self._model_list["Robusta"] = {
|
|
153
|
+
"base_url": ROBUSTA_API_ENDPOINT,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def log_useful_info(self):
|
|
157
|
+
if self._model_list:
|
|
158
|
+
logging.info(f"loaded models: {list(self._model_list.keys())}")
|
|
159
|
+
|
|
160
|
+
if not self.is_latest_version and self._holmes_info:
|
|
161
|
+
logging.warning(
|
|
162
|
+
f"You are running version {self._version} of holmes, but the latest version is {self._holmes_info.latest_version}. Please update.",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def load_from_file(cls, config_file: Optional[Path], **kwargs) -> "Config":
|
|
167
|
+
"""
|
|
168
|
+
Load configuration from file and merge with CLI options.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
config_file: Path to configuration file
|
|
172
|
+
**kwargs: CLI options to override config file values
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Config instance with merged settings
|
|
176
|
+
"""
|
|
177
|
+
config_from_file: Optional[Config] = None
|
|
178
|
+
if config_file is not None and config_file.exists():
|
|
179
|
+
logging.debug(f"Loading config from {config_file}")
|
|
180
|
+
config_from_file = load_model_from_file(cls, config_file)
|
|
181
|
+
|
|
182
|
+
cli_options = {k: v for k, v in kwargs.items() if v is not None and v != []}
|
|
183
|
+
|
|
184
|
+
if config_from_file is None:
|
|
185
|
+
result = cls(**cli_options)
|
|
186
|
+
else:
|
|
187
|
+
logging.debug(f"Overriding config from cli options {cli_options}")
|
|
188
|
+
merged_config = config_from_file.dict()
|
|
189
|
+
merged_config.update(cli_options)
|
|
190
|
+
result = cls(**merged_config)
|
|
191
|
+
|
|
192
|
+
result.log_useful_info()
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def load_from_env(cls):
|
|
197
|
+
kwargs = {}
|
|
198
|
+
for field_name in [
|
|
199
|
+
"model",
|
|
200
|
+
"api_key",
|
|
201
|
+
"max_steps",
|
|
202
|
+
"alertmanager_url",
|
|
203
|
+
"alertmanager_username",
|
|
204
|
+
"alertmanager_password",
|
|
205
|
+
"jira_url",
|
|
206
|
+
"jira_username",
|
|
207
|
+
"jira_api_key",
|
|
208
|
+
"jira_query",
|
|
209
|
+
"slack_token",
|
|
210
|
+
"slack_channel",
|
|
211
|
+
"github_url",
|
|
212
|
+
"github_owner",
|
|
213
|
+
"github_repository",
|
|
214
|
+
"github_pat",
|
|
215
|
+
"github_query",
|
|
216
|
+
# TODO
|
|
217
|
+
# custom_runbooks
|
|
218
|
+
]:
|
|
219
|
+
val = os.getenv(field_name.upper(), None)
|
|
220
|
+
if val is not None:
|
|
221
|
+
kwargs[field_name] = val
|
|
222
|
+
kwargs["cluster_name"] = Config.__get_cluster_name()
|
|
223
|
+
result = cls(**kwargs)
|
|
224
|
+
result.log_useful_info()
|
|
225
|
+
return result
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def __get_cluster_name() -> Optional[str]:
|
|
229
|
+
config_file_path = ROBUSTA_CONFIG_PATH
|
|
230
|
+
env_cluster_name = os.environ.get("CLUSTER_NAME")
|
|
231
|
+
if env_cluster_name:
|
|
232
|
+
return env_cluster_name
|
|
233
|
+
|
|
234
|
+
if not os.path.exists(config_file_path):
|
|
235
|
+
logging.info(f"No robusta config in {config_file_path}")
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
logging.info(f"loading config {config_file_path}")
|
|
239
|
+
with open(config_file_path) as file:
|
|
240
|
+
yaml_content = yaml.safe_load(file)
|
|
241
|
+
config = RobustaConfig(**yaml_content)
|
|
242
|
+
return config.global_config.get("cluster_name")
|
|
243
|
+
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def get_runbook_catalog() -> Optional[RunbookCatalog]:
|
|
248
|
+
# TODO(mainred): besides the built-in runbooks, we need to allow the user to bring their own runbooks
|
|
249
|
+
runbook_catalog = load_runbook_catalog()
|
|
250
|
+
return runbook_catalog
|
|
251
|
+
|
|
252
|
+
def create_console_tool_executor(self, dal: Optional[SupabaseDal]) -> ToolExecutor:
|
|
253
|
+
"""
|
|
254
|
+
Creates a ToolExecutor instance configured for CLI usage. This executor manages the available tools
|
|
255
|
+
and their execution in the command-line interface.
|
|
256
|
+
|
|
257
|
+
The method loads toolsets in this order, with later sources overriding earlier ones:
|
|
258
|
+
1. Built-in toolsets (tagged as CORE or CLI)
|
|
259
|
+
2. toolsets from config file will override and be merged into built-in toolsets with the same name.
|
|
260
|
+
3. Custom toolsets from config files which can not override built-in toolsets
|
|
261
|
+
"""
|
|
262
|
+
cli_toolsets = self.toolset_manager.list_console_toolsets(dal=dal)
|
|
263
|
+
return ToolExecutor(cli_toolsets)
|
|
264
|
+
|
|
265
|
+
def create_tool_executor(self, dal: Optional[SupabaseDal]) -> ToolExecutor:
|
|
266
|
+
"""
|
|
267
|
+
Creates ToolExecutor for the server endpoints
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
if self._server_tool_executor:
|
|
271
|
+
return self._server_tool_executor
|
|
272
|
+
|
|
273
|
+
toolsets = self.toolset_manager.list_server_toolsets(dal=dal)
|
|
274
|
+
|
|
275
|
+
self._server_tool_executor = ToolExecutor(toolsets)
|
|
276
|
+
|
|
277
|
+
logging.debug(
|
|
278
|
+
f"Starting AI session with tools: {[tn for tn in self._server_tool_executor.tools_by_name.keys()]}"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
return self._server_tool_executor
|
|
282
|
+
|
|
283
|
+
def create_console_toolcalling_llm(
|
|
284
|
+
self, dal: Optional[SupabaseDal] = None
|
|
285
|
+
) -> ToolCallingLLM:
|
|
286
|
+
tool_executor = self.create_console_tool_executor(dal)
|
|
287
|
+
return ToolCallingLLM(tool_executor, self.max_steps, self._get_llm())
|
|
288
|
+
|
|
289
|
+
def create_toolcalling_llm(
|
|
290
|
+
self, dal: Optional[SupabaseDal] = None, model: Optional[str] = None
|
|
291
|
+
) -> ToolCallingLLM:
|
|
292
|
+
tool_executor = self.create_tool_executor(dal)
|
|
293
|
+
return ToolCallingLLM(tool_executor, self.max_steps, self._get_llm(model))
|
|
294
|
+
|
|
295
|
+
def create_issue_investigator(
|
|
296
|
+
self, dal: Optional[SupabaseDal] = None, model: Optional[str] = None
|
|
297
|
+
) -> IssueInvestigator:
|
|
298
|
+
all_runbooks = load_builtin_runbooks()
|
|
299
|
+
for runbook_path in self.custom_runbooks:
|
|
300
|
+
all_runbooks.extend(load_runbooks_from_file(runbook_path))
|
|
301
|
+
|
|
302
|
+
runbook_manager = RunbookManager(all_runbooks)
|
|
303
|
+
tool_executor = self.create_tool_executor(dal)
|
|
304
|
+
return IssueInvestigator(
|
|
305
|
+
tool_executor, runbook_manager, self.max_steps, self._get_llm(model)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def create_console_issue_investigator(
|
|
309
|
+
self, dal: Optional[SupabaseDal] = None
|
|
310
|
+
) -> IssueInvestigator:
|
|
311
|
+
all_runbooks = load_builtin_runbooks()
|
|
312
|
+
for runbook_path in self.custom_runbooks:
|
|
313
|
+
all_runbooks.extend(load_runbooks_from_file(runbook_path))
|
|
314
|
+
|
|
315
|
+
runbook_manager = RunbookManager(all_runbooks)
|
|
316
|
+
tool_executor = self.create_console_tool_executor(dal=dal)
|
|
317
|
+
return IssueInvestigator(
|
|
318
|
+
tool_executor, runbook_manager, self.max_steps, self._get_llm()
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def validate_jira_config(self):
|
|
322
|
+
if self.jira_url is None:
|
|
323
|
+
raise ValueError("--jira-url must be specified")
|
|
324
|
+
if not (
|
|
325
|
+
self.jira_url.startswith("http://") or self.jira_url.startswith("https://")
|
|
326
|
+
):
|
|
327
|
+
raise ValueError("--jira-url must start with http:// or https://")
|
|
328
|
+
if self.jira_username is None:
|
|
329
|
+
raise ValueError("--jira-username must be specified")
|
|
330
|
+
if self.jira_api_key is None:
|
|
331
|
+
raise ValueError("--jira-api-key must be specified")
|
|
332
|
+
|
|
333
|
+
def create_jira_source(self) -> JiraSource:
|
|
334
|
+
self.validate_jira_config()
|
|
335
|
+
|
|
336
|
+
return JiraSource(
|
|
337
|
+
url=self.jira_url, # type: ignore
|
|
338
|
+
username=self.jira_username, # type: ignore
|
|
339
|
+
api_key=self.jira_api_key.get_secret_value(), # type: ignore
|
|
340
|
+
jql_query=self.jira_query, # type: ignore
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def create_jira_service_management_source(self) -> JiraServiceManagementSource:
|
|
344
|
+
self.validate_jira_config()
|
|
345
|
+
|
|
346
|
+
return JiraServiceManagementSource(
|
|
347
|
+
url=self.jira_url, # type: ignore
|
|
348
|
+
username=self.jira_username, # type: ignore
|
|
349
|
+
api_key=self.jira_api_key.get_secret_value(), # type: ignore
|
|
350
|
+
jql_query=self.jira_query, # type: ignore
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def create_github_source(self) -> GitHubSource:
|
|
354
|
+
if not self.github_url or not (
|
|
355
|
+
self.github_url.startswith("http://")
|
|
356
|
+
or self.github_url.startswith("https://")
|
|
357
|
+
):
|
|
358
|
+
raise ValueError("--github-url must start with http:// or https://")
|
|
359
|
+
if self.github_owner is None:
|
|
360
|
+
raise ValueError("--github-owner must be specified")
|
|
361
|
+
if self.github_repository is None:
|
|
362
|
+
raise ValueError("--github-repository must be specified")
|
|
363
|
+
if self.github_pat is None:
|
|
364
|
+
raise ValueError("--github-pat must be specified")
|
|
365
|
+
|
|
366
|
+
return GitHubSource(
|
|
367
|
+
url=self.github_url,
|
|
368
|
+
owner=self.github_owner,
|
|
369
|
+
pat=self.github_pat.get_secret_value(),
|
|
370
|
+
repository=self.github_repository,
|
|
371
|
+
query=self.github_query,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def create_pagerduty_source(self) -> PagerDutySource:
|
|
375
|
+
if self.pagerduty_api_key is None:
|
|
376
|
+
raise ValueError("--pagerduty-api-key must be specified")
|
|
377
|
+
|
|
378
|
+
return PagerDutySource(
|
|
379
|
+
api_key=self.pagerduty_api_key.get_secret_value(),
|
|
380
|
+
user_email=self.pagerduty_user_email, # type: ignore
|
|
381
|
+
incident_key=self.pagerduty_incident_key,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def create_opsgenie_source(self) -> OpsGenieSource:
|
|
385
|
+
if self.opsgenie_api_key is None:
|
|
386
|
+
raise ValueError("--opsgenie-api-key must be specified")
|
|
387
|
+
|
|
388
|
+
return OpsGenieSource(
|
|
389
|
+
api_key=self.opsgenie_api_key.get_secret_value(),
|
|
390
|
+
query=self.opsgenie_query, # type: ignore
|
|
391
|
+
team_integration_key=(
|
|
392
|
+
self.opsgenie_team_integration_key.get_secret_value()
|
|
393
|
+
if self.opsgenie_team_integration_key
|
|
394
|
+
else None
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def create_alertmanager_source(self) -> AlertManagerSource:
|
|
399
|
+
return AlertManagerSource(
|
|
400
|
+
url=self.alertmanager_url, # type: ignore
|
|
401
|
+
username=self.alertmanager_username,
|
|
402
|
+
alertname_filter=self.alertmanager_alertname, # type: ignore
|
|
403
|
+
label_filter=self.alertmanager_label, # type: ignore
|
|
404
|
+
filepath=self.alertmanager_file,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def create_slack_destination(self):
|
|
408
|
+
if self.slack_token is None:
|
|
409
|
+
raise ValueError("--slack-token must be specified")
|
|
410
|
+
if self.slack_channel is None:
|
|
411
|
+
raise ValueError("--slack-channel must be specified")
|
|
412
|
+
return SlackDestination(self.slack_token.get_secret_value(), self.slack_channel)
|
|
413
|
+
|
|
414
|
+
def _get_llm(self, model_key: Optional[str] = None) -> LLM:
|
|
415
|
+
api_key = self.api_key.get_secret_value() if self.api_key else None
|
|
416
|
+
model = self.model
|
|
417
|
+
model_params = {}
|
|
418
|
+
if self._model_list:
|
|
419
|
+
# get requested model or the first credentials if no model requested.
|
|
420
|
+
model_params = (
|
|
421
|
+
self._model_list.get(model_key, {}).copy()
|
|
422
|
+
if model_key
|
|
423
|
+
else next(iter(self._model_list.values())).copy()
|
|
424
|
+
)
|
|
425
|
+
api_key = model_params.pop("api_key", api_key)
|
|
426
|
+
model = model_params.pop("model", model)
|
|
427
|
+
|
|
428
|
+
return DefaultLLM(model, api_key, model_params) # type: ignore
|
|
429
|
+
|
|
430
|
+
def get_models_list(self) -> List[str]:
|
|
431
|
+
if self._model_list:
|
|
432
|
+
return json.dumps(list(self._model_list.keys())) # type: ignore
|
|
433
|
+
|
|
434
|
+
return json.dumps([self.model]) # type: ignore
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class TicketSource(BaseModel):
|
|
438
|
+
config: Config
|
|
439
|
+
output_instructions: list[str]
|
|
440
|
+
source: Union[JiraServiceManagementSource, PagerDutySource]
|
|
441
|
+
|
|
442
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class SourceFactory(BaseModel):
|
|
446
|
+
@staticmethod
|
|
447
|
+
def create_source(
|
|
448
|
+
source: SupportedTicketSources,
|
|
449
|
+
config_file: Optional[Path],
|
|
450
|
+
ticket_url: Optional[str],
|
|
451
|
+
ticket_username: Optional[str],
|
|
452
|
+
ticket_api_key: Optional[str],
|
|
453
|
+
ticket_id: Optional[str],
|
|
454
|
+
) -> TicketSource:
|
|
455
|
+
supported_sources = [s.value for s in SupportedTicketSources]
|
|
456
|
+
if source not in supported_sources:
|
|
457
|
+
raise ValueError(
|
|
458
|
+
f"Source '{source}' is not supported. Supported sources: {', '.join(supported_sources)}"
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
if source == SupportedTicketSources.JIRA_SERVICE_MANAGEMENT:
|
|
462
|
+
config = Config.load_from_file(
|
|
463
|
+
config_file=config_file,
|
|
464
|
+
api_key=None,
|
|
465
|
+
model=None,
|
|
466
|
+
max_steps=None,
|
|
467
|
+
jira_url=ticket_url,
|
|
468
|
+
jira_username=ticket_username,
|
|
469
|
+
jira_api_key=ticket_api_key,
|
|
470
|
+
jira_query=None,
|
|
471
|
+
custom_toolsets=None,
|
|
472
|
+
custom_runbooks=None,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
if not (
|
|
476
|
+
config.jira_url
|
|
477
|
+
and config.jira_username
|
|
478
|
+
and config.jira_api_key
|
|
479
|
+
and ticket_id
|
|
480
|
+
):
|
|
481
|
+
raise ValueError(
|
|
482
|
+
"URL, username, API key, and ticket ID are required for jira-service-management"
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
output_instructions = [
|
|
486
|
+
"All output links/urls must **always** be of this format : [link text here|http://your.url.here.com] and **never*** the format [link text here](http://your.url.here.com)"
|
|
487
|
+
]
|
|
488
|
+
source_instance = config.create_jira_service_management_source()
|
|
489
|
+
return TicketSource(
|
|
490
|
+
config=config,
|
|
491
|
+
output_instructions=output_instructions,
|
|
492
|
+
source=source_instance,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
elif source == SupportedTicketSources.PAGERDUTY:
|
|
496
|
+
config = Config.load_from_file(
|
|
497
|
+
config_file=config_file,
|
|
498
|
+
api_key=None,
|
|
499
|
+
model=None,
|
|
500
|
+
max_steps=None,
|
|
501
|
+
pagerduty_api_key=ticket_api_key,
|
|
502
|
+
pagerduty_user_email=ticket_username,
|
|
503
|
+
pagerduty_incident_key=None,
|
|
504
|
+
custom_toolsets=None,
|
|
505
|
+
custom_runbooks=None,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
if not (
|
|
509
|
+
config.pagerduty_user_email and config.pagerduty_api_key and ticket_id
|
|
510
|
+
):
|
|
511
|
+
raise ValueError(
|
|
512
|
+
"username, API key, and ticket ID are required for pagerduty"
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
output_instructions = [
|
|
516
|
+
"All output links/urls must **always** be of this format : \n link text here: http://your.url.here.com\n **never*** use the url the format [link text here](http://your.url.here.com)"
|
|
517
|
+
]
|
|
518
|
+
source_instance = config.create_pagerduty_source() # type: ignore
|
|
519
|
+
return TicketSource(
|
|
520
|
+
config=config,
|
|
521
|
+
output_instructions=output_instructions,
|
|
522
|
+
source=source_instance,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
else:
|
|
526
|
+
raise NotImplementedError(f"Source '{source}' is not yet implemented")
|
holmes/core/__init__.py
ADDED
|
File without changes
|