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.
Files changed (134) hide show
  1. holmes/__init__.py +1 -1
  2. holmes/clients/robusta_client.py +17 -4
  3. holmes/common/env_vars.py +40 -1
  4. holmes/config.py +114 -144
  5. holmes/core/conversations.py +53 -14
  6. holmes/core/feedback.py +191 -0
  7. holmes/core/investigation.py +18 -22
  8. holmes/core/llm.py +489 -88
  9. holmes/core/models.py +103 -1
  10. holmes/core/openai_formatting.py +13 -0
  11. holmes/core/prompt.py +1 -1
  12. holmes/core/safeguards.py +4 -4
  13. holmes/core/supabase_dal.py +293 -100
  14. holmes/core/tool_calling_llm.py +423 -323
  15. holmes/core/tools.py +311 -33
  16. holmes/core/tools_utils/token_counting.py +14 -0
  17. holmes/core/tools_utils/tool_context_window_limiter.py +57 -0
  18. holmes/core/tools_utils/tool_executor.py +13 -8
  19. holmes/core/toolset_manager.py +155 -4
  20. holmes/core/tracing.py +6 -1
  21. holmes/core/transformers/__init__.py +23 -0
  22. holmes/core/transformers/base.py +62 -0
  23. holmes/core/transformers/llm_summarize.py +174 -0
  24. holmes/core/transformers/registry.py +122 -0
  25. holmes/core/transformers/transformer.py +31 -0
  26. holmes/core/truncation/compaction.py +59 -0
  27. holmes/core/truncation/dal_truncation_utils.py +23 -0
  28. holmes/core/truncation/input_context_window_limiter.py +218 -0
  29. holmes/interactive.py +177 -24
  30. holmes/main.py +7 -4
  31. holmes/plugins/prompts/_fetch_logs.jinja2 +26 -1
  32. holmes/plugins/prompts/_general_instructions.jinja2 +1 -2
  33. holmes/plugins/prompts/_runbook_instructions.jinja2 +23 -12
  34. holmes/plugins/prompts/conversation_history_compaction.jinja2 +88 -0
  35. holmes/plugins/prompts/generic_ask.jinja2 +2 -4
  36. holmes/plugins/prompts/generic_ask_conversation.jinja2 +2 -1
  37. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +2 -1
  38. holmes/plugins/prompts/generic_investigation.jinja2 +2 -1
  39. holmes/plugins/prompts/investigation_procedure.jinja2 +48 -0
  40. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +2 -1
  41. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +2 -1
  42. holmes/plugins/runbooks/__init__.py +117 -18
  43. holmes/plugins/runbooks/catalog.json +2 -0
  44. holmes/plugins/toolsets/__init__.py +21 -8
  45. holmes/plugins/toolsets/aks-node-health.yaml +46 -0
  46. holmes/plugins/toolsets/aks.yaml +64 -0
  47. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +26 -36
  48. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +0 -1
  49. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +10 -7
  50. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +9 -6
  51. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +8 -6
  52. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +8 -6
  53. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +9 -6
  54. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +9 -7
  55. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +9 -6
  56. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +9 -6
  57. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +9 -6
  58. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +9 -6
  59. holmes/plugins/toolsets/bash/bash_toolset.py +10 -13
  60. holmes/plugins/toolsets/bash/common/bash.py +7 -7
  61. holmes/plugins/toolsets/cilium.yaml +284 -0
  62. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +5 -3
  63. holmes/plugins/toolsets/datadog/datadog_api.py +490 -24
  64. holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +21 -10
  65. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +349 -216
  66. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +190 -19
  67. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +101 -44
  68. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +13 -16
  69. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +25 -31
  70. holmes/plugins/toolsets/git.py +51 -46
  71. holmes/plugins/toolsets/grafana/common.py +15 -3
  72. holmes/plugins/toolsets/grafana/grafana_api.py +46 -24
  73. holmes/plugins/toolsets/grafana/grafana_tempo_api.py +454 -0
  74. holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +9 -0
  75. holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +117 -0
  76. holmes/plugins/toolsets/grafana/toolset_grafana.py +211 -91
  77. holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +27 -0
  78. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
  79. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +653 -293
  80. holmes/plugins/toolsets/grafana/trace_parser.py +1 -1
  81. holmes/plugins/toolsets/internet/internet.py +6 -7
  82. holmes/plugins/toolsets/internet/notion.py +5 -6
  83. holmes/plugins/toolsets/investigator/core_investigation.py +42 -34
  84. holmes/plugins/toolsets/kafka.py +25 -36
  85. holmes/plugins/toolsets/kubernetes.yaml +58 -84
  86. holmes/plugins/toolsets/kubernetes_logs.py +6 -6
  87. holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
  88. holmes/plugins/toolsets/logging_utils/logging_api.py +80 -4
  89. holmes/plugins/toolsets/mcp/toolset_mcp.py +181 -55
  90. holmes/plugins/toolsets/newrelic/__init__.py +0 -0
  91. holmes/plugins/toolsets/newrelic/new_relic_api.py +125 -0
  92. holmes/plugins/toolsets/newrelic/newrelic.jinja2 +41 -0
  93. holmes/plugins/toolsets/newrelic/newrelic.py +163 -0
  94. holmes/plugins/toolsets/opensearch/opensearch.py +10 -17
  95. holmes/plugins/toolsets/opensearch/opensearch_logs.py +7 -7
  96. holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2 +1616 -0
  97. holmes/plugins/toolsets/opensearch/opensearch_query_assist.py +78 -0
  98. holmes/plugins/toolsets/opensearch/opensearch_query_assist_instructions.jinja2 +223 -0
  99. holmes/plugins/toolsets/opensearch/opensearch_traces.py +13 -16
  100. holmes/plugins/toolsets/openshift.yaml +283 -0
  101. holmes/plugins/toolsets/prometheus/prometheus.py +915 -390
  102. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +43 -2
  103. holmes/plugins/toolsets/prometheus/utils.py +28 -0
  104. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +9 -10
  105. holmes/plugins/toolsets/robusta/robusta.py +236 -65
  106. holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +26 -9
  107. holmes/plugins/toolsets/runbook/runbook_fetcher.py +137 -26
  108. holmes/plugins/toolsets/service_discovery.py +1 -1
  109. holmes/plugins/toolsets/servicenow_tables/instructions.jinja2 +83 -0
  110. holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +426 -0
  111. holmes/plugins/toolsets/utils.py +88 -0
  112. holmes/utils/config_utils.py +91 -0
  113. holmes/utils/default_toolset_installation_guide.jinja2 +1 -22
  114. holmes/utils/env.py +7 -0
  115. holmes/utils/global_instructions.py +75 -10
  116. holmes/utils/holmes_status.py +2 -1
  117. holmes/utils/holmes_sync_toolsets.py +0 -2
  118. holmes/utils/krr_utils.py +188 -0
  119. holmes/utils/sentry_helper.py +41 -0
  120. holmes/utils/stream.py +61 -7
  121. holmes/version.py +34 -14
  122. holmesgpt-0.16.2a0.dist-info/LICENSE +178 -0
  123. {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/METADATA +29 -27
  124. {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/RECORD +126 -102
  125. holmes/core/performance_timing.py +0 -72
  126. holmes/plugins/toolsets/grafana/tempo_api.py +0 -124
  127. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +0 -110
  128. holmes/plugins/toolsets/newrelic.py +0 -231
  129. holmes/plugins/toolsets/servicenow/install.md +0 -37
  130. holmes/plugins/toolsets/servicenow/instructions.jinja2 +0 -3
  131. holmes/plugins/toolsets/servicenow/servicenow.py +0 -219
  132. holmesgpt-0.13.2.dist-info/LICENSE.txt +0 -21
  133. {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/WHEEL +0 -0
  134. {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}"
@@ -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
- {% if enabled %}
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