Skip to content

Commit e36bdb0

Browse files
authored
Subtasks (#851)
1 parent 822f4b3 commit e36bdb0

28 files changed

+1102
-14
lines changed

docs/installation/python-installation.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ messages = build_initial_ask_messages(
4848
initial_user_prompt=question,
4949
file_paths=None,
5050
tool_executor=ai.tool_executor,
51+
investigation_id=ai.investigation_id,
5152
runbooks=config.get_runbook_catalog(),
5253
system_prompt_additions=None
5354
)
@@ -129,6 +130,7 @@ def main():
129130
initial_user_prompt=question,
130131
file_paths=None,
131132
tool_executor=ai.tool_executor,
133+
investigation_id=ai.investigation_id,
132134
runbooks=config.get_runbook_catalog(),
133135
system_prompt_additions=None
134136
)
@@ -222,6 +224,7 @@ def main():
222224
initial_user_prompt=first_question,
223225
file_paths=None,
224226
tool_executor=ai.tool_executor,
227+
investigation_id=ai.investigation_id,
225228
runbooks=config.get_runbook_catalog(),
226229
system_prompt_additions=None
227230
)

examples/custom_llm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def ask_holmes():
5555
)
5656

5757
tool_executor = ToolExecutor(load_builtin_toolsets())
58-
ai = ToolCallingLLM(tool_executor, max_steps=10, llm=MyCustomLLM())
58+
ai = ToolCallingLLM(tool_executor, max_steps=40, llm=MyCustomLLM())
5959

6060
response = ai.prompt_call(system_prompt, prompt)
6161

holmes/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ class Config(RobustaBaseConfig):
7474
None # if None, read from OPENAI_API_KEY or AZURE_OPENAI_ENDPOINT env var
7575
)
7676
model: Optional[str] = "gpt-4o"
77-
max_steps: int = 10
77+
max_steps: int = 40
7878
cluster_name: Optional[str] = None
7979

8080
alertmanager_url: Optional[str] = None

