holmesgpt 0.13.2__py3-none-any.whl → 0.16.2a0__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.
- holmes/__init__.py +1 -1
- holmes/clients/robusta_client.py +17 -4
- holmes/common/env_vars.py +40 -1
- holmes/config.py +114 -144
- holmes/core/conversations.py +53 -14
- holmes/core/feedback.py +191 -0
- holmes/core/investigation.py +18 -22
- holmes/core/llm.py +489 -88
- holmes/core/models.py +103 -1
- holmes/core/openai_formatting.py +13 -0
- holmes/core/prompt.py +1 -1
- holmes/core/safeguards.py +4 -4
- holmes/core/supabase_dal.py +293 -100
- holmes/core/tool_calling_llm.py +423 -323
- holmes/core/tools.py +311 -33
- holmes/core/tools_utils/token_counting.py +14 -0
- holmes/core/tools_utils/tool_context_window_limiter.py +57 -0
- holmes/core/tools_utils/tool_executor.py +13 -8
- holmes/core/toolset_manager.py +155 -4
- holmes/core/tracing.py +6 -1
- holmes/core/transformers/__init__.py +23 -0
- holmes/core/transformers/base.py +62 -0
- holmes/core/transformers/llm_summarize.py +174 -0
- holmes/core/transformers/registry.py +122 -0
- holmes/core/transformers/transformer.py +31 -0
- holmes/core/truncation/compaction.py +59 -0
- holmes/core/truncation/dal_truncation_utils.py +23 -0
- holmes/core/truncation/input_context_window_limiter.py +218 -0
- holmes/interactive.py +177 -24
- holmes/main.py +7 -4
- holmes/plugins/prompts/_fetch_logs.jinja2 +26 -1
- holmes/plugins/prompts/_general_instructions.jinja2 +1 -2
- holmes/plugins/prompts/_runbook_instructions.jinja2 +23 -12
- holmes/plugins/prompts/conversation_history_compaction.jinja2 +88 -0
- holmes/plugins/prompts/generic_ask.jinja2 +2 -4
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +2 -1
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +2 -1
- holmes/plugins/prompts/generic_investigation.jinja2 +2 -1
- holmes/plugins/prompts/investigation_procedure.jinja2 +48 -0
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +2 -1
- holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +2 -1
- holmes/plugins/runbooks/__init__.py +117 -18
- holmes/plugins/runbooks/catalog.json +2 -0
- holmes/plugins/toolsets/__init__.py +21 -8
- holmes/plugins/toolsets/aks-node-health.yaml +46 -0
- holmes/plugins/toolsets/aks.yaml +64 -0
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +26 -36
- holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +0 -1
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +10 -7
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +9 -6
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +8 -6
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +8 -6
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +9 -6
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +9 -7
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +9 -6
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +9 -6
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +9 -6
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +9 -6
- holmes/plugins/toolsets/bash/bash_toolset.py +10 -13
- holmes/plugins/toolsets/bash/common/bash.py +7 -7
- holmes/plugins/toolsets/cilium.yaml +284 -0
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +5 -3
- holmes/plugins/toolsets/datadog/datadog_api.py +490 -24
- holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +21 -10
- holmes/plugins/toolsets/datadog/toolset_datadog_general.py +349 -216
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +190 -19
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +101 -44
- holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +13 -16
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +25 -31
- holmes/plugins/toolsets/git.py +51 -46
- holmes/plugins/toolsets/grafana/common.py +15 -3
- holmes/plugins/toolsets/grafana/grafana_api.py +46 -24
- holmes/plugins/toolsets/grafana/grafana_tempo_api.py +454 -0
- holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +9 -0
- holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +117 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +211 -91
- holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +27 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +653 -293
- holmes/plugins/toolsets/grafana/trace_parser.py +1 -1
- holmes/plugins/toolsets/internet/internet.py +6 -7
- holmes/plugins/toolsets/internet/notion.py +5 -6
- holmes/plugins/toolsets/investigator/core_investigation.py +42 -34
- holmes/plugins/toolsets/kafka.py +25 -36
- holmes/plugins/toolsets/kubernetes.yaml +58 -84
- holmes/plugins/toolsets/kubernetes_logs.py +6 -6
- holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
- holmes/plugins/toolsets/logging_utils/logging_api.py +80 -4
- holmes/plugins/toolsets/mcp/toolset_mcp.py +181 -55
- holmes/plugins/toolsets/newrelic/__init__.py +0 -0
- holmes/plugins/toolsets/newrelic/new_relic_api.py +125 -0
- holmes/plugins/toolsets/newrelic/newrelic.jinja2 +41 -0
- holmes/plugins/toolsets/newrelic/newrelic.py +163 -0
- holmes/plugins/toolsets/opensearch/opensearch.py +10 -17
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +7 -7
- holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2 +1616 -0
- holmes/plugins/toolsets/opensearch/opensearch_query_assist.py +78 -0
- holmes/plugins/toolsets/opensearch/opensearch_query_assist_instructions.jinja2 +223 -0
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +13 -16
- holmes/plugins/toolsets/openshift.yaml +283 -0
- holmes/plugins/toolsets/prometheus/prometheus.py +915 -390
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +43 -2
- holmes/plugins/toolsets/prometheus/utils.py +28 -0
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +9 -10
- holmes/plugins/toolsets/robusta/robusta.py +236 -65
- holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +26 -9
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +137 -26
- holmes/plugins/toolsets/service_discovery.py +1 -1
- holmes/plugins/toolsets/servicenow_tables/instructions.jinja2 +83 -0
- holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +426 -0
- holmes/plugins/toolsets/utils.py +88 -0
- holmes/utils/config_utils.py +91 -0
- holmes/utils/default_toolset_installation_guide.jinja2 +1 -22
- holmes/utils/env.py +7 -0
- holmes/utils/global_instructions.py +75 -10
- holmes/utils/holmes_status.py +2 -1
- holmes/utils/holmes_sync_toolsets.py +0 -2
- holmes/utils/krr_utils.py +188 -0
- holmes/utils/sentry_helper.py +41 -0
- holmes/utils/stream.py +61 -7
- holmes/version.py +34 -14
- holmesgpt-0.16.2a0.dist-info/LICENSE +178 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/METADATA +29 -27
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/RECORD +126 -102
- holmes/core/performance_timing.py +0 -72
- holmes/plugins/toolsets/grafana/tempo_api.py +0 -124
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +0 -110
- holmes/plugins/toolsets/newrelic.py +0 -231
- holmes/plugins/toolsets/servicenow/install.md +0 -37
- holmes/plugins/toolsets/servicenow/instructions.jinja2 +0 -3
- holmes/plugins/toolsets/servicenow/servicenow.py +0 -219
- holmesgpt-0.13.2.dist-info/LICENSE.txt +0 -21
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/WHEEL +0 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from typing import Dict, Optional, cast, Type, ClassVar, Tuple, Any
|
|
4
|
+
from urllib.parse import urljoin
|
|
5
|
+
|
|
6
|
+
import requests # type: ignore
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from holmes.core.tools import (
|
|
10
|
+
CallablePrerequisite,
|
|
11
|
+
StructuredToolResult,
|
|
12
|
+
StructuredToolResultStatus,
|
|
13
|
+
Tool,
|
|
14
|
+
ToolInvokeContext,
|
|
15
|
+
ToolParameter,
|
|
16
|
+
Toolset,
|
|
17
|
+
)
|
|
18
|
+
from holmes.plugins.toolsets.utils import toolset_name_for_one_liner
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ServiceNowTablesConfig(BaseModel):
|
|
22
|
+
"""Configuration for ServiceNow Tables API access.
|
|
23
|
+
|
|
24
|
+
Example configuration:
|
|
25
|
+
```yaml
|
|
26
|
+
api_key: "now_1234567890abcdef"
|
|
27
|
+
instance_url: "https://your-instance.service-now.com"
|
|
28
|
+
```
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
api_key: str
|
|
32
|
+
instance_url: str
|
|
33
|
+
api_key_header: str = "x-sn-apikey"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ServiceNowTablesToolset(Toolset):
|
|
37
|
+
config_class: ClassVar[Type[ServiceNowTablesConfig]] = ServiceNowTablesConfig
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
super().__init__(
|
|
41
|
+
name="servicenow/tables",
|
|
42
|
+
description="Tools for retrieving records from ServiceNow tables",
|
|
43
|
+
icon_url="https://www.servicenow.com/content/dam/servicenow-assets/public/en-us/images/og-images/favicon.ico",
|
|
44
|
+
docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/servicenow/",
|
|
45
|
+
prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
|
|
46
|
+
tools=[
|
|
47
|
+
GetRecords(self),
|
|
48
|
+
GetRecord(self),
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self._load_llm_instructions_from_file(
|
|
53
|
+
os.path.dirname(__file__), "instructions.jinja2"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
|
|
57
|
+
"""Check if the ServiceNow configuration is valid and complete."""
|
|
58
|
+
try:
|
|
59
|
+
# Validate the config using Pydantic - this will raise if required fields are missing
|
|
60
|
+
self.config = ServiceNowTablesConfig(**config)
|
|
61
|
+
|
|
62
|
+
# Perform health check
|
|
63
|
+
return self._perform_health_check()
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
return False, f"Failed to validate ServiceNow configuration: {str(e)}"
|
|
67
|
+
|
|
68
|
+
def _perform_health_check(self) -> Tuple[bool, str]:
|
|
69
|
+
"""Perform a health check by making a minimal API call."""
|
|
70
|
+
try:
|
|
71
|
+
# Query sys_db_object table with minimal data
|
|
72
|
+
data, headers = self._make_api_request(
|
|
73
|
+
endpoint="api/now/v2/table/sys_db_object",
|
|
74
|
+
query_params={"sysparm_limit": 1, "sysparm_fields": "sys_id"},
|
|
75
|
+
timeout=10,
|
|
76
|
+
)
|
|
77
|
+
return True, "ServiceNow configuration is valid and API is accessible."
|
|
78
|
+
|
|
79
|
+
except requests.exceptions.HTTPError as e:
|
|
80
|
+
if e.response.status_code == 401:
|
|
81
|
+
return (
|
|
82
|
+
False,
|
|
83
|
+
"ServiceNow authentication failed. Please check your API key.",
|
|
84
|
+
)
|
|
85
|
+
elif e.response.status_code == 403:
|
|
86
|
+
return (
|
|
87
|
+
False,
|
|
88
|
+
"ServiceNow access denied. Please ensure your user has Table API access.",
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
return (
|
|
92
|
+
False,
|
|
93
|
+
f"ServiceNow API returned error: {e.response.status_code} - {e.response.text}",
|
|
94
|
+
)
|
|
95
|
+
except requests.exceptions.ConnectionError:
|
|
96
|
+
return (
|
|
97
|
+
False,
|
|
98
|
+
f"Failed to connect to ServiceNow instance at {self.config.instance_url if self.config else 'unknown'}",
|
|
99
|
+
)
|
|
100
|
+
except requests.exceptions.Timeout:
|
|
101
|
+
return False, "ServiceNow health check timed out"
|
|
102
|
+
except Exception as e:
|
|
103
|
+
return False, f"ServiceNow health check failed: {str(e)}"
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def servicenow_config(self) -> ServiceNowTablesConfig:
|
|
107
|
+
return cast(ServiceNowTablesConfig, self.config)
|
|
108
|
+
|
|
109
|
+
def get_example_config(self) -> Dict[str, Any]:
|
|
110
|
+
"""Return an example configuration for this toolset."""
|
|
111
|
+
example_config = ServiceNowTablesConfig(
|
|
112
|
+
api_key="now_1234567890abcdef",
|
|
113
|
+
instance_url="https://your-instance.service-now.com",
|
|
114
|
+
)
|
|
115
|
+
return example_config.model_dump()
|
|
116
|
+
|
|
117
|
+
def _make_api_request(
|
|
118
|
+
self,
|
|
119
|
+
endpoint: str,
|
|
120
|
+
query_params: Optional[Dict] = None,
|
|
121
|
+
timeout: int = 30,
|
|
122
|
+
) -> Tuple[Dict[str, Any], Dict[str, str]]:
|
|
123
|
+
"""Make a GET request to ServiceNow API and return JSON data and headers.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
endpoint: API endpoint path (e.g., "api/now/v2/table/incident")
|
|
127
|
+
query_params: Optional query parameters for the request
|
|
128
|
+
timeout: Request timeout in seconds
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Tuple of (parsed JSON response data, response headers dict)
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
requests.exceptions.HTTPError: For HTTP error responses (4xx, 5xx)
|
|
135
|
+
requests.exceptions.ConnectionError: For connection problems
|
|
136
|
+
requests.exceptions.Timeout: For timeout errors
|
|
137
|
+
requests.exceptions.RequestException: For other request errors
|
|
138
|
+
"""
|
|
139
|
+
url = urljoin(
|
|
140
|
+
self.servicenow_config.instance_url.rstrip("/") + "/", endpoint.lstrip("/")
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
headers = {
|
|
144
|
+
self.servicenow_config.api_key_header: self.servicenow_config.api_key,
|
|
145
|
+
"Accept": "application/json",
|
|
146
|
+
"Content-Type": "application/json",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
response = requests.get(
|
|
150
|
+
url, headers=headers, params=query_params, timeout=timeout
|
|
151
|
+
)
|
|
152
|
+
response.raise_for_status()
|
|
153
|
+
return response.json(), dict(response.headers)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class BaseServiceNowTool(Tool, ABC):
|
|
157
|
+
"""Base class for ServiceNow tools with common HTTP request functionality."""
|
|
158
|
+
|
|
159
|
+
def __init__(self, toolset: ServiceNowTablesToolset, *args, **kwargs):
|
|
160
|
+
super().__init__(*args, **kwargs)
|
|
161
|
+
self._toolset = toolset
|
|
162
|
+
|
|
163
|
+
def _make_servicenow_request(
|
|
164
|
+
self,
|
|
165
|
+
endpoint: str,
|
|
166
|
+
params: dict,
|
|
167
|
+
query_params: Optional[Dict] = None,
|
|
168
|
+
timeout: int = 30,
|
|
169
|
+
) -> StructuredToolResult:
|
|
170
|
+
"""Make a GET request to ServiceNow API and return structured result.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
endpoint: API endpoint path (e.g., "/api/now/v2/table/incident")
|
|
174
|
+
params: Original parameters passed to the tool
|
|
175
|
+
query_params: Optional query parameters for the request
|
|
176
|
+
timeout: Request timeout in seconds
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
StructuredToolResult with the API response data
|
|
180
|
+
"""
|
|
181
|
+
# TODO: Add URL to the result for better debugging and error messages
|
|
182
|
+
|
|
183
|
+
# Use the toolset's shared API request method
|
|
184
|
+
data, headers = self._toolset._make_api_request(
|
|
185
|
+
endpoint=endpoint, query_params=query_params, timeout=timeout
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return StructuredToolResult(
|
|
189
|
+
status=StructuredToolResultStatus.SUCCESS,
|
|
190
|
+
data=data,
|
|
191
|
+
params=params,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class GetRecords(BaseServiceNowTool):
|
|
196
|
+
def __init__(self, toolset: ServiceNowTablesToolset):
|
|
197
|
+
super().__init__(
|
|
198
|
+
toolset=toolset,
|
|
199
|
+
name="servicenow_get_records",
|
|
200
|
+
description="Retrieves multiple records for the specified table using GET /api/now/v2/table/{tableName}. Returns the records data along with response headers including 'Link' (for pagination) and 'X-Total-Count' (total number of records) if provided by the API.",
|
|
201
|
+
parameters={
|
|
202
|
+
"table_name": ToolParameter(
|
|
203
|
+
description="The name of the ServiceNow table to query",
|
|
204
|
+
type="string",
|
|
205
|
+
required=True,
|
|
206
|
+
),
|
|
207
|
+
"sysparm_query": ToolParameter(
|
|
208
|
+
description=(
|
|
209
|
+
"An encoded query string used to filter the results. "
|
|
210
|
+
"Use ^ for AND, ^OR for OR. "
|
|
211
|
+
"Common operators: = (equals), != (not equals), LIKE (contains), "
|
|
212
|
+
"STARTSWITH, ENDSWITH, CONTAINS, ISNOTEMPTY, ISEMPTY, "
|
|
213
|
+
"< (less than), <= (less than or equal), > (greater than), >= (greater than or equal). "
|
|
214
|
+
"Date queries: Use >= and <= operators. Date-only format (YYYY-MM-DD) includes entire day. "
|
|
215
|
+
"Examples: sys_created_on>=2024-01-01^sys_created_on<=2024-01-31 or with time: sys_created_on>=2024-01-01 00:00:00"
|
|
216
|
+
),
|
|
217
|
+
type="string",
|
|
218
|
+
required=False,
|
|
219
|
+
),
|
|
220
|
+
"sysparm_display_value": ToolParameter(
|
|
221
|
+
description="Return field display values (true), actual values (false), or both (all) (default: true)",
|
|
222
|
+
type="string",
|
|
223
|
+
required=False,
|
|
224
|
+
),
|
|
225
|
+
"sysparm_exclude_reference_link": ToolParameter(
|
|
226
|
+
description="True to exclude Table API links for reference fields (default: false)",
|
|
227
|
+
type="boolean",
|
|
228
|
+
required=False,
|
|
229
|
+
),
|
|
230
|
+
"sysparm_suppress_pagination_header": ToolParameter(
|
|
231
|
+
description="Flag that indicates whether to remove the Link header from the response. The Link header provides various URLs to relative pages in the record set which you can use to paginate the returned record set.",
|
|
232
|
+
type="boolean",
|
|
233
|
+
required=False,
|
|
234
|
+
),
|
|
235
|
+
"sysparm_fields": ToolParameter(
|
|
236
|
+
description="Comma-separated list of fields to return in the response. If not provided, all fields will be returned. Invalid fields are ignored.",
|
|
237
|
+
type="string",
|
|
238
|
+
required=False,
|
|
239
|
+
),
|
|
240
|
+
"sysparm_limit": ToolParameter(
|
|
241
|
+
description=(
|
|
242
|
+
"Maximum number of records to return (default: 100). "
|
|
243
|
+
"For requests that exceed this number of records, use the sysparm_offset parameter to paginate record retrieval. "
|
|
244
|
+
"This limit is applied before ACL evaluation. If no records return, including records you have access to, "
|
|
245
|
+
"rearrange the record order so records you have access to return first."
|
|
246
|
+
),
|
|
247
|
+
type="integer",
|
|
248
|
+
required=False,
|
|
249
|
+
),
|
|
250
|
+
"sysparm_offset": ToolParameter(
|
|
251
|
+
description=(
|
|
252
|
+
"Starting record index for pagination. Use this with sysparm_limit to paginate through large result sets. "
|
|
253
|
+
"For example, to get records 101-200, use sysparm_offset=100 with sysparm_limit=100."
|
|
254
|
+
),
|
|
255
|
+
type="integer",
|
|
256
|
+
required=False,
|
|
257
|
+
),
|
|
258
|
+
"sysparm_view": ToolParameter(
|
|
259
|
+
description=(
|
|
260
|
+
"UI view for which to render the data. Determines the fields returned in the response. "
|
|
261
|
+
"Valid values: desktop, mobile, both. If you also specify the sysparm_fields parameter, it takes precedent. "
|
|
262
|
+
"In case you are not sure about the fields for sysparm_fields and want to get a short summary, use sysparm_view with 'mobile'."
|
|
263
|
+
),
|
|
264
|
+
type="string",
|
|
265
|
+
required=False,
|
|
266
|
+
),
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
271
|
+
"""
|
|
272
|
+
Note: The following parameters are available in the ServiceNow API but are not used in this implementation:
|
|
273
|
+
- name-value pairs: Name-value pairs to use to filter the result set. This parameter is mutually exclusive with sysparm_query.
|
|
274
|
+
- sysparm_query_category: Name of the query category (read replica category) to use for queries
|
|
275
|
+
- sysparm_query_no_domain: True to access data across domains if authorized (default: false)
|
|
276
|
+
- sysparm_no_count: Do not execute a select count(*) on table (default: false)
|
|
277
|
+
"""
|
|
278
|
+
table_name = params["table_name"]
|
|
279
|
+
query_params = {}
|
|
280
|
+
|
|
281
|
+
# Handle sysparm_query
|
|
282
|
+
if params.get("sysparm_query"):
|
|
283
|
+
query_params["sysparm_query"] = params["sysparm_query"]
|
|
284
|
+
|
|
285
|
+
# Handle sysparm_display_value with default of 'true' instead of 'false'
|
|
286
|
+
if params.get("sysparm_display_value") is not None:
|
|
287
|
+
query_params["sysparm_display_value"] = params["sysparm_display_value"]
|
|
288
|
+
else:
|
|
289
|
+
query_params["sysparm_display_value"] = "true"
|
|
290
|
+
|
|
291
|
+
# Handle other parameters
|
|
292
|
+
if params.get("sysparm_exclude_reference_link") is not None:
|
|
293
|
+
query_params["sysparm_exclude_reference_link"] = str(
|
|
294
|
+
params["sysparm_exclude_reference_link"]
|
|
295
|
+
).lower()
|
|
296
|
+
|
|
297
|
+
if params.get("sysparm_suppress_pagination_header") is not None:
|
|
298
|
+
query_params["sysparm_suppress_pagination_header"] = str(
|
|
299
|
+
params["sysparm_suppress_pagination_header"]
|
|
300
|
+
).lower()
|
|
301
|
+
|
|
302
|
+
if params.get("sysparm_fields"):
|
|
303
|
+
query_params["sysparm_fields"] = params["sysparm_fields"]
|
|
304
|
+
|
|
305
|
+
# Handle sysparm_limit with default of 100 instead of 10000
|
|
306
|
+
if params.get("sysparm_limit") is not None:
|
|
307
|
+
query_params["sysparm_limit"] = params["sysparm_limit"]
|
|
308
|
+
else:
|
|
309
|
+
query_params["sysparm_limit"] = 100
|
|
310
|
+
|
|
311
|
+
# Handle sysparm_offset for pagination
|
|
312
|
+
if params.get("sysparm_offset") is not None:
|
|
313
|
+
query_params["sysparm_offset"] = params["sysparm_offset"]
|
|
314
|
+
|
|
315
|
+
if params.get("sysparm_view"):
|
|
316
|
+
query_params["sysparm_view"] = params["sysparm_view"]
|
|
317
|
+
|
|
318
|
+
endpoint = f"/api/now/v2/table/{table_name}"
|
|
319
|
+
|
|
320
|
+
# Get data and headers from the API request
|
|
321
|
+
data, headers = self._toolset._make_api_request(
|
|
322
|
+
endpoint=endpoint, query_params=query_params, timeout=30
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Create the response with records and relevant headers
|
|
326
|
+
response_data = {
|
|
327
|
+
"result": data.get("result", []),
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# Include Link header if present
|
|
331
|
+
if "Link" in headers:
|
|
332
|
+
response_data["Link"] = headers["Link"]
|
|
333
|
+
|
|
334
|
+
# Include X-Total-Count header if present
|
|
335
|
+
if "X-Total-Count" in headers:
|
|
336
|
+
response_data["X-Total-Count"] = headers["X-Total-Count"]
|
|
337
|
+
|
|
338
|
+
return StructuredToolResult(
|
|
339
|
+
status=StructuredToolResultStatus.SUCCESS,
|
|
340
|
+
data=response_data,
|
|
341
|
+
params=params,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def get_parameterized_one_liner(self, params: Dict) -> str:
|
|
345
|
+
table_name = params.get("table_name", "unknown")
|
|
346
|
+
return f"{toolset_name_for_one_liner(self._toolset.name)}: Get records from {table_name}"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class GetRecord(BaseServiceNowTool):
|
|
350
|
+
def __init__(self, toolset: ServiceNowTablesToolset):
|
|
351
|
+
super().__init__(
|
|
352
|
+
toolset=toolset,
|
|
353
|
+
name="servicenow_get_record",
|
|
354
|
+
description="Retrieves the record identified by the specified sys_id from the specified table using GET /api/now/v2/table/{tableName}/{sys_id}",
|
|
355
|
+
parameters={
|
|
356
|
+
"table_name": ToolParameter(
|
|
357
|
+
description="The name of the ServiceNow table",
|
|
358
|
+
type="string",
|
|
359
|
+
required=True,
|
|
360
|
+
),
|
|
361
|
+
"sys_id": ToolParameter(
|
|
362
|
+
description="The EXACT sys_id value from a real ServiceNow record. WARNING: You MUST NOT fabricate, guess, or create sys_id values. This value MUST come from: 1) The user providing it explicitly, or 2) A previous servicenow_get_records API response. If you don't have a real sys_id, use servicenow_get_records first to search for records.",
|
|
363
|
+
type="string",
|
|
364
|
+
required=True,
|
|
365
|
+
),
|
|
366
|
+
"sysparm_display_value": ToolParameter(
|
|
367
|
+
description="Return field display values (true), actual values (false), or both (all) (default: true)",
|
|
368
|
+
type="string",
|
|
369
|
+
required=False,
|
|
370
|
+
),
|
|
371
|
+
"sysparm_exclude_reference_link": ToolParameter(
|
|
372
|
+
description="True to exclude Table API links for reference fields (default: false)",
|
|
373
|
+
type="boolean",
|
|
374
|
+
required=False,
|
|
375
|
+
),
|
|
376
|
+
"sysparm_fields": ToolParameter(
|
|
377
|
+
description="Comma-separated list of fields to return in the response. If not provided, all fields will be returned. Invalid fields are ignored.",
|
|
378
|
+
type="string",
|
|
379
|
+
required=False,
|
|
380
|
+
),
|
|
381
|
+
"sysparm_view": ToolParameter(
|
|
382
|
+
description=(
|
|
383
|
+
"UI view for which to render the data. Determines the fields returned in the response. "
|
|
384
|
+
"Valid values: desktop, mobile, both. If you also specify the sysparm_fields parameter, it takes precedent. "
|
|
385
|
+
"In case you are not sure about the fields for sysparm_fields and want to get a short summary, use sysparm_view with 'mobile'."
|
|
386
|
+
),
|
|
387
|
+
type="string",
|
|
388
|
+
required=False,
|
|
389
|
+
),
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
394
|
+
"""
|
|
395
|
+
Note: The following parameter is available in the ServiceNow API but is not used in this implementation:
|
|
396
|
+
- sysparm_query_no_domain: True to access data across domains if authorized (default: false)
|
|
397
|
+
"""
|
|
398
|
+
table_name = params["table_name"]
|
|
399
|
+
sys_id = params["sys_id"]
|
|
400
|
+
query_params = {}
|
|
401
|
+
|
|
402
|
+
# Handle sysparm_display_value with default of 'true' instead of 'false'
|
|
403
|
+
if params.get("sysparm_display_value") is not None:
|
|
404
|
+
query_params["sysparm_display_value"] = params["sysparm_display_value"]
|
|
405
|
+
else:
|
|
406
|
+
query_params["sysparm_display_value"] = "true"
|
|
407
|
+
|
|
408
|
+
# Handle other parameters
|
|
409
|
+
if params.get("sysparm_exclude_reference_link") is not None:
|
|
410
|
+
query_params["sysparm_exclude_reference_link"] = str(
|
|
411
|
+
params["sysparm_exclude_reference_link"]
|
|
412
|
+
).lower()
|
|
413
|
+
|
|
414
|
+
if params.get("sysparm_fields"):
|
|
415
|
+
query_params["sysparm_fields"] = params["sysparm_fields"]
|
|
416
|
+
|
|
417
|
+
if params.get("sysparm_view"):
|
|
418
|
+
query_params["sysparm_view"] = params["sysparm_view"]
|
|
419
|
+
|
|
420
|
+
endpoint = f"/api/now/v2/table/{table_name}/{sys_id}"
|
|
421
|
+
return self._make_servicenow_request(endpoint, params, query_params)
|
|
422
|
+
|
|
423
|
+
def get_parameterized_one_liner(self, params: Dict) -> str:
|
|
424
|
+
table_name = params.get("table_name", "unknown")
|
|
425
|
+
sys_id = params.get("sys_id", "")
|
|
426
|
+
return f"{toolset_name_for_one_liner(self._toolset.name)}: Get {table_name} record {sys_id}"
|
holmes/plugins/toolsets/utils.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
import math
|
|
2
3
|
import time
|
|
4
|
+
import re
|
|
3
5
|
from typing import Dict, Optional, Tuple, Union
|
|
4
6
|
|
|
5
7
|
from dateutil import parser
|
|
@@ -134,6 +136,92 @@ def process_timestamps_to_int(
|
|
|
134
136
|
return (start, end) # type: ignore
|
|
135
137
|
|
|
136
138
|
|
|
139
|
+
def seconds_to_duration_string(seconds: int) -> str:
|
|
140
|
+
"""Convert seconds into a compact duration string like '2h30m15s'.
|
|
141
|
+
If the value is less than 1 minute, return just the number of seconds (e.g. '45').
|
|
142
|
+
"""
|
|
143
|
+
if seconds < 0:
|
|
144
|
+
raise ValueError("seconds must be non-negative")
|
|
145
|
+
|
|
146
|
+
parts = []
|
|
147
|
+
weeks, seconds = divmod(seconds, 7 * 24 * 3600)
|
|
148
|
+
days, seconds = divmod(seconds, 24 * 3600)
|
|
149
|
+
hours, seconds = divmod(seconds, 3600)
|
|
150
|
+
minutes, seconds = divmod(seconds, 60)
|
|
151
|
+
|
|
152
|
+
if weeks:
|
|
153
|
+
parts.append(f"{weeks}w")
|
|
154
|
+
if days:
|
|
155
|
+
parts.append(f"{days}d")
|
|
156
|
+
if hours:
|
|
157
|
+
parts.append(f"{hours}h")
|
|
158
|
+
if minutes:
|
|
159
|
+
parts.append(f"{minutes}m")
|
|
160
|
+
if seconds or not parts:
|
|
161
|
+
parts.append(f"{seconds}s")
|
|
162
|
+
|
|
163
|
+
return "".join(parts)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def duration_string_to_seconds(duration_string: str) -> int:
|
|
167
|
+
"""Convert a duration string like '2h30m15s' or '300' into total seconds.
|
|
168
|
+
A bare integer string is treated as seconds.
|
|
169
|
+
"""
|
|
170
|
+
if not duration_string:
|
|
171
|
+
raise ValueError("duration_string cannot be empty")
|
|
172
|
+
|
|
173
|
+
# Pure number? Assume seconds
|
|
174
|
+
if duration_string.isdigit():
|
|
175
|
+
return int(duration_string)
|
|
176
|
+
|
|
177
|
+
pattern = re.compile(r"(?P<value>\d+)(?P<unit>[wdhms])")
|
|
178
|
+
matches = pattern.findall(duration_string)
|
|
179
|
+
if not matches:
|
|
180
|
+
raise ValueError(f"Invalid duration string: {duration_string}")
|
|
181
|
+
|
|
182
|
+
unit_multipliers = {
|
|
183
|
+
"w": 7 * 24 * 3600,
|
|
184
|
+
"d": 24 * 3600,
|
|
185
|
+
"h": 3600,
|
|
186
|
+
"m": 60,
|
|
187
|
+
"s": 1,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
total_seconds = 0
|
|
191
|
+
for value, unit in matches:
|
|
192
|
+
if unit not in unit_multipliers:
|
|
193
|
+
raise ValueError(f"Unknown unit: {unit}")
|
|
194
|
+
total_seconds += int(value) * unit_multipliers[unit]
|
|
195
|
+
|
|
196
|
+
return total_seconds
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def adjust_step_for_max_points(
|
|
200
|
+
time_range_seconds: int,
|
|
201
|
+
max_points: int,
|
|
202
|
+
step: Optional[int] = None,
|
|
203
|
+
) -> int:
|
|
204
|
+
"""
|
|
205
|
+
Adjusts the step parameter to ensure the number of data points doesn't exceed max_points.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
time_range_seconds: time range in seconds
|
|
209
|
+
step: The requested step duration in seconds
|
|
210
|
+
max_points: The requested maximum number of data points
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Adjusted step value in seconds that ensures points <= max_points
|
|
214
|
+
"""
|
|
215
|
+
smallest_allowed_step = int(
|
|
216
|
+
math.ceil(float(time_range_seconds) / float(max_points))
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if not step:
|
|
220
|
+
return smallest_allowed_step
|
|
221
|
+
|
|
222
|
+
return max(smallest_allowed_step, step)
|
|
223
|
+
|
|
224
|
+
|
|
137
225
|
def get_param_or_raise(dict: Dict, param: str) -> str:
|
|
138
226
|
value = dict.get(param)
|
|
139
227
|
if not value:
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration utility functions for HolmesGPT.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from holmes.core.transformers import Transformer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def merge_transformers(
|
|
12
|
+
base_transformers: Optional[List["Transformer"]],
|
|
13
|
+
override_transformers: Optional[List["Transformer"]],
|
|
14
|
+
only_merge_when_override_exists: bool = False,
|
|
15
|
+
) -> Optional[List["Transformer"]]:
|
|
16
|
+
"""
|
|
17
|
+
Merge transformer configurations with intelligent field-level merging.
|
|
18
|
+
|
|
19
|
+
Logic:
|
|
20
|
+
- Override transformers take precedence for existing fields
|
|
21
|
+
- Base transformers provide missing fields
|
|
22
|
+
- Merge at transformer-type level (e.g., "llm_summarize")
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
base_transformers: Base transformer configurations (e.g., global transformers)
|
|
26
|
+
override_transformers: Override transformer configurations (e.g., toolset transformers)
|
|
27
|
+
only_merge_when_override_exists: If True, only merge when override_transformers exist.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Merged transformer configuration list or None if both inputs are None/empty
|
|
31
|
+
"""
|
|
32
|
+
if not base_transformers and not override_transformers:
|
|
33
|
+
return None
|
|
34
|
+
if not base_transformers:
|
|
35
|
+
return override_transformers
|
|
36
|
+
if not override_transformers:
|
|
37
|
+
if only_merge_when_override_exists:
|
|
38
|
+
return None # Don't apply base transformers if override doesn't exist
|
|
39
|
+
else:
|
|
40
|
+
return base_transformers # Original behavior: return base transformers
|
|
41
|
+
|
|
42
|
+
# Convert lists to dicts keyed by transformer name for easier merging
|
|
43
|
+
base_dict = {}
|
|
44
|
+
for transformer in base_transformers:
|
|
45
|
+
base_dict[transformer.name] = transformer
|
|
46
|
+
|
|
47
|
+
override_dict = {}
|
|
48
|
+
for transformer in override_transformers:
|
|
49
|
+
override_dict[transformer.name] = transformer
|
|
50
|
+
|
|
51
|
+
# Merge configurations at field level
|
|
52
|
+
merged_transformers = []
|
|
53
|
+
|
|
54
|
+
# Start with all base transformer types
|
|
55
|
+
for transformer_name, base_transformer in base_dict.items():
|
|
56
|
+
if transformer_name in override_dict:
|
|
57
|
+
# Merge fields: override takes precedence, base provides missing fields
|
|
58
|
+
override_transformer = override_dict[transformer_name]
|
|
59
|
+
merged_config = dict(base_transformer.config) # Start with base
|
|
60
|
+
merged_config.update(
|
|
61
|
+
override_transformer.config
|
|
62
|
+
) # Override with specific fields
|
|
63
|
+
|
|
64
|
+
# IMPORTANT: Preserve global_fast_model from both base and override
|
|
65
|
+
# This ensures our injected global_fast_model settings aren't lost during merging
|
|
66
|
+
if "global_fast_model" in base_transformer.config:
|
|
67
|
+
merged_config["global_fast_model"] = base_transformer.config[
|
|
68
|
+
"global_fast_model"
|
|
69
|
+
]
|
|
70
|
+
if "global_fast_model" in override_transformer.config:
|
|
71
|
+
merged_config["global_fast_model"] = override_transformer.config[
|
|
72
|
+
"global_fast_model"
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
# Create new transformer with merged config
|
|
76
|
+
from holmes.core.transformers import Transformer
|
|
77
|
+
|
|
78
|
+
merged_transformer = Transformer(
|
|
79
|
+
name=transformer_name, config=merged_config
|
|
80
|
+
)
|
|
81
|
+
merged_transformers.append(merged_transformer)
|
|
82
|
+
else:
|
|
83
|
+
# No override, use base transformer as-is
|
|
84
|
+
merged_transformers.append(base_transformer)
|
|
85
|
+
|
|
86
|
+
# Add any override-only transformer types
|
|
87
|
+
for transformer_name, override_transformer in override_dict.items():
|
|
88
|
+
if transformer_name not in base_dict:
|
|
89
|
+
merged_transformers.append(override_transformer)
|
|
90
|
+
|
|
91
|
+
return merged_transformers
|
|
@@ -1,21 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
{% if is_default %}
|
|
3
|
-
This integration is enabled by default.
|
|
4
|
-
|
|
5
|
-
If you would like to disable this toolset (not recommended), you need to update the `generated_values.yaml` configuration.
|
|
6
|
-
{% else %}
|
|
7
|
-
To disable this integration, you need to update the `generated_values.yaml` configuration.
|
|
8
|
-
{% endif %}
|
|
9
|
-
|
|
10
|
-
```yaml
|
|
11
|
-
holmes:
|
|
12
|
-
toolsets:
|
|
13
|
-
{{toolset_name}}:
|
|
14
|
-
enabled: false
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
{% else %}
|
|
18
|
-
To enable this integration, update the Helm values for Robusta (`generated_values.yaml`):
|
|
1
|
+
To enable/disable this integration, update the Helm values for Robusta (`generated_values.yaml`):
|
|
19
2
|
|
|
20
3
|
```yaml
|
|
21
4
|
holmes:
|
|
@@ -34,11 +17,7 @@ holmes:
|
|
|
34
17
|
{{ example_config | indent(8) }}
|
|
35
18
|
{% endif %}
|
|
36
19
|
```
|
|
37
|
-
|
|
38
|
-
{% endif %}
|
|
39
|
-
|
|
40
20
|
And deploy the updated configuration using Helm:
|
|
41
|
-
|
|
42
21
|
```bash
|
|
43
22
|
helm upgrade robusta robusta/robusta --values=generated_values.yaml --set clusterName=<YOUR_CLUSTER_NAME>
|
|
44
23
|
```
|
holmes/utils/env.py
CHANGED
|
@@ -6,6 +6,13 @@ from typing import Any, Optional
|
|
|
6
6
|
from pydantic import SecretStr
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
def environ_get_safe_int(env_var: str, default: str = "0") -> int:
|
|
10
|
+
try:
|
|
11
|
+
return max(int(os.environ.get(env_var, default)), 0)
|
|
12
|
+
except ValueError:
|
|
13
|
+
return int(default)
|
|
14
|
+
|
|
15
|
+
|
|
9
16
|
def get_env_replacement(value: str) -> Optional[str]:
|
|
10
17
|
env_patterns = re.findall(r"{{\s*env\.([^}]*)\s*}}", value)
|
|
11
18
|
|