Skip to content

Commit 9298bac

Browse files
committed
allow appending content to system prompt
1 parent ae27f55 commit 9298bac

File tree

5 files changed

+207
-2
lines changed

5 files changed

+207
-2
lines changed

holmes/core/prompt.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
def append_file_to_user_prompt(user_prompt: str, file_path: Path) -> str:
99
with file_path.open("r") as f:
10-
user_prompt += f"\n\n<attached-file path='{file_path.absolute()}>'\n{f.read()}\n</attached-file>"
10+
user_prompt += f"\n\n<attached-file path='{file_path.absolute()}'>\n{f.read()}\n</attached-file>"
1111

1212
return user_prompt
1313

@@ -31,6 +31,7 @@ def build_initial_ask_messages(
3131
file_paths: Optional[List[Path]],
3232
tool_executor: Any, # ToolExecutor type
3333
runbooks: Union[RunbookCatalog, Dict, None] = None,
34+
system_prompt_additions: Optional[str] = None,
3435
) -> List[Dict]:
3536
"""Build the initial messages for the AI call.
3637
@@ -40,12 +41,14 @@ def build_initial_ask_messages(
4041
file_paths: Optional list of files to include
4142
tool_executor: The tool executor with available toolsets
4243
runbooks: Optional runbook catalog
44+
system_prompt_additions: Optional additional system prompt content
4345
"""
4446
# Load and render system prompt internally
4547
system_prompt_template = "builtin://generic_ask.jinja2"
4648
template_context = {
4749
"toolsets": tool_executor.toolsets,
4850
"runbooks": runbooks or {},
51+
"system_prompt_additions": system_prompt_additions or "",
4952
}
5053
system_prompt_rendered = load_and_render_prompt(
5154
system_prompt_template, template_context

holmes/interactive.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,7 @@ def run_interactive_loop(
762762
show_tool_output: bool,
763763
tracer=None,
764764
runbooks=None,
765+
system_prompt_additions: Optional[str] = None,
765766
) -> None:
766767
# Initialize tracer - use DummyTracer if no tracer provided
767768
if tracer is None:
@@ -960,7 +961,12 @@ def get_bottom_toolbar():
960961

961962
if messages is None:
962963
messages = build_initial_ask_messages(
963-
console, user_input, include_files, ai.tool_executor, runbooks
964+
console,
965+
user_input,
966+
include_files,
967+
ai.tool_executor,
968+
runbooks,
969+
system_prompt_additions,
964970
)
965971
else:
966972
messages.append({"role": "user", "content": user_input})

holmes/main.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ def ask(
210210
"--trace",
211211
help="Enable tracing to the specified provider (e.g., 'braintrust')",
212212
),
213+
system_prompt_additions: Optional[str] = typer.Option(
214+
None,
215+
"--system-prompt-additions",
216+
help="Additional content to append to the system prompt",
217+
),
213218
):
214219
"""
215220
Ask any question and answer using available tools
@@ -291,6 +296,7 @@ def ask(
291296
show_tool_output,
292297
tracer,
293298
config.get_runbook_catalog(),
299+
system_prompt_additions,
294300
)
295301
return
296302

@@ -300,6 +306,7 @@ def ask(
300306
include_file,
301307
ai.tool_executor,
302308
config.get_runbook_catalog(),
309+
system_prompt_additions,
303310
)
304311

305312
with tracer.start_trace(

holmes/plugins/prompts/generic_ask.jinja2

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,7 @@ Relevant logs:
3434
```
3535

3636
Validation error led to unhandled Java exception causing a crash.
37+
38+
{% if system_prompt_additions %}
39+
{{ system_prompt_additions }}
40+
{% endif %}

tests/core/test_prompt.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import pytest
2+
from unittest.mock import Mock
3+
from rich.console import Console
4+
5+
from holmes.core.prompt import (
6+
build_initial_ask_messages,
7+
append_file_to_user_prompt,
8+
append_all_files_to_user_prompt,
9+
)
10+
11+
12+
@pytest.fixture
13+
def console():
14+
return Console(force_terminal=False, force_jupyter=False)
15+
16+
17+
@pytest.fixture
18+
def mock_tool_executor():
19+
tool_executor = Mock()
20+
tool_executor.toolsets = []
21+
return tool_executor
22+
23+
24+
def test_build_initial_ask_messages_basic(console, mock_tool_executor):
25+
"""Test basic message building without any optional parameters."""
26+
messages = build_initial_ask_messages(
27+
console,
28+
"Test prompt",
29+
None,
30+
mock_tool_executor,
31+
None,
32+
None,
33+
)
34+
35+
assert len(messages) == 2
36+
assert messages[0]["role"] == "system"
37+
assert messages[1]["role"] == "user"
38+
assert messages[1]["content"] == "Test prompt"
39+
40+
41+
def test_build_initial_ask_messages_with_system_prompt_additions(
42+
console, mock_tool_executor
43+
):
44+
"""Test message building with system prompt additions."""
45+
system_additions = "Additional system instructions here."
46+
messages = build_initial_ask_messages(
47+
console,
48+
"Test prompt",
49+
None,
50+
mock_tool_executor,
51+
None,
52+
system_additions,
53+
)
54+
55+
assert len(messages) == 2
56+
assert messages[0]["role"] == "system"
57+
# Check for unique word from the system additions
58+
assert "Additional" in messages[0]["content"]
59+
assert messages[1]["role"] == "user"
60+
assert messages[1]["content"] == "Test prompt"
61+
62+
63+
def test_build_initial_ask_messages_with_file(console, mock_tool_executor, tmp_path):
64+
"""Test message building with file attachment."""
65+
# Create a temporary file
66+
test_file = tmp_path / "test.txt"
67+
test_file.write_text("File content here")
68+
69+
messages = build_initial_ask_messages(
70+
console,
71+
"Test prompt",
72+
[test_file],
73+
mock_tool_executor,
74+
None,
75+
None,
76+
)
77+
78+
assert len(messages) == 2
79+
assert messages[0]["role"] == "system"
80+
assert messages[1]["role"] == "user"
81+
assert "Test prompt" in messages[1]["content"]
82+
assert "File content here" in messages[1]["content"]
83+
# Check for file attachment markers
84+
assert "<attached-file" in messages[1]["content"]
85+
assert "test.txt" in messages[1]["content"]
86+
assert "</attached-file>" in messages[1]["content"]
87+
88+
89+
def test_build_initial_ask_messages_with_runbooks(console, mock_tool_executor):
90+
"""Test message building with runbooks."""
91+
runbooks = {"test_runbook": {"description": "Test runbook"}}
92+
93+
messages = build_initial_ask_messages(
94+
console,
95+
"Test prompt",
96+
None,
97+
mock_tool_executor,
98+
runbooks,
99+
None,
100+
)
101+
102+
assert len(messages) == 2
103+
assert messages[0]["role"] == "system"
104+
# The runbook should be passed to the template context
105+
assert messages[1]["role"] == "user"
106+
assert messages[1]["content"] == "Test prompt"
107+
108+
109+
def test_build_initial_ask_messages_all_parameters(
110+
console, mock_tool_executor, tmp_path
111+
):
112+
"""Test message building with all parameters."""
113+
# Create a temporary file
114+
test_file = tmp_path / "test.txt"
115+
test_file.write_text("File content")
116+
117+
runbooks = {"test_runbook": {"description": "Test runbook"}}
118+
system_additions = "Extra system instructions"
119+
120+
messages = build_initial_ask_messages(
121+
console,
122+
"Test prompt",
123+
[test_file],
124+
mock_tool_executor,
125+
runbooks,
126+
system_additions,
127+
)
128+
129+
assert len(messages) == 2
130+
assert messages[0]["role"] == "system"
131+
# Check for unique word from system additions
132+
assert "Extra" in messages[0]["content"]
133+
assert messages[1]["role"] == "user"
134+
assert "Test prompt" in messages[1]["content"]
135+
assert "File content" in messages[1]["content"]
136+
137+
138+
def test_append_file_to_user_prompt(tmp_path):
139+
"""Test appending a single file to user prompt."""
140+
test_file = tmp_path / "test.txt"
141+
test_file.write_text("Test file content")
142+
143+
prompt = "Original prompt"
144+
result = append_file_to_user_prompt(prompt, test_file)
145+
146+
assert "Original prompt" in result
147+
assert "Test file content" in result
148+
# Check for file attachment markers
149+
assert "<attached-file" in result
150+
assert "test.txt" in result
151+
assert "</attached-file>" in result
152+
153+
154+
def test_append_all_files_to_user_prompt(console, tmp_path):
155+
"""Test appending multiple files to user prompt."""
156+
# Create multiple test files
157+
file1 = tmp_path / "file1.txt"
158+
file1.write_text("Content 1")
159+
160+
file2 = tmp_path / "file2.txt"
161+
file2.write_text("Content 2")
162+
163+
prompt = "Original prompt"
164+
result = append_all_files_to_user_prompt(console, prompt, [file1, file2])
165+
166+
assert "Original prompt" in result
167+
assert "Content 1" in result
168+
assert "Content 2" in result
169+
# Check for file attachment markers
170+
assert "<attached-file" in result
171+
assert "file1.txt" in result
172+
assert "file2.txt" in result
173+
assert result.count("</attached-file>") == 2
174+
175+
176+
def test_append_all_files_to_user_prompt_no_files(console):
177+
"""Test appending files when no files are provided."""
178+
prompt = "Original prompt"
179+
result = append_all_files_to_user_prompt(console, prompt, None)
180+
181+
assert result == "Original prompt"
182+
183+
# Also test with empty list
184+
result = append_all_files_to_user_prompt(console, prompt, [])
185+
assert result == "Original prompt"

0 commit comments

Comments
 (0)