feat: add compaction_settings to agents (#6625)

* initial commit

* Add database migration for compaction_settings field

This migration adds the compaction_settings column to the agents table
to support customized summarization configuration for each agent.

🐾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta <noreply@letta.com>

* fix

* rename

* update apis

* fix tests

* update web test

---------

Co-authored-by: Letta <noreply@letta.com>
Co-authored-by: Kian Jones <kian@letta.com>
This commit is contained in:
Sarah Wooders
2025-12-11 14:14:04 -08:00
committed by Caren Thomas
parent 4309ecf606
commit 7ea297231a
13 changed files with 324 additions and 37 deletions

View File

@@ -0,0 +1,28 @@
"""add compaction_settings to agents table
Revision ID: d0880aae6cee
Revises: 2e5e90d3cdf8
Create Date: 2025-12-10 16:17:23.595775
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from letta.orm.custom_columns import CompactionSettingsColumn
# revision identifiers, used by Alembic.
revision: str = "d0880aae6cee"
down_revision: Union[str, None] = "2e5e90d3cdf8"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column("agents", sa.Column("compaction_settings", CompactionSettingsColumn(), nullable=True))
def downgrade() -> None:
op.drop_column("agents", "compaction_settings")

View File

@@ -20669,6 +20669,17 @@
"title": "Model Settings", "title": "Model Settings",
"description": "The model settings used by the agent." "description": "The model settings used by the agent."
}, },
"compaction_settings": {
"anyOf": [
{
"$ref": "#/components/schemas/CompactionSettings"
},
{
"type": "null"
}
],
"description": "The compaction settings configuration used for compaction."
},
"response_format": { "response_format": {
"anyOf": [ "anyOf": [
{ {
@@ -24254,6 +24265,53 @@
"required": ["code"], "required": ["code"],
"title": "CodeInput" "title": "CodeInput"
}, },
"CompactionSettings": {
"properties": {
"model_settings": {
"$ref": "#/components/schemas/ModelSettings",
"description": "The model settings to use for summarization."
},
"prompt": {
"type": "string",
"title": "Prompt",
"description": "The prompt to use for summarization."
},
"prompt_acknowledgement": {
"type": "string",
"title": "Prompt Acknowledgement",
"description": "Whether to include an acknowledgement post-prompt (helps prevent non-summary outputs)."
},
"clip_chars": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Clip Chars",
"description": "The maximum length of the summary in characters. If none, no clipping is performed.",
"default": 2000
},
"mode": {
"type": "string",
"enum": ["all", "sliding_window"],
"title": "Mode",
"description": "The type of summarization technique use.",
"default": "sliding_window"
},
"sliding_window_percentage": {
"type": "number",
"title": "Sliding Window Percentage",
"description": "The percentage of the context window to keep post-summarization (only used in sliding window mode).",
"default": 0.3
}
},
"type": "object",
"required": ["model_settings", "prompt", "prompt_acknowledgement"],
"title": "CompactionSettings"
},
"ComparisonOperator": { "ComparisonOperator": {
"type": "string", "type": "string",
"enum": ["eq", "gte", "lte"], "enum": ["eq", "gte", "lte"],
@@ -25051,6 +25109,17 @@
"title": "Model Settings", "title": "Model Settings",
"description": "The model settings for the agent." "description": "The model settings for the agent."
}, },
"compaction_settings": {
"anyOf": [
{
"$ref": "#/components/schemas/CompactionSettings"
},
{
"type": "null"
}
],
"description": "The compaction settings configuration used for compaction."
},
"context_window_limit": { "context_window_limit": {
"anyOf": [ "anyOf": [
{ {
@@ -29267,6 +29336,17 @@
"title": "Model Settings", "title": "Model Settings",
"description": "The model settings for the agent." "description": "The model settings for the agent."
}, },
"compaction_settings": {
"anyOf": [
{
"$ref": "#/components/schemas/CompactionSettings"
},
{
"type": "null"
}
],
"description": "The compaction settings configuration used for compaction."
},
"context_window_limit": { "context_window_limit": {
"anyOf": [ "anyOf": [
{ {
@@ -32637,6 +32717,25 @@
], ],
"title": "Model" "title": "Model"
}, },
"ModelSettings": {
"properties": {
"max_output_tokens": {
"type": "integer",
"title": "Max Output Tokens",
"description": "The maximum number of tokens the model can generate.",
"default": 4096
},
"parallel_tool_calls": {
"type": "boolean",
"title": "Parallel Tool Calls",
"description": "Whether to enable parallel tool calling.",
"default": false
}
},
"type": "object",
"title": "ModelSettings",
"description": "Schema for defining settings for a model"
},
"ModifyApprovalRequest": { "ModifyApprovalRequest": {
"properties": { "properties": {
"requires_approval": { "requires_approval": {
@@ -38498,6 +38597,17 @@
"title": "Model Settings", "title": "Model Settings",
"description": "The model settings for the agent." "description": "The model settings for the agent."
}, },
"compaction_settings": {
"anyOf": [
{
"$ref": "#/components/schemas/CompactionSettings"
},
{
"type": "null"
}
],
"description": "The compaction settings configuration used for compaction."
},
"context_window_limit": { "context_window_limit": {
"anyOf": [ "anyOf": [
{ {
@@ -39713,6 +39823,17 @@
"title": "Model Settings", "title": "Model Settings",
"description": "The model settings for the agent." "description": "The model settings for the agent."
}, },
"compaction_settings": {
"anyOf": [
{
"$ref": "#/components/schemas/CompactionSettings"
},
{
"type": "null"
}
],
"description": "The compaction settings configuration used for compaction."
},
"context_window_limit": { "context_window_limit": {
"anyOf": [ "anyOf": [
{ {

View File

@@ -48,7 +48,7 @@ from letta.server.rest_api.utils import (
) )
from letta.services.helpers.tool_parser_helper import runtime_override_tool_json_schema from letta.services.helpers.tool_parser_helper import runtime_override_tool_json_schema
from letta.services.summarizer.summarizer_all import summarize_all from letta.services.summarizer.summarizer_all import summarize_all
from letta.services.summarizer.summarizer_config import SummarizerConfig, get_default_summarizer_config from letta.services.summarizer.summarizer_config import CompactionSettings, get_default_compaction_settings
from letta.services.summarizer.summarizer_sliding_window import ( from letta.services.summarizer.summarizer_sliding_window import (
count_tokens, count_tokens,
summarize_via_sliding_window, summarize_via_sliding_window,
@@ -1318,10 +1318,10 @@ class LettaAgentV3(LettaAgentV2):
Simplified compaction method. Does NOT do any persistence (handled in the loop) Simplified compaction method. Does NOT do any persistence (handled in the loop)
""" """
# compact the current in-context messages (self.in_context_messages) # compact the current in-context messages (self.in_context_messages)
# Use agent's summarizer_config if set, otherwise fall back to defaults # Use agent's compaction_settings if set, otherwise fall back to defaults
# TODO: add this back summarizer_config = self.agent_state.compaction_settings or get_default_compaction_settings(
# summarizer_config = self.agent_state.summarizer_config or get_default_summarizer_config(self.agent_state.llm_config) self.agent_state.llm_config._to_model_settings()
summarizer_config = get_default_summarizer_config(self.agent_state.llm_config._to_model_settings()) )
summarization_mode_used = summarizer_config.mode summarization_mode_used = summarizer_config.mode
if summarizer_config.mode == "all": if summarizer_config.mode == "all":
summary, compacted_messages = await summarize_all( summary, compacted_messages = await summarize_all(

View File

@@ -88,6 +88,32 @@ def deserialize_embedding_config(data: Optional[Dict]) -> Optional[EmbeddingConf
return EmbeddingConfig(**data) if data else None return EmbeddingConfig(**data) if data else None
# --------------------------
# CompactionSettings Serialization
# --------------------------
def serialize_compaction_settings(config: Union[Optional["CompactionSettings"], Dict]) -> Optional[Dict]:
"""Convert a CompactionSettings object into a JSON-serializable dictionary."""
if config:
# Import here to avoid circular dependency
from letta.services.summarizer.summarizer_config import CompactionSettings
if isinstance(config, CompactionSettings):
return config.model_dump(mode="json")
return config
def deserialize_compaction_settings(data: Optional[Dict]) -> Optional["CompactionSettings"]:
"""Convert a dictionary back into a CompactionSettings object."""
if data:
# Import here to avoid circular dependency
from letta.services.summarizer.summarizer_config import CompactionSettings
return CompactionSettings(**data)
return None
# -------------------------- # --------------------------
# ToolRule Serialization # ToolRule Serialization
# -------------------------- # --------------------------

View File

@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from letta.orm.block import Block from letta.orm.block import Block
from letta.orm.custom_columns import EmbeddingConfigColumn, LLMConfigColumn, ResponseFormatColumn, ToolRulesColumn from letta.orm.custom_columns import CompactionSettingsColumn, EmbeddingConfigColumn, LLMConfigColumn, ResponseFormatColumn, ToolRulesColumn
from letta.orm.identity import Identity from letta.orm.identity import Identity
from letta.orm.mixins import OrganizationMixin, ProjectMixin, TemplateEntityMixin, TemplateMixin from letta.orm.mixins import OrganizationMixin, ProjectMixin, TemplateEntityMixin, TemplateMixin
from letta.orm.organization import Organization from letta.orm.organization import Organization
@@ -32,6 +32,7 @@ if TYPE_CHECKING:
from letta.orm.run import Run from letta.orm.run import Run
from letta.orm.source import Source from letta.orm.source import Source
from letta.orm.tool import Tool from letta.orm.tool import Tool
from letta.services.summarizer.summarizer_config import CompactionSettings
class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin, TemplateMixin, AsyncAttrs): class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin, TemplateMixin, AsyncAttrs):
@@ -74,6 +75,9 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
embedding_config: Mapped[Optional[EmbeddingConfig]] = mapped_column( embedding_config: Mapped[Optional[EmbeddingConfig]] = mapped_column(
EmbeddingConfigColumn, doc="the embedding configuration object for this agent." EmbeddingConfigColumn, doc="the embedding configuration object for this agent."
) )
compaction_settings: Mapped[Optional[dict]] = mapped_column(
CompactionSettingsColumn, nullable=True, doc="the compaction settings configuration object for compaction."
)
# Tool rules # Tool rules
tool_rules: Mapped[Optional[List[ToolRule]]] = mapped_column(ToolRulesColumn, doc="the tool rules for this agent.") tool_rules: Mapped[Optional[List[ToolRule]]] = mapped_column(ToolRulesColumn, doc="the tool rules for this agent.")
@@ -222,6 +226,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
"metadata": self.metadata_, # Exposed as 'metadata' to Pydantic "metadata": self.metadata_, # Exposed as 'metadata' to Pydantic
"llm_config": self.llm_config, "llm_config": self.llm_config,
"embedding_config": self.embedding_config, "embedding_config": self.embedding_config,
"compaction_settings": self.compaction_settings,
"project_id": self.project_id, "project_id": self.project_id,
"template_id": self.template_id, "template_id": self.template_id,
"base_template_id": self.base_template_id, "base_template_id": self.base_template_id,
@@ -328,6 +333,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
"metadata": self.metadata_, # Exposed as 'metadata' to Pydantic "metadata": self.metadata_, # Exposed as 'metadata' to Pydantic
"llm_config": self.llm_config, "llm_config": self.llm_config,
"embedding_config": self.embedding_config, "embedding_config": self.embedding_config,
"compaction_settings": self.compaction_settings,
"project_id": self.project_id, "project_id": self.project_id,
"template_id": self.template_id, "template_id": self.template_id,
"base_template_id": self.base_template_id, "base_template_id": self.base_template_id,

View File

@@ -5,6 +5,7 @@ from letta.helpers.converters import (
deserialize_agent_step_state, deserialize_agent_step_state,
deserialize_approvals, deserialize_approvals,
deserialize_batch_request_result, deserialize_batch_request_result,
deserialize_compaction_settings,
deserialize_create_batch_response, deserialize_create_batch_response,
deserialize_embedding_config, deserialize_embedding_config,
deserialize_llm_config, deserialize_llm_config,
@@ -19,6 +20,7 @@ from letta.helpers.converters import (
serialize_agent_step_state, serialize_agent_step_state,
serialize_approvals, serialize_approvals,
serialize_batch_request_result, serialize_batch_request_result,
serialize_compaction_settings,
serialize_create_batch_response, serialize_create_batch_response,
serialize_embedding_config, serialize_embedding_config,
serialize_llm_config, serialize_llm_config,
@@ -59,6 +61,19 @@ class EmbeddingConfigColumn(TypeDecorator):
return deserialize_embedding_config(value) return deserialize_embedding_config(value)
class CompactionSettingsColumn(TypeDecorator):
"""Custom SQLAlchemy column type for storing CompactionSettings as JSON."""
impl = JSON
cache_ok = True
def process_bind_param(self, value, dialect):
return serialize_compaction_settings(value)
def process_result_value(self, value, dialect):
return deserialize_compaction_settings(value)
class ToolRulesColumn(TypeDecorator): class ToolRulesColumn(TypeDecorator):
"""Custom SQLAlchemy column type for storing a list of ToolRules as JSON.""" """Custom SQLAlchemy column type for storing a list of ToolRules as JSON."""

View File

@@ -24,7 +24,7 @@ from letta.schemas.response_format import ResponseFormatUnion
from letta.schemas.source import Source from letta.schemas.source import Source
from letta.schemas.tool import Tool from letta.schemas.tool import Tool
from letta.schemas.tool_rule import ToolRule from letta.schemas.tool_rule import ToolRule
from letta.services.summarizer.summarizer_config import SummarizerConfig from letta.services.summarizer.summarizer_config import CompactionSettings
from letta.utils import calculate_file_defaults_based_on_context_window, create_random_username from letta.utils import calculate_file_defaults_based_on_context_window, create_random_username
@@ -87,9 +87,9 @@ class AgentState(OrmMetadataBase, validate_assignment=True):
model: Optional[str] = Field(None, description="The model handle used by the agent (format: provider/model-name).") model: Optional[str] = Field(None, description="The model handle used by the agent (format: provider/model-name).")
embedding: Optional[str] = Field(None, description="The embedding model handle used by the agent (format: provider/model-name).") embedding: Optional[str] = Field(None, description="The embedding model handle used by the agent (format: provider/model-name).")
model_settings: Optional[ModelSettingsUnion] = Field(None, description="The model settings used by the agent.") model_settings: Optional[ModelSettingsUnion] = Field(None, description="The model settings used by the agent.")
compaction_settings: Optional[CompactionSettings] = Field(
# TODO: add this back None, description="The compaction settings configuration used for compaction."
# summarizer_config: Optional[SummarizerConfig] = Field(None, description="The summarizer configuration used by the agent.") )
response_format: Optional[ResponseFormatUnion] = Field( response_format: Optional[ResponseFormatUnion] = Field(
None, None,
@@ -245,9 +245,9 @@ class CreateAgent(BaseModel, validate_assignment=True): #
) )
embedding: Optional[str] = Field(None, description="The embedding model handle used by the agent (format: provider/model-name).") embedding: Optional[str] = Field(None, description="The embedding model handle used by the agent (format: provider/model-name).")
model_settings: Optional[ModelSettingsUnion] = Field(None, description="The model settings for the agent.") model_settings: Optional[ModelSettingsUnion] = Field(None, description="The model settings for the agent.")
compaction_settings: Optional[CompactionSettings] = Field(
# TODO: add this back None, description="The compaction settings configuration used for compaction."
# summarizer_config: Optional[SummarizerConfig] = Field(None, description="The summarizer configuration used by the agent.") )
context_window_limit: Optional[int] = Field(None, description="The context window limit used by the agent.") context_window_limit: Optional[int] = Field(None, description="The context window limit used by the agent.")
embedding_chunk_size: Optional[int] = Field( embedding_chunk_size: Optional[int] = Field(
@@ -441,9 +441,9 @@ class UpdateAgent(BaseModel):
) )
embedding: Optional[str] = Field(None, description="The embedding model handle used by the agent (format: provider/model-name).") embedding: Optional[str] = Field(None, description="The embedding model handle used by the agent (format: provider/model-name).")
model_settings: Optional[ModelSettingsUnion] = Field(None, description="The model settings for the agent.") model_settings: Optional[ModelSettingsUnion] = Field(None, description="The model settings for the agent.")
compaction_settings: Optional[CompactionSettings] = Field(
# TODO: add this back None, description="The compaction settings configuration used for compaction."
# summarizer_config: Optional[SummarizerConfig] = Field(None, description="The summarizer configuration used by the agent.") )
context_window_limit: Optional[int] = Field(None, description="The context window limit used by the agent.") context_window_limit: Optional[int] = Field(None, description="The context window limit used by the agent.")
reasoning: Optional[bool] = Field( reasoning: Optional[bool] = Field(

View File

@@ -491,6 +491,7 @@ class AgentManager:
agent_type=agent_create.agent_type, agent_type=agent_create.agent_type,
llm_config=agent_create.llm_config, llm_config=agent_create.llm_config,
embedding_config=agent_create.embedding_config, embedding_config=agent_create.embedding_config,
compaction_settings=agent_create.compaction_settings,
organization_id=actor.organization_id, organization_id=actor.organization_id,
description=agent_create.description, description=agent_create.description,
metadata_=agent_create.metadata, metadata_=agent_create.metadata,
@@ -750,6 +751,7 @@ class AgentManager:
"system": agent_update.system, "system": agent_update.system,
"llm_config": agent_update.llm_config, "llm_config": agent_update.llm_config,
"embedding_config": agent_update.embedding_config, "embedding_config": agent_update.embedding_config,
"compaction_settings": agent_update.compaction_settings,
"message_ids": agent_update.message_ids, "message_ids": agent_update.message_ids,
"tool_rules": agent_update.tool_rules, "tool_rules": agent_update.tool_rules,
"description": agent_update.description, "description": agent_update.description,

View File

@@ -6,7 +6,7 @@ from letta.schemas.llm_config import LLMConfig
from letta.schemas.message import Message, MessageRole from letta.schemas.message import Message, MessageRole
from letta.schemas.user import User from letta.schemas.user import User
from letta.services.summarizer.summarizer import simple_summary from letta.services.summarizer.summarizer import simple_summary
from letta.services.summarizer.summarizer_config import SummarizerConfig from letta.services.summarizer.summarizer_config import CompactionSettings
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -18,7 +18,7 @@ async def summarize_all(
# LLM config for the summarizer model # LLM config for the summarizer model
llm_config: LLMConfig, llm_config: LLMConfig,
# Actual summarization configuration # Actual summarization configuration
summarizer_config: SummarizerConfig, summarizer_config: CompactionSettings,
in_context_messages: List[Message], in_context_messages: List[Message],
# new_messages: List[Message], # new_messages: List[Message],
) -> str: ) -> str:

View File

@@ -6,7 +6,7 @@ from letta.schemas.llm_config import LLMConfig
from letta.schemas.model import ModelSettings from letta.schemas.model import ModelSettings
class SummarizerConfig(BaseModel): class CompactionSettings(BaseModel):
# summarizer_model: LLMConfig = Field(default=..., description="The model to use for summarization.") # summarizer_model: LLMConfig = Field(default=..., description="The model to use for summarization.")
model_settings: ModelSettings = Field(default=..., description="The model settings to use for summarization.") model_settings: ModelSettings = Field(default=..., description="The model settings to use for summarization.")
prompt: str = Field(default=..., description="The prompt to use for summarization.") prompt: str = Field(default=..., description="The prompt to use for summarization.")
@@ -23,20 +23,20 @@ class SummarizerConfig(BaseModel):
) )
def get_default_summarizer_config(model_settings: ModelSettings) -> SummarizerConfig: def get_default_compaction_settings(model_settings: ModelSettings) -> CompactionSettings:
"""Build a default SummarizerConfig from global settings for backward compatibility. """Build a default CompactionSettings from global settings for backward compatibility.
Args: Args:
llm_config: The LLMConfig to use for the summarizer model (typically the agent's llm_config). llm_config: The LLMConfig to use for the summarizer model (typically the agent's llm_config).
Returns: Returns:
A SummarizerConfig with default values from global settings. A CompactionSettings with default values from global settings.
""" """
from letta.constants import MESSAGE_SUMMARY_REQUEST_ACK from letta.constants import MESSAGE_SUMMARY_REQUEST_ACK
from letta.prompts import gpt_summarize from letta.prompts import gpt_summarize
from letta.settings import summarizer_settings from letta.settings import summarizer_settings
return SummarizerConfig( return CompactionSettings(
mode="sliding_window", mode="sliding_window",
model_settings=model_settings, model_settings=model_settings,
prompt=gpt_summarize.SYSTEM, prompt=gpt_summarize.SYSTEM,

View File

@@ -12,7 +12,7 @@ from letta.schemas.user import User
from letta.services.context_window_calculator.token_counter import create_token_counter from letta.services.context_window_calculator.token_counter import create_token_counter
from letta.services.message_manager import MessageManager from letta.services.message_manager import MessageManager
from letta.services.summarizer.summarizer import simple_summary from letta.services.summarizer.summarizer import simple_summary
from letta.services.summarizer.summarizer_config import SummarizerConfig from letta.services.summarizer.summarizer_config import CompactionSettings
from letta.system import package_summarize_message_no_counts from letta.system import package_summarize_message_no_counts
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -48,7 +48,7 @@ async def summarize_via_sliding_window(
actor: User, actor: User,
# Actual summarization configuration # Actual summarization configuration
llm_config: LLMConfig, llm_config: LLMConfig,
summarizer_config: SummarizerConfig, summarizer_config: CompactionSettings,
in_context_messages: List[Message], in_context_messages: List[Message],
# new_messages: List[Message], # new_messages: List[Message],
) -> Tuple[str, List[Message]]: ) -> Tuple[str, List[Message]]:

View File

@@ -618,12 +618,12 @@ async def test_summarize_multiple_large_tool_calls(server: SyncServer, actor, ll
# #
# ====================================================================================================================== # ======================================================================================================================
# SummarizerConfig Mode Tests (with pytest.patch) - Using LettaAgentV3 # CompactionSettings Mode Tests (with pytest.patch) - Using LettaAgentV3
# ====================================================================================================================== # ======================================================================================================================
from unittest.mock import patch from unittest.mock import patch
from letta.services.summarizer.summarizer_config import SummarizerConfig, get_default_summarizer_config from letta.services.summarizer.summarizer_config import CompactionSettings, get_default_compaction_settings
# Test both summarizer modes: "all" summarizes entire history, "sliding_window" keeps recent messages # Test both summarizer modes: "all" summarizes entire history, "sliding_window" keeps recent messages
SUMMARIZER_CONFIG_MODES: list[Literal["all", "sliding_window"]] = ["all", "sliding_window"] SUMMARIZER_CONFIG_MODES: list[Literal["all", "sliding_window"]] = ["all", "sliding_window"]
@@ -634,7 +634,7 @@ SUMMARIZER_CONFIG_MODES: list[Literal["all", "sliding_window"]] = ["all", "slidi
@pytest.mark.parametrize("llm_config", TESTED_LLM_CONFIGS, ids=[c.model for c in TESTED_LLM_CONFIGS]) @pytest.mark.parametrize("llm_config", TESTED_LLM_CONFIGS, ids=[c.model for c in TESTED_LLM_CONFIGS])
async def test_summarize_with_mode(server: SyncServer, actor, llm_config: LLMConfig, mode: Literal["all", "sliding_window"]): async def test_summarize_with_mode(server: SyncServer, actor, llm_config: LLMConfig, mode: Literal["all", "sliding_window"]):
""" """
Test summarization with different SummarizerConfig modes using LettaAgentV3. Test summarization with different CompactionSettings modes using LettaAgentV3.
This test verifies that both summarization modes work correctly: This test verifies that both summarization modes work correctly:
- "all": Summarizes the entire conversation history into a single summary - "all": Summarizes the entire conversation history into a single summary
@@ -674,11 +674,11 @@ async def test_summarize_with_mode(server: SyncServer, actor, llm_config: LLMCon
# Persist the new messages # Persist the new messages
new_letta_messages = await server.message_manager.create_many_messages_async(new_letta_messages, actor=actor) new_letta_messages = await server.message_manager.create_many_messages_async(new_letta_messages, actor=actor)
# Create a custom SummarizerConfig with the desired mode # Create a custom CompactionSettings with the desired mode
def mock_get_default_summarizer_config(model_settings): def mock_get_default_compaction_settings(model_settings):
config = get_default_summarizer_config(model_settings) config = get_default_compaction_settings(model_settings)
# Override the mode # Override the mode
return SummarizerConfig( return CompactionSettings(
model_settings=config.model_settings, model_settings=config.model_settings,
prompt=config.prompt, prompt=config.prompt,
prompt_acknowledgement=config.prompt_acknowledgement, prompt_acknowledgement=config.prompt_acknowledgement,
@@ -687,7 +687,7 @@ async def test_summarize_with_mode(server: SyncServer, actor, llm_config: LLMCon
sliding_window_percentage=config.sliding_window_percentage, sliding_window_percentage=config.sliding_window_percentage,
) )
with patch("letta.agents.letta_agent_v3.get_default_summarizer_config", mock_get_default_summarizer_config): with patch("letta.agents.letta_agent_v3.get_default_compaction_settings", mock_get_default_compaction_settings):
agent_loop = LettaAgentV3(agent_state=agent_state, actor=actor) agent_loop = LettaAgentV3(agent_state=agent_state, actor=actor)
summary, result = await agent_loop.compact(messages=in_context_messages) summary, result = await agent_loop.compact(messages=in_context_messages)
@@ -848,13 +848,13 @@ async def test_sliding_window_cutoff_index_does_not_exceed_message_count(server:
the sliding window logic works with actual token counting. the sliding window logic works with actual token counting.
""" """
from letta.schemas.model import ModelSettings from letta.schemas.model import ModelSettings
from letta.services.summarizer.summarizer_config import get_default_summarizer_config from letta.services.summarizer.summarizer_config import get_default_compaction_settings
from letta.services.summarizer.summarizer_sliding_window import summarize_via_sliding_window from letta.services.summarizer.summarizer_sliding_window import summarize_via_sliding_window
# Create a real summarizer config using the default factory # Create a real summarizer config using the default factory
# Override sliding_window_percentage to 0.3 for this test # Override sliding_window_percentage to 0.3 for this test
model_settings = ModelSettings() # Use defaults model_settings = ModelSettings() # Use defaults
summarizer_config = get_default_summarizer_config(model_settings) summarizer_config = get_default_compaction_settings(model_settings)
summarizer_config.sliding_window_percentage = 0.3 summarizer_config.sliding_window_percentage = 0.3
# Create 65 messages (similar to the failing case in the bug report) # Create 65 messages (similar to the failing case in the bug report)
@@ -1401,11 +1401,11 @@ async def test_summarize_all(server: SyncServer, actor, llm_config: LLMConfig):
""" """
from letta.schemas.model import ModelSettings from letta.schemas.model import ModelSettings
from letta.services.summarizer.summarizer_all import summarize_all from letta.services.summarizer.summarizer_all import summarize_all
from letta.services.summarizer.summarizer_config import get_default_summarizer_config from letta.services.summarizer.summarizer_config import get_default_compaction_settings
# Create a summarizer config with "all" mode # Create a summarizer config with "all" mode
model_settings = ModelSettings() model_settings = ModelSettings()
summarizer_config = get_default_summarizer_config(model_settings) summarizer_config = get_default_compaction_settings(model_settings)
summarizer_config.mode = "all" summarizer_config.mode = "all"
# Create test messages - a simple conversation # Create test messages - a simple conversation

View File

@@ -81,6 +81,7 @@ from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
from letta.schemas.llm_batch_job import AgentStepState, LLMBatchItem from letta.schemas.llm_batch_job import AgentStepState, LLMBatchItem
from letta.schemas.llm_config import LLMConfig from letta.schemas.llm_config import LLMConfig
from letta.schemas.message import Message as PydanticMessage, MessageCreate, MessageUpdate from letta.schemas.message import Message as PydanticMessage, MessageCreate, MessageUpdate
from letta.schemas.model import ModelSettings
from letta.schemas.openai.chat_completion_response import UsageStatistics from letta.schemas.openai.chat_completion_response import UsageStatistics
from letta.schemas.organization import Organization, Organization as PydanticOrganization, OrganizationUpdate from letta.schemas.organization import Organization, Organization as PydanticOrganization, OrganizationUpdate
from letta.schemas.passage import Passage as PydanticPassage from letta.schemas.passage import Passage as PydanticPassage
@@ -96,6 +97,7 @@ from letta.server.server import SyncServer
from letta.services.block_manager import BlockManager from letta.services.block_manager import BlockManager
from letta.services.helpers.agent_manager_helper import calculate_base_tools, calculate_multi_agent_tools, validate_agent_exists_async from letta.services.helpers.agent_manager_helper import calculate_base_tools, calculate_multi_agent_tools, validate_agent_exists_async
from letta.services.step_manager import FeedbackType from letta.services.step_manager import FeedbackType
from letta.services.summarizer.summarizer_config import CompactionSettings
from letta.settings import settings, tool_settings from letta.settings import settings, tool_settings
from letta.utils import calculate_file_defaults_based_on_context_window from letta.utils import calculate_file_defaults_based_on_context_window
from tests.helpers.utils import comprehensive_agent_checks, validate_context_window_overview from tests.helpers.utils import comprehensive_agent_checks, validate_context_window_overview
@@ -526,6 +528,91 @@ async def test_update_agent(server: SyncServer, comprehensive_test_agent_fixture
assert updated_agent.updated_at > last_updated_timestamp assert updated_agent.updated_at > last_updated_timestamp
@pytest.mark.asyncio
async def test_create_agent_with_compaction_settings(server: SyncServer, default_user, default_block):
"""Test that agents can be created with custom compaction_settings"""
# Upsert base tools
await server.tool_manager.upsert_base_tools_async(actor=default_user)
# Create custom compaction settings
llm_config = LLMConfig.default_config("gpt-4o-mini")
model_settings = llm_config._to_model_settings()
compaction_settings = CompactionSettings(
model_settings=model_settings,
prompt="Custom summarization prompt",
prompt_acknowledgement="Acknowledged",
clip_chars=1500,
mode="all",
sliding_window_percentage=0.5,
)
# Create agent with compaction settings
create_agent_request = CreateAgent(
name="test_compaction_agent",
agent_type="memgpt_v2_agent",
system="test system",
llm_config=llm_config,
embedding_config=EmbeddingConfig.default_config(provider="openai"),
block_ids=[default_block.id],
include_base_tools=True,
compaction_settings=compaction_settings,
)
created_agent = await server.agent_manager.create_agent_async(
create_agent_request,
actor=default_user,
)
# Verify compaction settings were stored correctly
assert created_agent.compaction_settings is not None
assert created_agent.compaction_settings.mode == "all"
assert created_agent.compaction_settings.clip_chars == 1500
assert created_agent.compaction_settings.sliding_window_percentage == 0.5
assert created_agent.compaction_settings.prompt == "Custom summarization prompt"
assert created_agent.compaction_settings.prompt_acknowledgement == "Acknowledged"
# Clean up
await server.agent_manager.delete_agent_async(agent_id=created_agent.id, actor=default_user)
@pytest.mark.asyncio
async def test_update_agent_compaction_settings(server: SyncServer, comprehensive_test_agent_fixture, default_user):
"""Test that an agent's compaction_settings can be updated"""
agent, _ = comprehensive_test_agent_fixture
# Verify initial state (should be None or default)
assert agent.compaction_settings is None
# Create new compaction settings
llm_config = LLMConfig.default_config("gpt-4o-mini")
model_settings = llm_config._to_model_settings()
new_compaction_settings = CompactionSettings(
model_settings=model_settings,
prompt="Updated summarization prompt",
prompt_acknowledgement="Updated acknowledgement",
clip_chars=3000,
mode="sliding_window",
sliding_window_percentage=0.4,
)
# Update agent with compaction settings
update_agent_request = UpdateAgent(
compaction_settings=new_compaction_settings,
)
updated_agent = await server.agent_manager.update_agent_async(agent.id, update_agent_request, actor=default_user)
# Verify compaction settings were updated correctly
assert updated_agent.compaction_settings is not None
assert updated_agent.compaction_settings.mode == "sliding_window"
assert updated_agent.compaction_settings.clip_chars == 3000
assert updated_agent.compaction_settings.sliding_window_percentage == 0.4
assert updated_agent.compaction_settings.prompt == "Updated summarization prompt"
assert updated_agent.compaction_settings.prompt_acknowledgement == "Updated acknowledgement"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_agent_file_defaults_based_on_context_window(server: SyncServer, default_user, default_block): async def test_agent_file_defaults_based_on_context_window(server: SyncServer, default_user, default_block):
"""Test that file-related defaults are set based on the model's context window size""" """Test that file-related defaults are set based on the model's context window size"""
@@ -1282,6 +1369,7 @@ async def test_agent_state_schema_unchanged(server: SyncServer):
from letta.schemas.source import Source from letta.schemas.source import Source
from letta.schemas.tool import Tool from letta.schemas.tool import Tool
from letta.schemas.tool_rule import ToolRule from letta.schemas.tool_rule import ToolRule
from letta.services.summarizer.summarizer_config import CompactionSettings
# Define the expected schema structure # Define the expected schema structure
expected_schema = { expected_schema = {
@@ -1298,6 +1386,7 @@ async def test_agent_state_schema_unchanged(server: SyncServer):
"agent_type": AgentType, "agent_type": AgentType,
# LLM information # LLM information
"llm_config": LLMConfig, "llm_config": LLMConfig,
"compaction_settings": CompactionSettings,
"model": str, "model": str,
"embedding": str, "embedding": str,
"embedding_config": EmbeddingConfig, "embedding_config": EmbeddingConfig,