holmesgpt 0.14.1a0__py3-none-any.whl → 0.14.3a0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of holmesgpt might be problematic. Click here for more details.

Files changed (73) hide show
  1. holmes/__init__.py +1 -1
  2. holmes/clients/robusta_client.py +5 -2
  3. holmes/common/env_vars.py +8 -2
  4. holmes/config.py +4 -7
  5. holmes/core/conversations.py +12 -2
  6. holmes/core/feedback.py +191 -0
  7. holmes/core/llm.py +52 -10
  8. holmes/core/models.py +101 -1
  9. holmes/core/supabase_dal.py +23 -9
  10. holmes/core/tool_calling_llm.py +206 -16
  11. holmes/core/tools.py +20 -7
  12. holmes/core/tools_utils/token_counting.py +13 -0
  13. holmes/core/tools_utils/tool_context_window_limiter.py +45 -23
  14. holmes/core/tools_utils/tool_executor.py +11 -6
  15. holmes/core/toolset_manager.py +7 -3
  16. holmes/core/truncation/dal_truncation_utils.py +23 -0
  17. holmes/interactive.py +146 -14
  18. holmes/plugins/prompts/_fetch_logs.jinja2 +13 -1
  19. holmes/plugins/runbooks/__init__.py +6 -1
  20. holmes/plugins/toolsets/__init__.py +11 -4
  21. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +9 -20
  22. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +2 -3
  23. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +2 -3
  24. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +6 -4
  25. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +6 -4
  26. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +2 -3
  27. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +6 -4
  28. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +2 -3
  29. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +2 -3
  30. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +2 -3
  31. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +2 -3
  32. holmes/plugins/toolsets/bash/bash_toolset.py +4 -7
  33. holmes/plugins/toolsets/cilium.yaml +284 -0
  34. holmes/plugins/toolsets/datadog/datadog_api.py +490 -24
  35. holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +21 -10
  36. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +333 -199
  37. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +181 -9
  38. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +80 -22
  39. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +5 -8
  40. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +7 -12
  41. holmes/plugins/toolsets/git.py +14 -12
  42. holmes/plugins/toolsets/grafana/grafana_tempo_api.py +23 -42
  43. holmes/plugins/toolsets/grafana/toolset_grafana.py +2 -3
  44. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +2 -1
  45. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +21 -39
  46. holmes/plugins/toolsets/internet/internet.py +2 -3
  47. holmes/plugins/toolsets/internet/notion.py +2 -3
  48. holmes/plugins/toolsets/investigator/core_investigation.py +7 -9
  49. holmes/plugins/toolsets/kafka.py +7 -18
  50. holmes/plugins/toolsets/logging_utils/logging_api.py +80 -4
  51. holmes/plugins/toolsets/mcp/toolset_mcp.py +2 -3
  52. holmes/plugins/toolsets/newrelic/__init__.py +0 -0
  53. holmes/plugins/toolsets/newrelic/new_relic_api.py +125 -0
  54. holmes/plugins/toolsets/newrelic/newrelic.jinja2 +41 -0
  55. holmes/plugins/toolsets/newrelic/newrelic.py +211 -0
  56. holmes/plugins/toolsets/opensearch/opensearch.py +5 -12
  57. holmes/plugins/toolsets/opensearch/opensearch_traces.py +3 -6
  58. holmes/plugins/toolsets/prometheus/prometheus.py +808 -419
  59. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +27 -11
  60. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +3 -6
  61. holmes/plugins/toolsets/robusta/robusta.py +4 -9
  62. holmes/plugins/toolsets/runbook/runbook_fetcher.py +93 -13
  63. holmes/plugins/toolsets/servicenow/servicenow.py +5 -10
  64. holmes/utils/sentry_helper.py +1 -1
  65. holmes/utils/stream.py +22 -7
  66. holmes/version.py +34 -14
  67. {holmesgpt-0.14.1a0.dist-info → holmesgpt-0.14.3a0.dist-info}/METADATA +7 -9
  68. {holmesgpt-0.14.1a0.dist-info → holmesgpt-0.14.3a0.dist-info}/RECORD +71 -65
  69. holmes/core/tools_utils/data_types.py +0 -81
  70. holmes/plugins/toolsets/newrelic.py +0 -231
  71. {holmesgpt-0.14.1a0.dist-info → holmesgpt-0.14.3a0.dist-info}/LICENSE.txt +0 -0
  72. {holmesgpt-0.14.1a0.dist-info → holmesgpt-0.14.3a0.dist-info}/WHEEL +0 -0
  73. {holmesgpt-0.14.1a0.dist-info → holmesgpt-0.14.3a0.dist-info}/entry_points.txt +0 -0
