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,418 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, List, Optional
|
|
5
|
+
|
|
6
|
+
from benedict import benedict
|
|
7
|
+
from pydantic import FilePath
|
|
8
|
+
|
|
9
|
+
from holmes.core.supabase_dal import SupabaseDal
|
|
10
|
+
from holmes.core.tools import Toolset, ToolsetStatusEnum, ToolsetTag, ToolsetType
|
|
11
|
+
from holmes.plugins.toolsets import load_builtin_toolsets, load_toolsets_from_config
|
|
12
|
+
from holmes.utils.definitions import CUSTOM_TOOLSET_LOCATION
|
|
13
|
+
|
|
14
|
+
DEFAULT_TOOLSET_STATUS_LOCATION = os.path.expanduser("~/.holmes/toolsets_status.json")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ToolsetManager:
|
|
18
|
+
"""
|
|
19
|
+
ToolsetManager is responsible for managing toolset locally.
|
|
20
|
+
It can refresh the status of all toolsets and cache the status to a file.
|
|
21
|
+
It also provides methods to get toolsets by name and to get the list of all toolsets.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
toolsets: Optional[dict[str, dict[str, Any]]] = None,
|
|
27
|
+
custom_toolsets: Optional[List[FilePath]] = None,
|
|
28
|
+
custom_toolsets_from_cli: Optional[List[FilePath]] = None,
|
|
29
|
+
toolset_status_location: Optional[FilePath] = None,
|
|
30
|
+
):
|
|
31
|
+
self.toolsets = toolsets
|
|
32
|
+
self.custom_toolsets = custom_toolsets
|
|
33
|
+
|
|
34
|
+
if toolset_status_location is None:
|
|
35
|
+
toolset_status_location = FilePath(DEFAULT_TOOLSET_STATUS_LOCATION)
|
|
36
|
+
|
|
37
|
+
# holmes container uses CUSTOM_TOOLSET_LOCATION to load custom toolsets
|
|
38
|
+
if os.path.isfile(CUSTOM_TOOLSET_LOCATION):
|
|
39
|
+
if self.custom_toolsets is None:
|
|
40
|
+
self.custom_toolsets = []
|
|
41
|
+
self.custom_toolsets.append(FilePath(CUSTOM_TOOLSET_LOCATION))
|
|
42
|
+
|
|
43
|
+
self.custom_toolsets_from_cli = custom_toolsets_from_cli
|
|
44
|
+
self.toolset_status_location = toolset_status_location
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def cli_tool_tags(self) -> List[ToolsetTag]:
|
|
48
|
+
"""
|
|
49
|
+
Returns the list of toolset tags that are relevant for CLI tools.
|
|
50
|
+
"""
|
|
51
|
+
return [ToolsetTag.CORE, ToolsetTag.CLI]
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def server_tool_tags(self) -> List[ToolsetTag]:
|
|
55
|
+
"""
|
|
56
|
+
Returns the list of toolset tags that are relevant for server tools.
|
|
57
|
+
"""
|
|
58
|
+
return [ToolsetTag.CORE, ToolsetTag.CLUSTER]
|
|
59
|
+
|
|
60
|
+
def _list_all_toolsets(
|
|
61
|
+
self,
|
|
62
|
+
dal: Optional[SupabaseDal] = None,
|
|
63
|
+
check_prerequisites=True,
|
|
64
|
+
enable_all_toolsets=False,
|
|
65
|
+
toolset_tags: Optional[List[ToolsetTag]] = None,
|
|
66
|
+
) -> List[Toolset]:
|
|
67
|
+
"""
|
|
68
|
+
List all built-in and custom toolsets.
|
|
69
|
+
|
|
70
|
+
The method loads toolsets in this order, with later sources overriding earlier ones:
|
|
71
|
+
1. Built-in toolsets
|
|
72
|
+
2. Toolsets defined in self.toolsets can override both built-in and add new custom toolsets
|
|
73
|
+
3. custom toolset from config can override both built-in and add new custom toolsets # for backward compatibility
|
|
74
|
+
"""
|
|
75
|
+
# Load built-in toolsets
|
|
76
|
+
builtin_toolsets = load_builtin_toolsets(dal)
|
|
77
|
+
toolsets_by_name: dict[str, Toolset] = {
|
|
78
|
+
toolset.name: toolset for toolset in builtin_toolsets
|
|
79
|
+
}
|
|
80
|
+
builtin_toolsets_names = list(toolsets_by_name.keys())
|
|
81
|
+
|
|
82
|
+
if enable_all_toolsets:
|
|
83
|
+
for toolset in toolsets_by_name.values():
|
|
84
|
+
toolset.enabled = True
|
|
85
|
+
|
|
86
|
+
# build-in toolset is enabled when it's explicitly enabled in the toolset or custom toolset config
|
|
87
|
+
if self.toolsets is not None:
|
|
88
|
+
toolsets_from_config = self._load_toolsets_from_config(
|
|
89
|
+
self.toolsets, builtin_toolsets_names, dal
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if toolsets_from_config:
|
|
93
|
+
self.add_or_merge_onto_toolsets(
|
|
94
|
+
toolsets_from_config,
|
|
95
|
+
toolsets_by_name,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# custom toolset should not override built-in toolsets
|
|
99
|
+
# to test the new change of built-in toolset, we should make code change and re-compile the program
|
|
100
|
+
custom_toolsets = self.load_custom_toolsets(builtin_toolsets_names)
|
|
101
|
+
self.add_or_merge_onto_toolsets(
|
|
102
|
+
custom_toolsets,
|
|
103
|
+
toolsets_by_name,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if toolset_tags is not None:
|
|
107
|
+
toolsets_by_name = {
|
|
108
|
+
name: toolset
|
|
109
|
+
for name, toolset in toolsets_by_name.items()
|
|
110
|
+
if any(tag in toolset_tags for tag in toolset.tags)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# check_prerequisites against each enabled toolset
|
|
114
|
+
if not check_prerequisites:
|
|
115
|
+
return list(toolsets_by_name.values())
|
|
116
|
+
for _, toolset in toolsets_by_name.items():
|
|
117
|
+
if toolset.enabled:
|
|
118
|
+
toolset.check_prerequisites()
|
|
119
|
+
else:
|
|
120
|
+
toolset.status = ToolsetStatusEnum.DISABLED
|
|
121
|
+
|
|
122
|
+
return list(toolsets_by_name.values())
|
|
123
|
+
|
|
124
|
+
def _load_toolsets_from_config(
|
|
125
|
+
self,
|
|
126
|
+
toolsets: dict[str, dict[str, Any]],
|
|
127
|
+
builtin_toolset_names: list[str],
|
|
128
|
+
dal: Optional[SupabaseDal] = None,
|
|
129
|
+
) -> List[Toolset]:
|
|
130
|
+
if toolsets is None:
|
|
131
|
+
logging.debug("No toolsets configured, skipping loading toolsets")
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
builtin_toolsets_dict: dict[str, dict[str, Any]] = {}
|
|
135
|
+
custom_toolsets_dict: dict[str, dict[str, Any]] = {}
|
|
136
|
+
for toolset_name, toolset_config in toolsets.items():
|
|
137
|
+
if toolset_name in builtin_toolset_names:
|
|
138
|
+
# build-in types was assigned when loaded
|
|
139
|
+
builtin_toolsets_dict[toolset_name] = toolset_config
|
|
140
|
+
else:
|
|
141
|
+
if toolset_config.get("type") is None:
|
|
142
|
+
toolset_config["type"] = ToolsetType.CUSTOMIZED.value
|
|
143
|
+
# custom toolsets defaults to enabled when not explicitly disabled
|
|
144
|
+
if toolset_config.get("enabled", True) is False:
|
|
145
|
+
toolset_config["enabled"] = False
|
|
146
|
+
else:
|
|
147
|
+
toolset_config["enabled"] = True
|
|
148
|
+
custom_toolsets_dict[toolset_name] = toolset_config
|
|
149
|
+
|
|
150
|
+
# built-in toolsets and built-in MCP servers in the config can override the existing fields of built-in toolsets
|
|
151
|
+
builtin_toolsets = load_toolsets_from_config(
|
|
152
|
+
builtin_toolsets_dict, strict_check=False
|
|
153
|
+
)
|
|
154
|
+
# custom toolsets or MCP servers are expected to defined required fields
|
|
155
|
+
custom_toolsets = load_toolsets_from_config(
|
|
156
|
+
toolsets=custom_toolsets_dict, strict_check=True
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return builtin_toolsets + custom_toolsets
|
|
160
|
+
|
|
161
|
+
def refresh_toolset_status(
|
|
162
|
+
self,
|
|
163
|
+
dal: Optional[SupabaseDal] = None,
|
|
164
|
+
enable_all_toolsets=False,
|
|
165
|
+
toolset_tags: Optional[List[ToolsetTag]] = None,
|
|
166
|
+
):
|
|
167
|
+
"""
|
|
168
|
+
Refresh the status of all toolsets and cache the status to a file.
|
|
169
|
+
Loading cached toolsets status saves the time for runtime tool executor checking the status of each toolset
|
|
170
|
+
|
|
171
|
+
enabled toolset when:
|
|
172
|
+
- build-in toolset specified in the config and not explicitly disabled
|
|
173
|
+
- custom toolset not explicitly disabled
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
all_toolsets = self._list_all_toolsets(
|
|
177
|
+
dal=dal,
|
|
178
|
+
check_prerequisites=True,
|
|
179
|
+
enable_all_toolsets=enable_all_toolsets,
|
|
180
|
+
toolset_tags=toolset_tags,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if self.toolset_status_location and not os.path.exists(
|
|
184
|
+
os.path.dirname(self.toolset_status_location)
|
|
185
|
+
):
|
|
186
|
+
os.makedirs(os.path.dirname(self.toolset_status_location))
|
|
187
|
+
with open(self.toolset_status_location, "w") as f:
|
|
188
|
+
toolset_status = [
|
|
189
|
+
json.loads(
|
|
190
|
+
toolset.model_dump_json(
|
|
191
|
+
include={"name", "status", "enabled", "type", "path", "error"}
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
for toolset in all_toolsets
|
|
195
|
+
]
|
|
196
|
+
json.dump(toolset_status, f, indent=2)
|
|
197
|
+
logging.info(f"Toolset statuses are cached to {self.toolset_status_location}")
|
|
198
|
+
|
|
199
|
+
def load_toolset_with_status(
|
|
200
|
+
self,
|
|
201
|
+
dal: Optional[SupabaseDal] = None,
|
|
202
|
+
refresh_status: bool = False,
|
|
203
|
+
enable_all_toolsets=False,
|
|
204
|
+
toolset_tags: Optional[List[ToolsetTag]] = None,
|
|
205
|
+
) -> List[Toolset]:
|
|
206
|
+
"""
|
|
207
|
+
Load the toolset with status from the cache file.
|
|
208
|
+
1. load the built-in toolsets
|
|
209
|
+
2. load the custom toolsets from config, and override the built-in toolsets
|
|
210
|
+
3. load the custom toolsets from CLI, and raise error if the custom toolset from CLI conflicts with existing toolsets
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
if not os.path.exists(self.toolset_status_location) or refresh_status:
|
|
214
|
+
logging.info("Refreshing available datasources (toolsets)")
|
|
215
|
+
self.refresh_toolset_status(
|
|
216
|
+
dal, enable_all_toolsets=enable_all_toolsets, toolset_tags=toolset_tags
|
|
217
|
+
)
|
|
218
|
+
using_cached = False
|
|
219
|
+
else:
|
|
220
|
+
using_cached = True
|
|
221
|
+
|
|
222
|
+
cached_toolsets: List[dict[str, Any]] = []
|
|
223
|
+
with open(self.toolset_status_location, "r") as f:
|
|
224
|
+
cached_toolsets = json.load(f)
|
|
225
|
+
|
|
226
|
+
# load status from cached file and update the toolset details
|
|
227
|
+
toolsets_status_by_name: dict[str, dict[str, Any]] = {
|
|
228
|
+
cached_toolset["name"]: cached_toolset for cached_toolset in cached_toolsets
|
|
229
|
+
}
|
|
230
|
+
all_toolsets_with_status = self._list_all_toolsets(
|
|
231
|
+
dal=dal, check_prerequisites=False, toolset_tags=toolset_tags
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
for toolset in all_toolsets_with_status:
|
|
235
|
+
if toolset.name in toolsets_status_by_name:
|
|
236
|
+
# Update the status and error from the cached status
|
|
237
|
+
cached_status = toolsets_status_by_name[toolset.name]
|
|
238
|
+
toolset.status = ToolsetStatusEnum(cached_status["status"])
|
|
239
|
+
toolset.error = cached_status.get("error", None)
|
|
240
|
+
toolset.enabled = cached_status.get("enabled", True)
|
|
241
|
+
toolset.type = ToolsetType(
|
|
242
|
+
cached_status.get("type", ToolsetType.BUILTIN)
|
|
243
|
+
)
|
|
244
|
+
toolset.path = cached_status.get("path", None)
|
|
245
|
+
# check prerequisites for only enabled toolset when the toolset is loaded from cache
|
|
246
|
+
if (
|
|
247
|
+
toolset.enabled
|
|
248
|
+
and toolset.status == ToolsetStatusEnum.ENABLED
|
|
249
|
+
and using_cached
|
|
250
|
+
):
|
|
251
|
+
toolset.check_prerequisites() # type: ignore
|
|
252
|
+
|
|
253
|
+
# CLI custom toolsets status are not cached, and their prerequisites are always checked whenever the CLI runs.
|
|
254
|
+
custom_toolsets_from_cli = self._load_toolsets_from_paths(
|
|
255
|
+
self.custom_toolsets_from_cli,
|
|
256
|
+
list(toolsets_status_by_name.keys()),
|
|
257
|
+
check_conflict_default=True,
|
|
258
|
+
)
|
|
259
|
+
# custom toolsets from cli as experimental toolset should not override custom toolsets from config
|
|
260
|
+
for custom_toolset_from_cli in custom_toolsets_from_cli:
|
|
261
|
+
if custom_toolset_from_cli.name in toolsets_status_by_name:
|
|
262
|
+
raise ValueError(
|
|
263
|
+
f"Toolset {custom_toolset_from_cli.name} from cli is already defined in existing toolset"
|
|
264
|
+
)
|
|
265
|
+
# status of custom toolsets from cli is not cached, and we need to check prerequisites every time the cli runs.
|
|
266
|
+
custom_toolset_from_cli.check_prerequisites()
|
|
267
|
+
|
|
268
|
+
all_toolsets_with_status.extend(custom_toolsets_from_cli)
|
|
269
|
+
if using_cached:
|
|
270
|
+
num_available_toolsets = len(
|
|
271
|
+
[toolset for toolset in all_toolsets_with_status if toolset.enabled]
|
|
272
|
+
)
|
|
273
|
+
logging.info(
|
|
274
|
+
f"Using {num_available_toolsets} datasources (toolsets). To refresh: `holmes toolset refresh`"
|
|
275
|
+
)
|
|
276
|
+
return all_toolsets_with_status
|
|
277
|
+
|
|
278
|
+
def list_console_toolsets(
|
|
279
|
+
self, dal: Optional[SupabaseDal] = None, refresh_status=False
|
|
280
|
+
) -> List[Toolset]:
|
|
281
|
+
"""
|
|
282
|
+
List all enabled toolsets that cli tools can use.
|
|
283
|
+
|
|
284
|
+
listing console toolset does not refresh toolset status by default, and expects the status to be
|
|
285
|
+
refreshed specifically and cached locally.
|
|
286
|
+
"""
|
|
287
|
+
toolsets_with_status = self.load_toolset_with_status(
|
|
288
|
+
dal,
|
|
289
|
+
refresh_status=refresh_status,
|
|
290
|
+
enable_all_toolsets=True,
|
|
291
|
+
toolset_tags=self.cli_tool_tags,
|
|
292
|
+
)
|
|
293
|
+
return toolsets_with_status
|
|
294
|
+
|
|
295
|
+
# TODO(mainred): cache and refresh periodically toolset status for server if necessary
|
|
296
|
+
def list_server_toolsets(
|
|
297
|
+
self, dal: Optional[SupabaseDal] = None, refresh_status=True
|
|
298
|
+
) -> List[Toolset]:
|
|
299
|
+
"""
|
|
300
|
+
List all toolsets that are enabled and have the server tool tags.
|
|
301
|
+
|
|
302
|
+
server will sync the status of toolsets to DB during startup instead of local cache.
|
|
303
|
+
Refreshing the status by default for server to keep the toolsets up-to-date instead of relying on local cache.
|
|
304
|
+
"""
|
|
305
|
+
toolsets_with_status = self._list_all_toolsets(
|
|
306
|
+
dal,
|
|
307
|
+
check_prerequisites=True,
|
|
308
|
+
enable_all_toolsets=False,
|
|
309
|
+
toolset_tags=self.server_tool_tags,
|
|
310
|
+
)
|
|
311
|
+
return toolsets_with_status
|
|
312
|
+
|
|
313
|
+
def _load_toolsets_from_paths(
|
|
314
|
+
self,
|
|
315
|
+
toolset_paths: Optional[List[FilePath]],
|
|
316
|
+
builtin_toolsets_names: list[str],
|
|
317
|
+
check_conflict_default: bool = False,
|
|
318
|
+
) -> List[Toolset]:
|
|
319
|
+
if not toolset_paths:
|
|
320
|
+
logging.debug("No toolsets configured, skipping loading toolsets")
|
|
321
|
+
return []
|
|
322
|
+
|
|
323
|
+
loaded_custom_toolsets: List[Toolset] = []
|
|
324
|
+
for toolset_path in toolset_paths:
|
|
325
|
+
if not os.path.isfile(toolset_path):
|
|
326
|
+
raise FileNotFoundError(f"toolset file {toolset_path} does not exist")
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
parsed_yaml = benedict(toolset_path)
|
|
330
|
+
except Exception as e:
|
|
331
|
+
raise ValueError(
|
|
332
|
+
f"Failed to load toolsets from {toolset_path}, error: {e}"
|
|
333
|
+
) from e
|
|
334
|
+
toolsets_config: dict[str, dict[str, Any]] = parsed_yaml.get("toolsets", {})
|
|
335
|
+
mcp_config: dict[str, dict[str, Any]] = parsed_yaml.get("mcp_servers", {})
|
|
336
|
+
|
|
337
|
+
for server_config in mcp_config.values():
|
|
338
|
+
server_config["type"] = ToolsetType.MCP
|
|
339
|
+
|
|
340
|
+
for toolset_config in toolsets_config.values():
|
|
341
|
+
toolset_config["path"] = toolset_path
|
|
342
|
+
|
|
343
|
+
toolsets_config.update(mcp_config)
|
|
344
|
+
|
|
345
|
+
if not toolsets_config:
|
|
346
|
+
raise ValueError(
|
|
347
|
+
f"No 'toolsets' or 'mcp_servers' key found in: {toolset_path}"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
toolsets_from_config = self._load_toolsets_from_config(
|
|
351
|
+
toolsets_config, builtin_toolsets_names
|
|
352
|
+
)
|
|
353
|
+
if check_conflict_default:
|
|
354
|
+
for toolset in toolsets_from_config:
|
|
355
|
+
if toolset.name in builtin_toolsets_names:
|
|
356
|
+
raise Exception(
|
|
357
|
+
f"Toolset {toolset.name} is already defined in the built-in toolsets. "
|
|
358
|
+
"Please rename the custom toolset or remove it from the custom toolsets configuration."
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
loaded_custom_toolsets.extend(toolsets_from_config)
|
|
362
|
+
|
|
363
|
+
return loaded_custom_toolsets
|
|
364
|
+
|
|
365
|
+
def load_custom_toolsets(self, builtin_toolsets_names: list[str]) -> list[Toolset]:
|
|
366
|
+
"""
|
|
367
|
+
Loads toolsets config from custom toolset path with YAMLToolset class.
|
|
368
|
+
|
|
369
|
+
Example configuration:
|
|
370
|
+
# override the built-in toolsets with custom toolsets
|
|
371
|
+
kubernetes/logs:
|
|
372
|
+
enabled: false
|
|
373
|
+
|
|
374
|
+
# define a custom toolset with strictly defined fields
|
|
375
|
+
test/configurations:
|
|
376
|
+
enabled: true
|
|
377
|
+
icon_url: "example.com"
|
|
378
|
+
description: "test_description"
|
|
379
|
+
docs_url: "https://docs.docker.com/"
|
|
380
|
+
prerequisites:
|
|
381
|
+
- env:
|
|
382
|
+
- API_ENDPOINT
|
|
383
|
+
- command: "curl ${API_ENDPOINT}"
|
|
384
|
+
additional_instructions: "jq -r '.result.results[].userData | fromjson | .text | fromjson | .log'"
|
|
385
|
+
tools:
|
|
386
|
+
- name: "curl_example"
|
|
387
|
+
description: "Perform a curl request to example.com using variables"
|
|
388
|
+
command: "curl -X GET '{{api_endpoint}}?query={{ query_param }}' "
|
|
389
|
+
"""
|
|
390
|
+
if not self.custom_toolsets and not self.custom_toolsets_from_cli:
|
|
391
|
+
logging.debug(
|
|
392
|
+
"No custom toolsets configured, skipping loading custom toolsets"
|
|
393
|
+
)
|
|
394
|
+
return []
|
|
395
|
+
|
|
396
|
+
loaded_custom_toolsets: List[Toolset] = []
|
|
397
|
+
custom_toolsets = self._load_toolsets_from_paths(
|
|
398
|
+
self.custom_toolsets, builtin_toolsets_names
|
|
399
|
+
)
|
|
400
|
+
loaded_custom_toolsets.extend(custom_toolsets)
|
|
401
|
+
|
|
402
|
+
return loaded_custom_toolsets
|
|
403
|
+
|
|
404
|
+
def add_or_merge_onto_toolsets(
|
|
405
|
+
self,
|
|
406
|
+
new_toolsets: list[Toolset],
|
|
407
|
+
existing_toolsets_by_name: dict[str, Toolset],
|
|
408
|
+
) -> None:
|
|
409
|
+
"""
|
|
410
|
+
Add new or merge toolsets onto existing toolsets.
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
for new_toolset in new_toolsets:
|
|
414
|
+
if new_toolset.name in existing_toolsets_by_name.keys():
|
|
415
|
+
existing_toolsets_by_name[new_toolset.name].override_with(new_toolset)
|
|
416
|
+
else:
|
|
417
|
+
existing_toolsets_by_name[new_toolset.name] = new_toolset
|
|
418
|
+
existing_toolsets_by_name[new_toolset.name] = new_toolset
|
holmes/interactive.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from prompt_toolkit import PromptSession
|
|
8
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
9
|
+
from prompt_toolkit.history import InMemoryHistory
|
|
10
|
+
from prompt_toolkit.styles import Style
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.markdown import Markdown, Panel
|
|
13
|
+
|
|
14
|
+
from holmes.core.prompt import build_initial_ask_messages
|
|
15
|
+
from holmes.core.tool_calling_llm import ToolCallingLLM, ToolCallResult
|
|
16
|
+
from holmes.core.tools import pretty_print_toolset_status
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SlashCommands(Enum):
|
|
20
|
+
EXIT = "/exit"
|
|
21
|
+
HELP = "/help"
|
|
22
|
+
RESET = "/reset"
|
|
23
|
+
TOOLS_CONFIG = "/config"
|
|
24
|
+
TOGGLE_TOOL_OUTPUT = "/toggle-output"
|
|
25
|
+
SHOW_OUTPUT = "/output"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
SLASH_COMMANDS_REFERENCE = {
|
|
29
|
+
SlashCommands.EXIT.value: "Exit interactive mode",
|
|
30
|
+
SlashCommands.HELP.value: "Show help message with all commands",
|
|
31
|
+
SlashCommands.RESET.value: "Reset the conversation context",
|
|
32
|
+
SlashCommands.TOOLS_CONFIG.value: "Show available toolsets and their status",
|
|
33
|
+
SlashCommands.TOGGLE_TOOL_OUTPUT.value: "Toggle tool output display on/off",
|
|
34
|
+
SlashCommands.SHOW_OUTPUT.value: "Show all tool outputs from last response",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ALL_SLASH_COMMANDS = [cmd.value for cmd in SlashCommands]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SlashCommandCompleter(Completer):
|
|
41
|
+
def __init__(self):
|
|
42
|
+
self.commands = SLASH_COMMANDS_REFERENCE
|
|
43
|
+
|
|
44
|
+
def get_completions(self, document, complete_event):
|
|
45
|
+
text = document.text_before_cursor
|
|
46
|
+
if text.startswith("/"):
|
|
47
|
+
word = text
|
|
48
|
+
for cmd, description in self.commands.items():
|
|
49
|
+
if cmd.startswith(word):
|
|
50
|
+
yield Completion(
|
|
51
|
+
cmd, start_position=-len(word), display=f"{cmd} - {description}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
USER_COLOR = "#DEFCC0" # light green
|
|
56
|
+
AI_COLOR = "#00FFFF" # cyan
|
|
57
|
+
TOOLS_COLOR = "magenta"
|
|
58
|
+
HELP_COLOR = "cyan" # same as AI_COLOR for now
|
|
59
|
+
ERROR_COLOR = "red"
|
|
60
|
+
STATUS_COLOR = "yellow"
|
|
61
|
+
|
|
62
|
+
WELCOME_BANNER = f"[bold {HELP_COLOR}]Welcome to HolmesGPT:[/bold {HELP_COLOR}] Type '{SlashCommands.EXIT.value}' to exit, '{SlashCommands.HELP.value}' for commands."
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def format_tool_call_output(tool_call: ToolCallResult) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Format a single tool call result for display in a rich panel.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
tool_call: ToolCallResult object containing the tool execution result
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Formatted string for display in a rich panel
|
|
74
|
+
"""
|
|
75
|
+
result = tool_call.result
|
|
76
|
+
output_str = result.get_stringified_data()
|
|
77
|
+
|
|
78
|
+
color = result.status.to_color()
|
|
79
|
+
MAX_CHARS = 500
|
|
80
|
+
if len(output_str) == 0:
|
|
81
|
+
content = f"[{color}]<empty>[/{color}]"
|
|
82
|
+
elif len(output_str) > MAX_CHARS:
|
|
83
|
+
truncated = output_str[:MAX_CHARS].strip()
|
|
84
|
+
remaining_chars = len(output_str) - MAX_CHARS
|
|
85
|
+
content = f"[{color}]{truncated}[/{color}]\n\n[dim]... truncated ({remaining_chars:,} more chars)[/dim]"
|
|
86
|
+
else:
|
|
87
|
+
content = f"[{color}]{output_str}[/{color}]"
|
|
88
|
+
|
|
89
|
+
return content
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def display_tool_calls(tool_calls: List[ToolCallResult], console: Console) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Display tool calls in rich panels.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
tool_calls: List of ToolCallResult objects to display
|
|
98
|
+
console: Rich console for output
|
|
99
|
+
"""
|
|
100
|
+
console.print(
|
|
101
|
+
f"[bold {TOOLS_COLOR}]Used {len(tool_calls)} tools[/bold {TOOLS_COLOR}]"
|
|
102
|
+
)
|
|
103
|
+
for tool_call in tool_calls:
|
|
104
|
+
preview_output = format_tool_call_output(tool_call)
|
|
105
|
+
title = f"{tool_call.result.status.to_emoji()} {tool_call.description} -> returned {tool_call.result.return_code}"
|
|
106
|
+
|
|
107
|
+
console.print(
|
|
108
|
+
Panel(
|
|
109
|
+
preview_output,
|
|
110
|
+
padding=(1, 2),
|
|
111
|
+
border_style=TOOLS_COLOR,
|
|
112
|
+
title=title,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def run_interactive_loop(
|
|
118
|
+
ai: ToolCallingLLM,
|
|
119
|
+
console: Console,
|
|
120
|
+
system_prompt_rendered: str,
|
|
121
|
+
initial_user_input: Optional[str],
|
|
122
|
+
include_files: Optional[List[Path]],
|
|
123
|
+
post_processing_prompt: Optional[str],
|
|
124
|
+
show_tool_output: bool,
|
|
125
|
+
) -> None:
|
|
126
|
+
style = Style.from_dict(
|
|
127
|
+
{
|
|
128
|
+
"prompt": USER_COLOR,
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
command_completer = SlashCommandCompleter()
|
|
133
|
+
history = InMemoryHistory()
|
|
134
|
+
if initial_user_input:
|
|
135
|
+
history.append_string(initial_user_input)
|
|
136
|
+
session = PromptSession(
|
|
137
|
+
completer=command_completer,
|
|
138
|
+
history=history,
|
|
139
|
+
) # type: ignore
|
|
140
|
+
input_prompt = [("class:prompt", "User: ")]
|
|
141
|
+
|
|
142
|
+
console.print(WELCOME_BANNER)
|
|
143
|
+
if initial_user_input:
|
|
144
|
+
console.print(
|
|
145
|
+
f"[bold {USER_COLOR}]User:[/bold {USER_COLOR}] {initial_user_input}"
|
|
146
|
+
)
|
|
147
|
+
messages = None
|
|
148
|
+
last_response = None
|
|
149
|
+
|
|
150
|
+
while True:
|
|
151
|
+
try:
|
|
152
|
+
if initial_user_input:
|
|
153
|
+
user_input = initial_user_input
|
|
154
|
+
initial_user_input = None
|
|
155
|
+
else:
|
|
156
|
+
user_input = session.prompt(input_prompt, style=style) # type: ignore
|
|
157
|
+
|
|
158
|
+
if user_input.startswith("/"):
|
|
159
|
+
command = user_input.strip().lower()
|
|
160
|
+
if command == SlashCommands.EXIT.value:
|
|
161
|
+
return
|
|
162
|
+
elif command == SlashCommands.HELP.value:
|
|
163
|
+
console.print(
|
|
164
|
+
f"[bold {HELP_COLOR}]Available commands:[/bold {HELP_COLOR}]"
|
|
165
|
+
)
|
|
166
|
+
for cmd, description in SLASH_COMMANDS_REFERENCE.items():
|
|
167
|
+
console.print(f" [bold]{cmd}[/bold] - {description}")
|
|
168
|
+
elif command == SlashCommands.RESET.value:
|
|
169
|
+
console.print(
|
|
170
|
+
f"[bold {STATUS_COLOR}]Context reset. You can now ask a new question.[/bold {STATUS_COLOR}]"
|
|
171
|
+
)
|
|
172
|
+
messages = None
|
|
173
|
+
continue
|
|
174
|
+
elif command == SlashCommands.TOOLS_CONFIG.value:
|
|
175
|
+
pretty_print_toolset_status(ai.tool_executor.toolsets, console)
|
|
176
|
+
elif command == SlashCommands.TOGGLE_TOOL_OUTPUT.value:
|
|
177
|
+
show_tool_output = not show_tool_output
|
|
178
|
+
status = "enabled" if show_tool_output else "disabled"
|
|
179
|
+
console.print(
|
|
180
|
+
f"[bold yellow]Tool output display {status}.[/bold yellow]"
|
|
181
|
+
)
|
|
182
|
+
elif command == SlashCommands.SHOW_OUTPUT.value:
|
|
183
|
+
if last_response is None or not last_response.tool_calls:
|
|
184
|
+
console.print(
|
|
185
|
+
f"[bold {ERROR_COLOR}]No tool calls available from the last response.[/bold {ERROR_COLOR}]"
|
|
186
|
+
)
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
display_tool_calls(last_response.tool_calls, console)
|
|
190
|
+
else:
|
|
191
|
+
console.print(f"Unknown command: {command}")
|
|
192
|
+
continue
|
|
193
|
+
elif not user_input.strip():
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
if messages is None:
|
|
197
|
+
messages = build_initial_ask_messages(
|
|
198
|
+
console, system_prompt_rendered, user_input, include_files
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
messages.append({"role": "user", "content": user_input})
|
|
202
|
+
|
|
203
|
+
console.print(f"\n[bold {AI_COLOR}]Thinking...[/bold {AI_COLOR}]\n")
|
|
204
|
+
response = ai.call(messages, post_processing_prompt)
|
|
205
|
+
messages = response.messages # type: ignore
|
|
206
|
+
last_response = response
|
|
207
|
+
|
|
208
|
+
if show_tool_output and response.tool_calls:
|
|
209
|
+
display_tool_calls(response.tool_calls, console)
|
|
210
|
+
console.print(
|
|
211
|
+
Panel(
|
|
212
|
+
Markdown(f"{response.result}"),
|
|
213
|
+
padding=(1, 2),
|
|
214
|
+
border_style=AI_COLOR,
|
|
215
|
+
title=f"[bold {AI_COLOR}]AI Response[/bold {AI_COLOR}]",
|
|
216
|
+
title_align="left",
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
console.print("")
|
|
220
|
+
except typer.Abort:
|
|
221
|
+
break
|
|
222
|
+
except EOFError: # Handle Ctrl+D
|
|
223
|
+
break
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logging.error("An error occurred during interactive mode:", exc_info=e)
|
|
226
|
+
console.print(f"[bold {ERROR_COLOR}]Error: {e}[/bold {ERROR_COLOR}]")
|
|
227
|
+
console.print(
|
|
228
|
+
f"[bold {STATUS_COLOR}]Exiting interactive mode.[/bold {STATUS_COLOR}]"
|
|
229
|
+
)
|