From 9221aa8fbbe62fdb1afa8c648317d24090e0379c Mon Sep 17 00:00:00 2001 From: Dennis Soong Date: Sat, 30 May 2026 21:29:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A5=97=E7=94=A8=E8=87=AA=E8=A8=82?= =?UTF-8?q?=E6=8C=87=E7=A4=BA=E5=88=B0=20session=20summaries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/summarizer.py | 33 ++++++++++++++++-- tests/conftest.py | 2 ++ tests/utils/test_summarizer.py | 64 ++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) 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):