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
@@ -1,6 +1,7 @@
1
- from typing import Dict, List, Optional, Any
2
- from dataclasses import dataclass, field
3
1
  import base64
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, Dict, List, Optional
4
+
4
5
  from holmes.plugins.toolsets.utils import unix_nano_to_rfc3339
5
6
 
6
7
 
@@ -187,7 +188,7 @@ def format_traces_list(trace_data: Dict) -> str:
187
188
  else "\n"
188
189
  )
189
190
  trace_str += f"\tstartTime={unix_nano_to_rfc3339(int(trace.get('startTimeUnixNano')))}"
190
- trace_str += f" rootServiceName={trace.get('trootServiceName')}"
191
+ trace_str += f" rootServiceName={trace.get('rootServiceName')}"
191
192
  trace_str += f" rootTraceName={trace.get('rootTraceName')}"
192
193
  traces_str.append(trace_str)
193
194
  return "\n".join(traces_str)
@@ -1,31 +1,32 @@
1
- import re
2
- import os
3
1
  import logging
4
- from typing import Any, Optional, Tuple, Dict, List
2
+ import os
3
+ import re
4
+ from typing import Any, Dict, List, Optional, Tuple
5
5
 
6
+ import requests # type: ignore
7
+ from bs4 import BeautifulSoup
8
+ from markdownify import markdownify
6
9
  from requests import RequestException, Timeout # type: ignore
10
+
7
11
  from holmes.core.tools import (
12
+ CallablePrerequisite,
13
+ StructuredToolResult,
14
+ StructuredToolResultStatus,
8
15
  Tool,
16
+ ToolInvokeContext,
9
17
  ToolParameter,
10
18
  Toolset,
11
19
  ToolsetTag,
12
- CallablePrerequisite,
13
20
  )
14
- from markdownify import markdownify
15
- from bs4 import BeautifulSoup
16
-
17
- import requests # type: ignore
18
- from holmes.core.tools import StructuredToolResult, ToolResultStatus
19
21
  from holmes.plugins.toolsets.utils import toolset_name_for_one_liner
20
22
 
21
-
22
23
  # TODO: change and make it holmes
23
24
  INTERNET_TOOLSET_USER_AGENT = os.environ.get(
24
25
  "INTERNET_TOOLSET_USER_AGENT",
25
26
  "Mozilla/5.0 (X11; Linux x86_64; rv:128.0; holmesgpt;) Gecko/20100101 Firefox/128.0",
26
27
  )
27
28
  INTERNET_TOOLSET_TIMEOUT_SECONDS = int(
28
- os.environ.get("INTERNET_TOOLSET_TIMEOUT_SECONDS", "60")
29
+ os.environ.get("INTERNET_TOOLSET_TIMEOUT_SECONDS", "5")
29
30
  )
30
31
 
31
32
  SELECTORS_TO_REMOVE = [
@@ -186,9 +187,7 @@ class FetchWebpage(Tool):
186
187
  toolset=toolset, # type: ignore
187
188
  )
188
189
 
189
- def _invoke(
190
- self, params: dict, user_approved: bool = False
191
- ) -> StructuredToolResult:
190
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
192
191
  url: str = params["url"]
193
192
 
