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.
- holmes/__init__.py +3 -5
- holmes/clients/robusta_client.py +20 -6
- holmes/common/env_vars.py +58 -3
- holmes/common/openshift.py +1 -1
- holmes/config.py +123 -148
- holmes/core/conversations.py +71 -15
- holmes/core/feedback.py +191 -0
- holmes/core/investigation.py +31 -39
- holmes/core/investigation_structured_output.py +3 -3
- holmes/core/issue.py +1 -1
- holmes/core/llm.py +508 -88
- holmes/core/models.py +108 -4
- holmes/core/openai_formatting.py +14 -1
- holmes/core/prompt.py +48 -3
- holmes/core/runbooks.py +1 -0
- holmes/core/safeguards.py +8 -6
- holmes/core/supabase_dal.py +295 -100
- holmes/core/tool_calling_llm.py +489 -428
- holmes/core/tools.py +325 -56
- holmes/core/tools_utils/token_counting.py +21 -0
- holmes/core/tools_utils/tool_context_window_limiter.py +40 -0
- holmes/core/tools_utils/tool_executor.py +0 -13
- holmes/core/tools_utils/toolset_utils.py +1 -0
- holmes/core/toolset_manager.py +191 -5
- holmes/core/tracing.py +19 -3
- holmes/core/transformers/__init__.py +23 -0
- holmes/core/transformers/base.py +63 -0
- holmes/core/transformers/llm_summarize.py +175 -0
- holmes/core/transformers/registry.py +123 -0
- holmes/core/transformers/transformer.py +32 -0
- holmes/core/truncation/compaction.py +94 -0
- holmes/core/truncation/dal_truncation_utils.py +23 -0
- holmes/core/truncation/input_context_window_limiter.py +219 -0
- holmes/interactive.py +228 -31
- holmes/main.py +23 -40
- holmes/plugins/interfaces.py +2 -1
- holmes/plugins/prompts/__init__.py +2 -1
- holmes/plugins/prompts/_fetch_logs.jinja2 +31 -6
- holmes/plugins/prompts/_general_instructions.jinja2 +1 -2
- holmes/plugins/prompts/_runbook_instructions.jinja2 +24 -12
- holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
- holmes/plugins/prompts/conversation_history_compaction.jinja2 +89 -0
- holmes/plugins/prompts/generic_ask.jinja2 +0 -4
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -1
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -1
- holmes/plugins/prompts/generic_investigation.jinja2 +0 -1
- holmes/plugins/prompts/investigation_procedure.jinja2 +50 -1
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -1
- holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -1
- holmes/plugins/runbooks/__init__.py +145 -17
- holmes/plugins/runbooks/catalog.json +2 -0
- holmes/plugins/sources/github/__init__.py +4 -2
- holmes/plugins/sources/prometheus/models.py +1 -0
- holmes/plugins/toolsets/__init__.py +44 -27
- holmes/plugins/toolsets/aks-node-health.yaml +46 -0
- holmes/plugins/toolsets/aks.yaml +64 -0
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +38 -47
- holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +3 -2
- holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
- holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +3 -2
- holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +3 -1
- holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +3 -1
- holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +12 -13
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +15 -12
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +15 -12
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +11 -11
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +11 -9
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +15 -12
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +15 -15
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/utils.py +0 -32
- holmes/plugins/toolsets/bash/argocd/__init__.py +3 -3
- holmes/plugins/toolsets/bash/aws/__init__.py +4 -4
- holmes/plugins/toolsets/bash/azure/__init__.py +4 -4
- holmes/plugins/toolsets/bash/bash_toolset.py +11 -15
- holmes/plugins/toolsets/bash/common/bash.py +23 -13
- holmes/plugins/toolsets/bash/common/bash_command.py +1 -1
- holmes/plugins/toolsets/bash/common/stringify.py +1 -1
- holmes/plugins/toolsets/bash/kubectl/__init__.py +2 -1
- holmes/plugins/toolsets/bash/kubectl/constants.py +0 -1
- holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +3 -4
- holmes/plugins/toolsets/bash/parse_command.py +12 -13
- holmes/plugins/toolsets/cilium.yaml +284 -0
- holmes/plugins/toolsets/connectivity_check.py +124 -0
- holmes/plugins/toolsets/coralogix/api.py +132 -119
- holmes/plugins/toolsets/coralogix/coralogix.jinja2 +14 -0
- holmes/plugins/toolsets/coralogix/toolset_coralogix.py +219 -0
- holmes/plugins/toolsets/coralogix/utils.py +15 -79
- holmes/plugins/toolsets/datadog/datadog_api.py +525 -26
- holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +55 -11
- holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +3 -3
- holmes/plugins/toolsets/datadog/datadog_models.py +59 -0
- holmes/plugins/toolsets/datadog/datadog_url_utils.py +213 -0
- holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +165 -28
- holmes/plugins/toolsets/datadog/toolset_datadog_general.py +417 -241
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +234 -214
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +167 -79
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +374 -363
- holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
- holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
- holmes/plugins/toolsets/elasticsearch/opensearch_ppl_query_docs.jinja2 +1616 -0
- holmes/plugins/toolsets/elasticsearch/opensearch_query_assist.py +78 -0
- holmes/plugins/toolsets/elasticsearch/opensearch_query_assist_instructions.jinja2 +223 -0
- holmes/plugins/toolsets/git.py +54 -50
- holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
- holmes/plugins/toolsets/grafana/common.py +13 -29
- holmes/plugins/toolsets/grafana/grafana_tempo_api.py +455 -0
- holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +25 -0
- holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +191 -0
- holmes/plugins/toolsets/grafana/loki_api.py +4 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +293 -89
- holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +49 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +820 -292
- holmes/plugins/toolsets/grafana/trace_parser.py +4 -3
- holmes/plugins/toolsets/internet/internet.py +15 -16
- holmes/plugins/toolsets/internet/notion.py +9 -11
- holmes/plugins/toolsets/investigator/core_investigation.py +44 -36
- holmes/plugins/toolsets/investigator/model.py +3 -1
- holmes/plugins/toolsets/json_filter_mixin.py +134 -0
- holmes/plugins/toolsets/kafka.py +36 -42
- holmes/plugins/toolsets/kubernetes.yaml +317 -113
- holmes/plugins/toolsets/kubernetes_logs.py +9 -9
- holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
- holmes/plugins/toolsets/logging_utils/logging_api.py +94 -8
- holmes/plugins/toolsets/mcp/toolset_mcp.py +218 -64
- holmes/plugins/toolsets/newrelic/new_relic_api.py +165 -0
- holmes/plugins/toolsets/newrelic/newrelic.jinja2 +65 -0
- holmes/plugins/toolsets/newrelic/newrelic.py +320 -0
- holmes/plugins/toolsets/openshift.yaml +283 -0
- holmes/plugins/toolsets/prometheus/prometheus.py +1202 -421
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +54 -5
- holmes/plugins/toolsets/prometheus/utils.py +28 -0
- holmes/plugins/toolsets/rabbitmq/api.py +23 -4
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +13 -14
- holmes/plugins/toolsets/robusta/robusta.py +239 -68
- holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +26 -9
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +157 -27
- holmes/plugins/toolsets/service_discovery.py +1 -1
- holmes/plugins/toolsets/servicenow_tables/instructions.jinja2 +83 -0
- holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +426 -0
- holmes/plugins/toolsets/utils.py +88 -0
- holmes/utils/config_utils.py +91 -0
- holmes/utils/connection_utils.py +31 -0
- holmes/utils/console/result.py +10 -0
- holmes/utils/default_toolset_installation_guide.jinja2 +1 -22
- holmes/utils/env.py +7 -0
- holmes/utils/file_utils.py +2 -1
- holmes/utils/global_instructions.py +60 -11
- holmes/utils/holmes_status.py +6 -4
- holmes/utils/holmes_sync_toolsets.py +0 -2
- holmes/utils/krr_utils.py +188 -0
- holmes/utils/log.py +15 -0
- holmes/utils/markdown_utils.py +2 -3
- holmes/utils/memory_limit.py +58 -0
- holmes/utils/sentry_helper.py +64 -0
- holmes/utils/stream.py +69 -8
- holmes/utils/tags.py +4 -3
- holmes/version.py +37 -15
- holmesgpt-0.18.4.dist-info/LICENSE +178 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +35 -31
- holmesgpt-0.18.4.dist-info/RECORD +258 -0
- holmes/core/performance_timing.py +0 -72
- holmes/plugins/toolsets/aws.yaml +0 -80
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -112
- holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
- holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -739
- holmes/plugins/toolsets/grafana/grafana_api.py +0 -42
- holmes/plugins/toolsets/grafana/tempo_api.py +0 -124
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +0 -110
- holmes/plugins/toolsets/newrelic.py +0 -231
- holmes/plugins/toolsets/opensearch/opensearch.py +0 -257
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -218
- holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
- holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
- holmes/plugins/toolsets/servicenow/install.md +0 -37
- holmes/plugins/toolsets/servicenow/instructions.jinja2 +0 -3
- holmes/plugins/toolsets/servicenow/servicenow.py +0 -219
- holmes/utils/keygen_utils.py +0 -6
- holmesgpt-0.13.2.dist-info/LICENSE.txt +0 -21
- holmesgpt-0.13.2.dist-info/RECORD +0 -234
- /holmes/plugins/toolsets/{opensearch → newrelic}/__init__.py +0 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
- {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('
|
|
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
|
-
|
|
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", "
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
90
|
+
tasks_to_display.append(row)
|
|
74
91
|
|
|
75
|
-
logging.info(
|
|
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.
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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
|