holmes/core/conversations.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ def build_issue_chat_messages(
133133
"issue": issue_chat_request.issue_type,
134134
"toolsets": ai.tool_executor.toolsets,
135135
"cluster_name": config.cluster_name,
136+
"investigation_id": ai.investigation_id,
136137
},
137138
)
138139
messages = [
@@ -153,6 +154,7 @@ def build_issue_chat_messages(
153154
"issue": issue_chat_request.issue_type,
154155
"toolsets": ai.tool_executor.toolsets,
155156
"cluster_name": config.cluster_name,
157+
"investigation_id": ai.investigation_id,
156158
}
157159
system_prompt_without_tools = load_and_render_prompt(
158160
template_path, template_context_without_tools
@@ -186,6 +188,7 @@ def build_issue_chat_messages(
186188
"issue": issue_chat_request.issue_type,
187189
"toolsets": ai.tool_executor.toolsets,
188190
"cluster_name": config.cluster_name,
191+
"investigation_id": ai.investigation_id,
189192
}
190193
system_prompt_with_truncated_tools = load_and_render_prompt(
191194
template_path, truncated_template_context
@@ -227,6 +230,7 @@ def build_issue_chat_messages(
227230
"issue": issue_chat_request.issue_type,
228231
"toolsets": ai.tool_executor.toolsets,
229232
"cluster_name": config.cluster_name,
233+
"investigation_id": ai.investigation_id,
230234
}
231235
system_prompt_without_tools = load_and_render_prompt(
232236
template_path, template_context_without_tools
@@ -250,6 +254,7 @@ def build_issue_chat_messages(
250254
"issue": issue_chat_request.issue_type,
251255
"toolsets": ai.tool_executor.toolsets,
252256
"cluster_name": config.cluster_name,
257+
"investigation_id": ai.investigation_id,
253258
}
254259
system_prompt_with_truncated_tools = load_and_render_prompt(
255260
template_path, template_context
@@ -274,6 +279,7 @@ def add_or_update_system_prompt(
274279
context = {
275280
"toolsets": ai.tool_executor.toolsets,
276281
"cluster_name": config.cluster_name,
282+
"investigation_id": ai.investigation_id,
277283
}
278284

279285
system_prompt = load_and_render_prompt(template_path, context)
@@ -465,6 +471,7 @@ def build_workload_health_chat_messages(
465471
"resource": resource,
466472
"toolsets": ai.tool_executor.toolsets,
467473
"cluster_name": config.cluster_name,
474+
"investigation_id": ai.investigation_id,
468475
},
469476
)
470477
messages = [
@@ -485,6 +492,7 @@ def build_workload_health_chat_messages(
485492
"resource": resource,
486493
"toolsets": ai.tool_executor.toolsets,
487494
"cluster_name": config.cluster_name,
495+
"investigation_id": ai.investigation_id,
488496
}
489497
system_prompt_without_tools = load_and_render_prompt(
490498
template_path, template_context_without_tools
@@ -518,6 +526,7 @@ def build_workload_health_chat_messages(
518526
"resource": resource,
519527
"toolsets": ai.tool_executor.toolsets,
520528
"cluster_name": config.cluster_name,
529+
"investigation_id": ai.investigation_id,
521530
}
522531
system_prompt_with_truncated_tools = load_and_render_prompt(
523532
template_path, truncated_template_context
@@ -559,6 +568,7 @@ def build_workload_health_chat_messages(
559568
"resource": resource,
560569
"toolsets": ai.tool_executor.toolsets,
561570
"cluster_name": config.cluster_name,
571+
"investigation_id": ai.investigation_id,
562572
}
563573
system_prompt_without_tools = load_and_render_prompt(
564574
template_path, template_context_without_tools
@@ -582,6 +592,7 @@ def build_workload_health_chat_messages(
582592
"resource": resource,
583593
"toolsets": ai.tool_executor.toolsets,
584594
"cluster_name": config.cluster_name,
595+
"investigation_id": ai.investigation_id,
585596
}
586597
system_prompt_with_truncated_tools = load_and_render_prompt(
587598
template_path, template_context

holmes/core/investigation.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from holmes.core.tracing import DummySpan, SpanType
1111
from holmes.utils.global_instructions import add_global_instructions_to_user_prompt
1212
from holmes.utils.robusta import load_robusta_api_key
13+
from holmes.core.todo_manager import get_todo_manager
1314

1415
from holmes.core.investigation_structured_output import (
1516
DEFAULT_SECTIONS,
@@ -133,6 +134,9 @@ def get_investigation_context(
133134
else:
134135
logging.info("Structured output is disabled for this request")
135136

137+
todo_manager = get_todo_manager()
138+
todo_context = todo_manager.format_tasks_for_prompt(ai.investigation_id)
139+
136140
system_prompt = load_and_render_prompt(
137141
investigate_request.prompt_template,
138142
{
@@ -141,6 +145,8 @@ def get_investigation_context(
141145
"structured_output": request_structured_output_from_llm,
142146
"toolsets": ai.tool_executor.toolsets,
143147
"cluster_name": config.cluster_name,
148+
"todo_list": todo_context,
149+
"investigation_id": ai.investigation_id,
144150
},
145151
)
146152

holmes/core/openai_formatting.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ def type_to_open_ai_schema(param_attributes: Any, strict_mode: bool) -> dict[str
2424
type_obj = {"type": "object"}
2525
if strict_mode:
2626
type_obj["additionalProperties"] = False
27+
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())
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
2747
else:
2848
match = re.match(pattern, param_type)
2949

@@ -33,10 +53,9 @@ def type_to_open_ai_schema(param_attributes: Any, strict_mode: bool) -> dict[str
3353
if match.group("inner_type"):
3454
inner_type = match.group("inner_type")
3555
if inner_type == "object":
36-
items_obj: dict[str, Any] = {"type": "object"}
37-
if strict_mode:
38-
items_obj["additionalProperties"] = False
39-
type_obj = {"type": "array", "items": items_obj}
56+
raise ValueError(
57+
"object inner type must have schema. Use ToolParameter.items"
58+
)
4059
else:
4160
type_obj = {"type": "array", "items": {"type": inner_type}}
4261
else:

holmes/core/prompt.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,22 @@ def append_all_files_to_user_prompt(
2525
return user_prompt
2626

2727

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+
2838
def build_initial_ask_messages(
2939
console: Console,
3040
initial_user_prompt: str,
3141
file_paths: Optional[List[Path]],
3242
tool_executor: Any, # ToolExecutor type
43+
investigation_id: str,
3344
runbooks: Union[RunbookCatalog, Dict, None] = None,
3445
system_prompt_additions: Optional[str] = None,
3546
) -> List[Dict]:
@@ -49,6 +60,7 @@ def build_initial_ask_messages(
4960
"toolsets": tool_executor.toolsets,
5061
"runbooks": runbooks or {},
5162
"system_prompt_additions": system_prompt_additions or "",
63+
"investigation_id": investigation_id,
5264
}
5365
system_prompt_rendered = load_and_render_prompt(
5466
system_prompt_template, template_context
@@ -59,6 +71,7 @@ def build_initial_ask_messages(
5971
console, initial_user_prompt, file_paths
6072
)
6173

74+
user_prompt_with_files += get_tasks_management_system_reminder()
6275
messages = [
6376
{"role": "system", "content": system_prompt_rendered},
6477
{"role": "user", "content": user_prompt_with_files},

holmes/core/todo_manager.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from typing import Dict, List
2+
from threading import Lock
3+
4+
from holmes.plugins.toolsets.investigator.model import Task, TaskStatus
5+
6+
7+
class TodoListManager:
8+
"""
9+
Session-based storage manager for investigation TodoLists.
10+
Stores TodoLists per session and provides methods to get/update tasks.
11+
"""
12+
13+
def __init__(self):
14+
self._sessions: Dict[str, List[Task]] = {}
15+
self._lock: Lock = Lock()
16+
17+
def get_session_tasks(self, session_id: str) -> List[Task]:
18+
with self._lock:
19+
return self._sessions.get(session_id, []).copy()
20+
21+
def update_session_tasks(self, session_id: str, tasks: List[Task]) -> None:
22+
with self._lock:
23+
self._sessions[session_id] = tasks.copy()
24+
25+
def clear_session(self, session_id: str) -> None:
26+
with self._lock:
27+
if session_id in self._sessions:
28+
del self._sessions[session_id]
29+
30+
def get_session_count(self) -> int:
31+
with self._lock:
32+
return len(self._sessions)
33+
34+
def format_tasks_for_prompt(self, session_id: str) -> str:
35+
"""
36+
Format tasks for injection into system prompt.
37+
Returns empty string if no tasks exist.
38+
"""
39+
tasks = self.get_session_tasks(session_id)
40+
41+
if not tasks:
42+
return ""
43+
44+
status_order = {
45+
TaskStatus.PENDING: 0,
46+
TaskStatus.IN_PROGRESS: 1,
47+
TaskStatus.COMPLETED: 2,
48+
}
49+
50+
sorted_tasks = sorted(
51+
tasks,
52+
key=lambda t: (status_order.get(t.status, 3),),
53+
)
54+
55+
lines = ["# CURRENT INVESTIGATION TASKS"]
56+
lines.append("")
57+
58+
pending_count = sum(1 for t in tasks if t.status == TaskStatus.PENDING)
59+
progress_count = sum(1 for t in tasks if t.status == TaskStatus.IN_PROGRESS)
60+
completed_count = sum(1 for t in tasks if t.status == TaskStatus.COMPLETED)
61+
62+
lines.append(
63+
f"**Task Status**: {completed_count} completed, {progress_count} in progress, {pending_count} pending"
64+
)
65+
lines.append("")
66+
67+
for task in sorted_tasks:
68+
status_indicator = {
69+
TaskStatus.PENDING: "[ ]",
70+
TaskStatus.IN_PROGRESS: "[~]",
71+
TaskStatus.COMPLETED: "[✓]",
72+
}.get(task.status, "[?]")
73+
74+
lines.append(f"{status_indicator} [{task.id}] {task.content}")
75+
76+
lines.append("")
77+
lines.append(
78+
"**Instructions**: Use TodoWrite tool to update task status as you work. Mark tasks as 'in_progress' when starting, 'completed' when finished."
79+
)
80+
81+
return "\n".join(lines)
82+
83+
84+
_todo_manager = TodoListManager()
85+
86+
87+
def get_todo_manager() -> TodoListManager:
88+
return _todo_manager

holmes/core/tool_calling_llm.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import logging
44
import textwrap
5+
import uuid
56
from typing import Dict, List, Optional, Type, Union
67

78
import sentry_sdk
@@ -38,6 +39,9 @@
3839
from holmes.core.tracing import DummySpan
3940
from holmes.utils.colors import AI_COLOR
4041
from holmes.utils.stream import StreamEvents, StreamMessage
42+
from holmes.core.todo_manager import (
43+
get_todo_manager,
44+
)
4145

4246

4347
def format_tool_result_data(tool_result: StructuredToolResult) -> str:
@@ -207,6 +211,7 @@ def __init__(
207211
self.max_steps = max_steps
208212
self.tracer = tracer
209213
self.llm = llm
214+
self.investigation_id = str(uuid.uuid4())
210215

211216
def prompt_call(
212217
self,
@@ -780,6 +785,9 @@ def investigate(
780785
"[bold]No runbooks found for this issue. Using default behaviour. (Add runbooks to guide the investigation.)[/bold]"
781786
)
782787

788+
todo_manager = get_todo_manager()
789+
todo_context = todo_manager.format_tasks_for_prompt(self.investigation_id)
790+
783791
system_prompt = load_and_render_prompt(
784792
prompt,
785793
{
@@ -788,6 +796,8 @@ def investigate(
788796
"structured_output": request_structured_output_from_llm,
789797
"toolsets": self.tool_executor.toolsets,
790798
"cluster_name": self.cluster_name,
799+
"todo_list": todo_context,
800+
"investigation_id": self.investigation_id,
791801
},
792802
)
793803

holmes/core/tools.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ class ToolParameter(BaseModel):
122122
description: Optional[str] = None
123123
type: str = "string"
124124
required: bool = True
125+
properties: Optional[Dict[str, "ToolParameter"]] = None # For object types
126+
items: Optional["ToolParameter"] = None # For array item schemas
125127

126128

127129
class Tool(ABC, BaseModel):

0 commit comments

Comments
 (0)