diff --git a/letta/server/rest_api/routers/v1/agents.py b/letta/server/rest_api/routers/v1/agents.py index 947245ae..3264c03f 100644 --- a/letta/server/rest_api/routers/v1/agents.py +++ b/letta/server/rest_api/routers/v1/agents.py @@ -2250,6 +2250,7 @@ async def summarize_messages( summary_message, messages, summary = await agent_loop.compact( messages=in_context_messages, compaction_settings=compaction_settings, + use_summary_role=True, ) num_messages_after = len(messages) diff --git a/letta/server/rest_api/routers/v1/conversations.py b/letta/server/rest_api/routers/v1/conversations.py index eb8bd948..d1646f20 100644 --- a/letta/server/rest_api/routers/v1/conversations.py +++ b/letta/server/rest_api/routers/v1/conversations.py @@ -515,6 +515,7 @@ async def compact_conversation( summary_message, messages, summary = await agent_loop.compact( messages=in_context_messages, compaction_settings=compaction_settings, + use_summary_role=True, ) num_messages_after = len(messages) diff --git a/tests/integration_test_conversations_sdk.py b/tests/integration_test_conversations_sdk.py index 7899bf44..2b863e33 100644 --- a/tests/integration_test_conversations_sdk.py +++ b/tests/integration_test_conversations_sdk.py @@ -617,6 +617,45 @@ class TestConversationCompact: ) assert len(compacted_messages) < initial_count + def test_compact_conversation_creates_summary_role_message(self, client: Letta, agent, server_url: str): + """Test that compaction creates a summary message with role='summary'.""" + # Create a conversation + conversation = client.conversations.create(agent_id=agent.id) + + # Send multiple messages to create a history worth summarizing + for i in range(5): + list( + client.conversations.messages.create( + conversation_id=conversation.id, + messages=[{"role": "user", "content": f"Message {i}: Tell me about topic {i}."}], + ) + ) + + # Call compact endpoint with 'all' mode to ensure a single summary + response = requests.post( + f"{server_url}/v1/conversations/{conversation.id}/compact", + json={ + "compaction_settings": { + "mode": "all", + } + }, + ) + assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" + + # Get compacted messages + compacted_messages = client.conversations.messages.list( + conversation_id=conversation.id, + order="asc", + ) + + # After 'all' mode compaction, we expect: system message + summary message + # The summary message should have role='summary' + summary_messages = [msg for msg in compacted_messages if msg.role == "summary"] + assert len(summary_messages) == 1, ( + f"Expected exactly 1 summary message after compaction, found {len(summary_messages)}. " + f"Message roles: {[msg.role for msg in compacted_messages]}" + ) + def test_compact_conversation_with_settings(self, client: Letta, agent, server_url: str): """Test conversation compaction with custom compaction settings.""" # Create a conversation with multiple messages diff --git a/tests/integration_test_summarizer.py b/tests/integration_test_summarizer.py index f865f368..8c3bfa42 100644 --- a/tests/integration_test_summarizer.py +++ b/tests/integration_test_summarizer.py @@ -846,6 +846,68 @@ async def test_compact_returns_valid_summary_message_and_event_message(server: S assert "context_window" in event_msg.event_data +@pytest.mark.asyncio +@pytest.mark.parametrize( + "llm_config", + TESTED_LLM_CONFIGS, + ids=[c.model for c in TESTED_LLM_CONFIGS], +) +async def test_compact_with_use_summary_role_creates_summary_message_role(server: SyncServer, actor, llm_config: LLMConfig): + """ + Test that compact() with use_summary_role=True creates a message with role=MessageRole.summary. + + This validates that manual compaction endpoints (which pass use_summary_role=True) + will store summary messages with the dedicated 'summary' role instead of the legacy 'user' role. + """ + # Create a conversation with enough messages to summarize + messages = [ + PydanticMessage( + role=MessageRole.system, + content=[TextContent(type="text", text="You are a helpful assistant.")], + ) + ] + for i in range(10): + messages.append( + PydanticMessage( + role=MessageRole.user, + content=[TextContent(type="text", text=f"User message {i}: Test message {i}.")], + ) + ) + messages.append( + PydanticMessage( + role=MessageRole.assistant, + content=[TextContent(type="text", text=f"Assistant response {i}: Acknowledged message {i}.")], + ) + ) + + agent_state, in_context_messages = await create_agent_with_messages(server, actor, llm_config, messages) + + handle = llm_config.handle or f"{llm_config.model_endpoint_type}/{llm_config.model}" + agent_state.compaction_settings = CompactionSettings(model=handle, mode="all") + + agent_loop = LettaAgentV3(agent_state=agent_state, actor=actor) + + # Call compact with use_summary_role=True (as the REST endpoints now do) + summary_message_obj, compacted_messages, summary_text = await agent_loop.compact( + messages=in_context_messages, + use_summary_role=True, + ) + + # Verify the summary message has role=summary (not user) + assert summary_message_obj.role == MessageRole.summary, ( + f"Expected summary message to have role=summary when use_summary_role=True, got {summary_message_obj.role}" + ) + + # Verify the compacted messages list structure + assert len(compacted_messages) == 2, f"Expected 2 messages (system + summary), got {len(compacted_messages)}" + assert compacted_messages[0].role == MessageRole.system + assert compacted_messages[1].role == MessageRole.summary + + # Verify summary text is non-empty + assert isinstance(summary_text, str) + assert len(summary_text) > 0 + + @pytest.mark.asyncio async def test_v3_compact_uses_compaction_settings_model_and_model_settings(server: SyncServer, actor): """Integration test: LettaAgentV3.compact uses the LLMConfig implied by CompactionSettings.