holmesgpt 0.11.5__py3-none-any.whl → 0.12.0a0__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/common/env_vars.py +8 -4
- holmes/config.py +54 -14
- holmes/core/investigation_structured_output.py +7 -0
- holmes/core/llm.py +14 -4
- holmes/core/models.py +24 -0
- holmes/core/tool_calling_llm.py +48 -6
- holmes/core/tools.py +7 -4
- holmes/core/toolset_manager.py +24 -5
- holmes/core/tracing.py +224 -0
- holmes/interactive.py +761 -44
- holmes/main.py +59 -127
- holmes/plugins/prompts/_fetch_logs.jinja2 +4 -0
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +2 -10
- holmes/plugins/toolsets/__init__.py +10 -2
- holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +3 -0
- holmes/plugins/toolsets/datadog/datadog_api.py +161 -0
- holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +26 -0
- holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +310 -0
- holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +51 -0
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +267 -0
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +488 -0
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +689 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +3 -0
- holmes/plugins/toolsets/internet/internet.py +1 -1
- holmes/plugins/toolsets/logging_utils/logging_api.py +9 -3
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +3 -0
- holmes/plugins/toolsets/utils.py +6 -2
- holmes/utils/cache.py +4 -4
- holmes/utils/console/consts.py +2 -0
- holmes/utils/console/logging.py +95 -0
- holmes/utils/console/result.py +37 -0
- holmes/utils/robusta.py +2 -3
- {holmesgpt-0.11.5.dist-info → holmesgpt-0.12.0a0.dist-info}/METADATA +3 -4
- {holmesgpt-0.11.5.dist-info → holmesgpt-0.12.0a0.dist-info}/RECORD +39 -30
- {holmesgpt-0.11.5.dist-info → holmesgpt-0.12.0a0.dist-info}/WHEEL +1 -1
- holmes/__init__.py.bak +0 -76
- holmes/plugins/toolsets/datadog.py +0 -153
- {holmesgpt-0.11.5.dist-info → holmesgpt-0.12.0a0.dist-info}/LICENSE.txt +0 -0
- {holmesgpt-0.11.5.dist-info → holmesgpt-0.12.0a0.dist-info}/entry_points.txt +0 -0
holmes/__init__.py
CHANGED
holmes/common/env_vars.py
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import json
|
|
3
|
+
from typing import Optional
|
|
3
4
|
|
|
4
5
|
|
|
5
|
-
def load_bool(env_var, default: bool):
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
def load_bool(env_var, default: Optional[bool]) -> Optional[bool]:
|
|
7
|
+
env_value = os.environ.get(env_var)
|
|
8
|
+
if env_value is None:
|
|
9
|
+
return default
|
|
10
|
+
|
|
11
|
+
return json.loads(env_value.lower())
|
|
8
12
|
|
|
9
13
|
|
|
10
14
|
ENABLED_BY_DEFAULT_TOOLSETS = os.environ.get(
|
|
@@ -22,7 +26,7 @@ STORE_API_KEY = os.environ.get("STORE_API_KEY", "")
|
|
|
22
26
|
STORE_EMAIL = os.environ.get("STORE_EMAIL", "")
|
|
23
27
|
STORE_PASSWORD = os.environ.get("STORE_PASSWORD", "")
|
|
24
28
|
HOLMES_POST_PROCESSING_PROMPT = os.environ.get("HOLMES_POST_PROCESSING_PROMPT", "")
|
|
25
|
-
ROBUSTA_AI = load_bool("ROBUSTA_AI",
|
|
29
|
+
ROBUSTA_AI = load_bool("ROBUSTA_AI", None)
|
|
26
30
|
ROBUSTA_API_ENDPOINT = os.environ.get("ROBUSTA_API_ENDPOINT", "https://api.robusta.dev")
|
|
27
31
|
|
|
28
32
|
LOG_PERFORMANCE = os.environ.get("LOG_PERFORMANCE", None)
|
holmes/config.py
CHANGED
|
@@ -39,6 +39,7 @@ DEFAULT_CONFIG_LOCATION = os.path.expanduser("~/.holmes/config.yaml")
|
|
|
39
39
|
MODEL_LIST_FILE_LOCATION = os.environ.get(
|
|
40
40
|
"MODEL_LIST_FILE_LOCATION", "/etc/holmes/config/model_list.yaml"
|
|
41
41
|
)
|
|
42
|
+
ROBUSTA_AI_MODEL_NAME = "Robusta"
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
class SupportedTicketSources(str, Enum):
|
|
@@ -58,7 +59,7 @@ def is_old_toolset_config(
|
|
|
58
59
|
def parse_models_file(path: str):
|
|
59
60
|
models = load_yaml_file(path, raise_error=False, warn_not_found=False)
|
|
60
61
|
|
|
61
|
-
for
|
|
62
|
+
for _, params in models.items():
|
|
62
63
|
params = replace_env_vars_values(params)
|
|
63
64
|
|
|
64
65
|
return models
|
|
@@ -109,6 +110,7 @@ class Config(RobustaBaseConfig):
|
|
|
109
110
|
# custom_toolsets_from_cli is passed from CLI option `--custom-toolsets` as 'experimental' custom toolsets.
|
|
110
111
|
# The status of toolset here won't be cached, so the toolset from cli will always be loaded when specified in the CLI.
|
|
111
112
|
custom_toolsets_from_cli: Optional[List[FilePath]] = None
|
|
113
|
+
should_try_robusta_ai: bool = False # if True, we will try to load the Robusta AI model, in cli we aren't trying to load it.
|
|
112
114
|
|
|
113
115
|
toolsets: Optional[dict[str, dict[str, Any]]] = None
|
|
114
116
|
|
|
@@ -148,11 +150,31 @@ class Config(RobustaBaseConfig):
|
|
|
148
150
|
self._version = get_version()
|
|
149
151
|
self._holmes_info = fetch_holmes_info()
|
|
150
152
|
self._model_list = parse_models_file(MODEL_LIST_FILE_LOCATION)
|
|
151
|
-
if
|
|
152
|
-
|
|
153
|
+
if self._should_load_robusta_ai():
|
|
154
|
+
logging.info("Loading Robusta AI model")
|
|
155
|
+
self._model_list[ROBUSTA_AI_MODEL_NAME] = {
|
|
153
156
|
"base_url": ROBUSTA_API_ENDPOINT,
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
def _should_load_robusta_ai(self) -> bool:
|
|
160
|
+
if not self.should_try_robusta_ai:
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
# ROBUSTA_AI were set in the env vars, so we can use it directly
|
|
164
|
+
if ROBUSTA_AI is not None:
|
|
165
|
+
return ROBUSTA_AI
|
|
166
|
+
|
|
167
|
+
# MODEL is set in the env vars, e.g. the user is using a custom model
|
|
168
|
+
# so we don't need to load the robusta AI model and keep the behavior backward compatible
|
|
169
|
+
if "MODEL" in os.environ:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
# if the user has provided a model list, we don't need to load the robusta AI model
|
|
173
|
+
if self._model_list:
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
return True
|
|
177
|
+
|
|
156
178
|
def log_useful_info(self):
|
|
157
179
|
if self._model_list:
|
|
158
180
|
logging.info(f"loaded models: {list(self._model_list.keys())}")
|
|
@@ -220,6 +242,7 @@ class Config(RobustaBaseConfig):
|
|
|
220
242
|
if val is not None:
|
|
221
243
|
kwargs[field_name] = val
|
|
222
244
|
kwargs["cluster_name"] = Config.__get_cluster_name()
|
|
245
|
+
kwargs["should_try_robusta_ai"] = True
|
|
223
246
|
result = cls(**kwargs)
|
|
224
247
|
result.log_useful_info()
|
|
225
248
|
return result
|
|
@@ -249,7 +272,9 @@ class Config(RobustaBaseConfig):
|
|
|
249
272
|
runbook_catalog = load_runbook_catalog()
|
|
250
273
|
return runbook_catalog
|
|
251
274
|
|
|
252
|
-
def create_console_tool_executor(
|
|
275
|
+
def create_console_tool_executor(
|
|
276
|
+
self, dal: Optional[SupabaseDal], refresh_status: bool = False
|
|
277
|
+
) -> ToolExecutor:
|
|
253
278
|
"""
|
|
254
279
|
Creates a ToolExecutor instance configured for CLI usage. This executor manages the available tools
|
|
255
280
|
and their execution in the command-line interface.
|
|
@@ -259,7 +284,9 @@ class Config(RobustaBaseConfig):
|
|
|
259
284
|
2. toolsets from config file will override and be merged into built-in toolsets with the same name.
|
|
260
285
|
3. Custom toolsets from config files which can not override built-in toolsets
|
|
261
286
|
"""
|
|
262
|
-
cli_toolsets = self.toolset_manager.list_console_toolsets(
|
|
287
|
+
cli_toolsets = self.toolset_manager.list_console_toolsets(
|
|
288
|
+
dal=dal, refresh_status=refresh_status
|
|
289
|
+
)
|
|
263
290
|
return ToolExecutor(cli_toolsets)
|
|
264
291
|
|
|
265
292
|
def create_tool_executor(self, dal: Optional[SupabaseDal]) -> ToolExecutor:
|
|
@@ -281,19 +308,32 @@ class Config(RobustaBaseConfig):
|
|
|
281
308
|
return self._server_tool_executor
|
|
282
309
|
|
|
283
310
|
def create_console_toolcalling_llm(
|
|
284
|
-
self,
|
|
311
|
+
self,
|
|
312
|
+
dal: Optional[SupabaseDal] = None,
|
|
313
|
+
refresh_toolsets: bool = False,
|
|
314
|
+
tracer=None,
|
|
285
315
|
) -> ToolCallingLLM:
|
|
286
|
-
tool_executor = self.create_console_tool_executor(dal)
|
|
287
|
-
return ToolCallingLLM(
|
|
316
|
+
tool_executor = self.create_console_tool_executor(dal, refresh_toolsets)
|
|
317
|
+
return ToolCallingLLM(
|
|
318
|
+
tool_executor, self.max_steps, self._get_llm(tracer=tracer)
|
|
319
|
+
)
|
|
288
320
|
|
|
289
321
|
def create_toolcalling_llm(
|
|
290
|
-
self,
|
|
322
|
+
self,
|
|
323
|
+
dal: Optional[SupabaseDal] = None,
|
|
324
|
+
model: Optional[str] = None,
|
|
325
|
+
tracer=None,
|
|
291
326
|
) -> ToolCallingLLM:
|
|
292
327
|
tool_executor = self.create_tool_executor(dal)
|
|
293
|
-
return ToolCallingLLM(
|
|
328
|
+
return ToolCallingLLM(
|
|
329
|
+
tool_executor, self.max_steps, self._get_llm(model, tracer)
|
|
330
|
+
)
|
|
294
331
|
|
|
295
332
|
def create_issue_investigator(
|
|
296
|
-
self,
|
|
333
|
+
self,
|
|
334
|
+
dal: Optional[SupabaseDal] = None,
|
|
335
|
+
model: Optional[str] = None,
|
|
336
|
+
tracer=None,
|
|
297
337
|
) -> IssueInvestigator:
|
|
298
338
|
all_runbooks = load_builtin_runbooks()
|
|
299
339
|
for runbook_path in self.custom_runbooks:
|
|
@@ -302,7 +342,7 @@ class Config(RobustaBaseConfig):
|
|
|
302
342
|
runbook_manager = RunbookManager(all_runbooks)
|
|
303
343
|
tool_executor = self.create_tool_executor(dal)
|
|
304
344
|
return IssueInvestigator(
|
|
305
|
-
tool_executor, runbook_manager, self.max_steps, self._get_llm(model)
|
|
345
|
+
tool_executor, runbook_manager, self.max_steps, self._get_llm(model, tracer)
|
|
306
346
|
)
|
|
307
347
|
|
|
308
348
|
def create_console_issue_investigator(
|
|
@@ -411,7 +451,7 @@ class Config(RobustaBaseConfig):
|
|
|
411
451
|
raise ValueError("--slack-channel must be specified")
|
|
412
452
|
return SlackDestination(self.slack_token.get_secret_value(), self.slack_channel)
|
|
413
453
|
|
|
414
|
-
def _get_llm(self, model_key: Optional[str] = None) -> LLM:
|
|
454
|
+
def _get_llm(self, model_key: Optional[str] = None, tracer=None) -> LLM:
|
|
415
455
|
api_key = self.api_key.get_secret_value() if self.api_key else None
|
|
416
456
|
model = self.model
|
|
417
457
|
model_params = {}
|
|
@@ -425,7 +465,7 @@ class Config(RobustaBaseConfig):
|
|
|
425
465
|
api_key = model_params.pop("api_key", api_key)
|
|
426
466
|
model = model_params.pop("model", model)
|
|
427
467
|
|
|
428
|
-
return DefaultLLM(model, api_key, model_params) # type: ignore
|
|
468
|
+
return DefaultLLM(model, api_key, model_params, tracer) # type: ignore
|
|
429
469
|
|
|
430
470
|
def get_models_list(self) -> List[str]:
|
|
431
471
|
if self._model_list:
|
holmes/core/llm.py
CHANGED
|
@@ -64,12 +64,19 @@ class DefaultLLM(LLM):
|
|
|
64
64
|
base_url: Optional[str]
|
|
65
65
|
args: Dict
|
|
66
66
|
|
|
67
|
-
def __init__(
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
model: str,
|
|
70
|
+
api_key: Optional[str] = None,
|
|
71
|
+
args: Optional[Dict] = None,
|
|
72
|
+
tracer=None,
|
|
73
|
+
):
|
|
68
74
|
self.model = model
|
|
69
75
|
self.api_key = api_key
|
|
70
|
-
self.args = args
|
|
76
|
+
self.args = args or {}
|
|
77
|
+
self.tracer = tracer
|
|
71
78
|
|
|
72
|
-
if not args:
|
|
79
|
+
if not self.args:
|
|
73
80
|
self.check_llm(self.model, self.api_key)
|
|
74
81
|
|
|
75
82
|
def check_llm(self, model: str, api_key: Optional[str]):
|
|
@@ -214,7 +221,10 @@ class DefaultLLM(LLM):
|
|
|
214
221
|
if self.args.get("thinking", None):
|
|
215
222
|
litellm.modify_params = True
|
|
216
223
|
|
|
217
|
-
|
|
224
|
+
# Get the litellm module to use (wrapped or unwrapped)
|
|
225
|
+
litellm_to_use = self.tracer.wrap_llm(litellm) if self.tracer else litellm
|
|
226
|
+
|
|
227
|
+
result = litellm_to_use.completion(
|
|
218
228
|
model=self.model,
|
|
219
229
|
api_key=self.api_key,
|
|
220
230
|
messages=messages,
|
holmes/core/models.py
CHANGED
|
@@ -155,3 +155,27 @@ class WorkloadHealthChatRequest(ChatRequestBaseModel):
|
|
|
155
155
|
ask: str
|
|
156
156
|
workload_health_result: WorkloadHealthInvestigationResult
|
|
157
157
|
resource: dict
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
workload_health_structured_output = {
|
|
161
|
+
"type": "json_schema",
|
|
162
|
+
"json_schema": {
|
|
163
|
+
"name": "WorkloadHealthResult",
|
|
164
|
+
"strict": False,
|
|
165
|
+
"schema": {
|
|
166
|
+
"type": "object",
|
|
167
|
+
"properties": {
|
|
168
|
+
"workload_healthy": {
|
|
169
|
+
"type": "boolean",
|
|
170
|
+
"description": "is the workload in healthy state or in error state",
|
|
171
|
+
},
|
|
172
|
+
"root_cause_summary": {
|
|
173
|
+
"type": "string",
|
|
174
|
+
"description": "concise short explaination leading to the workload_healthy result, pinpoint reason and root cause for the workload issues if any.",
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
"required": ["root_cause_summary", "workload_healthy"],
|
|
178
|
+
"additionalProperties": False,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
}
|
holmes/core/tool_calling_llm.py
CHANGED
|
@@ -39,6 +39,7 @@ from holmes.utils.global_instructions import (
|
|
|
39
39
|
)
|
|
40
40
|
from holmes.utils.tags import format_tags_in_string, parse_messages_tags
|
|
41
41
|
from holmes.core.tools_utils.tool_executor import ToolExecutor
|
|
42
|
+
from holmes.core.tracing import DummySpan, SpanType
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
def format_tool_result_data(tool_result: StructuredToolResult) -> str:
|
|
@@ -200,9 +201,12 @@ class LLMResult(BaseModel):
|
|
|
200
201
|
class ToolCallingLLM:
|
|
201
202
|
llm: LLM
|
|
202
203
|
|
|
203
|
-
def __init__(
|
|
204
|
+
def __init__(
|
|
205
|
+
self, tool_executor: ToolExecutor, max_steps: int, llm: LLM, tracer=None
|
|
206
|
+
):
|
|
204
207
|
self.tool_executor = tool_executor
|
|
205
208
|
self.max_steps = max_steps
|
|
209
|
+
self.tracer = tracer
|
|
206
210
|
self.llm = llm
|
|
207
211
|
|
|
208
212
|
def prompt_call(
|
|
@@ -230,8 +234,11 @@ class ToolCallingLLM:
|
|
|
230
234
|
messages: List[Dict[str, str]],
|
|
231
235
|
post_process_prompt: Optional[str] = None,
|
|
232
236
|
response_format: Optional[Union[dict, Type[BaseModel]]] = None,
|
|
237
|
+
trace_span=DummySpan(),
|
|
233
238
|
) -> LLMResult:
|
|
234
|
-
return self.call(
|
|
239
|
+
return self.call(
|
|
240
|
+
messages, post_process_prompt, response_format, trace_span=trace_span
|
|
241
|
+
)
|
|
235
242
|
|
|
236
243
|
@sentry_sdk.trace
|
|
237
244
|
def call( # type: ignore
|
|
@@ -241,6 +248,7 @@ class ToolCallingLLM:
|
|
|
241
248
|
response_format: Optional[Union[dict, Type[BaseModel]]] = None,
|
|
242
249
|
user_prompt: Optional[str] = None,
|
|
243
250
|
sections: Optional[InputSectionsDataType] = None,
|
|
251
|
+
trace_span=DummySpan(),
|
|
244
252
|
) -> LLMResult:
|
|
245
253
|
perf_timing = PerformanceTiming("tool_calling_llm.call")
|
|
246
254
|
tool_calls = [] # type: ignore
|
|
@@ -270,6 +278,7 @@ class ToolCallingLLM:
|
|
|
270
278
|
perf_timing.measure("truncate_messages_to_fit_context")
|
|
271
279
|
|
|
272
280
|
logging.debug(f"sending messages={messages}\n\ntools={tools}")
|
|
281
|
+
|
|
273
282
|
try:
|
|
274
283
|
full_response = self.llm.completion(
|
|
275
284
|
messages=parse_messages_tags(messages),
|
|
@@ -291,6 +300,7 @@ class ToolCallingLLM:
|
|
|
291
300
|
)
|
|
292
301
|
else:
|
|
293
302
|
raise
|
|
303
|
+
|
|
294
304
|
response = full_response.choices[0] # type: ignore
|
|
295
305
|
|
|
296
306
|
response_message = response.message # type: ignore
|
|
@@ -347,15 +357,17 @@ class ToolCallingLLM:
|
|
|
347
357
|
)
|
|
348
358
|
|
|
349
359
|
perf_timing.measure("pre-tool-calls")
|
|
350
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=
|
|
360
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor:
|
|
351
361
|
futures = []
|
|
352
|
-
for t in tools_to_call:
|
|
362
|
+
for tool_index, t in enumerate(tools_to_call, 1):
|
|
353
363
|
logging.debug(f"Tool to call: {t}")
|
|
354
364
|
futures.append(
|
|
355
365
|
executor.submit(
|
|
356
366
|
self._invoke_tool,
|
|
357
367
|
tool_to_call=t,
|
|
358
368
|
previous_tool_calls=tool_calls,
|
|
369
|
+
trace_span=trace_span,
|
|
370
|
+
tool_number=tool_index,
|
|
359
371
|
)
|
|
360
372
|
)
|
|
361
373
|
|
|
@@ -367,10 +379,16 @@ class ToolCallingLLM:
|
|
|
367
379
|
|
|
368
380
|
perf_timing.measure(f"tool completed {tool_call_result.tool_name}")
|
|
369
381
|
|
|
382
|
+
# Add a blank line after all tools in this batch complete
|
|
383
|
+
if tools_to_call:
|
|
384
|
+
logging.info("")
|
|
385
|
+
|
|
370
386
|
def _invoke_tool(
|
|
371
387
|
self,
|
|
372
388
|
tool_to_call: ChatCompletionMessageToolCall,
|
|
373
389
|
previous_tool_calls: list[dict],
|
|
390
|
+
trace_span=DummySpan(),
|
|
391
|
+
tool_number=None,
|
|
374
392
|
) -> ToolCallResult:
|
|
375
393
|
tool_name = tool_to_call.function.name
|
|
376
394
|
tool_params = None
|
|
@@ -399,6 +417,10 @@ class ToolCallingLLM:
|
|
|
399
417
|
)
|
|
400
418
|
|
|
401
419
|
tool_response = None
|
|
420
|
+
|
|
421
|
+
# Create tool span if tracing is enabled
|
|
422
|
+
tool_span = trace_span.start_span(name=tool_name, type=SpanType.TOOL)
|
|
423
|
+
|
|
402
424
|
try:
|
|
403
425
|
tool_response = prevent_overly_repeated_tool_call(
|
|
404
426
|
tool_name=tool.name,
|
|
@@ -406,7 +428,7 @@ class ToolCallingLLM:
|
|
|
406
428
|
tool_calls=previous_tool_calls,
|
|
407
429
|
)
|
|
408
430
|
if not tool_response:
|
|
409
|
-
tool_response = tool.invoke(tool_params)
|
|
431
|
+
tool_response = tool.invoke(tool_params, tool_number=tool_number)
|
|
410
432
|
|
|
411
433
|
if not isinstance(tool_response, StructuredToolResult):
|
|
412
434
|
# Should never be needed but ensure Holmes does not crash if one of the tools does not return the right type
|
|
@@ -419,6 +441,16 @@ class ToolCallingLLM:
|
|
|
419
441
|
params=tool_params,
|
|
420
442
|
)
|
|
421
443
|
|
|
444
|
+
# Log tool execution to trace span
|
|
445
|
+
tool_span.log(
|
|
446
|
+
input=tool_params,
|
|
447
|
+
output=tool_response.data,
|
|
448
|
+
metadata={
|
|
449
|
+
"status": tool_response.status.value,
|
|
450
|
+
"error": tool_response.error,
|
|
451
|
+
},
|
|
452
|
+
)
|
|
453
|
+
|
|
422
454
|
except Exception as e:
|
|
423
455
|
logging.error(
|
|
424
456
|
f"Tool call to {tool_name} failed with an Exception", exc_info=True
|
|
@@ -428,6 +460,14 @@ class ToolCallingLLM:
|
|
|
428
460
|
error=f"Tool call failed: {e}",
|
|
429
461
|
params=tool_params,
|
|
430
462
|
)
|
|
463
|
+
|
|
464
|
+
# Log error to trace span
|
|
465
|
+
tool_span.log(
|
|
466
|
+
input=tool_params, output=str(e), metadata={"status": "ERROR"}
|
|
467
|
+
)
|
|
468
|
+
finally:
|
|
469
|
+
# End tool span
|
|
470
|
+
tool_span.end()
|
|
431
471
|
return ToolCallResult(
|
|
432
472
|
tool_call_id=tool_call_id,
|
|
433
473
|
tool_name=tool_name,
|
|
@@ -650,12 +690,14 @@ class ToolCallingLLM:
|
|
|
650
690
|
perf_timing.measure("pre-tool-calls")
|
|
651
691
|
with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor:
|
|
652
692
|
futures = []
|
|
653
|
-
for t in tools_to_call: # type: ignore
|
|
693
|
+
for tool_index, t in enumerate(tools_to_call, 1): # type: ignore
|
|
654
694
|
futures.append(
|
|
655
695
|
executor.submit(
|
|
656
696
|
self._invoke_tool,
|
|
657
697
|
tool_to_call=t, # type: ignore
|
|
658
698
|
previous_tool_calls=tool_calls,
|
|
699
|
+
trace_span=DummySpan(), # Streaming mode doesn't support tracing yet
|
|
700
|
+
tool_number=tool_index,
|
|
659
701
|
)
|
|
660
702
|
)
|
|
661
703
|
yield create_sse_message(
|
holmes/core/tools.py
CHANGED
|
@@ -139,9 +139,12 @@ class Tool(ABC, BaseModel):
|
|
|
139
139
|
tool_parameters=self.parameters,
|
|
140
140
|
)
|
|
141
141
|
|
|
142
|
-
def invoke(
|
|
142
|
+
def invoke(
|
|
143
|
+
self, params: Dict, tool_number: Optional[int] = None
|
|
144
|
+
) -> StructuredToolResult:
|
|
145
|
+
tool_number_str = f"#{tool_number} " if tool_number else ""
|
|
143
146
|
logging.info(
|
|
144
|
-
f"Running tool [bold]{self.name}[/bold]: {self.get_parameterized_one_liner(params)}"
|
|
147
|
+
f"Running tool {tool_number_str}[bold]{self.name}[/bold]: {self.get_parameterized_one_liner(params)}"
|
|
145
148
|
)
|
|
146
149
|
start_time = time.time()
|
|
147
150
|
result = self._invoke(params)
|
|
@@ -152,7 +155,7 @@ class Tool(ABC, BaseModel):
|
|
|
152
155
|
else str(result)
|
|
153
156
|
)
|
|
154
157
|
logging.info(
|
|
155
|
-
f" [dim]Finished in {elapsed:.2f}s, output length: {len(output_str):,} characters[/dim]
|
|
158
|
+
f" [dim]Finished {tool_number_str}in {elapsed:.2f}s, output length: {len(output_str):,} characters - /show to view contents[/dim]"
|
|
156
159
|
)
|
|
157
160
|
return result
|
|
158
161
|
|
|
@@ -370,7 +373,7 @@ class Toolset(BaseModel):
|
|
|
370
373
|
exclude_unset=True,
|
|
371
374
|
exclude=("name"), # type: ignore
|
|
372
375
|
).items():
|
|
373
|
-
if field in self.model_fields and value not in (None, [], {}, ""):
|
|
376
|
+
if field in self.__class__.model_fields and value not in (None, [], {}, ""):
|
|
374
377
|
setattr(self, field, value)
|
|
375
378
|
|
|
376
379
|
@model_validator(mode="before")
|
holmes/core/toolset_manager.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import concurrent.futures
|
|
1
2
|
import json
|
|
2
3
|
import logging
|
|
3
4
|
import os
|
|
@@ -113,14 +114,27 @@ class ToolsetManager:
|
|
|
113
114
|
# check_prerequisites against each enabled toolset
|
|
114
115
|
if not check_prerequisites:
|
|
115
116
|
return list(toolsets_by_name.values())
|
|
117
|
+
|
|
118
|
+
enabled_toolsets: List[Toolset] = []
|
|
116
119
|
for _, toolset in toolsets_by_name.items():
|
|
117
120
|
if toolset.enabled:
|
|
118
|
-
|
|
121
|
+
enabled_toolsets.append(toolset)
|
|
119
122
|
else:
|
|
120
123
|
toolset.status = ToolsetStatusEnum.DISABLED
|
|
124
|
+
self.check_toolset_prerequisites(enabled_toolsets)
|
|
121
125
|
|
|
122
126
|
return list(toolsets_by_name.values())
|
|
123
127
|
|
|
128
|
+
@classmethod
|
|
129
|
+
def check_toolset_prerequisites(cls, toolsets: list[Toolset]):
|
|
130
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
|
131
|
+
futures = []
|
|
132
|
+
for toolset in toolsets:
|
|
133
|
+
futures.append(executor.submit(toolset.check_prerequisites))
|
|
134
|
+
|
|
135
|
+
for _ in concurrent.futures.as_completed(futures):
|
|
136
|
+
pass
|
|
137
|
+
|
|
124
138
|
def _load_toolsets_from_config(
|
|
125
139
|
self,
|
|
126
140
|
toolsets: dict[str, dict[str, Any]],
|
|
@@ -231,6 +245,7 @@ class ToolsetManager:
|
|
|
231
245
|
dal=dal, check_prerequisites=False, toolset_tags=toolset_tags
|
|
232
246
|
)
|
|
233
247
|
|
|
248
|
+
enabled_toolsets_from_cache: List[Toolset] = []
|
|
234
249
|
for toolset in all_toolsets_with_status:
|
|
235
250
|
if toolset.name in toolsets_status_by_name:
|
|
236
251
|
# Update the status and error from the cached status
|
|
@@ -242,13 +257,15 @@ class ToolsetManager:
|
|
|
242
257
|
cached_status.get("type", ToolsetType.BUILTIN)
|
|
243
258
|
)
|
|
244
259
|
toolset.path = cached_status.get("path", None)
|
|
245
|
-
# check prerequisites for only enabled toolset when the toolset is loaded from cache
|
|
260
|
+
# check prerequisites for only enabled toolset when the toolset is loaded from cache. When the toolset is
|
|
261
|
+
# not loaded from cache, the prerequisites are checked in the refresh_toolset_status method.
|
|
246
262
|
if (
|
|
247
263
|
toolset.enabled
|
|
248
264
|
and toolset.status == ToolsetStatusEnum.ENABLED
|
|
249
265
|
and using_cached
|
|
250
266
|
):
|
|
251
|
-
|
|
267
|
+
enabled_toolsets_from_cache.append(toolset)
|
|
268
|
+
self.check_toolset_prerequisites(enabled_toolsets_from_cache)
|
|
252
269
|
|
|
253
270
|
# CLI custom toolsets status are not cached, and their prerequisites are always checked whenever the CLI runs.
|
|
254
271
|
custom_toolsets_from_cli = self._load_toolsets_from_paths(
|
|
@@ -257,13 +274,15 @@ class ToolsetManager:
|
|
|
257
274
|
check_conflict_default=True,
|
|
258
275
|
)
|
|
259
276
|
# custom toolsets from cli as experimental toolset should not override custom toolsets from config
|
|
277
|
+
enabled_toolsets_from_cli: List[Toolset] = []
|
|
260
278
|
for custom_toolset_from_cli in custom_toolsets_from_cli:
|
|
261
279
|
if custom_toolset_from_cli.name in toolsets_status_by_name:
|
|
262
280
|
raise ValueError(
|
|
263
281
|
f"Toolset {custom_toolset_from_cli.name} from cli is already defined in existing toolset"
|
|
264
282
|
)
|
|
265
|
-
|
|
266
|
-
|
|
283
|
+
enabled_toolsets_from_cli.append(custom_toolset_from_cli)
|
|
284
|
+
# status of custom toolsets from cli is not cached, and we need to check prerequisites every time the cli runs.
|
|
285
|
+
self.check_toolset_prerequisites(enabled_toolsets_from_cli)
|
|
267
286
|
|
|
268
287
|
all_toolsets_with_status.extend(custom_toolsets_from_cli)
|
|
269
288
|
if using_cached:
|