holmesgpt 0.12.4__py3-none-any.whl → 0.13.0__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.
- holmes/__init__.py +1 -1
- holmes/clients/robusta_client.py +19 -1
- holmes/common/env_vars.py +13 -0
- holmes/config.py +69 -9
- holmes/core/conversations.py +11 -0
- holmes/core/investigation.py +16 -3
- holmes/core/investigation_structured_output.py +12 -0
- holmes/core/llm.py +10 -0
- holmes/core/models.py +9 -1
- holmes/core/openai_formatting.py +72 -12
- holmes/core/prompt.py +13 -0
- holmes/core/supabase_dal.py +3 -0
- holmes/core/todo_manager.py +88 -0
- holmes/core/tool_calling_llm.py +121 -149
- holmes/core/tools.py +10 -1
- holmes/core/tools_utils/tool_executor.py +7 -2
- holmes/core/tools_utils/toolset_utils.py +7 -2
- holmes/core/tracing.py +8 -7
- holmes/interactive.py +1 -0
- holmes/main.py +2 -1
- holmes/plugins/prompts/__init__.py +7 -1
- holmes/plugins/prompts/_ai_safety.jinja2 +43 -0
- holmes/plugins/prompts/_current_date_time.jinja2 +1 -0
- holmes/plugins/prompts/_default_log_prompt.jinja2 +4 -2
- holmes/plugins/prompts/_fetch_logs.jinja2 +6 -1
- holmes/plugins/prompts/_general_instructions.jinja2 +16 -0
- holmes/plugins/prompts/_permission_errors.jinja2 +1 -1
- holmes/plugins/prompts/_toolsets_instructions.jinja2 +4 -4
- holmes/plugins/prompts/generic_ask.jinja2 +4 -3
- holmes/plugins/prompts/investigation_procedure.jinja2 +210 -0
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +4 -0
- holmes/plugins/toolsets/__init__.py +19 -6
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +27 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +2 -2
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +2 -1
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -1
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +2 -1
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +2 -1
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +3 -1
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +2 -1
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +2 -1
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +2 -1
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +2 -1
- holmes/plugins/toolsets/coralogix/api.py +6 -6
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +7 -1
- holmes/plugins/toolsets/datadog/datadog_api.py +20 -8
- holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +8 -1
- holmes/plugins/toolsets/datadog/datadog_rds_instructions.jinja2 +82 -0
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +12 -5
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +20 -11
- holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +735 -0
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +18 -11
- holmes/plugins/toolsets/git.py +15 -15
- holmes/plugins/toolsets/grafana/grafana_api.py +12 -1
- holmes/plugins/toolsets/grafana/toolset_grafana.py +5 -1
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +9 -4
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +12 -5
- holmes/plugins/toolsets/internet/internet.py +2 -1
- holmes/plugins/toolsets/internet/notion.py +2 -1
- holmes/plugins/toolsets/investigator/__init__.py +0 -0
- holmes/plugins/toolsets/investigator/core_investigation.py +157 -0
- holmes/plugins/toolsets/investigator/investigator_instructions.jinja2 +253 -0
- holmes/plugins/toolsets/investigator/model.py +15 -0
- holmes/plugins/toolsets/kafka.py +14 -7
- holmes/plugins/toolsets/kubernetes.yaml +7 -7
- holmes/plugins/toolsets/kubernetes_logs.py +454 -25
- holmes/plugins/toolsets/logging_utils/logging_api.py +115 -55
- holmes/plugins/toolsets/mcp/toolset_mcp.py +1 -1
- holmes/plugins/toolsets/newrelic.py +8 -3
- holmes/plugins/toolsets/opensearch/opensearch.py +8 -4
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +9 -2
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +6 -2
- holmes/plugins/toolsets/prometheus/prometheus.py +149 -44
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +8 -2
- holmes/plugins/toolsets/robusta/robusta.py +4 -4
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +6 -5
- holmes/plugins/toolsets/servicenow/servicenow.py +18 -3
- holmes/plugins/toolsets/utils.py +8 -1
- holmes/utils/llms.py +20 -0
- holmes/utils/stream.py +90 -0
- {holmesgpt-0.12.4.dist-info → holmesgpt-0.13.0.dist-info}/METADATA +48 -35
- {holmesgpt-0.12.4.dist-info → holmesgpt-0.13.0.dist-info}/RECORD +85 -75
- holmes/utils/robusta.py +0 -9
- {holmesgpt-0.12.4.dist-info → holmesgpt-0.13.0.dist-info}/LICENSE.txt +0 -0
- {holmesgpt-0.12.4.dist-info → holmesgpt-0.13.0.dist-info}/WHEEL +0 -0
- {holmesgpt-0.12.4.dist-info → holmesgpt-0.13.0.dist-info}/entry_points.txt +0 -0
holmes/__init__.py
CHANGED
holmes/clients/robusta_client.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import logging
|
|
2
|
+
from typing import List, Optional
|
|
2
3
|
import requests # type: ignore
|
|
3
4
|
from functools import cache
|
|
4
5
|
from pydantic import BaseModel, ConfigDict
|
|
@@ -13,6 +14,23 @@ class HolmesInfo(BaseModel):
|
|
|
13
14
|
latest_version: Optional[str] = None
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
@cache
|
|
18
|
+
def fetch_robusta_models(account_id, token) -> Optional[List[str]]:
|
|
19
|
+
try:
|
|
20
|
+
session_request = {"session_token": token, "account_id": account_id}
|
|
21
|
+
resp = requests.post(
|
|
22
|
+
f"{ROBUSTA_API_ENDPOINT}/api/llm/models",
|
|
23
|
+
json=session_request,
|
|
24
|
+
timeout=10,
|
|
25
|
+
)
|
|
26
|
+
resp.raise_for_status()
|
|
27
|
+
response_json = resp.json()
|
|
28
|
+
return response_json.get("models")
|
|
29
|
+
except Exception:
|
|
30
|
+
logging.exception("Failed to fetch robusta models")
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
16
34
|
@cache
|
|
17
35
|
def fetch_holmes_info() -> Optional[HolmesInfo]:
|
|
18
36
|
try:
|
holmes/common/env_vars.py
CHANGED
|
@@ -27,6 +27,7 @@ STORE_EMAIL = os.environ.get("STORE_EMAIL", "")
|
|
|
27
27
|
STORE_PASSWORD = os.environ.get("STORE_PASSWORD", "")
|
|
28
28
|
HOLMES_POST_PROCESSING_PROMPT = os.environ.get("HOLMES_POST_PROCESSING_PROMPT", "")
|
|
29
29
|
ROBUSTA_AI = load_bool("ROBUSTA_AI", None)
|
|
30
|
+
LOAD_ALL_ROBUSTA_MODELS = load_bool("LOAD_ALL_ROBUSTA_MODELS", True)
|
|
30
31
|
ROBUSTA_API_ENDPOINT = os.environ.get("ROBUSTA_API_ENDPOINT", "https://api.robusta.dev")
|
|
31
32
|
|
|
32
33
|
LOG_PERFORMANCE = os.environ.get("LOG_PERFORMANCE", None)
|
|
@@ -37,6 +38,7 @@ SENTRY_DSN = os.environ.get("SENTRY_DSN", "")
|
|
|
37
38
|
SENTRY_TRACES_SAMPLE_RATE = float(os.environ.get("SENTRY_TRACES_SAMPLE_RATE", "0.0"))
|
|
38
39
|
|
|
39
40
|
THINKING = os.environ.get("THINKING", "")
|
|
41
|
+
REASONING_EFFORT = os.environ.get("REASONING_EFFORT", "").strip().lower()
|
|
40
42
|
TEMPERATURE = float(os.environ.get("TEMPERATURE", "0.00000001"))
|
|
41
43
|
|
|
42
44
|
STREAM_CHUNKS_PER_PARSE = int(
|
|
@@ -50,3 +52,14 @@ KUBERNETES_LOGS_TIMEOUT_SECONDS = int(
|
|
|
50
52
|
|
|
51
53
|
TOOL_CALL_SAFEGUARDS_ENABLED = load_bool("TOOL_CALL_SAFEGUARDS_ENABLED", True)
|
|
52
54
|
IS_OPENSHIFT = load_bool("IS_OPENSHIFT", False)
|
|
55
|
+
|
|
56
|
+
LLMS_WITH_STRICT_TOOL_CALLS = os.environ.get(
|
|
57
|
+
"LLMS_WITH_STRICT_TOOL_CALLS", "azure/gpt-4o, openai/*"
|
|
58
|
+
)
|
|
59
|
+
TOOL_SCHEMA_NO_PARAM_OBJECT_IF_NO_PARAMS = load_bool(
|
|
60
|
+
"TOOL_SCHEMA_NO_PARAM_OBJECT_IF_NO_PARAMS", False
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
MAX_OUTPUT_TOKEN_RESERVATION = int(
|
|
64
|
+
os.environ.get("MAX_OUTPUT_TOKEN_RESERVATION", 16384)
|
|
65
|
+
) ## 16k
|
holmes/config.py
CHANGED
|
@@ -9,7 +9,15 @@ from typing import TYPE_CHECKING, Any, List, Optional, Union
|
|
|
9
9
|
import yaml # type: ignore
|
|
10
10
|
from pydantic import BaseModel, ConfigDict, FilePath, SecretStr
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
from holmes.clients.robusta_client import fetch_robusta_models
|
|
14
|
+
from holmes.core.llm import DefaultLLM
|
|
15
|
+
from holmes.common.env_vars import (
|
|
16
|
+
ROBUSTA_AI,
|
|
17
|
+
LOAD_ALL_ROBUSTA_MODELS,
|
|
18
|
+
ROBUSTA_API_ENDPOINT,
|
|
19
|
+
ROBUSTA_CONFIG_PATH,
|
|
20
|
+
)
|
|
13
21
|
from holmes.core.tools_utils.tool_executor import ToolExecutor
|
|
14
22
|
from holmes.core.toolset_manager import ToolsetManager
|
|
15
23
|
from holmes.plugins.runbooks import (
|
|
@@ -22,7 +30,6 @@ from holmes.plugins.runbooks import (
|
|
|
22
30
|
# Source plugin imports moved to their respective create methods to speed up startup
|
|
23
31
|
if TYPE_CHECKING:
|
|
24
32
|
from holmes.core.llm import LLM
|
|
25
|
-
from holmes.core.supabase_dal import SupabaseDal
|
|
26
33
|
from holmes.core.tool_calling_llm import IssueInvestigator, ToolCallingLLM
|
|
27
34
|
from holmes.plugins.destinations.slack import SlackDestination
|
|
28
35
|
from holmes.plugins.sources.github import GitHubSource
|
|
@@ -31,6 +38,7 @@ if TYPE_CHECKING:
|
|
|
31
38
|
from holmes.plugins.sources.pagerduty import PagerDutySource
|
|
32
39
|
from holmes.plugins.sources.prometheus.plugin import AlertManagerSource
|
|
33
40
|
|
|
41
|
+
from holmes.core.supabase_dal import SupabaseDal
|
|
34
42
|
from holmes.core.config import config_path_dir
|
|
35
43
|
from holmes.utils.definitions import RobustaConfig
|
|
36
44
|
from holmes.utils.env import replace_env_vars_values
|
|
@@ -71,8 +79,11 @@ class Config(RobustaBaseConfig):
|
|
|
71
79
|
api_key: Optional[SecretStr] = (
|
|
72
80
|
None # if None, read from OPENAI_API_KEY or AZURE_OPENAI_ENDPOINT env var
|
|
73
81
|
)
|
|
82
|
+
account_id: Optional[str] = None
|
|
83
|
+
session_token: Optional[SecretStr] = None
|
|
84
|
+
|
|
74
85
|
model: Optional[str] = "gpt-4o"
|
|
75
|
-
max_steps: int =
|
|
86
|
+
max_steps: int = 40
|
|
76
87
|
cluster_name: Optional[str] = None
|
|
77
88
|
|
|
78
89
|
alertmanager_url: Optional[str] = None
|
|
@@ -134,10 +145,51 @@ class Config(RobustaBaseConfig):
|
|
|
134
145
|
|
|
135
146
|
def model_post_init(self, __context: Any) -> None:
|
|
136
147
|
self._model_list = parse_models_file(MODEL_LIST_FILE_LOCATION)
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
|
|
149
|
+
if not self._should_load_robusta_ai():
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
self.configure_robusta_ai_model()
|
|
153
|
+
|
|
154
|
+
def configure_robusta_ai_model(self) -> None:
|
|
155
|
+
try:
|
|
156
|
+
if not self.cluster_name or not LOAD_ALL_ROBUSTA_MODELS:
|
|
157
|
+
self._load_default_robusta_config()
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
if not self.api_key:
|
|
161
|
+
dal = SupabaseDal(self.cluster_name)
|
|
162
|
+
self.load_robusta_api_key(dal)
|
|
163
|
+
|
|
164
|
+
if not self.account_id or not self.session_token:
|
|
165
|
+
self._load_default_robusta_config()
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
models = fetch_robusta_models(
|
|
169
|
+
self.account_id, self.session_token.get_secret_value()
|
|
170
|
+
)
|
|
171
|
+
if not models:
|
|
172
|
+
self._load_default_robusta_config()
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
for model in models:
|
|
176
|
+
logging.info(f"Loading Robusta AI model: {model}")
|
|
177
|
+
self._model_list[model] = {
|
|
178
|
+
"base_url": f"{ROBUSTA_API_ENDPOINT}/llm/{model}",
|
|
179
|
+
"is_robusta_model": True,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
except Exception:
|
|
183
|
+
logging.exception("Failed to get all robusta models")
|
|
184
|
+
# fallback to default behavior
|
|
185
|
+
self._load_default_robusta_config()
|
|
186
|
+
|
|
187
|
+
def _load_default_robusta_config(self):
|
|
188
|
+
if self._should_load_robusta_ai() and self.api_key:
|
|
189
|
+
logging.info("Loading default Robusta AI model")
|
|
139
190
|
self._model_list[ROBUSTA_AI_MODEL_NAME] = {
|
|
140
191
|
"base_url": ROBUSTA_API_ENDPOINT,
|
|
192
|
+
"is_robusta_model": True,
|
|
141
193
|
}
|
|
142
194
|
|
|
143
195
|
def _should_load_robusta_ai(self) -> bool:
|
|
@@ -465,7 +517,7 @@ class Config(RobustaBaseConfig):
|
|
|
465
517
|
return SlackDestination(self.slack_token.get_secret_value(), self.slack_channel)
|
|
466
518
|
|
|
467
519
|
def _get_llm(self, model_key: Optional[str] = None, tracer=None) -> "LLM":
|
|
468
|
-
api_key =
|
|
520
|
+
api_key: Optional[str] = None
|
|
469
521
|
model = self.model
|
|
470
522
|
model_params = {}
|
|
471
523
|
if self._model_list:
|
|
@@ -475,11 +527,12 @@ class Config(RobustaBaseConfig):
|
|
|
475
527
|
if model_key
|
|
476
528
|
else next(iter(self._model_list.values())).copy()
|
|
477
529
|
)
|
|
478
|
-
|
|
530
|
+
if model_params.get("is_robusta_model") and self.api_key:
|
|
531
|
+
api_key = self.api_key.get_secret_value()
|
|
532
|
+
else:
|
|
533
|
+
api_key = model_params.pop("api_key", api_key)
|
|
479
534
|
model = model_params.pop("model", model)
|
|
480
535
|
|
|
481
|
-
from holmes.core.llm import DefaultLLM
|
|
482
|
-
|
|
483
536
|
return DefaultLLM(model, api_key, model_params, tracer) # type: ignore
|
|
484
537
|
|
|
485
538
|
def get_models_list(self) -> List[str]:
|
|
@@ -488,6 +541,13 @@ class Config(RobustaBaseConfig):
|
|
|
488
541
|
|
|
489
542
|
return json.dumps([self.model]) # type: ignore
|
|
490
543
|
|
|
544
|
+
def load_robusta_api_key(self, dal: SupabaseDal):
|
|
545
|
+
if ROBUSTA_AI:
|
|
546
|
+
account_id, token = dal.get_ai_credentials()
|
|
547
|
+
self.api_key = SecretStr(f"{account_id} {token}")
|
|
548
|
+
self.account_id = account_id
|
|
549
|
+
self.session_token = SecretStr(token)
|
|
550
|
+
|
|
491
551
|
|
|
492
552
|
class TicketSource(BaseModel):
|
|
493
553
|
config: Config
|
holmes/core/conversations.py
CHANGED
|
@@ -133,6 +133,7 @@ def build_issue_chat_messages(
|
|
|
133
133
|
"issue": issue_chat_request.issue_type,
|
|
134
134
|
"toolsets": ai.tool_executor.toolsets,
|
|
135
135
|
"cluster_name": config.cluster_name,
|
|
136
|
+
"investigation_id": ai.investigation_id,
|
|
136
137
|
},
|
|
137
138
|
)
|
|
138
139
|
messages = [
|
|
@@ -153,6 +154,7 @@ def build_issue_chat_messages(
|
|
|
153
154
|
"issue": issue_chat_request.issue_type,
|
|
154
155
|
"toolsets": ai.tool_executor.toolsets,
|
|
155
156
|
"cluster_name": config.cluster_name,
|
|
157
|
+
"investigation_id": ai.investigation_id,
|
|
156
158
|
}
|
|
157
159
|
system_prompt_without_tools = load_and_render_prompt(
|
|
158
160
|
template_path, template_context_without_tools
|
|
@@ -186,6 +188,7 @@ def build_issue_chat_messages(
|
|
|
186
188
|
"issue": issue_chat_request.issue_type,
|
|
187
189
|
"toolsets": ai.tool_executor.toolsets,
|
|
188
190
|
"cluster_name": config.cluster_name,
|
|
191
|
+
"investigation_id": ai.investigation_id,
|
|
189
192
|
}
|
|
190
193
|
system_prompt_with_truncated_tools = load_and_render_prompt(
|
|
191
194
|
template_path, truncated_template_context
|
|
@@ -227,6 +230,7 @@ def build_issue_chat_messages(
|
|
|
227
230
|
"issue": issue_chat_request.issue_type,
|
|
228
231
|
"toolsets": ai.tool_executor.toolsets,
|
|
229
232
|
"cluster_name": config.cluster_name,
|
|
233
|
+
"investigation_id": ai.investigation_id,
|
|
230
234
|
}
|
|
231
235
|
system_prompt_without_tools = load_and_render_prompt(
|
|
232
236
|
template_path, template_context_without_tools
|
|
@@ -250,6 +254,7 @@ def build_issue_chat_messages(
|
|
|
250
254
|
"issue": issue_chat_request.issue_type,
|
|
251
255
|
"toolsets": ai.tool_executor.toolsets,
|
|
252
256
|
"cluster_name": config.cluster_name,
|
|
257
|
+
"investigation_id": ai.investigation_id,
|
|
253
258
|
}
|
|
254
259
|
system_prompt_with_truncated_tools = load_and_render_prompt(
|
|
255
260
|
template_path, template_context
|
|
@@ -274,6 +279,7 @@ def add_or_update_system_prompt(
|
|
|
274
279
|
context = {
|
|
275
280
|
"toolsets": ai.tool_executor.toolsets,
|
|
276
281
|
"cluster_name": config.cluster_name,
|
|
282
|
+
"investigation_id": ai.investigation_id,
|
|
277
283
|
}
|
|
278
284
|
|
|
279
285
|
system_prompt = load_and_render_prompt(template_path, context)
|
|
@@ -465,6 +471,7 @@ def build_workload_health_chat_messages(
|
|
|
465
471
|
"resource": resource,
|
|
466
472
|
"toolsets": ai.tool_executor.toolsets,
|
|
467
473
|
"cluster_name": config.cluster_name,
|
|
474
|
+
"investigation_id": ai.investigation_id,
|
|
468
475
|
},
|
|
469
476
|
)
|
|
470
477
|
messages = [
|
|
@@ -485,6 +492,7 @@ def build_workload_health_chat_messages(
|
|
|
485
492
|
"resource": resource,
|
|
486
493
|
"toolsets": ai.tool_executor.toolsets,
|
|
487
494
|
"cluster_name": config.cluster_name,
|
|
495
|
+
"investigation_id": ai.investigation_id,
|
|
488
496
|
}
|
|
489
497
|
system_prompt_without_tools = load_and_render_prompt(
|
|
490
498
|
template_path, template_context_without_tools
|
|
@@ -518,6 +526,7 @@ def build_workload_health_chat_messages(
|
|
|
518
526
|
"resource": resource,
|
|
519
527
|
"toolsets": ai.tool_executor.toolsets,
|
|
520
528
|
"cluster_name": config.cluster_name,
|
|
529
|
+
"investigation_id": ai.investigation_id,
|
|
521
530
|
}
|
|
522
531
|
system_prompt_with_truncated_tools = load_and_render_prompt(
|
|
523
532
|
template_path, truncated_template_context
|
|
@@ -559,6 +568,7 @@ def build_workload_health_chat_messages(
|
|
|
559
568
|
"resource": resource,
|
|
560
569
|
"toolsets": ai.tool_executor.toolsets,
|
|
561
570
|
"cluster_name": config.cluster_name,
|
|
571
|
+
"investigation_id": ai.investigation_id,
|
|
562
572
|
}
|
|
563
573
|
system_prompt_without_tools = load_and_render_prompt(
|
|
564
574
|
template_path, template_context_without_tools
|
|
@@ -582,6 +592,7 @@ def build_workload_health_chat_messages(
|
|
|
582
592
|
"resource": resource,
|
|
583
593
|
"toolsets": ai.tool_executor.toolsets,
|
|
584
594
|
"cluster_name": config.cluster_name,
|
|
595
|
+
"investigation_id": ai.investigation_id,
|
|
585
596
|
}
|
|
586
597
|
system_prompt_with_truncated_tools = load_and_render_prompt(
|
|
587
598
|
template_path, template_context
|
holmes/core/investigation.py
CHANGED
|
@@ -7,8 +7,9 @@ from holmes.core.investigation_structured_output import process_response_into_se
|
|
|
7
7
|
from holmes.core.issue import Issue
|
|
8
8
|
from holmes.core.models import InvestigateRequest, InvestigationResult
|
|
9
9
|
from holmes.core.supabase_dal import SupabaseDal
|
|
10
|
+
from holmes.core.tracing import DummySpan, SpanType
|
|
10
11
|
from holmes.utils.global_instructions import add_global_instructions_to_user_prompt
|
|
11
|
-
from holmes.
|
|
12
|
+
from holmes.core.todo_manager import get_todo_manager
|
|
12
13
|
|
|
13
14
|
from holmes.core.investigation_structured_output import (
|
|
14
15
|
DEFAULT_SECTIONS,
|
|
@@ -24,8 +25,9 @@ def investigate_issues(
|
|
|
24
25
|
dal: SupabaseDal,
|
|
25
26
|
config: Config,
|
|
26
27
|
model: Optional[str] = None,
|
|
28
|
+
trace_span=DummySpan(),
|
|
27
29
|
) -> InvestigationResult:
|
|
28
|
-
load_robusta_api_key(dal=dal
|
|
30
|
+
config.load_robusta_api_key(dal=dal)
|
|
29
31
|
context = dal.get_issue_data(investigate_request.context.get("robusta_issue_id"))
|
|
30
32
|
|
|
31
33
|
resource_instructions = dal.get_resource_instructions(
|
|
@@ -37,7 +39,12 @@ def investigate_issues(
|
|
|
37
39
|
if context:
|
|
38
40
|
raw_data["extra_context"] = context
|
|
39
41
|
|
|
42
|
+
# If config is not preinitilized
|
|
43
|
+
create_issue_investigator_span = trace_span.start_span(
|
|
44
|
+
"create_issue_investigator", SpanType.FUNCTION.value
|
|
45
|
+
)
|
|
40
46
|
ai = config.create_issue_investigator(dal=dal, model=model)
|
|
47
|
+
create_issue_investigator_span.end()
|
|
41
48
|
|
|
42
49
|
issue = Issue(
|
|
43
50
|
id=context["id"] if context else "",
|
|
@@ -54,6 +61,7 @@ def investigate_issues(
|
|
|
54
61
|
instructions=resource_instructions,
|
|
55
62
|
global_instructions=global_instructions,
|
|
56
63
|
sections=investigate_request.sections,
|
|
64
|
+
trace_span=trace_span,
|
|
57
65
|
)
|
|
58
66
|
|
|
59
67
|
(text_response, sections) = process_response_into_sections(investigation.result)
|
|
@@ -73,7 +81,7 @@ def get_investigation_context(
|
|
|
73
81
|
config: Config,
|
|
74
82
|
request_structured_output_from_llm: Optional[bool] = None,
|
|
75
83
|
):
|
|
76
|
-
load_robusta_api_key(dal=dal
|
|
84
|
+
config.load_robusta_api_key(dal=dal)
|
|
77
85
|
ai = config.create_issue_investigator(dal=dal, model=investigate_request.model)
|
|
78
86
|
|
|
79
87
|
raw_data = investigate_request.model_dump()
|
|
@@ -125,6 +133,9 @@ def get_investigation_context(
|
|
|
125
133
|
else:
|
|
126
134
|
logging.info("Structured output is disabled for this request")
|
|
127
135
|
|
|
136
|
+
todo_manager = get_todo_manager()
|
|
137
|
+
todo_context = todo_manager.format_tasks_for_prompt(ai.investigation_id)
|
|
138
|
+
|
|
128
139
|
system_prompt = load_and_render_prompt(
|
|
129
140
|
investigate_request.prompt_template,
|
|
130
141
|
{
|
|
@@ -133,6 +144,8 @@ def get_investigation_context(
|
|
|
133
144
|
"structured_output": request_structured_output_from_llm,
|
|
134
145
|
"toolsets": ai.tool_executor.toolsets,
|
|
135
146
|
"cluster_name": config.cluster_name,
|
|
147
|
+
"todo_list": todo_context,
|
|
148
|
+
"investigation_id": ai.investigation_id,
|
|
136
149
|
},
|
|
137
150
|
)
|
|
138
151
|
|
|
@@ -177,6 +177,18 @@ def pre_format_sections(response: Any) -> Any:
|
|
|
177
177
|
# In that case it gets parsed once to get rid of the first level of marshalling
|
|
178
178
|
with suppress(Exception):
|
|
179
179
|
response = json.loads(response)
|
|
180
|
+
|
|
181
|
+
# Try to find any embedded code block with or without "json" label and parse it
|
|
182
|
+
# This has been seen a lot in newer bedrock models
|
|
183
|
+
# This is a more robust check for patterns like ```json\n{...}\n``` or ```\n{...}\n```
|
|
184
|
+
matches = re.findall(r"```(?:json)?\s*\n(.*?)\n```", response, re.DOTALL)
|
|
185
|
+
for block in matches:
|
|
186
|
+
with suppress(Exception):
|
|
187
|
+
parsed = json.loads(block)
|
|
188
|
+
if isinstance(parsed, dict):
|
|
189
|
+
logging.info("Extracted and parsed embedded JSON block successfully.")
|
|
190
|
+
return json.dumps(parsed)
|
|
191
|
+
|
|
180
192
|
return response
|
|
181
193
|
|
|
182
194
|
|
holmes/core/llm.py
CHANGED
|
@@ -11,6 +11,7 @@ from pydantic import BaseModel
|
|
|
11
11
|
import litellm
|
|
12
12
|
import os
|
|
13
13
|
from holmes.common.env_vars import (
|
|
14
|
+
REASONING_EFFORT,
|
|
14
15
|
THINKING,
|
|
15
16
|
)
|
|
16
17
|
|
|
@@ -207,6 +208,8 @@ class DefaultLLM(LLM):
|
|
|
207
208
|
stream: Optional[bool] = None,
|
|
208
209
|
) -> Union[ModelResponse, CustomStreamWrapper]:
|
|
209
210
|
tools_args = {}
|
|
211
|
+
allowed_openai_params = None
|
|
212
|
+
|
|
210
213
|
if tools and len(tools) > 0 and tool_choice == "auto":
|
|
211
214
|
tools_args["tools"] = tools
|
|
212
215
|
tools_args["tool_choice"] = tool_choice # type: ignore
|
|
@@ -217,6 +220,12 @@ class DefaultLLM(LLM):
|
|
|
217
220
|
if self.args.get("thinking", None):
|
|
218
221
|
litellm.modify_params = True
|
|
219
222
|
|
|
223
|
+
if REASONING_EFFORT:
|
|
224
|
+
self.args.setdefault("reasoning_effort", REASONING_EFFORT)
|
|
225
|
+
allowed_openai_params = [
|
|
226
|
+
"reasoning_effort"
|
|
227
|
+
] # can be removed after next litelm version
|
|
228
|
+
|
|
220
229
|
self.args.setdefault("temperature", temperature)
|
|
221
230
|
# Get the litellm module to use (wrapped or unwrapped)
|
|
222
231
|
litellm_to_use = self.tracer.wrap_llm(litellm) if self.tracer else litellm
|
|
@@ -227,6 +236,7 @@ class DefaultLLM(LLM):
|
|
|
227
236
|
messages=messages,
|
|
228
237
|
response_format=response_format,
|
|
229
238
|
drop_params=drop_params,
|
|
239
|
+
allowed_openai_params=allowed_openai_params,
|
|
230
240
|
stream=stream,
|
|
231
241
|
**tools_args,
|
|
232
242
|
**self.args,
|
holmes/core/models.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from holmes.core.investigation_structured_output import InputSectionsDataType
|
|
2
2
|
from holmes.core.tool_calling_llm import ToolCallResult
|
|
3
3
|
from typing import Optional, List, Dict, Any, Union
|
|
4
|
-
from pydantic import BaseModel, model_validator
|
|
4
|
+
from pydantic import BaseModel, model_validator, Field
|
|
5
5
|
from enum import Enum
|
|
6
6
|
|
|
7
7
|
|
|
@@ -89,6 +89,7 @@ class ConversationRequest(BaseModel):
|
|
|
89
89
|
class ChatRequestBaseModel(BaseModel):
|
|
90
90
|
conversation_history: Optional[list[dict]] = None
|
|
91
91
|
model: Optional[str] = None
|
|
92
|
+
stream: bool = Field(default=False)
|
|
92
93
|
|
|
93
94
|
# In our setup with litellm, the first message in conversation_history
|
|
94
95
|
# should follow the structure [{"role": "system", "content": ...}],
|
|
@@ -150,6 +151,13 @@ class WorkloadHealthInvestigationResult(BaseModel):
|
|
|
150
151
|
analysis: Optional[str] = None
|
|
151
152
|
tools: Optional[List[ToolCallConversationResult]] = []
|
|
152
153
|
|
|
154
|
+
@model_validator(mode="before")
|
|
155
|
+
def check_analysis_and_result(cls, values):
|
|
156
|
+
if "result" in values and "analysis" not in values:
|
|
157
|
+
values["analysis"] = values["result"]
|
|
158
|
+
del values["result"]
|
|
159
|
+
return values
|
|
160
|
+
|
|
153
161
|
|
|
154
162
|
class WorkloadHealthChatRequest(ChatRequestBaseModel):
|
|
155
163
|
ask: str
|
holmes/core/openai_formatting.py
CHANGED
|
@@ -1,33 +1,87 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from holmes.common.env_vars import (
|
|
5
|
+
TOOL_SCHEMA_NO_PARAM_OBJECT_IF_NO_PARAMS,
|
|
6
|
+
LLMS_WITH_STRICT_TOOL_CALLS,
|
|
7
|
+
)
|
|
8
|
+
from holmes.utils.llms import model_matches_list
|
|
2
9
|
|
|
3
10
|
# parses both simple types: "int", "array", "string"
|
|
4
11
|
# but also arrays of those simpler types: "array[int]", "array[string]", etc.
|
|
5
12
|
pattern = r"^(array\[(?P<inner_type>\w+)\])|(?P<simple_type>\w+)$"
|
|
6
13
|
|
|
14
|
+
LLMS_WITH_STRICT_TOOL_CALLS_LIST = [
|
|
15
|
+
llm.strip() for llm in LLMS_WITH_STRICT_TOOL_CALLS.split(",")
|
|
16
|
+
]
|
|
17
|
+
|
|
7
18
|
|
|
8
|
-
def type_to_open_ai_schema(
|
|
9
|
-
|
|
19
|
+
def type_to_open_ai_schema(param_attributes: Any, strict_mode: bool) -> dict[str, Any]:
|
|
20
|
+
param_type = param_attributes.type.strip()
|
|
21
|
+
type_obj: Optional[dict[str, Any]] = None
|
|
10
22
|
|
|
11
|
-
if
|
|
12
|
-
|
|
23
|
+
if param_type == "object":
|
|
24
|
+
type_obj = {"type": "object"}
|
|
25
|
+
if strict_mode:
|
|
26
|
+
type_obj["additionalProperties"] = False
|
|
13
27
|
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
# Use explicit properties if provided
|
|
29
|
+
if hasattr(param_attributes, "properties") and param_attributes.properties:
|
|
30
|
+
type_obj["properties"] = {
|
|
31
|
+
name: type_to_open_ai_schema(prop, strict_mode)
|
|
32
|
+
for name, prop in param_attributes.properties.items()
|
|
33
|
+
}
|
|
34
|
+
if strict_mode:
|
|
35
|
+
type_obj["required"] = list(param_attributes.properties.keys())
|
|
16
36
|
|
|
37
|
+
elif param_type == "array":
|
|
38
|
+
# Handle arrays with explicit item schemas
|
|
39
|
+
if hasattr(param_attributes, "items") and param_attributes.items:
|
|
40
|
+
items_schema = type_to_open_ai_schema(param_attributes.items, strict_mode)
|
|
41
|
+
type_obj = {"type": "array", "items": items_schema}
|
|
42
|
+
else:
|
|
43
|
+
# Fallback for arrays without explicit item schema
|
|
44
|
+
type_obj = {"type": "array", "items": {"type": "object"}}
|
|
45
|
+
if strict_mode:
|
|
46
|
+
type_obj["items"]["additionalProperties"] = False
|
|
17
47
|
else:
|
|
18
|
-
|
|
48
|
+
match = re.match(pattern, param_type)
|
|
49
|
+
|
|
50
|
+
if not match:
|
|
51
|
+
raise ValueError(f"Invalid type format: {param_type}")
|
|
52
|
+
|
|
53
|
+
if match.group("inner_type"):
|
|
54
|
+
inner_type = match.group("inner_type")
|
|
55
|
+
if inner_type == "object":
|
|
56
|
+
raise ValueError(
|
|
57
|
+
"object inner type must have schema. Use ToolParameter.items"
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
type_obj = {"type": "array", "items": {"type": inner_type}}
|
|
61
|
+
else:
|
|
62
|
+
type_obj = {"type": match.group("simple_type")}
|
|
63
|
+
|
|
64
|
+
if strict_mode and type_obj and not param_attributes.required:
|
|
65
|
+
type_obj["type"] = [type_obj["type"], "null"]
|
|
66
|
+
|
|
67
|
+
return type_obj
|
|
19
68
|
|
|
20
69
|
|
|
21
70
|
def format_tool_to_open_ai_standard(
|
|
22
|
-
tool_name: str, tool_description: str, tool_parameters: dict
|
|
71
|
+
tool_name: str, tool_description: str, tool_parameters: dict, target_model: str
|
|
23
72
|
):
|
|
24
73
|
tool_properties = {}
|
|
74
|
+
|
|
75
|
+
strict_mode = model_matches_list(target_model, LLMS_WITH_STRICT_TOOL_CALLS_LIST)
|
|
76
|
+
|
|
25
77
|
for param_name, param_attributes in tool_parameters.items():
|
|
26
|
-
tool_properties[param_name] = type_to_open_ai_schema(
|
|
78
|
+
tool_properties[param_name] = type_to_open_ai_schema(
|
|
79
|
+
param_attributes=param_attributes, strict_mode=strict_mode
|
|
80
|
+
)
|
|
27
81
|
if param_attributes.description is not None:
|
|
28
82
|
tool_properties[param_name]["description"] = param_attributes.description
|
|
29
83
|
|
|
30
|
-
result = {
|
|
84
|
+
result: dict[str, Any] = {
|
|
31
85
|
"type": "function",
|
|
32
86
|
"function": {
|
|
33
87
|
"name": tool_name,
|
|
@@ -37,15 +91,21 @@ def format_tool_to_open_ai_standard(
|
|
|
37
91
|
"required": [
|
|
38
92
|
param_name
|
|
39
93
|
for param_name, param_attributes in tool_parameters.items()
|
|
40
|
-
if param_attributes.required
|
|
94
|
+
if param_attributes.required or strict_mode
|
|
41
95
|
],
|
|
42
96
|
"type": "object",
|
|
43
97
|
},
|
|
44
98
|
},
|
|
45
99
|
}
|
|
46
100
|
|
|
101
|
+
if strict_mode and result["function"]:
|
|
102
|
+
result["function"]["strict"] = True
|
|
103
|
+
result["function"]["parameters"]["additionalProperties"] = False
|
|
104
|
+
|
|
47
105
|
# gemini doesnt have parameters object if it is without params
|
|
48
|
-
if
|
|
106
|
+
if TOOL_SCHEMA_NO_PARAM_OBJECT_IF_NO_PARAMS and (
|
|
107
|
+
tool_properties is None or tool_properties == {}
|
|
108
|
+
):
|
|
49
109
|
result["function"].pop("parameters") # type: ignore
|
|
50
110
|
|
|
51
111
|
return result
|
holmes/core/prompt.py
CHANGED
|
@@ -25,11 +25,22 @@ def append_all_files_to_user_prompt(
|
|
|
25
25
|
return user_prompt
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def get_tasks_management_system_reminder() -> str:
|
|
29
|
+
return (
|
|
30
|
+
"\n\n<system-reminder>\nIMPORTANT: You have access to the TodoWrite tool. It creates a TodoList, in order to track progress. It's very important. You MUST use it:\n1. FIRST: Ask your self which sub problems you need to solve in order to answer the question."
|
|
31
|
+
"Do this, BEFORE any other tools\n2. "
|
|
32
|
+
"AFTER EVERY TOOL CALL: If required, update the TodoList\n3. "
|
|
33
|
+
"\n\nFAILURE TO UPDATE TodoList = INCOMPLETE INVESTIGATION\n\n"
|
|
34
|
+
"Example flow:\n- Think and divide to sub problems → create TodoList → Perform each task on the list → Update list → Verify your solution\n</system-reminder>"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
28
38
|
def build_initial_ask_messages(
|
|
29
39
|
console: Console,
|
|
30
40
|
initial_user_prompt: str,
|
|
31
41
|
file_paths: Optional[List[Path]],
|
|
32
42
|
tool_executor: Any, # ToolExecutor type
|
|
43
|
+
investigation_id: str,
|
|
33
44
|
runbooks: Union[RunbookCatalog, Dict, None] = None,
|
|
34
45
|
system_prompt_additions: Optional[str] = None,
|
|
35
46
|
) -> List[Dict]:
|
|
@@ -49,6 +60,7 @@ def build_initial_ask_messages(
|
|
|
49
60
|
"toolsets": tool_executor.toolsets,
|
|
50
61
|
"runbooks": runbooks or {},
|
|
51
62
|
"system_prompt_additions": system_prompt_additions or "",
|
|
63
|
+
"investigation_id": investigation_id,
|
|
52
64
|
}
|
|
53
65
|
system_prompt_rendered = load_and_render_prompt(
|
|
54
66
|
system_prompt_template, template_context
|
|
@@ -59,6 +71,7 @@ def build_initial_ask_messages(
|
|
|
59
71
|
console, initial_user_prompt, file_paths
|
|
60
72
|
)
|
|
61
73
|
|
|
74
|
+
user_prompt_with_files += get_tasks_management_system_reminder()
|
|
62
75
|
messages = [
|
|
63
76
|
{"role": "system", "content": system_prompt_rendered},
|
|
64
77
|
{"role": "user", "content": user_prompt_with_files},
|
holmes/core/supabase_dal.py
CHANGED