diff --git a/src/utils/summarizer.py b/src/utils/summarizer.py
index 42d18bcc..b6af0d15 100644
--- a/src/utils/summarizer.py
+++ b/src/utils/summarizer.py
@@ -97,12 +97,23 @@ class SummaryType(Enum):
LONG = "honcho_chat_summary_long"
+def _custom_instructions_section(custom_instructions: str | None) -> str:
+ if not custom_instructions or not custom_instructions.strip():
+ return ""
+ return c(f"""
+CUSTOM INSTRUCTIONS:
+{custom_instructions.strip()}
+""")
+
+
def short_summary_prompt(
formatted_messages: str,
output_words: int,
previous_summary_text: str,
+ custom_instructions: str | None = None,
) -> str:
"""Generate the short summary prompt."""
+ custom_instructions_section = _custom_instructions_section(custom_instructions)
return c(f"""
You are a system that summarizes parts of a conversation to create a concise and accurate summary. Focus on capturing:
@@ -117,6 +128,8 @@ Provide a concise, factual summary that captures the essence of the conversation
Return only the summary without any explanation or meta-commentary.
+{custom_instructions_section}
+
{previous_summary_text}
@@ -133,8 +146,10 @@ def long_summary_prompt(
formatted_messages: str,
output_words: int,
previous_summary_text: str,
+ custom_instructions: str | None = None,
) -> str:
"""Generate the long summary prompt."""
+ custom_instructions_section = _custom_instructions_section(custom_instructions)
return c(f"""
You are a system that creates thorough, comprehensive summaries of conversations. Focus on capturing:
@@ -151,6 +166,8 @@ Provide a thorough and detailed summary that captures the essence of the convers
Return only the summary without any explanation or meta-commentary.
+{custom_instructions_section}
+
{previous_summary_text}
@@ -201,6 +218,7 @@ async def create_short_summary(
input_tokens: int,
previous_summary: str | None = None,
*,
+ custom_instructions: str | None = None,
workspace_name: str | None = None,
) -> HonchoLLMCallResponse[str]:
# input_tokens indicates how many tokens the message list + previous summary take up
@@ -216,7 +234,10 @@ async def create_short_summary(
previous_summary_text = "There is no previous summary -- the messages are the beginning of the conversation."
prompt = short_summary_prompt(
- formatted_messages, output_words, previous_summary_text
+ formatted_messages,
+ output_words,
+ previous_summary_text,
+ custom_instructions,
)
return await honcho_llm_call(
@@ -236,6 +257,7 @@ async def create_long_summary(
formatted_messages: str,
previous_summary: str | None = None,
*,
+ custom_instructions: str | None = None,
workspace_name: str | None = None,
) -> HonchoLLMCallResponse[str]:
# the word/token ratio is roughly 4:3 so we multiply by 0.75.
@@ -248,7 +270,10 @@ async def create_long_summary(
previous_summary_text = "There is no previous summary -- the messages are the beginning of the conversation."
prompt = long_summary_prompt(
- formatted_messages, output_words, previous_summary_text
+ formatted_messages,
+ output_words,
+ previous_summary_text,
+ custom_instructions,
)
return await honcho_llm_call(
@@ -453,6 +478,7 @@ async def _create_and_save_summary(
last_message_id=last_message_id,
last_message_content_preview=last_message_content_preview,
message_count=message_count,
+ custom_instructions=configuration.reasoning.custom_instructions,
workspace_name=workspace_name,
)
@@ -553,6 +579,7 @@ async def _create_summary(
last_message_content_preview: str,
message_count: int,
*,
+ custom_instructions: str | None = None,
workspace_name: str | None = None,
) -> tuple[Summary, bool, int, int]:
"""
@@ -585,12 +612,14 @@ async def _create_summary(
formatted_messages,
input_tokens,
previous_summary_text,
+ custom_instructions=custom_instructions,
workspace_name=workspace_name,
)
else:
response = await create_long_summary(
formatted_messages,
previous_summary_text,
+ custom_instructions=custom_instructions,
workspace_name=workspace_name,
)
diff --git a/tests/conftest.py b/tests/conftest.py
index 06a31ac8..592c5bf8 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -77,6 +77,8 @@ _RUNTIME_MOCK_TEST_BLOCKLIST_PREFIXES = (
"tests/llm/",
# LLM transport tests mock providers directly and don't need database/runtime setup.
"tests/utils/test_length_finish_reason.py",
+ # Summary unit tests patch LLM calls directly and should not require database setup.
+ "tests/utils/test_summarizer.py",
"tests/utils/test_clients.py",
)
diff --git a/tests/utils/test_summarizer.py b/tests/utils/test_summarizer.py
index b842ced7..f7118790 100644
--- a/tests/utils/test_summarizer.py
+++ b/tests/utils/test_summarizer.py
@@ -18,6 +18,8 @@ from src.utils.summarizer import (
_create_summary, # pyright: ignore[reportPrivateUsage]
create_long_summary,
create_short_summary,
+ long_summary_prompt,
+ short_summary_prompt,
)
# Common test arguments for _create_summary
@@ -222,6 +224,68 @@ class TestCreateSummary:
assert summary["token_count"] == 0
+class TestSummaryCustomInstructions:
+ def test_short_summary_prompt_includes_custom_instructions(self):
+ prompt = short_summary_prompt(
+ formatted_messages=_FORMATTED_MESSAGES,
+ output_words=50,
+ previous_summary_text="previous context",
+ custom_instructions="Always write the summary in German.",
+ )
+
+ assert "CUSTOM INSTRUCTIONS:" in prompt
+ assert "Always write the summary in German." in prompt
+ assert "previous context" in prompt
+ assert _FORMATTED_MESSAGES in prompt
+
+ def test_long_summary_prompt_omits_blank_custom_instructions(self):
+ prompt = long_summary_prompt(
+ formatted_messages=_FORMATTED_MESSAGES,
+ output_words=250,
+ previous_summary_text="previous context",
+ custom_instructions=" \n\t ",
+ )
+
+ assert "CUSTOM INSTRUCTIONS:" not in prompt
+ assert "previous context" in prompt
+ assert _FORMATTED_MESSAGES in prompt
+
+ @pytest.mark.asyncio
+ async def test_create_summary_passes_custom_instructions_to_short_summary(self):
+ mock_response = HonchoLLMCallResponse(
+ content="Benutzer bevorzugt kurze Antworten.",
+ input_tokens=100,
+ output_tokens=8,
+ finish_reasons=["STOP"],
+ )
+
+ with patch(
+ "src.utils.summarizer.create_short_summary",
+ new_callable=AsyncMock,
+ return_value=mock_response,
+ ) as mock_short:
+ summary, is_fallback, _, _ = await _create_summary(
+ formatted_messages=_FORMATTED_MESSAGES,
+ previous_summary_text=None,
+ summary_type=SummaryType.SHORT,
+ input_tokens=_INPUT_TOKENS,
+ message_public_id=_MESSAGE_PUBLIC_ID,
+ last_message_id=_LAST_MESSAGE_ID,
+ last_message_content_preview=_LAST_MESSAGE_CONTENT_PREVIEW,
+ message_count=_MESSAGE_COUNT,
+ custom_instructions="Always write summaries in German.",
+ )
+
+ assert is_fallback is False
+ assert "Benutzer" in summary["content"]
+ await_args = mock_short.await_args
+ if await_args is None:
+ raise AssertionError("Expected short summary call")
+ assert await_args.kwargs["custom_instructions"] == (
+ "Always write summaries in German."
+ )
+
+
@pytest.mark.asyncio
class TestSummaryCallerMigration:
async def test_create_short_summary_uses_model_config(self):