194
193
  additional_headers = (
@@ -199,7 +198,7 @@ class FetchWebpage(Tool):
199
198
  if not content:
200
199
  logging.error(f"Failed to retrieve content from {url}")
201
200
  return StructuredToolResult(
202
- status=ToolResultStatus.ERROR,
201
+ status=StructuredToolResultStatus.ERROR,
203
202
  error=f"Failed to retrieve content from {url}",
204
203
  params=params,
205
204
  )
@@ -211,7 +210,7 @@ class FetchWebpage(Tool):
211
210
  content = html_to_markdown(content)
212
211
 
213
212
  return StructuredToolResult(
214
- status=ToolResultStatus.SUCCESS,
213
+ status=StructuredToolResultStatus.SUCCESS,
215
214
  data=content,
216
215
  params=params,
217
216
  )
@@ -1,9 +1,13 @@
1
- import re
2
- import logging
3
1
  import json
2
+ import logging
3
+ import re
4
4
  from typing import Any, Dict, Tuple
5
+
5
6
  from holmes.core.tools import (
7
+ StructuredToolResult,
8
+ StructuredToolResultStatus,
6
9
  Tool,
10
+ ToolInvokeContext,
7
11
  ToolParameter,
8
12
  ToolsetTag,
9
13
  )
@@ -11,10 +15,6 @@ from holmes.plugins.toolsets.internet.internet import (
11
15
  InternetBaseToolset,
12
16
  scrape,
13
17
  )
14
- from holmes.core.tools import (
15
- StructuredToolResult,
16
- ToolResultStatus,
17
- )
18
18
  from holmes.plugins.toolsets.utils import toolset_name_for_one_liner
19
19
 
20
20
 
@@ -44,9 +44,7 @@ class FetchNotion(Tool):
44
44
  return f"https://api.notion.com/v1/blocks/{notion_id}/children"
45
45
  return url # Return original URL if no match is found
46
46
 
47
- def _invoke(
48
- self, params: dict, user_approved: bool = False
49
- ) -> StructuredToolResult:
47
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
50
48
  url: str = params["url"]
51
49
 
52
50
  # Get headers from the toolset configuration
@@ -59,13 +57,13 @@ class FetchNotion(Tool):
59
57
  if not content:
60
58
  logging.error(f"Failed to retrieve content from {url}")
61
59
  return StructuredToolResult(
62
- status=ToolResultStatus.ERROR,
60
+ status=StructuredToolResultStatus.ERROR,
63
61
  error=f"Failed to retrieve content from {url}",
64
62
  params=params,
65
63
  )
66
64
 
67
65
  return StructuredToolResult(
68
- status=ToolResultStatus.SUCCESS,
66
+ status=StructuredToolResultStatus.SUCCESS,
69
67
  data=self.parse_notion_content(content),
70
68
  params=params,
71
69
  )
@@ -1,26 +1,44 @@
1
1
  import logging
2
2
  import os
3
3
  from typing import Any, Dict
4
-
5
4
  from uuid import uuid4
5
+
6
6
  from holmes.core.todo_tasks_formatter import format_tasks
7
7
  from holmes.core.tools import (
8
+ StructuredToolResult,
9
+ StructuredToolResultStatus,
10
+ Tool,
11
+ ToolInvokeContext,
12
+ ToolParameter,
8
13
  Toolset,
9
14
  ToolsetTag,
10
- ToolParameter,
11
- Tool,
12
- StructuredToolResult,
13
- ToolResultStatus,
14
15
  )
15
16
  from holmes.plugins.toolsets.investigator.model import Task, TaskStatus
16
17
 
18
+ TODO_WRITE_TOOL_NAME = "TodoWrite"
19
+
20
+
21
+ def parse_tasks(todos_data: Any) -> list[Task]:
22
+ tasks = []
23
+
24
+ for todo_item in todos_data:
25
+ if isinstance(todo_item, dict):
26
+ task = Task(
27
+ id=todo_item.get("id", str(uuid4())),
28
+ content=todo_item.get("content", ""),
29
+ status=TaskStatus(todo_item.get("status", "pending")),
30
+ )
31
+ tasks.append(task)
32
+
33
+ return tasks
34
+
17
35
 
18
36
  class TodoWriteTool(Tool):
19
- name: str = "TodoWrite"
37
+ name: str = TODO_WRITE_TOOL_NAME
20
38
  description: str = "Save investigation tasks to break down complex problems into manageable sub-tasks. ALWAYS provide the COMPLETE list of all tasks, not just the ones being updated."
21
39
  parameters: Dict[str, ToolParameter] = {
22
40
  "todos": ToolParameter(
23
- description="COMPLETE list of ALL tasks on the task list. Each task should have: id (string), content (string), status (pending/in_progress/completed)",
41
+ description="COMPLETE list of ALL tasks on the task list. Each task should have: id (string), content (string), status (pending/in_progress/completed/failed)",
24
42
  type="array",
25
43
  required=True,
26
44
  items=ToolParameter(
@@ -28,7 +46,11 @@ class TodoWriteTool(Tool):
28
46
  properties={
29
47
  "id": ToolParameter(type="string", required=True),
30
48
  "content": ToolParameter(type="string", required=True),
31
- "status": ToolParameter(type="string", required=True),
49
+ "status": ToolParameter(
50
+ type="string",
51
+ required=True,
52
+ enum=["pending", "in_progress", "completed", "failed"],
53
+ ),
32
54
  },
33
55
  ),
34
56
  ),
@@ -44,6 +66,7 @@ class TodoWriteTool(Tool):
44
66
  "pending": "[ ]",
45
67
  "in_progress": "[~]",
46
68
  "completed": "[✓]",
69
+ "failed": "[✗]",
47
70
  }
48
71
 
49
72
  max_id_width = max(len(str(task.id)) for task in tasks)
@@ -57,41 +80,28 @@ class TodoWriteTool(Tool):
57
80
  content_width = max(max_content_width, len("Content"))
58
81
  status_width = max(max_status_display_width, len("Status"))
59
82
 
60
- # Build table
61
83
  separator = f"+{'-' * (id_width + 2)}+{'-' * (content_width + 2)}+{'-' * (status_width + 2)}+"
62
84
  header = f"| {'ID':<{id_width}} | {'Content':<{content_width}} | {'Status':<{status_width}} |"
63
-
64
- # Log the table
65
- logging.info("Updated Investigation Tasks:")
66
- logging.info(separator)
67
- logging.info(header)
68
- logging.info(separator)
85
+ tasks_to_display = []
69
86
 
70
87
  for task in tasks:
71
88
  status_display = f"{status_icons[task.status.value]} {task.status.value}"
72
89
  row = f"| {task.id:<{id_width}} | {task.content:<{content_width}} | {status_display:<{status_width}} |"
73
- logging.info(row)
90
+ tasks_to_display.append(row)
74
91
 
75
- logging.info(separator)
92
+ logging.info(
93
+ f"Task List:\n{separator}\n{header}\n{separator}\n"
94
+ + "\n".join(tasks_to_display)
95
+ + f"\n{separator}"
96
+ )
76
97
 
77
- def _invoke(
78
- self, params: dict, user_approved: bool = False
79
- ) -> StructuredToolResult:
98
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
80
99
  try:
81
100
  todos_data = params.get("todos", [])
82
101
 
83
- tasks = []
84
-
85
- for todo_item in todos_data:
86
- if isinstance(todo_item, dict):
87
- task = Task(
88
- id=todo_item.get("id", str(uuid4())),
89
- content=todo_item.get("content", ""),
90
- status=TaskStatus(todo_item.get("status", "pending")),
91
- )
92
- tasks.append(task)
102
+ tasks = parse_tasks(todos_data=todos_data)
93
103
 
94
- logging.info(f"Tasks: {len(tasks)}")
104
+ logging.debug(f"Tasks: {len(tasks)}")
95
105
 
96
106
  self.print_tasks_table(tasks)
97
107
  formatted_tasks = format_tasks(tasks)
@@ -103,7 +113,7 @@ class TodoWriteTool(Tool):
103
113
  response_data += "No tasks currently in the investigation plan."
104
114
 
105
115
  return StructuredToolResult(
106
- status=ToolResultStatus.SUCCESS,
116
+ status=StructuredToolResultStatus.SUCCESS,
107
117
  data=response_data,
108
118
  params=params,
109
119
  )
@@ -111,14 +121,13 @@ class TodoWriteTool(Tool):
111
121
  except Exception as e:
112
122
  logging.exception("error using todowrite tool")
113
123
  return StructuredToolResult(
114
- status=ToolResultStatus.ERROR,
124
+ status=StructuredToolResultStatus.ERROR,
115
125
  error=f"Failed to process tasks: {str(e)}",
116
126
  params=params,
117
127
  )
118
128
 
119
129
  def get_parameterized_one_liner(self, params: Dict) -> str:
120
- todos = params.get("todos", [])
121
- return f"Write {todos} investigation tasks"
130
+ return "Update investigation tasks"
122
131
 
123
132
 
124
133
  class CoreInvestigationToolset(Toolset):
@@ -133,7 +142,6 @@ class CoreInvestigationToolset(Toolset):
133
142
  tags=[ToolsetTag.CORE],
134
143
  is_default=True,
135
144
  )
