holmesgpt 0.13.2__py3-none-any.whl → 0.16.2a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- holmes/__init__.py +1 -1
- holmes/clients/robusta_client.py +17 -4
- holmes/common/env_vars.py +40 -1
- holmes/config.py +114 -144
- holmes/core/conversations.py +53 -14
- holmes/core/feedback.py +191 -0
- holmes/core/investigation.py +18 -22
- holmes/core/llm.py +489 -88
- holmes/core/models.py +103 -1
- holmes/core/openai_formatting.py +13 -0
- holmes/core/prompt.py +1 -1
- holmes/core/safeguards.py +4 -4
- holmes/core/supabase_dal.py +293 -100
- holmes/core/tool_calling_llm.py +423 -323
- holmes/core/tools.py +311 -33
- holmes/core/tools_utils/token_counting.py +14 -0
- holmes/core/tools_utils/tool_context_window_limiter.py +57 -0
- holmes/core/tools_utils/tool_executor.py +13 -8
- holmes/core/toolset_manager.py +155 -4
- holmes/core/tracing.py +6 -1
- holmes/core/transformers/__init__.py +23 -0
- holmes/core/transformers/base.py +62 -0
- holmes/core/transformers/llm_summarize.py +174 -0
- holmes/core/transformers/registry.py +122 -0
- holmes/core/transformers/transformer.py +31 -0
- holmes/core/truncation/compaction.py +59 -0
- holmes/core/truncation/dal_truncation_utils.py +23 -0
- holmes/core/truncation/input_context_window_limiter.py +218 -0
- holmes/interactive.py +177 -24
- holmes/main.py +7 -4
- holmes/plugins/prompts/_fetch_logs.jinja2 +26 -1
- holmes/plugins/prompts/_general_instructions.jinja2 +1 -2
- holmes/plugins/prompts/_runbook_instructions.jinja2 +23 -12
- holmes/plugins/prompts/conversation_history_compaction.jinja2 +88 -0
- holmes/plugins/prompts/generic_ask.jinja2 +2 -4
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +2 -1
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +2 -1
- holmes/plugins/prompts/generic_investigation.jinja2 +2 -1
- holmes/plugins/prompts/investigation_procedure.jinja2 +48 -0
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +2 -1
- holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +2 -1
- holmes/plugins/runbooks/__init__.py +117 -18
- holmes/plugins/runbooks/catalog.json +2 -0
- holmes/plugins/toolsets/__init__.py +21 -8
- holmes/plugins/toolsets/aks-node-health.yaml +46 -0
- holmes/plugins/toolsets/aks.yaml +64 -0
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +26 -36
- holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +0 -1
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +10 -7
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +9 -6
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +8 -6
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +8 -6
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +9 -6
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +9 -7
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +9 -6
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +9 -6
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +9 -6
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +9 -6
- holmes/plugins/toolsets/bash/bash_toolset.py +10 -13
- holmes/plugins/toolsets/bash/common/bash.py +7 -7
- holmes/plugins/toolsets/cilium.yaml +284 -0
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +5 -3
- holmes/plugins/toolsets/datadog/datadog_api.py +490 -24
- holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +21 -10
- holmes/plugins/toolsets/datadog/toolset_datadog_general.py +349 -216
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +190 -19
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +101 -44
- holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +13 -16
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +25 -31
- holmes/plugins/toolsets/git.py +51 -46
- holmes/plugins/toolsets/grafana/common.py +15 -3
- holmes/plugins/toolsets/grafana/grafana_api.py +46 -24
- holmes/plugins/toolsets/grafana/grafana_tempo_api.py +454 -0
- holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +9 -0
- holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +117 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +211 -91
- holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +27 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +653 -293
- holmes/plugins/toolsets/grafana/trace_parser.py +1 -1
- holmes/plugins/toolsets/internet/internet.py +6 -7
- holmes/plugins/toolsets/internet/notion.py +5 -6
- holmes/plugins/toolsets/investigator/core_investigation.py +42 -34
- holmes/plugins/toolsets/kafka.py +25 -36
- holmes/plugins/toolsets/kubernetes.yaml +58 -84
- holmes/plugins/toolsets/kubernetes_logs.py +6 -6
- holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
- holmes/plugins/toolsets/logging_utils/logging_api.py +80 -4
- holmes/plugins/toolsets/mcp/toolset_mcp.py +181 -55
- holmes/plugins/toolsets/newrelic/__init__.py +0 -0
- holmes/plugins/toolsets/newrelic/new_relic_api.py +125 -0
- holmes/plugins/toolsets/newrelic/newrelic.jinja2 +41 -0
- holmes/plugins/toolsets/newrelic/newrelic.py +163 -0
- holmes/plugins/toolsets/opensearch/opensearch.py +10 -17
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +7 -7
- holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2 +1616 -0
- holmes/plugins/toolsets/opensearch/opensearch_query_assist.py +78 -0
- holmes/plugins/toolsets/opensearch/opensearch_query_assist_instructions.jinja2 +223 -0
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +13 -16
- holmes/plugins/toolsets/openshift.yaml +283 -0
- holmes/plugins/toolsets/prometheus/prometheus.py +915 -390
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +43 -2
- holmes/plugins/toolsets/prometheus/utils.py +28 -0
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +9 -10
- holmes/plugins/toolsets/robusta/robusta.py +236 -65
- holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +26 -9
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +137 -26
- 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/default_toolset_installation_guide.jinja2 +1 -22
- holmes/utils/env.py +7 -0
- holmes/utils/global_instructions.py +75 -10
- holmes/utils/holmes_status.py +2 -1
- holmes/utils/holmes_sync_toolsets.py +0 -2
- holmes/utils/krr_utils.py +188 -0
- holmes/utils/sentry_helper.py +41 -0
- holmes/utils/stream.py +61 -7
- holmes/version.py +34 -14
- holmesgpt-0.16.2a0.dist-info/LICENSE +178 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/METADATA +29 -27
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/RECORD +126 -102
- holmes/core/performance_timing.py +0 -72
- 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/servicenow/install.md +0 -37
- holmes/plugins/toolsets/servicenow/instructions.jinja2 +0 -3
- holmes/plugins/toolsets/servicenow/servicenow.py +0 -219
- holmesgpt-0.13.2.dist-info/LICENSE.txt +0 -21
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/WHEEL +0 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/entry_points.txt +0 -0
|
@@ -1,20 +1,85 @@
|
|
|
1
|
-
from typing import List,
|
|
2
|
-
|
|
1
|
+
from typing import Optional, List, TYPE_CHECKING
|
|
3
2
|
from pydantic import BaseModel
|
|
3
|
+
from holmes.plugins.prompts import load_and_render_prompt
|
|
4
|
+
from holmes.plugins.runbooks import RunbookCatalog
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from holmes.core.resource_instruction import ResourceInstructions
|
|
4
8
|
|
|
5
9
|
|
|
6
10
|
class Instructions(BaseModel):
|
|
7
11
|
instructions: List[str] = []
|
|
8
12
|
|
|
9
13
|
|
|
10
|
-
def
|
|
11
|
-
|
|
14
|
+
def _format_instructions_block(
|
|
15
|
+
items: List[str], header: str = "My instructions to check:"
|
|
16
|
+
) -> str:
|
|
17
|
+
lines = [f"* {s}" for s in items if isinstance(s, str) and s.strip()]
|
|
18
|
+
if not lines:
|
|
19
|
+
return ""
|
|
20
|
+
bullets = "\n".join(lines) + "\n"
|
|
21
|
+
return f"{header}\n{bullets}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _format_resource_instructions(
|
|
25
|
+
resource_instructions: Optional["ResourceInstructions"],
|
|
26
|
+
) -> List[str]: # type: ignore
|
|
27
|
+
items = []
|
|
28
|
+
if resource_instructions is not None:
|
|
29
|
+
if getattr(resource_instructions, "instructions", None):
|
|
30
|
+
items.extend(resource_instructions.instructions)
|
|
31
|
+
if getattr(resource_instructions, "documents", None):
|
|
32
|
+
for document in resource_instructions.documents:
|
|
33
|
+
items.append(f"fetch information from this URL: {document.url}")
|
|
34
|
+
return items
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def add_runbooks_to_user_prompt(
|
|
38
|
+
user_prompt: str,
|
|
39
|
+
runbook_catalog: Optional[RunbookCatalog],
|
|
40
|
+
global_instructions: Optional[Instructions] = None,
|
|
41
|
+
issue_instructions: Optional[List[str]] = None,
|
|
42
|
+
resource_instructions: Optional["ResourceInstructions"] = None, # type: ignore
|
|
12
43
|
) -> str:
|
|
13
44
|
if (
|
|
14
|
-
|
|
15
|
-
and
|
|
16
|
-
and
|
|
45
|
+
not runbook_catalog
|
|
46
|
+
and not issue_instructions
|
|
47
|
+
and not resource_instructions
|
|
48
|
+
and not global_instructions
|
|
17
49
|
):
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
50
|
+
return user_prompt
|
|
51
|
+
|
|
52
|
+
catalog_str = runbook_catalog.to_prompt_string() if runbook_catalog else ""
|
|
53
|
+
|
|
54
|
+
# Combine and format all instructions
|
|
55
|
+
combined_instructions = []
|
|
56
|
+
if issue_instructions:
|
|
57
|
+
combined_instructions.extend(issue_instructions)
|
|
58
|
+
combined_instructions.extend(_format_resource_instructions(resource_instructions))
|
|
59
|
+
issue_block = (
|
|
60
|
+
_format_instructions_block(combined_instructions)
|
|
61
|
+
if combined_instructions
|
|
62
|
+
else ""
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
gi_list = getattr(global_instructions, "instructions", None) or []
|
|
66
|
+
global_block = (
|
|
67
|
+
_format_instructions_block(
|
|
68
|
+
[s for s in gi_list if isinstance(s, str)], header=""
|
|
69
|
+
)
|
|
70
|
+
if gi_list
|
|
71
|
+
else ""
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
rendered = load_and_render_prompt(
|
|
75
|
+
"builtin://_runbook_instructions.jinja2",
|
|
76
|
+
context={
|
|
77
|
+
"runbook_catalog": catalog_str,
|
|
78
|
+
"custom_instructions": issue_block,
|
|
79
|
+
"global_instructions": global_block,
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if user_prompt and not user_prompt.endswith("\n"):
|
|
84
|
+
user_prompt += "\n"
|
|
85
|
+
return f"{user_prompt}\n{rendered}"
|
holmes/utils/holmes_status.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from holmes.core.supabase_dal import SupabaseDal
|
|
2
3
|
from holmes.config import Config
|
|
3
4
|
from holmes import get_version # type: ignore
|
|
@@ -16,7 +17,7 @@ def update_holmes_status_in_db(dal: SupabaseDal, config: Config):
|
|
|
16
17
|
dal.upsert_holmes_status(
|
|
17
18
|
{
|
|
18
19
|
"cluster_id": config.cluster_name,
|
|
19
|
-
"model": config.get_models_list(),
|
|
20
|
+
"model": json.dumps(config.get_models_list()),
|
|
20
21
|
"version": get_version(),
|
|
21
22
|
}
|
|
22
23
|
)
|
|
@@ -66,8 +66,6 @@ def render_default_installation_instructions_for_toolset(toolset: Toolset) -> st
|
|
|
66
66
|
context: dict[str, Any] = {
|
|
67
67
|
"env_vars": env_vars if env_vars else [],
|
|
68
68
|
"toolset_name": toolset.name,
|
|
69
|
-
"is_default": toolset.is_default,
|
|
70
|
-
"enabled": toolset.enabled,
|
|
71
69
|
}
|
|
72
70
|
|
|
73
71
|
example_config = toolset.get_example_config()
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Utilities for KRR (Kubernetes Resource Recommendations) data processing."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_cpu(cpu_value: Any) -> float:
|
|
8
|
+
"""Parse Kubernetes CPU value to float (in cores).
|
|
9
|
+
|
|
10
|
+
Handles:
|
|
11
|
+
- Numeric values (0.1, 1, etc.) - already in cores
|
|
12
|
+
- String values with 'm' suffix (100m = 0.1 cores)
|
|
13
|
+
- String numeric values ("0.5")
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
cpu_value: CPU value to parse (can be int, float, str, or None)
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
CPU value in cores as float, or 0.0 if invalid
|
|
20
|
+
"""
|
|
21
|
+
if cpu_value is None or cpu_value == "" or cpu_value == "?":
|
|
22
|
+
return 0.0
|
|
23
|
+
try:
|
|
24
|
+
if isinstance(cpu_value, (int, float)):
|
|
25
|
+
return float(cpu_value)
|
|
26
|
+
|
|
27
|
+
cpu_str = str(cpu_value).strip()
|
|
28
|
+
if cpu_str.endswith("m"):
|
|
29
|
+
return float(cpu_str[:-1]) / 1000.0
|
|
30
|
+
return float(cpu_str)
|
|
31
|
+
except (ValueError, AttributeError, TypeError):
|
|
32
|
+
return 0.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_memory(memory_value: Any) -> float:
|
|
36
|
+
"""Parse Kubernetes memory value to float (in bytes).
|
|
37
|
+
|
|
38
|
+
Handles:
|
|
39
|
+
- Numeric values (already in bytes)
|
|
40
|
+
- String values with units (100Mi, 1Gi, etc.)
|
|
41
|
+
- String numeric values ("1048576")
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
memory_value: Memory value to parse (can be int, float, str, or None)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Memory value in bytes as float, or 0.0 if invalid
|
|
48
|
+
"""
|
|
49
|
+
if memory_value is None or memory_value == "" or memory_value == "?":
|
|
50
|
+
return 0.0
|
|
51
|
+
try:
|
|
52
|
+
if isinstance(memory_value, (int, float)):
|
|
53
|
+
return float(memory_value)
|
|
54
|
+
|
|
55
|
+
memory_str = str(memory_value).strip()
|
|
56
|
+
units = {
|
|
57
|
+
"Ki": 1024,
|
|
58
|
+
"Mi": 1024**2,
|
|
59
|
+
"Gi": 1024**3,
|
|
60
|
+
"Ti": 1024**4,
|
|
61
|
+
"K": 1000,
|
|
62
|
+
"M": 1000**2,
|
|
63
|
+
"G": 1000**3,
|
|
64
|
+
"T": 1000**4,
|
|
65
|
+
}
|
|
66
|
+
for unit, multiplier in units.items():
|
|
67
|
+
if memory_str.endswith(unit):
|
|
68
|
+
return float(memory_str[: -len(unit)]) * multiplier
|
|
69
|
+
return float(memory_str)
|
|
70
|
+
except (ValueError, AttributeError, TypeError):
|
|
71
|
+
return 0.0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Helper to get numeric value from allocated/recommended, handling "?" strings
|
|
75
|
+
def get_value(data: Dict, field: str, subfield: str) -> Any:
|
|
76
|
+
if not data:
|
|
77
|
+
return 0.0
|
|
78
|
+
val = data.get(field, {}).get(subfield)
|
|
79
|
+
if val is None or val == "?":
|
|
80
|
+
return 0.0
|
|
81
|
+
return val
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def calculate_krr_savings(result: Dict, sort_by: str) -> float:
|
|
85
|
+
"""Calculate potential savings from KRR recommendation data.
|
|
86
|
+
|
|
87
|
+
The KRR data structure has a 'content' field that contains a list of resource
|
|
88
|
+
recommendations. Each item in the list represents either CPU or memory, with:
|
|
89
|
+
- resource: "cpu" or "memory"
|
|
90
|
+
- allocated: {request: value, limit: value} - current allocation
|
|
91
|
+
- recommended: {request: value, limit: value} - recommended allocation
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
result: KRR scan result dictionary with 'content' field
|
|
95
|
+
sort_by: Sorting criteria, one of:
|
|
96
|
+
- "cpu_total": Total CPU savings (requests + limits)
|
|
97
|
+
- "memory_total": Total memory savings (requests + limits)
|
|
98
|
+
- "cpu_requests": CPU requests savings only
|
|
99
|
+
- "memory_requests": Memory requests savings only
|
|
100
|
+
- "cpu_limits": CPU limits savings only
|
|
101
|
+
- "memory_limits": Memory limits savings only
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Calculated savings as a float (>= 0.0). Returns 0.0 for invalid data
|
|
105
|
+
or when recommended values are higher than allocated.
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
content_list = result.get("content", [])
|
|
109
|
+
if not content_list or not isinstance(content_list, list):
|
|
110
|
+
return 0.0
|
|
111
|
+
|
|
112
|
+
cpu_data = None
|
|
113
|
+
memory_data = None
|
|
114
|
+
for item in content_list:
|
|
115
|
+
if item.get("resource") == "cpu":
|
|
116
|
+
cpu_data = item
|
|
117
|
+
elif item.get("resource") == "memory":
|
|
118
|
+
memory_data = item
|
|
119
|
+
|
|
120
|
+
if not cpu_data and not memory_data:
|
|
121
|
+
return 0.0
|
|
122
|
+
|
|
123
|
+
savings = 0.0
|
|
124
|
+
|
|
125
|
+
if sort_by == "cpu_total" and cpu_data:
|
|
126
|
+
cpu_req_allocated = parse_cpu(get_value(cpu_data, "allocated", "request"))
|
|
127
|
+
cpu_req_recommended = parse_cpu(
|
|
128
|
+
get_value(cpu_data, "recommended", "request")
|
|
129
|
+
)
|
|
130
|
+
cpu_lim_allocated = parse_cpu(get_value(cpu_data, "allocated", "limit"))
|
|
131
|
+
cpu_lim_recommended = parse_cpu(get_value(cpu_data, "recommended", "limit"))
|
|
132
|
+
|
|
133
|
+
savings = (cpu_req_allocated - cpu_req_recommended) + (
|
|
134
|
+
cpu_lim_allocated - cpu_lim_recommended
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
elif sort_by == "memory_total" and memory_data:
|
|
138
|
+
mem_req_allocated = parse_memory(
|
|
139
|
+
get_value(memory_data, "allocated", "request")
|
|
140
|
+
)
|
|
141
|
+
mem_req_recommended = parse_memory(
|
|
142
|
+
get_value(memory_data, "recommended", "request")
|
|
143
|
+
)
|
|
144
|
+
mem_lim_allocated = parse_memory(
|
|
145
|
+
get_value(memory_data, "allocated", "limit")
|
|
146
|
+
)
|
|
147
|
+
mem_lim_recommended = parse_memory(
|
|
148
|
+
get_value(memory_data, "recommended", "limit")
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
savings = (mem_req_allocated - mem_req_recommended) + (
|
|
152
|
+
mem_lim_allocated - mem_lim_recommended
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
elif sort_by == "cpu_requests" and cpu_data:
|
|
156
|
+
cpu_req_allocated = parse_cpu(get_value(cpu_data, "allocated", "request"))
|
|
157
|
+
cpu_req_recommended = parse_cpu(
|
|
158
|
+
get_value(cpu_data, "recommended", "request")
|
|
159
|
+
)
|
|
160
|
+
savings = cpu_req_allocated - cpu_req_recommended
|
|
161
|
+
|
|
162
|
+
elif sort_by == "memory_requests" and memory_data:
|
|
163
|
+
mem_req_allocated = parse_memory(
|
|
164
|
+
get_value(memory_data, "allocated", "request")
|
|
165
|
+
)
|
|
166
|
+
mem_req_recommended = parse_memory(
|
|
167
|
+
get_value(memory_data, "recommended", "request")
|
|
168
|
+
)
|
|
169
|
+
savings = mem_req_allocated - mem_req_recommended
|
|
170
|
+
|
|
171
|
+
elif sort_by == "cpu_limits" and cpu_data:
|
|
172
|
+
cpu_lim_allocated = parse_cpu(get_value(cpu_data, "allocated", "limit"))
|
|
173
|
+
cpu_lim_recommended = parse_cpu(get_value(cpu_data, "recommended", "limit"))
|
|
174
|
+
savings = cpu_lim_allocated - cpu_lim_recommended
|
|
175
|
+
|
|
176
|
+
elif sort_by == "memory_limits" and memory_data:
|
|
177
|
+
mem_lim_allocated = parse_memory(
|
|
178
|
+
get_value(memory_data, "allocated", "limit")
|
|
179
|
+
)
|
|
180
|
+
mem_lim_recommended = parse_memory(
|
|
181
|
+
get_value(memory_data, "recommended", "limit")
|
|
182
|
+
)
|
|
183
|
+
savings = mem_lim_allocated - mem_lim_recommended
|
|
184
|
+
|
|
185
|
+
return savings
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logging.debug(f"Error calculating savings for result: {e}")
|
|
188
|
+
return 0.0
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import sentry_sdk
|
|
2
|
+
from holmes.core.models import ToolCallResult, TruncationMetadata
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def capture_tool_truncations(truncations: list[TruncationMetadata]):
|
|
6
|
+
for truncation in truncations:
|
|
7
|
+
_capture_tool_truncation(truncation)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _capture_tool_truncation(truncation: TruncationMetadata):
|
|
11
|
+
sentry_sdk.capture_message(
|
|
12
|
+
f"Tool {truncation.tool_name} was truncated",
|
|
13
|
+
level="warning",
|
|
14
|
+
tags={
|
|
15
|
+
"tool_name": truncation.tool_name,
|
|
16
|
+
"tool_original_token_count": truncation.original_token_count,
|
|
17
|
+
"tool_new_token_count": truncation.end_index,
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def capture_toolcall_contains_too_many_tokens(
|
|
23
|
+
tool_call_result: ToolCallResult, token_count: int, max_allowed_token_count: int
|
|
24
|
+
):
|
|
25
|
+
sentry_sdk.capture_message(
|
|
26
|
+
f"Tool call {tool_call_result.tool_name} contains too many tokens",
|
|
27
|
+
level="warning",
|
|
28
|
+
tags={
|
|
29
|
+
"tool_name": tool_call_result.tool_name,
|
|
30
|
+
"tool_original_token_count": token_count,
|
|
31
|
+
"tool_max_allowed_token_count": max_allowed_token_count,
|
|
32
|
+
"tool_description": tool_call_result.description,
|
|
33
|
+
},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def capture_structured_output_incorrect_tool_call():
|
|
38
|
+
sentry_sdk.capture_message(
|
|
39
|
+
"Structured output incorrect tool call",
|
|
40
|
+
level="warning",
|
|
41
|
+
)
|
holmes/utils/stream.py
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from enum import Enum
|
|
3
|
-
from typing import Generator, Optional, List
|
|
3
|
+
from typing import Generator, Optional, List, Union
|
|
4
4
|
import litellm
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
from holmes.core.investigation_structured_output import process_response_into_sections
|
|
7
7
|
from functools import partial
|
|
8
|
+
import logging
|
|
9
|
+
from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper
|
|
10
|
+
from litellm.types.utils import ModelResponse, TextCompletionResponse
|
|
11
|
+
|
|
12
|
+
from holmes.core.llm import TokenCountMetadata, get_llm_usage
|
|
8
13
|
|
|
9
14
|
|
|
10
15
|
class StreamEvents(str, Enum):
|
|
@@ -13,6 +18,9 @@ class StreamEvents(str, Enum):
|
|
|
13
18
|
TOOL_RESULT = "tool_calling_result"
|
|
14
19
|
ERROR = "error"
|
|
15
20
|
AI_MESSAGE = "ai_message"
|
|
21
|
+
APPROVAL_REQUIRED = "approval_required"
|
|
22
|
+
TOKEN_COUNT = "token_count"
|
|
23
|
+
CONVERSATION_HISTORY_COMPACTED = "conversation_history_compacted"
|
|
16
24
|
|
|
17
25
|
|
|
18
26
|
class StreamMessage(BaseModel):
|
|
@@ -61,6 +69,7 @@ def stream_investigate_formatter(
|
|
|
61
69
|
"sections": sections or {},
|
|
62
70
|
"analysis": text_response,
|
|
63
71
|
"instructions": runbooks or [],
|
|
72
|
+
"metadata": message.data.get("metadata") or {},
|
|
64
73
|
},
|
|
65
74
|
)
|
|
66
75
|
else:
|
|
@@ -76,15 +85,60 @@ def stream_chat_formatter(
|
|
|
76
85
|
try:
|
|
77
86
|
for message in call_stream:
|
|
78
87
|
if message.event == StreamEvents.ANSWER_END:
|
|
88
|
+
response_data = {
|
|
89
|
+
"analysis": message.data.get("content"),
|
|
90
|
+
"conversation_history": message.data.get("messages"),
|
|
91
|
+
"follow_up_actions": followups,
|
|
92
|
+
"metadata": message.data.get("metadata") or {},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
yield create_sse_message(StreamEvents.ANSWER_END.value, response_data)
|
|
96
|
+
elif message.event == StreamEvents.APPROVAL_REQUIRED:
|
|
97
|
+
response_data = {
|
|
98
|
+
"analysis": message.data.get("content"),
|
|
99
|
+
"conversation_history": message.data.get("messages"),
|
|
100
|
+
"follow_up_actions": followups,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
response_data["requires_approval"] = True
|
|
104
|
+
response_data["pending_approvals"] = message.data.get(
|
|
105
|
+
"pending_approvals", []
|
|
106
|
+
)
|
|
107
|
+
|
|
79
108
|
yield create_sse_message(
|
|
80
|
-
StreamEvents.
|
|
81
|
-
{
|
|
82
|
-
"analysis": message.data.get("content"),
|
|
83
|
-
"conversation_history": message.data.get("messages"),
|
|
84
|
-
"follow_up_actions": followups,
|
|
85
|
-
},
|
|
109
|
+
StreamEvents.APPROVAL_REQUIRED.value, response_data
|
|
86
110
|
)
|
|
87
111
|
else:
|
|
88
112
|
yield create_sse_message(message.event.value, message.data)
|
|
89
113
|
except litellm.exceptions.RateLimitError as e:
|
|
90
114
|
yield create_rate_limit_error_message(str(e))
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logging.error(e)
|
|
117
|
+
if "Model is getting throttled" in str(e): # happens for bedrock
|
|
118
|
+
yield create_rate_limit_error_message(str(e))
|
|
119
|
+
else:
|
|
120
|
+
yield create_sse_error_message(description=str(e), error_code=1, msg=str(e))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def add_token_count_to_metadata(
|
|
124
|
+
tokens: TokenCountMetadata,
|
|
125
|
+
metadata: dict,
|
|
126
|
+
max_context_size: int,
|
|
127
|
+
maximum_output_token: int,
|
|
128
|
+
full_llm_response: Union[
|
|
129
|
+
ModelResponse, CustomStreamWrapper, TextCompletionResponse
|
|
130
|
+
],
|
|
131
|
+
):
|
|
132
|
+
metadata["usage"] = get_llm_usage(full_llm_response)
|
|
133
|
+
metadata["tokens"] = tokens.model_dump()
|
|
134
|
+
metadata["max_tokens"] = max_context_size
|
|
135
|
+
metadata["max_output_tokens"] = maximum_output_token
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def build_stream_event_token_count(metadata: dict) -> StreamMessage:
|
|
139
|
+
return StreamMessage(
|
|
140
|
+
event=StreamEvents.TOKEN_COUNT,
|
|
141
|
+
data={
|
|
142
|
+
"metadata": metadata,
|
|
143
|
+
},
|
|
144
|
+
)
|
holmes/version.py
CHANGED
|
@@ -57,11 +57,41 @@ def get_version() -> str:
|
|
|
57
57
|
return __version__
|
|
58
58
|
|
|
59
59
|
# we are running from an unreleased dev version
|
|
60
|
+
archival_file_path = os.path.join(this_path, ".git_archival.json")
|
|
61
|
+
if os.path.exists(archival_file_path):
|
|
62
|
+
try:
|
|
63
|
+
with open(archival_file_path, "r") as f:
|
|
64
|
+
archival_data = json.load(f)
|
|
65
|
+
refs = archival_data.get("refs", "")
|
|
66
|
+
hash_short = archival_data.get("hash-short", "")
|
|
67
|
+
|
|
68
|
+
# Check if Git substitution didn't happen (placeholders are still present)
|
|
69
|
+
if "$Format:" in refs or "$Format:" in hash_short:
|
|
70
|
+
# Placeholders not substituted, skip to next method
|
|
71
|
+
pass
|
|
72
|
+
else:
|
|
73
|
+
# Valid archival data found
|
|
74
|
+
return f"dev-{refs}-{hash_short}"
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
# Now try git commands for development environments
|
|
60
79
|
try:
|
|
80
|
+
env = os.environ.copy()
|
|
81
|
+
# Set ceiling to prevent walking up beyond the project root
|
|
82
|
+
# We want to allow access to holmes/.git but not beyond holmes
|
|
83
|
+
project_root = os.path.dirname(this_path) # holmes
|
|
84
|
+
env["GIT_CEILING_DIRECTORIES"] = os.path.dirname(
|
|
85
|
+
project_root
|
|
86
|
+
) # holmes's parent
|
|
87
|
+
|
|
61
88
|
# Get the latest git tag
|
|
62
89
|
tag = (
|
|
63
90
|
subprocess.check_output(
|
|
64
|
-
["git", "describe", "--tags"],
|
|
91
|
+
["git", "describe", "--tags"],
|
|
92
|
+
stderr=subprocess.STDOUT,
|
|
93
|
+
cwd=this_path,
|
|
94
|
+
env=env,
|
|
65
95
|
)
|
|
66
96
|
.decode()
|
|
67
97
|
.strip()
|
|
@@ -73,6 +103,7 @@ def get_version() -> str:
|
|
|
73
103
|
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
74
104
|
stderr=subprocess.STDOUT,
|
|
75
105
|
cwd=this_path,
|
|
106
|
+
env=env,
|
|
76
107
|
)
|
|
77
108
|
.decode()
|
|
78
109
|
.strip()
|
|
@@ -84,6 +115,7 @@ def get_version() -> str:
|
|
|
84
115
|
["git", "status", "--porcelain"],
|
|
85
116
|
stderr=subprocess.STDOUT,
|
|
86
117
|
cwd=this_path,
|
|
118
|
+
env=env,
|
|
87
119
|
)
|
|
88
120
|
.decode()
|
|
89
121
|
.strip()
|
|
@@ -95,19 +127,7 @@ def get_version() -> str:
|
|
|
95
127
|
except Exception:
|
|
96
128
|
pass
|
|
97
129
|
|
|
98
|
-
|
|
99
|
-
archival_file_path = os.path.join(this_path, ".git_archival.json")
|
|
100
|
-
if os.path.exists(archival_file_path):
|
|
101
|
-
try:
|
|
102
|
-
with open(archival_file_path, "r") as f:
|
|
103
|
-
archival_data = json.load(f)
|
|
104
|
-
return f"dev-{archival_data['refs']}-{archival_data['hash-short']}"
|
|
105
|
-
except Exception:
|
|
106
|
-
pass
|
|
107
|
-
|
|
108
|
-
return "dev-version"
|
|
109
|
-
|
|
110
|
-
return "unknown-version"
|
|
130
|
+
return "dev-unknown"
|
|
111
131
|
|
|
112
132
|
|
|
113
133
|
@cache
|