@@ -10,6 +10,7 @@ from urllib.parse import urlparse
10
10
  from holmes.core.tools import (
11
11
  CallablePrerequisite,
12
12
  Tool,
13
+ ToolInvokeContext,
13
14
  ToolParameter,
14
15
  Toolset,
15
16
  StructuredToolResult,
@@ -23,85 +24,120 @@ from holmes.plugins.toolsets.datadog.datadog_api import (
23
24
  execute_datadog_http_request,
24
25
  get_headers,
25
26
  MAX_RETRY_COUNT_ON_RATE_LIMIT,
27
+ preprocess_time_fields,
28
+ enhance_error_message,
29
+ fetch_openapi_spec,
26
30
  )
27
31
  from holmes.plugins.toolsets.utils import toolset_name_for_one_liner
28
32
 
29
33
  # Maximum response size in bytes (10MB)
30
34
  MAX_RESPONSE_SIZE = 10 * 1024 * 1024
31
35
 
32
- # Whitelisted API endpoint patterns - READ ONLY operations
36
+ # Whitelisted API endpoint patterns with optional hints
37
+ # Format: (pattern, hint) - hint is empty string if no special instructions
33
38
  WHITELISTED_ENDPOINTS = [
34
39
  # Monitors
35
- r"^/api/v\d+/monitor(/search)?$",
36
- r"^/api/v\d+/monitor/\d+(/downtimes)?$",
37
- r"^/api/v\d+/monitor/groups/search$",
40
+ (r"^/api/v\d+/monitor(/search)?$", ""),
41
+ (r"^/api/v\d+/monitor/\d+(/downtimes)?$", ""),
42
+ (r"^/api/v\d+/monitor/groups/search$", ""),
38
43
  # Dashboards
39
- r"^/api/v\d+/dashboard(/lists)?$",
40
- r"^/api/v\d+/dashboard/[^/]+$",
41
- r"^/api/v\d+/dashboard/public/[^/]+$",
42
- # SLOs (Service Level Objectives)
43
- r"^/api/v\d+/slo(/search)?$",
44
- r"^/api/v\d+/slo/[^/]+(/history)?$",
45
- r"^/api/v\d+/slo/[^/]+/corrections$",
44
+ (r"^/api/v\d+/dashboard(/lists)?$", ""),
45
+ (r"^/api/v\d+/dashboard/[^/]+$", ""),
46
+ (r"^/api/v\d+/dashboard/public/[^/]+$", ""),
47
+ # SLOs
48
+ (r"^/api/v\d+/slo(/search)?$", ""),
49
+ (r"^/api/v\d+/slo/[^/]+(/history)?$", ""),
50
+ (r"^/api/v\d+/slo/[^/]+/corrections$", ""),
46
51
  # Events
47
- r"^/api/v\d+/events$",
48
- r"^/api/v\d+/events/\d+$",
52
+ (
53
+ r"^/api/v\d+/events$",
54
+ "Use time range parameters 'start' and 'end' as Unix timestamps",
55
+ ),
56
+ (r"^/api/v\d+/events/\d+$", ""),
49
57
  # Incidents
50
- r"^/api/v\d+/incidents(/search)?$",
51
- r"^/api/v\d+/incidents/[^/]+$",
52
- r"^/api/v\d+/incidents/[^/]+/attachments$",
53
- r"^/api/v\d+/incidents/[^/]+/connected_integrations$",
54
- r"^/api/v\d+/incidents/[^/]+/relationships$",
55
- r"^/api/v\d+/incidents/[^/]+/timeline$",
58
+ (r"^/api/v\d+/incidents(/search)?$", ""),
59
+ (r"^/api/v\d+/incidents/[^/]+$", ""),
60
+ (r"^/api/v\d+/incidents/[^/]+/attachments$", ""),
61
+ (r"^/api/v\d+/incidents/[^/]+/connected_integrations$", ""),
62
+ (r"^/api/v\d+/incidents/[^/]+/relationships$", ""),
63
+ (r"^/api/v\d+/incidents/[^/]+/timeline$", ""),
56
64
  # Synthetics
57
- r"^/api/v\d+/synthetics/tests(/search)?$",
58
- r"^/api/v\d+/synthetics/tests/[^/]+$",
59
- r"^/api/v\d+/synthetics/tests/[^/]+/results$",
60
- r"^/api/v\d+/synthetics/tests/browser/[^/]+/results$",
61
- r"^/api/v\d+/synthetics/tests/api/[^/]+/results$",
62
- r"^/api/v\d+/synthetics/locations$",
63
- # Security Monitoring
64
- r"^/api/v\d+/security_monitoring/rules(/search)?$",
65
- r"^/api/v\d+/security_monitoring/rules/[^/]+$",
66
- r"^/api/v\d+/security_monitoring/signals(/search)?$",
67
- r"^/api/v\d+/security_monitoring/signals/[^/]+$",
68
- # Service Map / APM Services
69
- r"^/api/v\d+/services$",
70
- r"^/api/v\d+/services/[^/]+$",
71
- r"^/api/v\d+/services/[^/]+/dependencies$",
65
+ (r"^/api/v\d+/synthetics/tests(/search)?$", ""),
66
+ (r"^/api/v\d+/synthetics/tests/[^/]+$", ""),
67
+ (r"^/api/v\d+/synthetics/tests/[^/]+/results$", ""),
68
+ (r"^/api/v\d+/synthetics/tests/browser/[^/]+/results$", ""),
69
+ (r"^/api/v\d+/synthetics/tests/api/[^/]+/results$", ""),
70
+ (r"^/api/v\d+/synthetics/locations$", ""),
71
+ # Security
72
+ (r"^/api/v\d+/security_monitoring/rules(/search)?$", ""),
73
+ (r"^/api/v\d+/security_monitoring/rules/[^/]+$", ""),
74
+ (r"^/api/v\d+/security_monitoring/signals(/search)?$", ""),
75
+ (r"^/api/v\d+/security_monitoring/signals/[^/]+$", ""),
76
+ # Services
77
+ (r"^/api/v\d+/services$", ""),
78
+ (r"^/api/v\d+/services/[^/]+$", ""),
79
+ (r"^/api/v\d+/services/[^/]+/dependencies$", ""),
80
+ (r"^/api/v\d+/service_dependencies$", ""),
72
81
  # Hosts
73
- r"^/api/v\d+/hosts$",
74
- r"^/api/v\d+/hosts/totals$",
75
- r"^/api/v\d+/hosts/[^/]+$",
76
- # Usage & Cost
77
- r"^/api/v\d+/usage/[^/]+$",
78
- r"^/api/v\d+/usage/summary$",
79
- r"^/api/v\d+/usage/billable-summary$",
80
- r"^/api/v\d+/usage/cost_by_org$",
81
- r"^/api/v\d+/usage/estimated_cost$",
82
+ (r"^/api/v\d+/hosts$", ""),
83
+ (r"^/api/v\d+/hosts/totals$", ""),
84
+ (r"^/api/v\d+/hosts/[^/]+$", ""),
85
+ # Usage
86
+ (r"^/api/v\d+/usage/[^/]+$", ""),
87
+ (r"^/api/v\d+/usage/summary$", ""),
88
+ (r"^/api/v\d+/usage/billable-summary$", ""),
89
+ (r"^/api/v\d+/usage/cost_by_org$", ""),
90
+ (r"^/api/v\d+/usage/estimated_cost$", ""),
82
91
  # Processes
83
- r"^/api/v\d+/processes$",
92
+ (r"^/api/v\d+/processes$", ""),
84
93
  # Tags
85
- r"^/api/v\d+/tags/hosts(/[^/]+)?$",
94
+ (r"^/api/v\d+/tags/hosts(/[^/]+)?$", ""),
86
95
  # Notebooks
87
- r"^/api/v\d+/notebooks$",
88
- r"^/api/v\d+/notebooks/\d+$",
89
- # Service Dependencies
90
- r"^/api/v\d+/service_dependencies$",
96
+ (r"^/api/v\d+/notebooks$", ""),
97
+ (r"^/api/v\d+/notebooks/\d+$", ""),
91
98
  # Organization
92
- r"^/api/v\d+/org$",
93
- r"^/api/v\d+/org/[^/]+$",
94
- # Users (read only)
95
- r"^/api/v\d+/users$",
96
- r"^/api/v\d+/users/[^/]+$",
97
- # Teams (read only)
98
- r"^/api/v\d+/teams$",
99
- r"^/api/v\d+/teams/[^/]+$",
100
- # Audit logs
101
- r"^/api/v\d+/audit/events$",
102
- # Service Accounts (read only)
103
- r"^/api/v\d+/service_accounts$",
104
- r"^/api/v\d+/service_accounts/[^/]+$",
99
+ (r"^/api/v\d+/org$", ""),
100
+ (r"^/api/v\d+/org/[^/]+$", ""),
101
+ # Users
102
+ (r"^/api/v\d+/users$", ""),
103
+ (r"^/api/v\d+/users/[^/]+$", ""),
104
+ # Teams
105
+ (r"^/api/v\d+/teams$", ""),
106
+ (r"^/api/v\d+/teams/[^/]+$", ""),
107
+ # Logs
108
+ (
109
+ r"^/api/v1/logs/config/indexes$",
110
+ "When available, prefer using fetch_pod_logs tool from datadog/logs toolset instead of calling this API directly with the datadog/general toolset",
111
+ ),
112
+ (
113
+ r"^/api/v2/logs/events$",
114
+ "When available, prefer using fetch_pod_logs tool from datadog/logs toolset instead of calling this API directly with the datadog/general toolset. Use RFC3339 timestamps (e.g., '2024-01-01T00:00:00Z')",
115
+ ),
116
+ (
117
+ r"^/api/v2/logs/events/search$",
118
+ 'When available, prefer using fetch_pod_logs tool from datadog/logs toolset instead of calling this API directly with the datadog/general toolset. RFC3339 time format. Example: {"filter": {"from": "2024-01-01T00:00:00Z", "to": "2024-01-02T00:00:00Z", "query": "*"}}',
119
+ ),
120
+ (
121
+ r"^/api/v2/logs/analytics/aggregate$",
122
+ "When available, prefer using fetch_pod_logs tool from datadog/logs toolset instead of calling this API directly with the datadog/general toolset. Do not include 'sort' parameter",
123
+ ),
124
+ # Metrics
125
+ (
126
+ r"^/api/v\d+/metrics$",
127
+ "When available, prefer using query_datadog_metrics tool from datadog/metrics toolset instead of calling this API directly with the datadog/general toolset",
128
+ ),
129
+ (
130
+ r"^/api/v\d+/metrics/[^/]+$",
131
+ "When available, prefer using get_datadog_metric_metadata tool from datadog/metrics toolset instead of calling this API directly with the datadog/general toolset",
132
+ ),
133
+ (
134
+ r"^/api/v\d+/query$",
135
+ "When available, prefer using query_datadog_metrics tool from datadog/metrics toolset instead of calling this API directly with the datadog/general toolset. Use 'from' and 'to' as Unix timestamps",
136
+ ),
137
+ (
138
+ r"^/api/v\d+/search/query$",
139
+ "When available, prefer using query_datadog_metrics tool from datadog/metrics toolset instead of calling this API directly with the datadog/general toolset",
140
+ ),
105
141
  ]
106
142
 
107
143
  # Blacklisted path segments that indicate write operations
@@ -146,9 +182,13 @@ WHITELISTED_POST_ENDPOINTS = [
146
182
  r"^/api/v\d+/security_monitoring/rules/search$",
147
183
  r"^/api/v\d+/security_monitoring/signals/search$",
148
184
  r"^/api/v\d+/logs/events/search$",
185
+ r"^/api/v2/logs/events/search$",
186
+ r"^/api/v2/logs/analytics/aggregate$",
149
187
  r"^/api/v\d+/spans/events/search$",
150
188
  r"^/api/v\d+/rum/events/search$",
151
189
  r"^/api/v\d+/audit/events/search$",
190
+ r"^/api/v\d+/query$",
191
+ r"^/api/v\d+/search/query$",
152
192
  ]
153
193
 
154
194
 
@@ -165,11 +205,12 @@ class DatadogGeneralToolset(Toolset):
165
205
  """General-purpose Datadog API toolset for read-only operations not covered by specialized toolsets."""
166
206
 
167
207
  dd_config: Optional[DatadogGeneralConfig] = None
208
+ openapi_spec: Optional[Dict[str, Any]] = None
168
209
 
169
210
  def __init__(self):
170
211
  super().__init__(
171
212
  name="datadog/general",
172
- description="General-purpose Datadog API access for read-only operations including monitors, dashboards, SLOs, incidents, synthetics, and more",
213
+ description="General-purpose Datadog API access for read-only operations including monitors, dashboards, SLOs, incidents, synthetics, logs, metrics, and more. Note: For logs and metrics, prefer using the specialized datadog/logs and datadog/metrics toolsets when available as they provide optimized functionality",
173
214
  docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/datadog/",
174
215
  icon_url="https://imgix.datadoghq.com//img/about/presskit/DDlogo.jpg",
175
216
  prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
@@ -190,11 +231,27 @@ class DatadogGeneralToolset(Toolset):
190
231
  def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
191
232
  """Check prerequisites with configuration."""
192
233
  if not config:
193
- return False, TOOLSET_CONFIG_MISSING_ERROR
234
+ return (
235
+ False,
236
+ "Missing config for dd_api_key, dd_app_key, or site_api_url. For details: https://holmesgpt.dev/data-sources/builtin-toolsets/datadog/",
237
+ )
194
238
 
195
239
  try:
196
240
  dd_config = DatadogGeneralConfig(**config)
197
241
  self.dd_config = dd_config
242
+
243
+ # Fetch OpenAPI spec on startup for better error messages and documentation
244
+ logging.debug("Fetching Datadog OpenAPI specification...")
245
+ self.openapi_spec = fetch_openapi_spec(version="both")
246
+ if self.openapi_spec:
247
+ logging.info(
248
+ f"Successfully loaded OpenAPI spec with {len(self.openapi_spec.get('paths', {}))} endpoints"
249
+ )
250
+ else:
251
+ logging.warning(
252
+ "Could not fetch OpenAPI spec; enhanced error messages will be limited"
253
+ )
254
+
198
255
  success, error_msg = self._perform_healthcheck(dd_config)
199
256
  return success, error_msg
200
257
  except Exception as e:
@@ -205,7 +262,8 @@ class DatadogGeneralToolset(Toolset):
205
262
  """Perform health check on Datadog API."""
206
263
  try:
207
264
  logging.info("Performing Datadog general API configuration healthcheck...")
208
- url = f"{dd_config.site_api_url}/api/v1/validate"
265
+ base_url = str(dd_config.site_api_url).rstrip("/")
266
+ url = f"{base_url}/api/v1/validate"
209
267
  headers = get_headers(dd_config)
210
268
 
211
269
  data = execute_datadog_http_request(
@@ -217,7 +275,7 @@ class DatadogGeneralToolset(Toolset):
217
275
  )
218
276
 
219
277
  if data.get("valid", False):
220
- logging.info("Datadog general API healthcheck completed successfully")
278
+ logging.debug("Datadog general API healthcheck completed successfully")
221
279
  return True, ""
222
280
  else:
223
281
  error_msg = "Datadog API key validation failed"
@@ -266,7 +324,7 @@ def is_endpoint_allowed(
266
324
  return False, f"POST method not allowed for endpoint: {path}"
267
325
 
268
326
  elif method == "GET":
269
- for pattern in WHITELISTED_ENDPOINTS:
327
+ for pattern, _ in WHITELISTED_ENDPOINTS:
270
328
  if re.match(pattern, path):
271
329
  return True, ""
272
330
 
@@ -280,6 +338,23 @@ def is_endpoint_allowed(
280
338
  return False, f"HTTP method {method} not allowed for {path}"
281
339
 
282
340
 
341
+ def get_endpoint_hint(endpoint: str) -> str:
342
+ """
343
+ Get hint for an endpoint if available.
344
+
345
+ Returns:
346
+ Hint string or empty string if no hint
347
+ """
348
+ parsed = urlparse(endpoint)
349
+ path = parsed.path
350
+
351
+ for pattern, hint in WHITELISTED_ENDPOINTS:
352
+ if re.match(pattern, path):
353
+ return hint
354
+
355
+ return ""
356
+
357
+
283
358
  class BaseDatadogGeneralTool(Tool):
284
359
  """Base class for general Datadog API tools."""
285
360
 
@@ -292,7 +367,7 @@ class DatadogAPIGet(BaseDatadogGeneralTool):
292
367
  def __init__(self, toolset: "DatadogGeneralToolset"):
293
368
  super().__init__(
294
369
  name="datadog_api_get",
295
- description="Make a GET request to a Datadog API endpoint for read-only operations",
370
+ description="[datadog/general toolset] Make a GET request to a Datadog API endpoint for read-only operations",
296
371
  parameters={
297
372
  "endpoint": ToolParameter(
298
373
  description="The API endpoint path (e.g., '/api/v1/monitors', '/api/v2/events')",
@@ -300,7 +375,14 @@ class DatadogAPIGet(BaseDatadogGeneralTool):
300
375
  required=True,
301
376
  ),
302
377
  "query_params": ToolParameter(
303
- description="Query parameters as a dictionary (e.g., {'from': '2024-01-01', 'to': '2024-01-02'})",
378
+ description="""Query parameters as a dictionary.
379
+ Time format requirements:
380
+ - v1 API: Unix timestamps in seconds (e.g., {'start': 1704067200, 'end': 1704153600})
381
+ - v2 API: RFC3339 format (e.g., {'from': '2024-01-01T00:00:00Z', 'to': '2024-01-02T00:00:00Z'})
382
+ - Relative times like '-24h', 'now', '-7d' will be auto-converted to proper format
383
+
384
+ Example for events: {'start': 1704067200, 'end': 1704153600}
385
+ Example for monitors: {'name': 'my-monitor', 'tags': 'env:prod'}""",
304
386
  type="object",
305
387
  required=False,
306
388
  ),
@@ -318,9 +400,7 @@ class DatadogAPIGet(BaseDatadogGeneralTool):
318
400
  description = params.get("description", "API call")
319
401
  return f"{toolset_name_for_one_liner(self.toolset.name)}: {description}"
320
402
 
321
- def _invoke(
322
- self, params: dict, user_approved: bool = False
323
- ) -> StructuredToolResult:
403
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
324
404
  """Execute the GET request."""
325
405
  logging.info("=" * 60)
326
406
  logging.info("DatadogAPIGet Tool Invocation:")
@@ -365,11 +445,14 @@ class DatadogAPIGet(BaseDatadogGeneralTool):
365
445
 
366
446
  logging.info(f"Full API URL: {url}")
367
447
 
448
+ # Preprocess time fields if any
449
+ processed_params = preprocess_time_fields(query_params, endpoint)
450
+
368
451
  # Execute request
369
452
  response = execute_datadog_http_request(
370
453
  url=url,
371
454
  headers=headers,
372
- payload_or_params=query_params,
455
+ payload_or_params=processed_params,
373
456
  timeout=self.toolset.dd_config.request_timeout,
374
457
  method="GET",
375
458
  )
@@ -403,6 +486,11 @@ class DatadogAPIGet(BaseDatadogGeneralTool):
403
486
  )
404
487
  elif e.status_code == 404:
405
488
  error_msg = f"Endpoint not found: {endpoint}"
489
+ elif e.status_code == 400:
490
+ # Use enhanced error message for 400 errors
491
+ error_msg = enhance_error_message(
492
+ e, endpoint, "GET", str(self.toolset.dd_config.site_api_url)
493
+ )
406
494
  else:
407
495
  error_msg = f"API error {e.status_code}: {str(e)}"
408
496
 
@@ -430,7 +518,7 @@ class DatadogAPIPostSearch(BaseDatadogGeneralTool):
430
518
  def __init__(self, toolset: "DatadogGeneralToolset"):
431
519
  super().__init__(
432
520
  name="datadog_api_post_search",
433
- description="Make a POST request to Datadog search/query endpoints for complex filtering",
521
+ description="[datadog/general toolset] Make a POST request to Datadog search/query endpoints for complex filtering",
434
522
  parameters={
435
523
  "endpoint": ToolParameter(
436
524
  description="The search API endpoint (e.g., '/api/v2/monitor/search', '/api/v2/events/search')",
@@ -438,7 +526,29 @@ class DatadogAPIPostSearch(BaseDatadogGeneralTool):
438
526
  required=True,
439
527
  ),
440
528
  "body": ToolParameter(
441
- description="Request body for the search/filter operation",
529
+ description="""Request body for the search/filter operation.
530
+ Time format requirements:
531
+ - v1 API: Unix timestamps (e.g., 1704067200)
532
+ - v2 API: RFC3339 format (e.g., '2024-01-01T00:00:00Z')
533
+ - Relative times like '-24h', 'now', '-7d' will be auto-converted
534
+
535
+ Example for logs search:
536
+ {
537
+ "filter": {
538
+ "from": "2024-01-01T00:00:00Z",
539
+ "to": "2024-01-02T00:00:00Z",
540
+ "query": "*"
541
+ },
542
+ "sort": "-timestamp",
543
+ "page": {"limit": 50}
544
+ }
545
+
546
+ Example for monitor search:
547
+ {
548
+ "query": "env:production",
549
+ "page": 0,
550
+ "per_page": 20
551
+ }""",
442
552
  type="object",
443
553
  required=True,
444
554
  ),
@@ -456,9 +566,7 @@ class DatadogAPIPostSearch(BaseDatadogGeneralTool):
456
566
  description = params.get("description", "Search")
457
567
  return f"{toolset_name_for_one_liner(self.toolset.name)}: {description}"
458
568
 
459
- def _invoke(
460
- self, params: dict, user_approved: bool = False
461
- ) -> StructuredToolResult:
569
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
462
570
  """Execute the POST search request."""
463
571
  logging.info("=" * 60)
464
572
  logging.info("DatadogAPIPostSearch Tool Invocation:")
@@ -501,11 +609,14 @@ class DatadogAPIPostSearch(BaseDatadogGeneralTool):
501
609
 
502
610
  logging.info(f"Full API URL: {url}")
503
611
 
612
+ # Preprocess time fields if any
613
+ processed_body = preprocess_time_fields(body, endpoint)
614
+
504
615
  # Execute request
505
616
  response = execute_datadog_http_request(
506
617
  url=url,
507
618
  headers=headers,
508
- payload_or_params=body,
619
+ payload_or_params=processed_body,
509
620
  timeout=self.toolset.dd_config.request_timeout,
510
621
  method="POST",
511
622
  )
@@ -539,6 +650,11 @@ class DatadogAPIPostSearch(BaseDatadogGeneralTool):
539
650
  )
540
651
  elif e.status_code == 404:
541
652
  error_msg = f"Endpoint not found: {endpoint}"
653
+ elif e.status_code == 400:
654
+ # Use enhanced error message for 400 errors
655
+ error_msg = enhance_error_message(
656
+ e, endpoint, "POST", str(self.toolset.dd_config.site_api_url)
657
+ )
542
658
  else:
543
659
  error_msg = f"API error {e.status_code}: {str(e)}"
544
660
 
@@ -564,10 +680,10 @@ class ListDatadogAPIResources(BaseDatadogGeneralTool):
564
680
  def __init__(self, toolset: "DatadogGeneralToolset"):
565
681
  super().__init__(
566
682
  name="list_datadog_api_resources",
567
- description="List available Datadog API resources and endpoints that can be accessed",
683
+ description="[datadog/general toolset] List available Datadog API resources and endpoints that can be accessed",
568
684
  parameters={
569
- "category": ToolParameter(
570
- description="Filter by category (e.g., 'monitors', 'dashboards', 'slos', 'incidents', 'synthetics', 'security', 'hosts', 'all')",
685
+ "search_regex": ToolParameter(
686
+ description="Optional regex pattern to filter endpoints (e.g., 'monitor', 'logs|metrics', 'security.*signals', 'v2/.*search$'). If not provided, shows all endpoints.",
571
687
  type="string",
572
688
  required=False,
573
689
  ),
@@ -577,142 +693,160 @@ class ListDatadogAPIResources(BaseDatadogGeneralTool):
577
693
 
578
694
  def get_parameterized_one_liner(self, params: dict) -> str:
579
695
  """Get a one-liner description of the tool invocation."""
580
- category = params.get("category", "all")
581
- return f"{toolset_name_for_one_liner(self.toolset.name)}: List API Resources ({category})"
696
+ search = params.get("search_regex", "all")
697
+ return f"{toolset_name_for_one_liner(self.toolset.name)}: List API Resources (search: {search})"
582
698
 
583
- def _invoke(
584
- self, params: dict, user_approved: bool = False
585
- ) -> StructuredToolResult:
699
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
586
700
  """List available API resources."""
587
- category = params.get("category", "all").lower()
701
+ search_regex = params.get("search_regex", "")
588
702
 
589
703
  logging.info("=" * 60)
590
704
  logging.info("ListDatadogAPIResources Tool Invocation:")
591
- logging.info(f" Category: {category}")
705
+ logging.info(f" Search regex: {search_regex or 'None (showing all)'}")
706
+ logging.info(f" OpenAPI Spec Loaded: {self.toolset.openapi_spec is not None}")
592
707
  logging.info("=" * 60)
593
708
 
594
- # Define categories and their endpoints
595
- resources = {
596
- "monitors": {
597
- "description": "Monitor management and alerting",
598
- "endpoints": [
599
- "GET /api/v1/monitor - List all monitors",
600
- "GET /api/v1/monitor/{id} - Get a monitor by ID",
601
- "POST /api/v1/monitor/search - Search monitors",
602
- "GET /api/v1/monitor/groups/search - Search monitor groups",
603
- ],
604
- },
605
- "dashboards": {
606
- "description": "Dashboard and visualization management",
607
- "endpoints": [
608
- "GET /api/v1/dashboard - List all dashboards",
609
- "GET /api/v1/dashboard/{id} - Get a dashboard by ID",
610
- "POST /api/v1/dashboard/lists - List dashboard lists",
611
- "GET /api/v1/dashboard/public/{token} - Get public dashboard",
612
- ],
613
- },
614
- "slos": {
615
- "description": "Service Level Objectives",
616
- "endpoints": [
617
- "GET /api/v1/slo - List all SLOs",
618
- "GET /api/v1/slo/{id} - Get an SLO by ID",
619
- "GET /api/v1/slo/{id}/history - Get SLO history",
620
- "POST /api/v1/slo/search - Search SLOs",
621
- "GET /api/v1/slo/{id}/corrections - Get SLO corrections",
622
- ],
623
- },
624
- "incidents": {
625
- "description": "Incident management",
626
- "endpoints": [
627
- "GET /api/v2/incidents - List incidents",
628
- "GET /api/v2/incidents/{id} - Get incident details",
629
- "POST /api/v2/incidents/search - Search incidents",
630
- "GET /api/v2/incidents/{id}/timeline - Get incident timeline",
631
- "GET /api/v2/incidents/{id}/attachments - Get incident attachments",
632
- ],
633
- },
634
- "synthetics": {
635
- "description": "Synthetic monitoring and testing",
636
- "endpoints": [
637
- "GET /api/v1/synthetics/tests - List synthetic tests",
638
- "GET /api/v1/synthetics/tests/{id} - Get test details",
639
- "POST /api/v1/synthetics/tests/search - Search tests",
640
- "GET /api/v1/synthetics/tests/{id}/results - Get test results",
641
- "GET /api/v1/synthetics/locations - List test locations",
642
- ],
643
- },
644
- "security": {
645
- "description": "Security monitoring and detection",
646
- "endpoints": [
647
- "GET /api/v2/security_monitoring/rules - List security rules",
648
- "GET /api/v2/security_monitoring/rules/{id} - Get rule details",
649
- "POST /api/v2/security_monitoring/rules/search - Search rules",
650
- "POST /api/v2/security_monitoring/signals/search - Search security signals",
651
- ],
652
- },
653
- "hosts": {
654
- "description": "Host and infrastructure monitoring",
655
- "endpoints": [
656
- "GET /api/v1/hosts - List all hosts",
657
- "GET /api/v1/hosts/{name} - Get host details",
658
- "GET /api/v1/hosts/totals - Get host totals",
659
- "GET /api/v1/tags/hosts - Get host tags",
660
- ],
661
- },
662
- "events": {
663
- "description": "Event stream and management",
664
- "endpoints": [
665
- "GET /api/v1/events - Query event stream",
666
- "GET /api/v1/events/{id} - Get event details",
667
- "POST /api/v2/events/search - Search events",
668
- ],
669
- },
670
- "usage": {
671
- "description": "Usage and billing information",
672
- "endpoints": [
673
- "GET /api/v1/usage/summary - Get usage summary",
674
- "GET /api/v1/usage/billable-summary - Get billable summary",
675
- "GET /api/v1/usage/estimated_cost - Get estimated costs",
676
- "GET /api/v2/usage/cost_by_org - Get costs by organization",
677
- ],
678
- },
679
- "services": {
680
- "description": "APM service information",
681
- "endpoints": [
682
- "GET /api/v2/services - List services",
683
- "GET /api/v2/services/{service} - Get service details",
684
- "GET /api/v2/services/{service}/dependencies - Get service dependencies",
685
- ],
686
- },
687
- }
709
+ # Filter endpoints based on regex search
710
+ matching_endpoints = []
688
711
 
689
- # Filter by category if specified
690
- if category != "all":
691
- matching_categories = {k: v for k, v in resources.items() if category in k}
692
- if not matching_categories:
712
+ if search_regex:
713
+ try:
714
+ search_pattern = re.compile(search_regex, re.IGNORECASE)
715
+ except re.error as e:
693
716
  return StructuredToolResult(
694
717
  status=StructuredToolResultStatus.ERROR,
695
- error=f"Unknown category: {category}. Available: {', '.join(resources.keys())}",
718
+ error=f"Invalid regex pattern: {e}",
696
719
  params=params,
697
720
  )
698
- resources = matching_categories
721
+ else:
722
+ search_pattern = None
723
+
724
+ # Build list of matching endpoints
725
+ for pattern, hint in WHITELISTED_ENDPOINTS:
726
+ # Create a readable endpoint example from the pattern
727
+ example_endpoint = pattern.replace(r"^/api/v\d+", "/api/v1")
728
+ example_endpoint = example_endpoint.replace(r"(/search)?$", "")
729
+ example_endpoint = example_endpoint.replace(r"(/[^/]+)?$", "/{id}")
730
+ example_endpoint = example_endpoint.replace(r"/[^/]+$", "/{id}")
731
+ example_endpoint = example_endpoint.replace(r"/\d+$", "/{id}")
732
+ example_endpoint = example_endpoint.replace("$", "")
733
+ example_endpoint = example_endpoint.replace("^", "")
734
+
735
+ # Apply search filter if provided
736
+ if search_pattern and not search_pattern.search(example_endpoint):
737
+ continue
738
+
739
+ # Determine HTTP methods
740
+ if "search" in pattern or "query" in pattern or "aggregate" in pattern:
741
+ methods = "POST"
742
+ elif "/search)?$" in pattern:
743
+ methods = "GET/POST"
744
+ else:
745
+ methods = "GET"
699
746
 
700
- # Format output
701
- output = ["Available Datadog API Resources", "=" * 40, ""]
747
+ endpoint_info = {
748
+ "endpoint": example_endpoint,
749
+ "methods": methods,
750
+ "hint": hint,
751
+ "pattern": pattern,
752
+ }
753
+ matching_endpoints.append(endpoint_info)
702
754
 
703
- for cat_name, cat_info in resources.items():
704
- output.append(f"## {cat_name.upper()}")
705
- output.append(f"Description: {cat_info['description']}")
706
- output.append("")
707
- output.append("Endpoints:")
708
- for endpoint in cat_info["endpoints"]:
709
- output.append(f" • {endpoint}")
710
- output.append("")
755
+ if not matching_endpoints:
756
+ return StructuredToolResult(
757
+ status=StructuredToolResultStatus.SUCCESS,
758
+ data=f"No endpoints found matching regex: {search_regex}",
759
+ params=params,
760
+ )
711
761
 
762
+ # Format output
763
+ output = ["Available Datadog API Endpoints", "=" * 40]
764
+
765
+ if search_regex:
766
+ output.append(f"Filter: {search_regex}")
767
+ output.append(f"Found: {len(matching_endpoints)} endpoints")
768
+ output.append("")
769
+
770
+ # List endpoints with spec info if available
771
+ for info in matching_endpoints:
772
+ line = f"{info['methods']:8} {info['endpoint']}"
773
+ if info["hint"]:
774
+ line += f"\n {info['hint']}"
775
+
776
+ # Add OpenAPI spec info for this specific endpoint if available
777
+ if self.toolset.openapi_spec and "paths" in self.toolset.openapi_spec:
778
+ # Try to find matching path in OpenAPI spec
779
+ spec_path = None
780
+ for path in self.toolset.openapi_spec["paths"].keys():
781
+ if re.match(info["pattern"], path):
782
+ spec_path = path
783
+ break
784
+
785
+ if spec_path and spec_path in self.toolset.openapi_spec["paths"]:
786
+ path_spec = self.toolset.openapi_spec["paths"][spec_path]
787
+ # Add actual OpenAPI schema for the endpoint
788
+ for method in ["get", "post", "put", "delete"]:
789
+ if method in path_spec:
790
+ method_spec = path_spec[method]
791
+ line += f"\n\n OpenAPI Schema ({method.upper()}):"
792
+
793
+ # Add summary if available
794
+ if "summary" in method_spec:
795
+ line += f"\n Summary: {method_spec['summary']}"
796
+
797
+ # Add parameters if available
798
+ if "parameters" in method_spec:
799
+ line += "\n Parameters:"
800
+ for param in method_spec["parameters"]:
801
+ param_info = f"\n - {param.get('name', 'unknown')} ({param.get('in', 'unknown')})"
802
+ if param.get("required", False):
803
+ param_info += " [required]"
804
+ if "description" in param:
805
+ param_info += f": {param['description'][:100]}"
806
+ line += param_info
807
+
808
+ # Add request body schema if available
809
+ if "requestBody" in method_spec:
810
+ line += "\n Request Body:"
811
+ if "content" in method_spec["requestBody"]:
812
+ for content_type, content_spec in method_spec[
813
+ "requestBody"
814
+ ]["content"].items():
815
+ if "schema" in content_spec:
816
+ # Show a compact version of the schema
817
+ schema_str = json.dumps(
818
+ content_spec["schema"], indent=10
819
+ )[:500]
820
+ if (
821
+ len(json.dumps(content_spec["schema"]))
822
+ > 500
823
+ ):
824
+ schema_str += "..."
825
+ line += f"\n Content-Type: {content_type}"
826
+ line += f"\n Schema: {schema_str}"
827
+
828
+ # Add response schema sample if available
829
+ if "responses" in method_spec:
830
+ if "200" in method_spec["responses"]:
831
+ line += "\n Response (200):"
832
+ resp = method_spec["responses"]["200"]
833
+ if "description" in resp:
834
+ line += f"\n {resp['description']}"
835
+ break
836
+
837
+ output.append(line)
838
+
839
+ output.append("")
712
840
  output.append(
713
841
  "Note: All endpoints are read-only. Use the appropriate tool with the endpoint path."
714
842
  )
715
843
  output.append("Example: datadog_api_get with endpoint='/api/v1/monitors'")
844
+ output.append("")
845
+ output.append("Search examples:")
846
+ output.append(" • 'monitor' - find all monitor endpoints")
847
+ output.append(" • 'logs|metrics' - find logs OR metrics endpoints")
848
+ output.append(" • 'v2.*search$' - find all v2 search endpoints")
849
+ output.append(" • 'security.*signals' - find security signals endpoints")
716
850
 
717
851
  return StructuredToolResult(
718
852
  status=StructuredToolResultStatus.SUCCESS,