136
- logging.info("Core investigation toolset loaded")
137
145
 
138
146
  def get_example_config(self) -> Dict[str, Any]:
139
147
  return {}
@@ -1,12 +1,14 @@
1
1
  from enum import Enum
2
- from pydantic import BaseModel, Field
3
2
  from uuid import uuid4
4
3
 
4
+ from pydantic import BaseModel, Field
5
+
5
6
 
6
7
  class TaskStatus(str, Enum):
7
8
  PENDING = "pending"
8
9
  IN_PROGRESS = "in_progress"
9
10
  COMPLETED = "completed"
11
+ FAILED = "failed"
10
12
 
11
13
 
12
14
  class Task(BaseModel):
@@ -0,0 +1,134 @@
1
+ import json
2
+ import logging
3
+ from typing import Any, Dict, Optional, Tuple
4
+
5
+ import jq
6
+
7
+ from holmes.core.tools import (
8
+ StructuredToolResult,
9
+ StructuredToolResultStatus,
10
+ ToolParameter,
11
+ )
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def _truncate_to_depth(value: Any, max_depth: Optional[int], current_depth: int = 0):
17
+ """Recursively truncate dictionaries/lists beyond the requested depth."""
18
+ if max_depth is None or max_depth < 0:
19
+ return value
20
+
21
+ if current_depth >= max_depth:
22
+ if isinstance(value, (dict, list)):
23
+ return f"...truncated at depth {max_depth}"
24
+ return value
25
+
26
+ if isinstance(value, dict):
27
+ return {
28
+ key: _truncate_to_depth(sub_value, max_depth, current_depth + 1)
29
+ for key, sub_value in value.items()
30
+ }
31
+ if isinstance(value, list):
32
+ return [
33
+ _truncate_to_depth(item, max_depth, current_depth + 1) for item in value
34
+ ]
35
+
36
+ return value
37
+
38
+
39
+ def _apply_jq_filter(data: Any, expression: str) -> Tuple[Optional[Any], Optional[str]]:
40
+ try:
41
+ compiled = jq.compile(expression)
42
+ matches = compiled.input(data).all()
43
+ if len(matches) == 1:
44
+ return matches[0], None
45
+ return matches, None
46
+ except Exception as exc: # pragma: no cover - defensive
47
+ logger.debug("Failed to apply jq filter", exc_info=exc)
48
+ return None, f"Invalid jq expression: {exc}"
49
+
50
+
51
+ class JsonFilterMixin:
52
+ """Opt-in mixin for tools that return JSON and want filtering controls."""
53
+
54
+ filter_parameters: Dict[str, ToolParameter] = {
55
+ "max_depth": ToolParameter(
56
+ description="Maximum nesting depth to return from the JSON (0 returns only top-level keys). Leave empty for full response.",
57
+ type="integer",
58
+ required=False,
59
+ ),
60
+ "jq": ToolParameter(
61
+ description="Optional jq expression to extract specific parts of the JSON. Supports full jq syntax including filters, slicing, transformations, and more (e.g., '.items[] | select(.price > 10)', '.items[0:5]', '.items[].name').",
62
+ type="string",
63
+ required=False,
64
+ ),
65
+ }
66
+
67
+ @classmethod
68
+ def extend_parameters(
69
+ cls, existing: Dict[str, ToolParameter]
70
+ ) -> Dict[str, ToolParameter]:
71
+ merged = dict(cls.filter_parameters)
72
+ merged.update(existing)
73
+ return merged
74
+
75
+ def _filter_result_data(self, data: Any, params: Dict) -> Tuple[Any, Optional[str]]:
76
+ parsed_data = data
77
+ if isinstance(data, str):
78
+ try:
79
+ parsed_data = json.loads(data)
80
+ except Exception:
81
+ # Not JSON, leave as-is
82
+ return data, None
83
+
84
+ if params.get("jq"):
85
+ parsed_data, error = _apply_jq_filter(parsed_data, params["jq"])
86
+ if error:
87
+ return None, error
88
+
89
+ parsed_data = _truncate_to_depth(parsed_data, params.get("max_depth"))
90
+ return parsed_data, None
91
+
92
+ @staticmethod
93
+ def _safe_string(value: Any) -> Optional[str]:
94
+ if value is None:
95
+ return None
96
+ if isinstance(value, str):
97
+ return value
98
+ try:
99
+ return str(value)
100
+ except Exception:
101
+ return None
102
+
103
+ def filter_result(
104
+ self, result: StructuredToolResult, params: Dict
105
+ ) -> StructuredToolResult:
106
+ base_result = result if isinstance(result, StructuredToolResult) else None
107
+ if base_result is None:
108
+ base_result = StructuredToolResult(
109
+ status=getattr(result, "status", StructuredToolResultStatus.SUCCESS),
110
+ data=getattr(result, "data", None),
111
+ params=getattr(result, "params", params),
112
+ url=self._safe_string(getattr(result, "url", None)),
113
+ invocation=self._safe_string(getattr(result, "invocation", None)),
114
+ icon_url=self._safe_string(getattr(result, "icon_url", None)),
115
+ )
116
+ else:
117
+ # Normalize string fields to avoid MagicMock validation failures
118
+ base_result.url = self._safe_string(base_result.url)
119
+ base_result.invocation = self._safe_string(base_result.invocation)
120
+ base_result.icon_url = self._safe_string(base_result.icon_url)
121
+
122
+ filtered_data, error = self._filter_result_data(base_result.data, params)
123
+ if error:
124
+ return StructuredToolResult(
125
+ status=StructuredToolResultStatus.ERROR,
126
+ error=error,
127
+ params=params,
128
+ url=base_result.url,
129
+ invocation=base_result.invocation,
130
+ icon_url=base_result.icon_url,
131
+ )
132
+
133
+ base_result.data = filtered_data
134
+ return base_result