holmesgpt 0.13.2__py3-none-any.whl → 0.18.4__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 (188) hide show
  1. holmes/__init__.py +3 -5
  2. holmes/clients/robusta_client.py +20 -6
  3. holmes/common/env_vars.py +58 -3
  4. holmes/common/openshift.py +1 -1
  5. holmes/config.py +123 -148
  6. holmes/core/conversations.py +71 -15
  7. holmes/core/feedback.py +191 -0
  8. holmes/core/investigation.py +31 -39
  9. holmes/core/investigation_structured_output.py +3 -3
  10. holmes/core/issue.py +1 -1
  11. holmes/core/llm.py +508 -88
  12. holmes/core/models.py +108 -4
  13. holmes/core/openai_formatting.py +14 -1
  14. holmes/core/prompt.py +48 -3
  15. holmes/core/runbooks.py +1 -0
  16. holmes/core/safeguards.py +8 -6
  17. holmes/core/supabase_dal.py +295 -100
  18. holmes/core/tool_calling_llm.py +489 -428
  19. holmes/core/tools.py +325 -56
  20. holmes/core/tools_utils/token_counting.py +21 -0
  21. holmes/core/tools_utils/tool_context_window_limiter.py +40 -0
  22. holmes/core/tools_utils/tool_executor.py +0 -13
  23. holmes/core/tools_utils/toolset_utils.py +1 -0
  24. holmes/core/toolset_manager.py +191 -5
  25. holmes/core/tracing.py +19 -3
  26. holmes/core/transformers/__init__.py +23 -0
  27. holmes/core/transformers/base.py +63 -0
  28. holmes/core/transformers/llm_summarize.py +175 -0
  29. holmes/core/transformers/registry.py +123 -0
  30. holmes/core/transformers/transformer.py +32 -0
  31. holmes/core/truncation/compaction.py +94 -0
  32. holmes/core/truncation/dal_truncation_utils.py +23 -0
  33. holmes/core/truncation/input_context_window_limiter.py +219 -0
  34. holmes/interactive.py +228 -31
  35. holmes/main.py +23 -40
  36. holmes/plugins/interfaces.py +2 -1
  37. holmes/plugins/prompts/__init__.py +2 -1
  38. holmes/plugins/prompts/_fetch_logs.jinja2 +31 -6
  39. holmes/plugins/prompts/_general_instructions.jinja2 +1 -2
  40. holmes/plugins/prompts/_runbook_instructions.jinja2 +24 -12
  41. holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
  42. holmes/plugins/prompts/conversation_history_compaction.jinja2 +89 -0
  43. holmes/plugins/prompts/generic_ask.jinja2 +0 -4
  44. holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -1
  45. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -1
  46. holmes/plugins/prompts/generic_investigation.jinja2 +0 -1
  47. holmes/plugins/prompts/investigation_procedure.jinja2 +50 -1
  48. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -1
  49. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -1
  50. holmes/plugins/runbooks/__init__.py +145 -17
  51. holmes/plugins/runbooks/catalog.json +2 -0
  52. holmes/plugins/sources/github/__init__.py +4 -2
  53. holmes/plugins/sources/prometheus/models.py +1 -0
  54. holmes/plugins/toolsets/__init__.py +44 -27
  55. holmes/plugins/toolsets/aks-node-health.yaml +46 -0
  56. holmes/plugins/toolsets/aks.yaml +64 -0
  57. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +38 -47
  58. holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +3 -2
  59. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
  60. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +3 -2
  61. holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +3 -1
  62. holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +3 -1
  63. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +12 -13
  64. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +15 -12
  65. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +15 -12
  66. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +11 -11
  67. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +11 -9
  68. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +15 -12
  69. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +15 -15
  70. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +11 -8
  71. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +11 -8
  72. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +11 -8
  73. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +11 -8
  74. holmes/plugins/toolsets/azure_sql/utils.py +0 -32
  75. holmes/plugins/toolsets/bash/argocd/__init__.py +3 -3
  76. holmes/plugins/toolsets/bash/aws/__init__.py +4 -4
  77. holmes/plugins/toolsets/bash/azure/__init__.py +4 -4
  78. holmes/plugins/toolsets/bash/bash_toolset.py +11 -15
  79. holmes/plugins/toolsets/bash/common/bash.py +23 -13
  80. holmes/plugins/toolsets/bash/common/bash_command.py +1 -1
  81. holmes/plugins/toolsets/bash/common/stringify.py +1 -1
  82. holmes/plugins/toolsets/bash/kubectl/__init__.py +2 -1
  83. holmes/plugins/toolsets/bash/kubectl/constants.py +0 -1
  84. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +3 -4
  85. holmes/plugins/toolsets/bash/parse_command.py +12 -13
  86. holmes/plugins/toolsets/cilium.yaml +284 -0
  87. holmes/plugins/toolsets/connectivity_check.py +124 -0
  88. holmes/plugins/toolsets/coralogix/api.py +132 -119
  89. holmes/plugins/toolsets/coralogix/coralogix.jinja2 +14 -0
  90. holmes/plugins/toolsets/coralogix/toolset_coralogix.py +219 -0
  91. holmes/plugins/toolsets/coralogix/utils.py +15 -79
  92. holmes/plugins/toolsets/datadog/datadog_api.py +525 -26
  93. holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +55 -11
  94. holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +3 -3
  95. holmes/plugins/toolsets/datadog/datadog_models.py +59 -0
  96. holmes/plugins/toolsets/datadog/datadog_url_utils.py +213 -0
  97. holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +165 -28
  98. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +417 -241
  99. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +234 -214
  100. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +167 -79
  101. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +374 -363
  102. holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
  103. holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
  104. holmes/plugins/toolsets/elasticsearch/opensearch_ppl_query_docs.jinja2 +1616 -0
  105. holmes/plugins/toolsets/elasticsearch/opensearch_query_assist.py +78 -0
  106. holmes/plugins/toolsets/elasticsearch/opensearch_query_assist_instructions.jinja2 +223 -0
  107. holmes/plugins/toolsets/git.py +54 -50
  108. holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
  109. holmes/plugins/toolsets/grafana/common.py +13 -29
  110. holmes/plugins/toolsets/grafana/grafana_tempo_api.py +455 -0
  111. holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +25 -0
  112. holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +191 -0
  113. holmes/plugins/toolsets/grafana/loki_api.py +4 -0
  114. holmes/plugins/toolsets/grafana/toolset_grafana.py +293 -89
  115. holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +49 -0
  116. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
  117. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +820 -292
  118. holmes/plugins/toolsets/grafana/trace_parser.py +4 -3
  119. holmes/plugins/toolsets/internet/internet.py +15 -16
  120. holmes/plugins/toolsets/internet/notion.py +9 -11
  121. holmes/plugins/toolsets/investigator/core_investigation.py +44 -36
  122. holmes/plugins/toolsets/investigator/model.py +3 -1
  123. holmes/plugins/toolsets/json_filter_mixin.py +134 -0
  124. holmes/plugins/toolsets/kafka.py +36 -42
  125. holmes/plugins/toolsets/kubernetes.yaml +317 -113
  126. holmes/plugins/toolsets/kubernetes_logs.py +9 -9
  127. holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
  128. holmes/plugins/toolsets/logging_utils/logging_api.py +94 -8
  129. holmes/plugins/toolsets/mcp/toolset_mcp.py +218 -64
  130. holmes/plugins/toolsets/newrelic/new_relic_api.py +165 -0
  131. holmes/plugins/toolsets/newrelic/newrelic.jinja2 +65 -0
  132. holmes/plugins/toolsets/newrelic/newrelic.py +320 -0
  133. holmes/plugins/toolsets/openshift.yaml +283 -0
  134. holmes/plugins/toolsets/prometheus/prometheus.py +1202 -421
  135. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +54 -5
  136. holmes/plugins/toolsets/prometheus/utils.py +28 -0
  137. holmes/plugins/toolsets/rabbitmq/api.py +23 -4
  138. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +13 -14
  139. holmes/plugins/toolsets/robusta/robusta.py +239 -68
  140. holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +26 -9
  141. holmes/plugins/toolsets/runbook/runbook_fetcher.py +157 -27
  142. holmes/plugins/toolsets/service_discovery.py +1 -1
  143. holmes/plugins/toolsets/servicenow_tables/instructions.jinja2 +83 -0
  144. holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +426 -0
  145. holmes/plugins/toolsets/utils.py +88 -0
  146. holmes/utils/config_utils.py +91 -0
  147. holmes/utils/connection_utils.py +31 -0
  148. holmes/utils/console/result.py +10 -0
  149. holmes/utils/default_toolset_installation_guide.jinja2 +1 -22
  150. holmes/utils/env.py +7 -0
  151. holmes/utils/file_utils.py +2 -1
  152. holmes/utils/global_instructions.py +60 -11
  153. holmes/utils/holmes_status.py +6 -4
  154. holmes/utils/holmes_sync_toolsets.py +0 -2
  155. holmes/utils/krr_utils.py +188 -0
  156. holmes/utils/log.py +15 -0
  157. holmes/utils/markdown_utils.py +2 -3
  158. holmes/utils/memory_limit.py +58 -0
  159. holmes/utils/sentry_helper.py +64 -0
  160. holmes/utils/stream.py +69 -8
  161. holmes/utils/tags.py +4 -3
  162. holmes/version.py +37 -15
  163. holmesgpt-0.18.4.dist-info/LICENSE +178 -0
  164. {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +35 -31
  165. holmesgpt-0.18.4.dist-info/RECORD +258 -0
  166. holmes/core/performance_timing.py +0 -72
  167. holmes/plugins/toolsets/aws.yaml +0 -80
  168. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -112
  169. holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
  170. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -739
  171. holmes/plugins/toolsets/grafana/grafana_api.py +0 -42
  172. holmes/plugins/toolsets/grafana/tempo_api.py +0 -124
  173. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +0 -110
  174. holmes/plugins/toolsets/newrelic.py +0 -231
  175. holmes/plugins/toolsets/opensearch/opensearch.py +0 -257
  176. holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
  177. holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -218
  178. holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
  179. holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
  180. holmes/plugins/toolsets/servicenow/install.md +0 -37
  181. holmes/plugins/toolsets/servicenow/instructions.jinja2 +0 -3
  182. holmes/plugins/toolsets/servicenow/servicenow.py +0 -219
  183. holmes/utils/keygen_utils.py +0 -6
  184. holmesgpt-0.13.2.dist-info/LICENSE.txt +0 -21
  185. holmesgpt-0.13.2.dist-info/RECORD +0 -234
  186. /holmes/plugins/toolsets/{opensearch → newrelic}/__init__.py +0 -0
  187. {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
  188. {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,78 @@
1
+ import logging
2
+ import os
3
+ from typing import Any, Dict
4
+
5
+ from holmes.core.tools import (
6
+ StructuredToolResult,
7
+ StructuredToolResultStatus,
8
+ Tool,
9
+ ToolInvokeContext,
10
+ ToolParameter,
11
+ Toolset,
12
+ ToolsetEnvironmentPrerequisite,
13
+ ToolsetTag,
14
+ )
15
+
16
+
17
+ class PplQueryAssistTool(Tool):
18
+ def __init__(self, toolset: "OpenSearchQueryAssistToolset"):
19
+ super().__init__(
20
+ name="opensearch_ppl_query_assist",
21
+ description="Generate valid OpenSearch Piped Processing Language (PPL) queries to suggest to users for execution",
22
+ parameters={
23
+ "query": ToolParameter(
24
+ description="Valid OpenSearch Piped Processing Language (PPL) query to suggest to users for execution",
25
+ type="string",
26
+ required=True,
27
+ ),
28
+ },
29
+ )
30
+ self._toolset = toolset
31
+
32
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
33
+ try:
34
+ query = params.get("query", "")
35
+ response_data = {"query": query}
36
+ return StructuredToolResult(
37
+ status=StructuredToolResultStatus.SUCCESS,
38
+ data=response_data,
39
+ params=params,
40
+ )
41
+
42
+ except Exception as e:
43
+ logging.exception(f"error using {self.name} tool")
44
+ return StructuredToolResult(
45
+ status=StructuredToolResultStatus.ERROR,
46
+ error=f"Failed to generate PPL query: {str(e)}",
47
+ params=params,
48
+ )
49
+
50
+ def get_parameterized_one_liner(self, params: Dict) -> str:
51
+ query = params.get("query", "")
52
+ return f"OpenSearchQueryToolset: Query ({query})"
53
+
54
+
55
+ class OpenSearchQueryAssistToolset(Toolset):
56
+ """OpenSearch query assist with PPL queries"""
57
+
58
+ def __init__(self):
59
+ super().__init__(
60
+ name="opensearch/query_assist",
61
+ description="OpenSearch query assist with PPL queries.",
62
+ experimental=True,
63
+ icon_url="https://opensearch.org/assets/brand/PNG/Mark/opensearch_mark_default.png",
64
+ tools=[PplQueryAssistTool(self)],
65
+ tags=[ToolsetTag.CORE],
66
+ prerequisites=[ToolsetEnvironmentPrerequisite(env=["OPENSEARCH_URL"])],
67
+ )
68
+
69
+ def get_example_config(self) -> Dict[str, Any]:
70
+ return {"opensearch_url": "http://localhost:9200"}
71
+
72
+ def _reload_instructions(self):
73
+ template_file_path = os.path.abspath(
74
+ os.path.join(
75
+ os.path.dirname(__file__), "opensearch_query_assist_instructions.jinja2"
76
+ )
77
+ )
78
+ self._load_llm_instructions(jinja_template=f"file://{template_file_path}")
@@ -0,0 +1,223 @@
1
+ # Query Generation
2
+ You have access to the opensearch_ppl_query_assist tool to help you generate your valid, accurate OpenSearch Piped Processing Language (PPL) queries.
3
+ DO NOT PROVIDE INVALID QUERIES. ALWAYS CHECK YOUR QUERY WITH VALID QUERIES FIRST.
4
+
5
+ Once a valid query is generated, you MUST provide a concise, but informative breakdown of each part of the query structure
6
+
7
+ ## CRITICAL: Query Intent Detection
8
+
9
+ ALWAYS check if the user's question is about:
10
+
11
+ * Log Analysis: Errors, warnings, messages, patterns, tool usage
12
+ * Metrics Analysis: Performance, latency, throughput, resource usage
13
+ * Time-based Analysis: "Last X hours/days", "recent", "today", "since"
14
+ * Aggregation Requests: Count, sum, average, top, frequency
15
+ * Troubleshooting: Issues, problems, failures, debugging
16
+
17
+ If ANY of the above apply → Generate PPL query IMMEDIATELY and use the OpenSearch Dashboards Page State
18
+
19
+ ### Example GOOD response:
20
+ I've retrieved your current query from the query bar `source=logs-otel-v1* | STAT count() BY severityText` and it
21
+ appears there is a typo in "STAT", it should be "STATS". Below is the fixed query:
22
+ ```
23
+ source=logs-otel-v1* | STATS count() BY severityText
24
+ ```
25
+
26
+
27
+ ## CRITICAL: OpenSearch Dashboards Page State
28
+ User may be using this agent from OpenSearch Dashboards (OSD) for which provides the current page state.
29
+ It may be included in the conversation history as a system message.
30
+
31
+ IMPORTANT: YOU CAN USE THE CURRENT USE QUERY TO HELP ENHANCE/MODIFY/FIX/SUGGEST VALID QUERY USING THE SAME INDEX PATTERN
32
+ REFER TO "Core PPL Commands" FOR SYNTAX
33
+
34
+ ```
35
+ ## OpenSearch PPL Query Language
36
+
37
+ ### PPL (Piped Processing Language) Overview
38
+ PPL is OpenSearch's query language for analyzing logs, metrics, and traces. It uses a pipe-based syntax similar to Unix commands, processing data through sequential transformations.
39
+
40
+ ### Core PPL Commands
41
+
42
+ **Data Source & Search:**
43
+ - `source=<index>` or `search source=<index>` - Specify data source
44
+ - `source=<cluster>:<index>` - Cross-cluster search
45
+ - `| where <condition>` - Filter results
46
+ - `| fields <field-list>` - Project specific fields
47
+ - `| fields - <field-list>` - Exclude specific fields
48
+
49
+ **Data Transformation:**
50
+ - `| stats <aggregation> by <field>` - Aggregate data (count(), sum(), avg(), min(), max())
51
+ - `| eval <field>=<expression>` - Create calculated fields
52
+ - `| sort [+|-] <field>` - Sort results (+ ascending, - descending)
53
+ - `| head <n>` - Return first n results
54
+ - `| tail <n>` - Return last n results
55
+ - `| dedup <field-list>` - Remove duplicates
56
+
57
+ **Advanced Analysis:**
58
+ - `| top [N] <field>` - Find most common values
59
+ - `| rare [N] <field>` - Find least common values
60
+ - `| parse <field> <regex>` - Extract fields using regex patterns
61
+ - `| grok <field> <pattern>` - Parse using grok patterns
62
+ - `| patterns <field> [SIMPLE_PATTERN|BRAIN]` - Extract log patterns
63
+
64
+ **Time Series:**
65
+ - `| trendline SMA(<period>, <field>)` - Calculate moving averages
66
+ - `| fillnull with <value> in <fields>` - Replace null values
67
+
68
+ **Joins & Lookups:**
69
+ - `| join <table>` - Join with another dataset
70
+ - `| lookup <table> <field>` - Enrich with lookup data (requires Calcite)
71
+
72
+ **Pattern Extraction:**
73
+ - `| patterns message BRAIN` - Semantic log pattern extraction
74
+ - `| patterns new_field='extracted' pattern='[0-9]' message` - Custom regex patterns
75
+
76
+ ### PPL Query Examples for Observability
77
+
78
+ **Error Analysis:**
79
+ ```ppl
80
+ source=ai-agent-logs-*
81
+ | where level="ERROR"
82
+ | stats count() by message
83
+ | sort - count
84
+ ```
85
+
86
+ **Service Latency Analysis:**
87
+ ```ppl
88
+ source=traces
89
+ | where service="checkout"
90
+ | stats avg(duration) as avg_latency, max(duration) as max_latency by endpoint
91
+ | where avg_latency > 100
92
+ ```
93
+
94
+ **Log Pattern Detection:**
95
+ ```ppl
96
+ source=ai-agent-audit-logs-*
97
+ | patterns message BRAIN
98
+ | stats count() by patterns_field
99
+ | top 10 patterns_field
100
+ ```
101
+
102
+ **Time-based Aggregation:**
103
+ ```ppl
104
+ source=metrics
105
+ | eval hour=date_format(timestamp, 'HH')
106
+ | stats avg(cpu_usage) by hour, host
107
+ | sort hour
108
+ ```
109
+
110
+ **Multi-field Correlation:**
111
+ ```ppl
112
+ source=ai-agent-logs-*
113
+ | parse message '.*thread_id=(?<tid>[^,]+).*run_id=(?<rid>[^,]+)'
114
+ | stats count() by tid, rid, level
115
+ | where count > 100
116
+ ```
117
+
118
+ **Advanced PPL Query Patterns:**
119
+
120
+ **Top N Analysis with Filtering:**
121
+ ```ppl
122
+ source=ai-agent-logs-*
123
+ | where timestamp >= now() - 1h
124
+ | top 20 message by level
125
+ | where level in ["ERROR", "WARN"]
126
+ ```
127
+
128
+ **Deduplication and Unique Values:**
129
+ ```ppl
130
+ source=ai-agent-audit-logs-*
131
+ | dedup thread_id
132
+ | fields thread_id, run_id, timestamp
133
+ | sort - timestamp
134
+ ```
135
+
136
+ **Fillnull for Missing Data Handling:**
137
+ ```ppl
138
+ source=ai-agent-metrics-*
139
+ | fillnull with 0 in cpu_usage, memory_usage
140
+ | stats avg(cpu_usage) as avg_cpu, avg(memory_usage) as avg_mem by host
141
+ ```
142
+
143
+ **Rare Events Detection:**
144
+ ```ppl
145
+ source=ai-agent-logs-*
146
+ | rare 10 error_code
147
+ | where count < 5
148
+ ```
149
+
150
+ **Field Extraction with Grok:**
151
+ ```ppl
152
+ source=ai-agent-logs-*
153
+ | grok message '%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} %{GREEDYDATA:msg}'
154
+ | stats count() by level
155
+ ```
156
+
157
+ **Time Span Aggregations:**
158
+ ```ppl
159
+ source=ai-agent-metrics-*
160
+ | stats count() by span(timestamp, 5m) as time_bucket, status
161
+ | where status != 200
162
+ ```
163
+
164
+ **Eval with Conditional Logic:**
165
+ ```ppl
166
+ source=ai-agent-logs-*
167
+ | eval severity = case(
168
+ level = "ERROR", 1,
169
+ level = "WARN", 2,
170
+ level = "INFO", 3,
171
+ else = 4
172
+ )
173
+ | stats count() by severity
174
+ ```
175
+
176
+ **Join Operations (with Calcite enabled):**
177
+ ```ppl
178
+ source=ai-agent-logs-*
179
+ | join left=l right=r on l.thread_id = r.thread_id
180
+ [ source=ai-agent-audit-logs-* ]
181
+ | fields l.timestamp, l.message, r.tool_name
182
+ ```
183
+
184
+ **Subquery for Complex Filtering:**
185
+ ```ppl
186
+ source=ai-agent-logs-*
187
+ | where thread_id in [
188
+ source=ai-agent-audit-logs-*
189
+ | where tool_name = "opensearch__search"
190
+ | fields thread_id
191
+ ]
192
+ ```
193
+
194
+ **Trendline for Moving Averages:**
195
+ ```ppl
196
+ source=ai-agent-metrics-*
197
+ | trendline SMA(5, cpu_usage) as cpu_trend
198
+ | fields timestamp, cpu_usage, cpu_trend
199
+ ```
200
+
201
+ ### PPL Best Practices
202
+
203
+ 1. **Index Patterns**: Use wildcards for daily indices: `source=ai-agent-logs-*`
204
+ 2. **Field Extraction**: Use `parse` for structured logs, `patterns` for unstructured
205
+ 3. **Performance**: Apply `where` filters early in the pipeline
206
+ 4. **Aggregations**: Use `stats` before `sort` for better performance
207
+ 5. **Null Handling**: Use `fillnull` to handle missing data in calculations
208
+
209
+ ### OpenSearch Index Patterns (Current Environment)
210
+ - `ai-agent-logs-YYYY.MM.DD` - Application logs
211
+ - `ai-agent-audit-logs-YYYY.MM.DD` - Audit logs
212
+ - `ai-agent-metrics-YYYY.MM.DD` - Prometheus metrics
213
+
214
+ ## Query Response Formatting
215
+ You MUST respond with queries in the following format. `ppl` contains the valid ppl query
216
+ ```typescript
217
+ query: {
218
+ ppl: string,
219
+ }
220
+ ```
221
+
222
+ ## More PPL Queries
223
+ {% include "opensearch_ppl_query_docs.jinja2" %}
@@ -1,17 +1,20 @@
1
1
  import base64
2
2
  import logging
3
- import requests # type: ignore
4
3
  import os
5
- from typing import Any, Optional, Dict, List, Tuple
4
+ from typing import Any, Dict, List, Optional, Tuple
5
+
6
+ import requests # type: ignore
6
7
  from pydantic import BaseModel
7
- from holmes.core.tools import StructuredToolResult, ToolResultStatus
8
8
 
9
9
  from holmes.core.tools import (
10
- Toolset,
10
+ CallablePrerequisite,
11
+ StructuredToolResult,
12
+ StructuredToolResultStatus,
11
13
  Tool,
14
+ ToolInvokeContext,
12
15
  ToolParameter,
16
+ Toolset,
13
17
  ToolsetTag,
14
- CallablePrerequisite,
15
18
  )
16
19
  from holmes.plugins.toolsets.utils import toolset_name_for_one_liner
17
20
 
@@ -20,10 +23,12 @@ class GitHubConfig(BaseModel):
20
23
  git_repo: str
21
24
  git_credentials: str
22
25
  git_branch: str = "main"
26
+ git_url: str = "https://api.github.com"
23
27
 
24
28
 
25
29
  class GitToolset(Toolset):
26
30
  git_repo: Optional[str] = None
31
+ git_url: Optional[str] = None
27
32
  git_credentials: Optional[str] = None
28
33
  git_branch: Optional[str] = None
29
34
  _created_branches: set[str] = set() # Track branches created by the tool
@@ -33,7 +38,7 @@ class GitToolset(Toolset):
33
38
  super().__init__(
34
39
  name="git",
35
40
  description="Runs git commands to read repos and create PRs",
36
- docs_url="https://docs.github.com/en/rest",
41
+ docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/github/",
37
42
  icon_url="https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg",
38
43
  prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
39
44
  tools=[
@@ -75,6 +80,7 @@ class GitToolset(Toolset):
75
80
 
76
81
  try:
77
82
  self.git_repo = os.getenv("GIT_REPO") or config.get("git_repo")
83
+ self.git_url = os.getenv("GIT_URL") or config.get("git_url")
78
84
  self.git_credentials = os.getenv("GIT_CREDENTIALS") or config.get(
79
85
  "git_credentials"
80
86
  )
@@ -82,7 +88,9 @@ class GitToolset(Toolset):
82
88
  "git_branch", "main"
83
89
  )
84
90
 
85
- if not all([self.git_repo, self.git_credentials, self.git_branch]):
91
+ if not all(
92
+ [self.git_repo, self.git_url, self.git_credentials, self.git_branch]
93
+ ):
86
94
  logging.error("Missing one or more required Git configuration values.")
87
95
  return False, "Missing one or more required Git configuration values."
88
96
  return True, ""
@@ -96,7 +104,7 @@ class GitToolset(Toolset):
96
104
  def list_open_prs(self) -> List[Dict[str, Any]]:
97
105
  """Helper method to list all open PRs in the repository."""
98
106
  headers = {"Authorization": f"token {self.git_credentials}"}
99
- url = f"https://api.github.com/repos/{self.git_repo}/pulls?state=open"
107
+ url = f"{self.git_url}/repos/{self.git_repo}/pulls?state=open"
100
108
  resp = requests.get(url, headers=headers)
101
109
  if resp.status_code != 200:
102
110
  raise Exception(self._sanitize_error(f"Error listing PRs: {resp.text}"))
@@ -105,9 +113,7 @@ class GitToolset(Toolset):
105
113
  def get_branch_ref(self, branch_name: str) -> Optional[str]:
106
114
  """Get the SHA of a branch reference."""
107
115
  headers = {"Authorization": f"token {self.git_credentials}"}
108
- url = (
109
- f"https://api.github.com/repos/{self.git_repo}/git/refs/heads/{branch_name}"
110
- )
116
+ url = f"{self.git_url}/repos/{self.git_repo}/git/refs/heads/{branch_name}"
111
117
  resp = requests.get(url, headers=headers)
112
118
  if resp.status_code == 404:
113
119
  return None
@@ -120,7 +126,7 @@ class GitToolset(Toolset):
120
126
  def create_branch(self, branch_name: str, base_sha: str) -> None:
121
127
  """Create a new branch from a base SHA."""
122
128
  headers = {"Authorization": f"token {self.git_credentials}"}
123
- url = f"https://api.github.com/repos/{self.git_repo}/git/refs"
129
+ url = f"{self.git_url}/repos/{self.git_repo}/git/refs"
124
130
  resp = requests.post(
125
131
  url,
126
132
  headers=headers,
@@ -136,7 +142,7 @@ class GitToolset(Toolset):
136
142
  def get_file_content(self, filepath: str, branch: str) -> tuple[str, str]:
137
143
  """Get file content and SHA from a specific branch."""
138
144
  headers = {"Authorization": f"token {self.git_credentials}"}
139
- url = f"https://api.github.com/repos/{self.git_repo}/contents/{filepath}?ref={branch}"
145
+ url = f"{self.git_url}/repos/{self.git_repo}/contents/{filepath}?ref={branch}"
140
146
  resp = requests.get(url, headers=headers)
141
147
  if resp.status_code == 404:
142
148
  raise Exception(f"File not found: {filepath}")
@@ -150,7 +156,7 @@ class GitToolset(Toolset):
150
156
  ) -> None:
151
157
  """Update a file in a specific branch."""
152
158
  headers = {"Authorization": f"token {self.git_credentials}"}
153
- url = f"https://api.github.com/repos/{self.git_repo}/contents/{filepath}"
159
+ url = f"{self.git_url}/repos/{self.git_repo}/contents/{filepath}"
154
160
  encoded_content = base64.b64encode(content.encode()).decode()
155
161
  resp = requests.put(
156
162
  url,
@@ -168,7 +174,7 @@ class GitToolset(Toolset):
168
174
  def create_pr(self, title: str, head: str, base: str, body: str) -> str:
169
175
  """Create a new pull request."""
170
176
  headers = {"Authorization": f"token {self.git_credentials}"}
171
- url = f"https://api.github.com/repos/{self.git_repo}/pulls"
177
+ url = f"{self.git_url}/repos/{self.git_repo}/pulls"
172
178
  resp = requests.post(
173
179
  url,
174
180
  headers=headers,
@@ -188,7 +194,7 @@ class GitToolset(Toolset):
188
194
  def get_pr_details(self, pr_number: int) -> Dict[str, Any]:
189
195
  """Get details of a specific PR."""
190
196
  headers = {"Authorization": f"token {self.git_credentials}"}
191
- url = f"https://api.github.com/repos/{self.git_repo}/pulls/{pr_number}"
197
+ url = f"{self.git_url}/repos/{self.git_repo}/pulls/{pr_number}"
192
198
  resp = requests.get(url, headers=headers)
193
199
  if resp.status_code != 200:
194
200
  raise Exception(
@@ -215,7 +221,7 @@ class GitToolset(Toolset):
215
221
 
216
222
  # Update file
217
223
  headers = {"Authorization": f"token {self.git_credentials}"}
218
- url = f"https://api.github.com/repos/{self.git_repo}/contents/{filepath}"
224
+ url = f"{self.git_url}/repos/{self.git_repo}/contents/{filepath}"
219
225
  encoded_content = base64.b64encode(content.encode()).decode()
220
226
  data = {
221
227
  "message": message,
@@ -250,16 +256,18 @@ class GitReadFileWithLineNumbers(Tool):
250
256
  )
251
257
 
252
258
  def _invoke(
253
- self, params: dict, user_approved: bool = False
259
+ self,
260
+ params: dict,
261
+ context: ToolInvokeContext,
254
262
  ) -> StructuredToolResult:
255
263
  filepath = params["filepath"]
256
264
  try:
257
265
  headers = {"Authorization": f"token {self.toolset.git_credentials}"}
258
- url = f"https://api.github.com/repos/{self.toolset.git_repo}/contents/{filepath}"
266
+ url = f"{self.toolset.git_url}/repos/{self.toolset.git_repo}/contents/{filepath}"
259
267
  resp = requests.get(url, headers=headers)
260
268
  if resp.status_code != 200:
261
269
  return StructuredToolResult(
262
- status=ToolResultStatus.ERROR,
270
+ status=StructuredToolResultStatus.ERROR,
263
271
  data=self.toolset._sanitize_error(
264
272
  f"Error fetching file: {resp.text}"
265
273
  ),
@@ -268,13 +276,13 @@ class GitReadFileWithLineNumbers(Tool):
268
276
  content = base64.b64decode(resp.json()["content"]).decode().splitlines()
269
277
  numbered = "\n".join(f"{i+1}: {line}" for i, line in enumerate(content))
270
278
  return StructuredToolResult(
271
- status=ToolResultStatus.SUCCESS,
279
+ status=StructuredToolResultStatus.SUCCESS,
272
280
  data=numbered,
273
281
  params=params,
274
282
  )
275
283
  except Exception as e:
276
284
  return StructuredToolResult(
277
- status=ToolResultStatus.ERROR,
285
+ status=StructuredToolResultStatus.ERROR,
278
286
  data=self.toolset._sanitize_error(str(e)),
279
287
  params=params,
280
288
  )
@@ -296,15 +304,17 @@ class GitListFiles(Tool):
296
304
  )
297
305
 
298
306
  def _invoke(
299
- self, params: dict, user_approved: bool = False
307
+ self,
308
+ params: dict,
309
+ context: ToolInvokeContext,
300
310
  ) -> StructuredToolResult:
301
311
  try:
302
312
  headers = {"Authorization": f"token {self.toolset.git_credentials}"}
303
- url = f"https://api.github.com/repos/{self.toolset.git_repo}/git/trees/{self.toolset.git_branch}?recursive=1"
313
+ url = f"{self.toolset.git_url}/repos/{self.toolset.git_repo}/git/trees/{self.toolset.git_branch}?recursive=1"
304
314
  resp = requests.get(url, headers=headers)
305
315
  if resp.status_code != 200:
306
316
  return StructuredToolResult(
307
- status=ToolResultStatus.ERROR,
317
+ status=StructuredToolResultStatus.ERROR,
308
318
  data=self.toolset._sanitize_error(
309
319
  f"Error listing files: {resp.text}"
310
320
  ),
@@ -312,13 +322,13 @@ class GitListFiles(Tool):
312
322
  )
313
323
  paths = [entry["path"] for entry in resp.json()["tree"]]
314
324
  return StructuredToolResult(
315
- status=ToolResultStatus.SUCCESS,
325
+ status=StructuredToolResultStatus.SUCCESS,
316
326
  data=paths,
317
327
  params=params,
318
328
  )
319
329
  except Exception as e:
320
330
  return StructuredToolResult(
321
- status=ToolResultStatus.ERROR,
331
+ status=StructuredToolResultStatus.ERROR,
322
332
  data=self.toolset._sanitize_error(str(e)),
323
333
  params=params,
324
334
  )
@@ -338,9 +348,7 @@ class GitListOpenPRs(Tool):
338
348
  toolset=toolset, # type: ignore
339
349
  )
340
350
 
341
- def _invoke(
342
- self, params: dict, user_approved: bool = False
343
- ) -> StructuredToolResult:
351
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
344
352
  try:
345
353
  prs = self.toolset.list_open_prs()
346
354
  formatted = [
@@ -353,13 +361,13 @@ class GitListOpenPRs(Tool):
353
361
  for pr in prs
354
362
  ]
355
363
  return StructuredToolResult(
356
- status=ToolResultStatus.SUCCESS,
364
+ status=StructuredToolResultStatus.SUCCESS,
357
365
  data=formatted,
358
366
  params=params,
359
367
  )
360
368
  except Exception as e:
361
369
  return StructuredToolResult(
362
- status=ToolResultStatus.ERROR,
370
+ status=StructuredToolResultStatus.ERROR,
363
371
  data=self.toolset._sanitize_error(str(e)),
364
372
  params=params,
365
373
  )
@@ -408,19 +416,17 @@ class GitExecuteChanges(Tool):
408
416
  toolset=toolset, # type: ignore
409
417
  )
410
418
 
411
- def _invoke(
412
- self, params: dict, user_approved: bool = False
413
- ) -> StructuredToolResult:
419
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
414
420
  def error(msg: str) -> StructuredToolResult:
415
421
  return StructuredToolResult(
416
- status=ToolResultStatus.ERROR,
422
+ status=StructuredToolResultStatus.ERROR,
417
423
  data=self.toolset._sanitize_error(msg),
418
424
  params=params,
419
425
  )
420
426
 
421
427
  def success(msg: Any) -> StructuredToolResult:
422
428
  return StructuredToolResult(
423
- status=ToolResultStatus.SUCCESS, data=msg, params=params
429
+ status=StructuredToolResultStatus.SUCCESS, data=msg, params=params
424
430
  )
425
431
 
426
432
  def modify_lines(lines: List[str]) -> List[str]:
@@ -628,9 +634,7 @@ class GitUpdatePR(Tool):
628
634
  toolset=toolset, # type: ignore
629
635
  )
630
636
 
631
- def _invoke(
632
- self, params: dict, user_approved: bool = False
633
- ) -> StructuredToolResult:
637
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
634
638
  try:
635
639
  line = params["line"]
636
640
  filename = params["filename"]
@@ -643,24 +647,24 @@ class GitUpdatePR(Tool):
643
647
  # Validate inputs
644
648
  if not commit_message.strip():
645
649
  return StructuredToolResult(
646
- status=ToolResultStatus.ERROR,
650
+ status=StructuredToolResultStatus.ERROR,
647
651
  error="Tool call failed to run: Commit message cannot be empty",
648
652
  )
649
653
  if not filename.strip():
650
654
  return StructuredToolResult(
651
- status=ToolResultStatus.ERROR,
655
+ status=StructuredToolResultStatus.ERROR,
652
656
  error="Tool call failed to run: Filename cannot be empty",
653
657
  )
654
658
  if line < 1:
655
659
  return StructuredToolResult(
656
- status=ToolResultStatus.ERROR,
660
+ status=StructuredToolResultStatus.ERROR,
657
661
  error="Tool call failed to run: Line number must be positive",
658
662
  )
659
663
 
660
664
  # Verify this is a PR created by our tool
661
665
  if not self.toolset.is_created_pr(pr_number):
662
666
  return StructuredToolResult(
663
- status=ToolResultStatus.ERROR,
667
+ status=StructuredToolResultStatus.ERROR,
664
668
  error=f"Tool call failed to run: PR #{pr_number} was not created by this tool. Only PRs created using git_execute_changes can be updated.",
665
669
  )
666
670
 
@@ -714,7 +718,7 @@ class GitUpdatePR(Tool):
714
718
  del content_lines[line - 1]
715
719
  else:
716
720
  return StructuredToolResult(
717
- status=ToolResultStatus.ERROR,
721
+ status=StructuredToolResultStatus.ERROR,
718
722
  error=f"Tool call failed to run: Invalid command: {command}",
719
723
  )
720
724
 
@@ -722,7 +726,7 @@ class GitUpdatePR(Tool):
722
726
 
723
727
  if dry_run:
724
728
  return StructuredToolResult(
725
- status=ToolResultStatus.SUCCESS,
729
+ status=StructuredToolResultStatus.SUCCESS,
726
730
  data=f"DRY RUN: Updated content for PR #{pr_number}:\n\n{updated_content}",
727
731
  )
728
732
 
@@ -731,13 +735,13 @@ class GitUpdatePR(Tool):
731
735
  pr_number, filename, updated_content, commit_message
732
736
  )
733
737
  return StructuredToolResult(
734
- status=ToolResultStatus.SUCCESS,
738
+ status=StructuredToolResultStatus.SUCCESS,
735
739
  data=f"Added commit to PR #{pr_number} successfully",
736
740
  )
737
741
 
738
742
  except Exception as e:
739
743
  return StructuredToolResult(
740
- status=ToolResultStatus.ERROR,
744
+ status=StructuredToolResultStatus.ERROR,
741
745
  error=self.toolset._sanitize_error(
742
746
  f"Tool call failed to run: Error updating PR: {str(e)}"
743
747
  ),
@@ -745,14 +749,14 @@ class GitUpdatePR(Tool):
745
749
 
746
750
  except requests.exceptions.RequestException as e:
747
751
  return StructuredToolResult(
748
- status=ToolResultStatus.ERROR,
752
+ status=StructuredToolResultStatus.ERROR,
749
753
  error=self.toolset._sanitize_error(
750
754
  f"Tool call failed to run: Network error: {str(e)}"
751
755
  ),
752
756
  )
753
757
  except Exception as e:
754
758
  return StructuredToolResult(
755
- status=ToolResultStatus.ERROR,
759
+ status=StructuredToolResultStatus.ERROR,
756
760
  error=self.toolset._sanitize_error(
757
761
  f"Tool call failed to run: Unexpected error: {str(e)}"
758
762
  ),