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
@@ -1,110 +0,0 @@
1
- from typing import Any, cast, Set
2
- from pydantic import BaseModel
3
-
4
- from holmes.core.tools import CallablePrerequisite
5
- from holmes.plugins.toolsets.grafana.common import (
6
- GrafanaConfig,
7
- format_log,
8
- get_base_url,
9
- )
10
- from holmes.plugins.toolsets.grafana.grafana_api import grafana_health_check
11
- from holmes.plugins.toolsets.logging_utils.logging_api import (
12
- BasePodLoggingToolset,
13
- FetchPodLogsParams,
14
- LoggingCapability,
15
- PodLoggingTool,
16
- DEFAULT_TIME_SPAN_SECONDS,
17
- )
18
- from holmes.plugins.toolsets.utils import (
19
- process_timestamps_to_rfc3339,
20
- )
21
-
22
- from holmes.plugins.toolsets.grafana.loki_api import (
23
- query_loki_logs_by_label,
24
- )
25
- from holmes.core.tools import StructuredToolResult, ToolResultStatus
26
-
27
-
28
- class GrafanaLokiLabelsConfig(BaseModel):
29
- pod: str = "pod"
30
- namespace: str = "namespace"
31
-
32
-
33
- class GrafanaLokiConfig(GrafanaConfig):
34
- labels: GrafanaLokiLabelsConfig = GrafanaLokiLabelsConfig()
35
-
36
-
37
- class GrafanaLokiToolset(BasePodLoggingToolset):
38
- @property
39
- def supported_capabilities(self) -> Set[LoggingCapability]:
40
- """Loki only supports substring matching, not regex or exclude filters"""
41
- return set() # No regex support, no exclude filter
42
-
43
- def __init__(self):
44
- super().__init__(
45
- name="grafana/loki",
46
- description="Fetches kubernetes pods logs from Loki",
47
- icon_url="https://grafana.com/media/docs/loki/logo-grafana-loki.png",
48
- docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/grafanaloki/",
49
- prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
50
- tools=[], # Initialize with empty tools first
51
- )
52
- # Now that parent is initialized and self.name exists, create the tool
53
- self.tools = [PodLoggingTool(self)]
54
-
55
- def prerequisites_callable(self, config: dict[str, Any]) -> tuple[bool, str]:
56
- if not config:
57
- return False, "Missing Loki configuration. Check your config."
58
-
59
- self.config = GrafanaLokiConfig(**config)
60
-
61
- return grafana_health_check(self.config)
62
-
63
- def get_example_config(self):
64
- example_config = GrafanaLokiConfig(
65
- api_key="YOUR API KEY",
66
- url="YOUR GRAFANA URL",
67
- grafana_datasource_uid="<UID of the loki datasource to use>",
68
- )
69
- return example_config.model_dump()
70
-
71
- @property
72
- def grafana_config(self) -> GrafanaLokiConfig:
73
- return cast(GrafanaLokiConfig, self.config)
74
-
75
- def logger_name(self) -> str:
76
- return "Loki"
77
-
78
- def fetch_pod_logs(self, params: FetchPodLogsParams) -> StructuredToolResult:
79
- (start, end) = process_timestamps_to_rfc3339(
80
- start_timestamp=params.start_time,
81
- end_timestamp=params.end_time,
82
- default_time_span_seconds=DEFAULT_TIME_SPAN_SECONDS,
83
- )
84
-
85
- base_url = get_base_url(self.grafana_config)
86
- logs = query_loki_logs_by_label(
87
- base_url=base_url,
88
- api_key=self.grafana_config.api_key,
89
- headers=self.grafana_config.headers,
90
- filter=params.filter,
91
- namespace=params.namespace,
92
- namespace_search_key=self.grafana_config.labels.namespace,
93
- label=self.grafana_config.labels.pod,
94
- label_value=params.pod_name,
95
- start=start,
96
- end=end,
97
- limit=params.limit or 2000,
98
- )
99
- if logs:
100
- logs.sort(key=lambda x: x["timestamp"])
101
- return StructuredToolResult(
102
- status=ToolResultStatus.SUCCESS,
103
- data="\n".join([format_log(log) for log in logs]),
104
- params=params.model_dump(),
105
- )
106
- else:
107
- return StructuredToolResult(
108
- status=ToolResultStatus.NO_DATA,
109
- params=params.model_dump(),
110
- )
@@ -1,231 +0,0 @@
1
- import requests # type: ignore
2
- import logging
3
- from typing import Any, Optional, Dict
4
- from holmes.core.tools import (
5
- CallablePrerequisite,
6
- Tool,
7
- ToolParameter,
8
- Toolset,
9
- ToolsetTag,
10
- )
11
- from pydantic import BaseModel
12
- from holmes.core.tools import StructuredToolResult, ToolResultStatus
13
- from holmes.plugins.toolsets.utils import get_param_or_raise, toolset_name_for_one_liner
14
-
15
-
16
- class BaseNewRelicTool(Tool):
17
- toolset: "NewRelicToolset"
18
-
19
-
20
- class GetLogs(BaseNewRelicTool):
21
- def __init__(self, toolset: "NewRelicToolset"):
22
- super().__init__(
23
- name="newrelic_get_logs",
24
- description="Retrieve logs from New Relic",
25
- parameters={
26
- "app": ToolParameter(
27
- description="The application name to filter logs",
28
- type="string",
29
- required=True,
30
- ),
31
- "since": ToolParameter(
32
- description="Time range to fetch logs (e.g., '1 hour ago')",
33
- type="string",
34
- required=True,
35
- ),
36
- },
37
- toolset=toolset,
38
- )
39
-
40
- def _invoke(
41
- self, params: dict, user_approved: bool = False
42
- ) -> StructuredToolResult:
43
- def success(msg: Any) -> StructuredToolResult:
44
- return StructuredToolResult(
45
- status=ToolResultStatus.SUCCESS,
46
- data=msg,
47
- params=params,
48
- )
49
-
50
- def error(msg: str) -> StructuredToolResult:
51
- return StructuredToolResult(
52
- status=ToolResultStatus.ERROR,
53
- data=msg,
54
- params=params,
55
- )
56
-
57
- app = params.get("app")
58
- since = params.get("since")
59
-
60
- query = {
61
- "query": f"""
62
- {{
63
- actor {{
64
- account(id: {self.toolset.nr_account_id}) {{
65
- nrql(query: \"SELECT * FROM Log WHERE app = '{app}' SINCE {since}\") {{
66
- results
67
- }}
68
- }}
69
- }}
70
- }}
71
- """
72
- }
73
-
74
- url = "https://api.newrelic.com/graphql"
75
- headers = {
76
- "Content-Type": "application/json",
77
- "Api-Key": self.toolset.nr_api_key,
78
- }
79
-
80
- try:
81
- logging.info(f"Getting New Relic logs for app {app} since {since}")
82
- response = requests.post(url, headers=headers, json=query)
83
-
84
- if response.status_code == 200:
85
- return success(response.json())
86
- else:
87
- return error(
88
- f"Failed to fetch logs. Status code: {response.status_code}\n{response.text}"
89
- )
90
- except Exception as e:
91
- logging.exception("Exception while fetching logs")
92
- return error(f"Error while fetching logs: {str(e)}")
93
-
94
- def get_parameterized_one_liner(self, params) -> str:
95
- app = params.get("app", "")
96
- since = params.get("since", "")
97
- return f"{toolset_name_for_one_liner(self.toolset.name)}: Get Logs ({app} - {since})"
98
-
99
-
100
- class GetTraces(BaseNewRelicTool):
101
- def __init__(self, toolset: "NewRelicToolset"):
102
- super().__init__(
103
- name="newrelic_get_traces",
104
- description="Retrieve traces from New Relic",
105
- parameters={
106
- "duration": ToolParameter(
107
- description="Minimum trace duration in seconds",
108
- type="number",
109
- required=True,
110
- ),
111
- "trace_id": ToolParameter(
112
- description="Specific trace ID to fetch details (optional)",
113
- type="string",
114
- required=False,
115
- ),
116
- },
117
- toolset=toolset,
118
- )
119
-
120
- def _invoke(
121
- self, params: dict, user_approved: bool = False
122
- ) -> StructuredToolResult:
123
- def success(msg: Any) -> StructuredToolResult:
124
- return StructuredToolResult(
125
- status=ToolResultStatus.SUCCESS,
126
- data=msg,
127
- params=params,
128
- )
129
-
130
- def error(msg: str) -> StructuredToolResult:
131
- return StructuredToolResult(
132
- status=ToolResultStatus.ERROR,
133
- data=msg,
134
- params=params,
135
- )
136
-
137
- duration = get_param_or_raise(params, "duration")
138
- trace_id = params.get("trace_id")
139
-
140
- if trace_id:
141
- query_string = f"SELECT * FROM Span WHERE trace.id = '{trace_id}' and duration.ms > {duration * 1000} and span.kind != 'internal'"
142
- else:
143
- query_string = f"SELECT * FROM Span WHERE duration.ms > {duration * 1000} and span.kind != 'internal'"
144
-
145
- query = {
146
- "query": f"""
147
- {{
148
- actor {{
149
- account(id: {self.toolset.nr_account_id}) {{
150
- nrql(query: \"{query_string}\") {{
151
- results
152
- }}
153
- }}
154
- }}
155
- }}
156
- """
157
- }
158
-
159
- url = "https://api.newrelic.com/graphql"
160
- headers = {
161
- "Content-Type": "application/json",
162
- "Api-Key": self.toolset.nr_api_key,
163
- }
164
-
165
- try:
166
- logging.info(f"Getting New Relic traces with duration > {duration}s")
167
- response = requests.post(url, headers=headers, json=query)
168
-
169
- if response.status_code == 200:
170
- return success(response.json())
171
- else:
172
- return error(
173
- f"Failed to fetch traces. Status code: {response.status_code}\n{response.text}"
174
- )
175
- except Exception as e:
176
- logging.exception("Exception while fetching traces")
177
- return error(f"Error while fetching traces: {str(e)}")
178
-
179
- def get_parameterized_one_liner(self, params) -> str:
180
- if "trace_id" in params and params["trace_id"]:
181
- trace_id = params.get("trace_id", "")
182
- return f"{toolset_name_for_one_liner(self.toolset.name)}: Get Trace Details ({trace_id})"
183
- duration = params.get("duration", "")
184
- return f"{toolset_name_for_one_liner(self.toolset.name)}: Get Traces (>{duration}s)"
185
-
186
-
187
- class NewrelicConfig(BaseModel):
188
- nr_api_key: Optional[str] = None
189
- nr_account_id: Optional[str] = None
190
-
191
-
192
- class NewRelicToolset(Toolset):
193
- nr_api_key: Optional[str] = None
194
- nr_account_id: Optional[str] = None
195
-
196
- def __init__(self):
197
- super().__init__(
198
- name="newrelic",
199
- description="Toolset for interacting with New Relic to fetch logs and traces",
200
- docs_url="https://docs.newrelic.com/docs/apis/nerdgraph-api/",
201
- icon_url="https://companieslogo.com/img/orig/NEWR-de5fcb2e.png?t=1720244493",
202
- prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
203
- tools=[
204
- GetLogs(self),
205
- GetTraces(self),
206
- ],
207
- experimental=True,
208
- tags=[ToolsetTag.CORE],
209
- )
210
-
211
- def prerequisites_callable(
212
- self, config: dict[str, Any]
213
- ) -> tuple[bool, Optional[str]]:
214
- if not config:
215
- return False, "No configuration provided"
216
-
217
- try:
218
- nr_config = NewrelicConfig(**config)
219
- self.nr_account_id = nr_config.nr_account_id
220
- self.nr_api_key = nr_config.nr_api_key
221
-
222
- if not self.nr_account_id or not self.nr_api_key:
223
- return False, "New Relic account ID or API key is missing"
224
-
225
- return True, None
226
- except Exception as e:
227
- logging.exception("Failed to set up New Relic toolset")
228
- return False, str(e)
229
-
230
- def get_example_config(self) -> Dict[str, Any]:
231
- return {}
@@ -1,37 +0,0 @@
1
- ## Configuration
2
-
3
- [Full guide for reference](https://www.servicenow.com/docs/bundle/yokohama-platform-security/page/integrate/authentication/task/configure-api-key.html)
4
-
5
- ### Create an inbound authentication profile.
6
-
7
- 1. Navigate to All > System Web Services > API Access Policies > Inbound Authentication Profiles.
8
- 2. Select New.
9
- 3. Select Create API Key authentication profiles
10
- 4. Auth Parameter > add x-sn-apikey: Auth Header
11
- 5. Submit the form.
12
-
13
- ### Create a REST API key
14
-
15
- 1. Navigate to All > System Web Services > API Access Policies > REST API Key.
16
- 2. Select New.
17
- 3. Set name, description and user. Set expiry date if desired. > Submit.
18
- 4. Open the record that was created to view the token generated by the ServiceNow AI Platform for the user.
19
-
20
- ### Create a REST API Access policy
21
-
22
- 1. Navigate to All > System Web Services > REST API Access Policies.
23
- 2. Select New.
24
- 3. REST API = Table API
25
- 4. Uncheck Apply to all tables > Select table > change_request
26
- 5. in select profile from step 1 (API Key)
27
-
28
-
29
- Use your `instance name` and `api_key` to set up Service Now configuration.
30
- ```yaml
31
- toolsets:
32
- ServiceNow:
33
- enabled: true
34
- config:
35
- api_key: <api-token>
36
- instance: <dev1234..>
37
- ```
@@ -1,3 +0,0 @@
1
- * ALWAYS fetch changes from servicenow, USE servicenow_return_changes_in_timerange to see changes in the relevant time range.
2
- * If you are investigating an issue on some subject , USE servicenow_return_changes_with_keyword with the object name to find related changes.
3
- * If you find a ServiceNow change that seems relevant to your investigation or the user question, USE servicenow_return_change_details with the change sys_id to get further information and improve your answer if possible.
@@ -1,219 +0,0 @@
1
- import requests # type: ignore
2
- import logging
3
- import os
4
- from typing import Any, Dict, Tuple, List
5
- from holmes.core.tools import (
6
- CallablePrerequisite,
7
- Tool,
8
- ToolParameter,
9
- Toolset,
10
- ToolsetTag,
11
- )
12
-
13
- from pydantic import BaseModel, PrivateAttr
14
- from holmes.core.tools import StructuredToolResult, ToolResultStatus
15
- from holmes.plugins.toolsets.utils import (
16
- process_timestamps_to_rfc3339,
17
- standard_start_datetime_tool_param_description,
18
- toolset_name_for_one_liner,
19
- )
20
- from holmes.plugins.toolsets.logging_utils.logging_api import (
21
- DEFAULT_TIME_SPAN_SECONDS,
22
- )
23
-
24
-
25
- class ServiceNowConfig(BaseModel):
26
- api_key: str
27
- instance: str
28
-
29
-
30
- class ServiceNowToolset(Toolset):
31
- name: str = "ServiceNow"
32
- description: str = "Database containing changes information related to keys, workloads or any service."
33
- tags: List[ToolsetTag] = [ToolsetTag.CORE]
34
- _session: requests.Session = PrivateAttr(default=requests.Session())
35
-
36
- def __init__(self):
37
- super().__init__(
38
- prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
39
- experimental=True,
40
- tools=[
41
- ReturnChangesInTimerange(toolset=self),
42
- ReturnChange(toolset=self),
43
- ReturnChangesWithKeyword(toolset=self),
44
- ],
45
- )
46
- instructions_filepath = os.path.abspath(
47
- os.path.join(os.path.dirname(__file__), "instructions.jinja2")
48
- )
49
- self._load_llm_instructions(jinja_template=f"file://{instructions_filepath}")
50
-
51
- def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
52
- if not config:
53
- return False, "Missing config credentials."
54
-
55
- try:
56
- self.config: Dict = ServiceNowConfig(**config).model_dump()
57
- self._session.headers.update(
58
- {
59
- "x-sn-apikey": self.config.get("api_key"),
60
- }
61
- )
62
-
63
- url = f"https://{self.config.get('instance')}.service-now.com/api/now/v2/table/change_request"
64
- response = self._session.get(url=url, params={"sysparm_limit": 1})
65
-
66
- return response.ok, ""
67
- except Exception as e:
68
- logging.exception(
69
- "Invalid ServiceNow config. Failed to set up ServiceNow toolset"
70
- )
71
- return False, f"Invalid ServiceNow config {e}"
72
-
73
- def get_example_config(self) -> Dict[str, Any]:
74
- example_config = ServiceNowConfig(
75
- api_key="now_xxxxxxxxxxxxxxxx", instance="dev12345"
76
- )
77
- return example_config.model_dump()
78
-
79
-
80
- class ServiceNowBaseTool(Tool):
81
- toolset: ServiceNowToolset
82
-
83
- def return_result(
84
- self, response: requests.Response, params: Any, field: str = "result"
85
- ) -> StructuredToolResult:
86
- response.raise_for_status()
87
- res = response.json()
88
- return StructuredToolResult(
89
- status=ToolResultStatus.SUCCESS
90
- if res.get(field, [])
91
- else ToolResultStatus.NO_DATA,
92
- data=res,
93
- params=params,
94
- )
95
-
96
- def get_parameterized_one_liner(self, params) -> str:
97
- # Default implementation - will be overridden by subclasses
98
- return f"{toolset_name_for_one_liner(self.toolset.name)}: ServiceNow {self.name} {params}"
99
-
100
-
101
- class ReturnChangesInTimerange(ServiceNowBaseTool):
102
- name: str = "servicenow_return_changes_in_timerange"
103
- description: str = "Returns all changes requests from a specific time range. These changes tickets can apply to all components. default to changes from the last 1 hour."
104
- parameters: Dict[str, ToolParameter] = {
105
- "start": ToolParameter(
106
- description=standard_start_datetime_tool_param_description(
107
- DEFAULT_TIME_SPAN_SECONDS
108
- ),
109
- type="string",
110
- required=False,
111
- )
112
- }
113
-
114
- def get_parameterized_one_liner(self, params) -> str:
115
- start = params.get("start", "last hour")
116
- return f"{toolset_name_for_one_liner(self.toolset.name)}: Get Change Requests ({start})"
117
-
118
- def _invoke(
119
- self, params: dict, user_approved: bool = False
120
- ) -> StructuredToolResult:
121
- parsed_params = {}
122
- try:
123
- (start, _) = process_timestamps_to_rfc3339(
124
- start_timestamp=params.get("start"),
125
- end_timestamp=None,
126
- default_time_span_seconds=DEFAULT_TIME_SPAN_SECONDS,
127
- )
128
-
129
- url = f"https://{self.toolset.config.get('instance')}.service-now.com/api/now/v2/table/change_request"
130
- parsed_params.update(
131
- {
132
- "sysparm_fields": "sys_id,number,short_description,type,active,sys_updated_on"
133
- }
134
- )
135
- parsed_params.update({"sysparm_query": f"sys_updated_on>={start}"})
136
-
137
- response = self.toolset._session.get(url=url, params=parsed_params)
138
- return self.return_result(response, parsed_params)
139
- except Exception as e:
140
- logging.exception(self.get_parameterized_one_liner(params))
141
- return StructuredToolResult(
142
- status=ToolResultStatus.ERROR,
143
- data=f"Exception {self.name}: {str(e)}",
144
- params=params,
145
- )
146
-
147
-
148
- class ReturnChange(ServiceNowBaseTool):
149
- name: str = "servicenow_return_change_details"
150
- description: str = "Returns detailed information for one specific ServiceNow change"
151
- parameters: Dict[str, ToolParameter] = {
152
- "sys_id": ToolParameter(
153
- description="The unique identifier of the change. Use servicenow_return_changes_in_timerange tool to fetch list of changes and use 'sys_id' for further information",
154
- type="string",
155
- required=True,
156
- )
157
- }
158
-
159
- def get_parameterized_one_liner(self, params) -> str:
160
- sys_id = params.get("sys_id", "")
161
- return f"{toolset_name_for_one_liner(self.toolset.name)}: Get Change Details ({sys_id})"
162
-
163
- def _invoke(
164
- self, params: dict, user_approved: bool = False
165
- ) -> StructuredToolResult:
166
- try:
167
- url = "https://{instance}.service-now.com/api/now/v2/table/change_request/{sys_id}".format(
168
- instance=self.toolset.config.get("instance"),
169
- sys_id=params.get("sys_id"),
170
- )
171
- response = self.toolset._session.get(url=url)
172
- return self.return_result(response, params)
173
- except Exception as e:
174
- logging.exception(self.get_parameterized_one_liner(params))
175
- return StructuredToolResult(
176
- status=ToolResultStatus.ERROR,
177
- data=f"Exception {self.name}: {str(e)}",
178
- params=params,
179
- )
180
-
181
-
182
- class ReturnChangesWithKeyword(ServiceNowBaseTool):
183
- name: str = "servicenow_return_changes_with_keyword"
184
- description: str = "Returns all changes requests where a keyword is contained in the description. good for finding changes related to a key, workload or any object."
185
- parameters: Dict[str, ToolParameter] = {
186
- "keyword": ToolParameter(
187
- description="key, workload or object name. Keyword that will filter service now changes that are related to this keyword or object.",
188
- type="string",
189
- required=True,
190
- )
191
- }
192
-
193
- def get_parameterized_one_liner(self, params) -> str:
194
- keyword = params.get("keyword", "")
195
- return f"{toolset_name_for_one_liner(self.toolset.name)}: Search Changes ({keyword})"
196
-
197
- def _invoke(
198
- self, params: dict, user_approved: bool = False
199
- ) -> StructuredToolResult:
200
- parsed_params = {}
201
- try:
202
- url = f"https://{self.toolset.config.get('instance')}.service-now.com/api/now/v2/table/change_request"
203
- parsed_params.update(
204
- {
205
- "sysparm_fields": "sys_id,number,short_description,type,active,sys_updated_on"
206
- }
207
- )
208
- parsed_params.update(
209
- {"sysparm_query": f"short_descriptionLIKE{params.get('keyword')}"}
210
- )
211
- response = self.toolset._session.get(url=url, params=parsed_params)
212
- return self.return_result(response, parsed_params)
213
- except Exception as e:
214
- logging.exception(self.get_parameterized_one_liner(params))
215
- return StructuredToolResult(
216
- status=ToolResultStatus.ERROR,
217
- data=f"Exception {self.name}: {str(e)}",
218
- params=params,
219
- )
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 Robusta Dev Ltd
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.