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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import logging
|
|
4
|
+
import yaml # type: ignore
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def write_json_file(json_output_file: str, json_ob_to_dump):
|
|
8
|
+
try:
|
|
9
|
+
dirname = os.path.dirname(json_output_file)
|
|
10
|
+
if dirname:
|
|
11
|
+
os.makedirs(dirname, exist_ok=True)
|
|
12
|
+
with open(json_output_file, "w", encoding="utf-8") as f:
|
|
13
|
+
json.dump(json_ob_to_dump, f, ensure_ascii=False, indent=4, default=str)
|
|
14
|
+
except Exception:
|
|
15
|
+
logging.exception("Failed to create the json file.")
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_yaml_file(
|
|
20
|
+
path: str, raise_error: bool = True, warn_not_found: bool = True
|
|
21
|
+
) -> dict:
|
|
22
|
+
try:
|
|
23
|
+
with open(path, "r", encoding="utf-8") as file:
|
|
24
|
+
parsed_yaml = yaml.safe_load(file)
|
|
25
|
+
except yaml.YAMLError as err:
|
|
26
|
+
logging.warning(f"Error parsing YAML from {path}: {err}")
|
|
27
|
+
if raise_error:
|
|
28
|
+
raise err
|
|
29
|
+
return {}
|
|
30
|
+
except FileNotFoundError as err:
|
|
31
|
+
if warn_not_found:
|
|
32
|
+
logging.warning(f"file {path} was not found.")
|
|
33
|
+
if raise_error:
|
|
34
|
+
raise err
|
|
35
|
+
return {}
|
|
36
|
+
except Exception as err:
|
|
37
|
+
logging.warning(f"Failed to open file {path}: {err}")
|
|
38
|
+
if raise_error:
|
|
39
|
+
raise err
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
if not parsed_yaml:
|
|
43
|
+
message = f"No content found in file: {path}"
|
|
44
|
+
logging.warning(message)
|
|
45
|
+
if raise_error:
|
|
46
|
+
raise ValueError(message)
|
|
47
|
+
return {}
|
|
48
|
+
|
|
49
|
+
if not isinstance(parsed_yaml, dict):
|
|
50
|
+
message = f"Invalid format: YAML file {path} does not contain a dictionary at the root."
|
|
51
|
+
logging.warning(message)
|
|
52
|
+
if raise_error:
|
|
53
|
+
raise ValueError(message)
|
|
54
|
+
return {}
|
|
55
|
+
|
|
56
|
+
return parsed_yaml
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Instructions(BaseModel):
|
|
7
|
+
instructions: List[str] = []
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def add_global_instructions_to_user_prompt(
|
|
11
|
+
user_prompt: str, global_instructions: Optional[Instructions]
|
|
12
|
+
) -> str:
|
|
13
|
+
if (
|
|
14
|
+
global_instructions
|
|
15
|
+
and global_instructions.instructions
|
|
16
|
+
and len(global_instructions.instructions[0]) > 0
|
|
17
|
+
):
|
|
18
|
+
instructions = "\n\n".join(global_instructions.instructions)
|
|
19
|
+
user_prompt += f"\n\nGlobal Instructions (use if relevant): {instructions}\n"
|
|
20
|
+
return user_prompt
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from holmes.core.supabase_dal import SupabaseDal
|
|
2
|
+
from holmes.config import Config
|
|
3
|
+
from holmes import get_version # type: ignore
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def update_holmes_status_in_db(dal: SupabaseDal, config: Config):
|
|
8
|
+
logging.info("Updating status of holmes")
|
|
9
|
+
|
|
10
|
+
if not config.cluster_name:
|
|
11
|
+
raise Exception(
|
|
12
|
+
"Cluster name is missing in the configuration. Please ensure 'CLUSTER_NAME' is defined in the environment variables, "
|
|
13
|
+
"or verify that a cluster name is provided in the Robusta configuration file."
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
dal.upsert_holmes_status(
|
|
17
|
+
{
|
|
18
|
+
"cluster_id": config.cluster_name,
|
|
19
|
+
"model": config.get_models_list(),
|
|
20
|
+
"version": get_version(),
|
|
21
|
+
}
|
|
22
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, List
|
|
4
|
+
|
|
5
|
+
import yaml # type: ignore
|
|
6
|
+
|
|
7
|
+
from holmes.config import Config
|
|
8
|
+
from holmes.core.supabase_dal import SupabaseDal
|
|
9
|
+
from holmes.core.tools import Toolset, ToolsetDBModel
|
|
10
|
+
from holmes.plugins.prompts import load_and_render_prompt
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def log_toolsets_statuses(toolsets: List[Toolset]):
|
|
14
|
+
enabled_toolsets = [
|
|
15
|
+
toolset.name for toolset in toolsets if toolset.status.value == "enabled"
|
|
16
|
+
]
|
|
17
|
+
disabled_toolsets = [
|
|
18
|
+
toolset.name for toolset in toolsets if toolset.status.value != "enabled"
|
|
19
|
+
]
|
|
20
|
+
logging.info(f"Enabled toolsets: {enabled_toolsets}")
|
|
21
|
+
logging.info(f"Disabled toolsets: {disabled_toolsets}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def holmes_sync_toolsets_status(dal: SupabaseDal, config: Config) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Method for synchronizing toolsets with the database:
|
|
27
|
+
1) Fetch all built-in toolsets from the holmes/plugins/toolsets directory
|
|
28
|
+
2) Load custom toolsets defined in /etc/holmes/config/custom_toolset.yaml
|
|
29
|
+
3) Override default toolsets with corresponding custom configurations
|
|
30
|
+
and add any new custom toolsets that are not part of the defaults
|
|
31
|
+
4) Run the check_prerequisites method for each toolset
|
|
32
|
+
5) Use sync_toolsets to upsert toolset's status and remove toolsets that are not loaded from configs or folder with default directory
|
|
33
|
+
"""
|
|
34
|
+
tool_executor = config.create_tool_executor(dal)
|
|
35
|
+
|
|
36
|
+
if not config.cluster_name:
|
|
37
|
+
raise Exception(
|
|
38
|
+
"Cluster name is missing in the configuration. Please ensure 'CLUSTER_NAME' is defined in the environment variables, "
|
|
39
|
+
"or verify that a cluster name is provided in the Robusta configuration file."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
db_toolsets = []
|
|
43
|
+
updated_at = datetime.now().isoformat()
|
|
44
|
+
for toolset in tool_executor.toolsets:
|
|
45
|
+
# hiding disabled experimental toolsets from the docs
|
|
46
|
+
if toolset.experimental and not toolset.enabled:
|
|
47
|
+
continue
|
|
48
|
+
if not toolset.installation_instructions:
|
|
49
|
+
instructions = render_default_installation_instructions_for_toolset(toolset)
|
|
50
|
+
toolset.installation_instructions = instructions
|
|
51
|
+
db_toolsets.append(
|
|
52
|
+
ToolsetDBModel(
|
|
53
|
+
**toolset.model_dump(exclude_none=True),
|
|
54
|
+
toolset_name=toolset.name,
|
|
55
|
+
cluster_id=config.cluster_name,
|
|
56
|
+
account_id=dal.account_id,
|
|
57
|
+
updated_at=updated_at,
|
|
58
|
+
).model_dump()
|
|
59
|
+
)
|
|
60
|
+
dal.sync_toolsets(db_toolsets, config.cluster_name)
|
|
61
|
+
log_toolsets_statuses(tool_executor.toolsets)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def render_default_installation_instructions_for_toolset(toolset: Toolset) -> str:
|
|
65
|
+
env_vars = toolset.get_environment_variables()
|
|
66
|
+
context: dict[str, Any] = {
|
|
67
|
+
"env_vars": env_vars if env_vars else [],
|
|
68
|
+
"toolset_name": toolset.name,
|
|
69
|
+
"is_default": toolset.is_default,
|
|
70
|
+
"enabled": toolset.enabled,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
example_config = toolset.get_example_config()
|
|
74
|
+
if example_config:
|
|
75
|
+
context["example_config"] = yaml.dump(example_config)
|
|
76
|
+
|
|
77
|
+
installation_instructions = load_and_render_prompt(
|
|
78
|
+
"file://holmes/utils/default_toolset_installation_guide.jinja2", context
|
|
79
|
+
)
|
|
80
|
+
return installation_instructions
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# based on https://github.com/kostyachum/python-markdown-plain-text/blob/main/markdown_plain_text/extention.py
|
|
2
|
+
# MIT licensed
|
|
3
|
+
from markdown import Extension, Markdown # type: ignore
|
|
4
|
+
|
|
5
|
+
from xml.etree.ElementTree import ProcessingInstruction
|
|
6
|
+
from xml.etree.ElementTree import Comment, ElementTree
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _serialize_plain_text(write, elem):
|
|
10
|
+
tag = elem.tag
|
|
11
|
+
text = elem.text
|
|
12
|
+
if tag is Comment:
|
|
13
|
+
pass
|
|
14
|
+
elif tag is ProcessingInstruction:
|
|
15
|
+
pass
|
|
16
|
+
elif tag is None:
|
|
17
|
+
if text:
|
|
18
|
+
write(text)
|
|
19
|
+
for e in elem:
|
|
20
|
+
_serialize_plain_text(write, e)
|
|
21
|
+
else:
|
|
22
|
+
if text:
|
|
23
|
+
if tag.lower() not in ["script", "style"]:
|
|
24
|
+
write(text)
|
|
25
|
+
for e in elem:
|
|
26
|
+
_serialize_plain_text(write, e)
|
|
27
|
+
|
|
28
|
+
if elem.tail:
|
|
29
|
+
write(elem.tail)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _write_plain_text(root):
|
|
33
|
+
assert root is not None
|
|
34
|
+
data = []
|
|
35
|
+
write = data.append
|
|
36
|
+
_serialize_plain_text(write, root)
|
|
37
|
+
return "".join(data)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def to_plain_text(element):
|
|
41
|
+
return _write_plain_text(ElementTree(element).getroot())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PlainTextExtension(Extension):
|
|
45
|
+
def extendMarkdown(self, md):
|
|
46
|
+
md.serializer = to_plain_text
|
|
47
|
+
md.stripTopLevelTags = False
|
|
48
|
+
|
|
49
|
+
# Extention register actually runs before the format is set and it ends up rewriting serializer that we have just changed
|
|
50
|
+
md.set_output_format = lambda x: x
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def markdown_to_plain_text(text):
|
|
54
|
+
md = Markdown(extensions=[PlainTextExtension()])
|
|
55
|
+
return md.convert(text)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Annotated, Any, Dict, List, Optional, Tuple, Type, Union
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from benedict import benedict # type: ignore
|
|
7
|
+
from pydantic import BaseModel, BeforeValidator, ConfigDict, ValidationError
|
|
8
|
+
|
|
9
|
+
from holmes.plugins.prompts import load_prompt
|
|
10
|
+
|
|
11
|
+
PromptField = Annotated[str, BeforeValidator(lambda v: load_prompt(v))]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RobustaBaseConfig(BaseModel):
|
|
15
|
+
model_config = ConfigDict(extra="forbid", validate_default=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def loc_to_dot_sep(loc: Tuple[Union[str, int], ...]) -> str:
|
|
19
|
+
path = ""
|
|
20
|
+
for i, x in enumerate(loc):
|
|
21
|
+
if isinstance(x, str):
|
|
22
|
+
if i > 0:
|
|
23
|
+
path += "."
|
|
24
|
+
path += x
|
|
25
|
+
elif isinstance(x, int):
|
|
26
|
+
path += f"[{x}]"
|
|
27
|
+
else:
|
|
28
|
+
raise TypeError("Unexpected type")
|
|
29
|
+
return path
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def convert_errors(e: ValidationError) -> List[Dict[str, Any]]:
|
|
33
|
+
new_errors: List[Dict[str, Any]] = e.errors() # type: ignore
|
|
34
|
+
for error in new_errors:
|
|
35
|
+
error["loc"] = loc_to_dot_sep(error["loc"])
|
|
36
|
+
return new_errors
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_model_from_file(
|
|
40
|
+
model: Type[BaseModel], file_path: Path, yaml_path: Optional[str] = None
|
|
41
|
+
):
|
|
42
|
+
try:
|
|
43
|
+
contents = benedict(file_path, format="yaml")
|
|
44
|
+
if yaml_path is not None:
|
|
45
|
+
contents = contents[yaml_path]
|
|
46
|
+
return model.model_validate(contents)
|
|
47
|
+
except ValidationError as e:
|
|
48
|
+
print(e)
|
|
49
|
+
bad_fields = [e["loc"] for e in convert_errors(e)]
|
|
50
|
+
typer.secho(
|
|
51
|
+
f"Invalid config file at {file_path}. Check the fields {bad_fields}.\nSee detailed errors above.",
|
|
52
|
+
fg="red",
|
|
53
|
+
)
|
|
54
|
+
sys.exit()
|
holmes/utils/robusta.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from holmes.config import Config
|
|
2
|
+
from holmes.core.supabase_dal import SupabaseDal
|
|
3
|
+
from pydantic import SecretStr
|
|
4
|
+
from holmes.common.env_vars import load_bool
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load_robusta_api_key(dal: SupabaseDal, config: Config):
|
|
8
|
+
if load_bool("ROBUSTA_AI", False):
|
|
9
|
+
account_id, token = dal.get_ai_credentials()
|
|
10
|
+
config.api_key = SecretStr(f"{account_id} {token}")
|
holmes/utils/tags.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from typing_extensions import Dict, List
|
|
4
|
+
import re
|
|
5
|
+
import json
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def stringify_tag(tag: Dict[str, str]) -> Optional[str]:
|
|
10
|
+
"""
|
|
11
|
+
This serializes a dictionary into something more readable to the LLM.
|
|
12
|
+
Although I have not seen much difference in quality of output, in theory this can help the LLM
|
|
13
|
+
understand better how to link the tag values with the tools.
|
|
14
|
+
|
|
15
|
+
Here are some examples of formatting (more can be found in the test for this function):
|
|
16
|
+
- { "type": "node", "name": "my-node" }
|
|
17
|
+
-> "node my-node"
|
|
18
|
+
- { "type": "issue", "id": "issue-id", "name": "KubeJobFailed", "subject_namespace": "my-namespace", "subject_name": "my-pod" }
|
|
19
|
+
-> issue issue-id (name=KubeJobFailed, subject_namespace=my-namespace, subject_name=my-pod)
|
|
20
|
+
"""
|
|
21
|
+
type = tag.pop("type")
|
|
22
|
+
if not type:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
key = ""
|
|
26
|
+
if tag.get("id"):
|
|
27
|
+
key = tag.pop("id")
|
|
28
|
+
elif tag.get("name"):
|
|
29
|
+
key = tag.pop("name")
|
|
30
|
+
|
|
31
|
+
if not key:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
formatted_string = f"{type} {key}"
|
|
35
|
+
|
|
36
|
+
if len(tag) > 0:
|
|
37
|
+
keyVals = []
|
|
38
|
+
for k, v in tag.items():
|
|
39
|
+
keyVals.append(f"{k}={v}")
|
|
40
|
+
formatted_string += f" ({', '.join(keyVals)})"
|
|
41
|
+
|
|
42
|
+
return formatted_string
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def format_tags_in_string(user_prompt: str) -> str:
|
|
46
|
+
"""
|
|
47
|
+
Formats the tags included in a user's message.
|
|
48
|
+
E.g.
|
|
49
|
+
'how many pods are running on << { "type": "node", "name": "my-node" } >>?'
|
|
50
|
+
-> 'how many pods are running on node my-node?'
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
pattern = r"<<(.*?)>>"
|
|
54
|
+
|
|
55
|
+
def replace_match(match):
|
|
56
|
+
try:
|
|
57
|
+
json_str = match.group(1)
|
|
58
|
+
json_obj = json.loads(json_str)
|
|
59
|
+
formatted = stringify_tag(json_obj)
|
|
60
|
+
return formatted if formatted else match.group(0)
|
|
61
|
+
except (json.JSONDecodeError, AttributeError):
|
|
62
|
+
logging.warning(f"Failed to parse tag in string: {user_prompt}")
|
|
63
|
+
return match.group(0)
|
|
64
|
+
|
|
65
|
+
return re.sub(pattern, replace_match, user_prompt)
|
|
66
|
+
except Exception:
|
|
67
|
+
logging.warning(f"Failed to parse string: {user_prompt}")
|
|
68
|
+
return user_prompt
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_messages_tags(messages: List[Dict[str, str]]) -> List[Dict[str, str]]:
|
|
72
|
+
"""
|
|
73
|
+
Parses the user messages for tags and format these.
|
|
74
|
+
System messages and llm responses are ignored and left as-is
|
|
75
|
+
|
|
76
|
+
This method returns a shallow copy of the messages list with the exception
|
|
77
|
+
of the messages that have been parsed.
|
|
78
|
+
"""
|
|
79
|
+
formatted_messages = []
|
|
80
|
+
for message in messages:
|
|
81
|
+
original_message = message.get("content")
|
|
82
|
+
if message.get("role") == "user" and original_message:
|
|
83
|
+
formatted_str = format_tags_in_string(original_message)
|
|
84
|
+
if formatted_str != message.get("content"):
|
|
85
|
+
formatted_message = deepcopy(message)
|
|
86
|
+
formatted_message["content"] = formatted_str
|
|
87
|
+
formatted_messages.append(formatted_message)
|
|
88
|
+
logging.debug(
|
|
89
|
+
f"Message with tags '{original_message}' formatted to '{formatted_message}'"
|
|
90
|
+
)
|
|
91
|
+
else:
|
|
92
|
+
formatted_messages.append(message)
|
|
93
|
+
|
|
94
|
+
else:
|
|
95
|
+
formatted_messages.append(message)
|
|
96
|
+
|
|
97
|
+
return formatted_messages
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Robusta Dev Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|