From 7ea297231a3126906ceb42a550461102356c9abc Mon Sep 17 00:00:00 2001 From: Sarah Wooders Date: Thu, 11 Dec 2025 14:14:04 -0800 Subject: [PATCH] feat: add `compaction_settings` to agents (#6625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * fix * rename * update apis * fix tests * update web test --------- Co-authored-by: Letta Co-authored-by: Kian Jones --- ...add_compaction_settings_to_agents_table.py | 28 ++++ fern/openapi.json | 121 ++++++++++++++++++ letta/agents/letta_agent_v3.py | 10 +- letta/helpers/converters.py | 26 ++++ letta/orm/agent.py | 8 +- letta/orm/custom_columns.py | 15 +++ letta/schemas/agent.py | 20 +-- letta/services/agent_manager.py | 2 + letta/services/summarizer/summarizer_all.py | 4 +- .../services/summarizer/summarizer_config.py | 10 +- .../summarizer/summarizer_sliding_window.py | 4 +- tests/integration_test_summarizer.py | 24 ++-- tests/managers/test_agent_manager.py | 89 +++++++++++++ 13 files changed, 324 insertions(+), 37 deletions(-) create mode 100644 alembic/versions/d0880aae6cee_add_compaction_settings_to_agents_table.py diff --git a/alembic/versions/d0880aae6cee_add_compaction_settings_to_agents_table.py b/alembic/versions/d0880aae6cee_add_compaction_settings_to_agents_table.py new file mode 100644 index 00000000..20846a11 --- /dev/null +++ b/alembic/versions/d0880aae6cee_add_compaction_settings_to_agents_table.py @@ -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") diff --git a/fern/openapi.json b/fern/openapi.json index 20bee654..198517b3 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -20669,6 +20669,17 @@ "title": "Model Settings", "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": { "anyOf": [ { @@ -24254,6 +24265,53 @@ "required": ["code"], "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": { "type": "string", "enum": ["eq", "gte", "lte"], @@ -25051,6 +25109,17 @@ "title": "Model Settings", "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": { "anyOf": [ { @@ -29267,6 +29336,17 @@ "title": "Model Settings", "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": { "anyOf": [ { @@ -32637,6 +32717,25 @@ ], "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": { "properties": { "requires_approval": { @@ -38498,6 +38597,17 @@ "title": "Model Settings", "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": { "anyOf": [ { @@ -39713,6 +39823,17 @@ "title": "Model Settings", "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": { "anyOf": [ { diff --git a/letta/agents/letta_agent_v3.py b/letta/agents/letta_agent_v3.py index 6d43a47f..f20265c0 100644 --- a/letta/agents/letta_agent_v3.py +++ b/letta/agents/letta_agent_v3.py @@ -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.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 ( count_tokens, summarize_via_sliding_window, @@ -1318,10 +1318,10 @@ class LettaAgentV3(LettaAgentV2): Simplified compaction method. Does NOT do any persistence (handled in the loop) """ # compact the current in-context messages (self.in_context_messages) - # Use agent's summarizer_config if set, otherwise fall back to defaults - # TODO: add this back - # summarizer_config = self.agent_state.summarizer_config or get_default_summarizer_config(self.agent_state.llm_config) - summarizer_config = get_default_summarizer_config(self.agent_state.llm_config._to_model_settings()) + # Use agent's compaction_settings if set, otherwise fall back to defaults + summarizer_config = self.agent_state.compaction_settings or get_default_compaction_settings( + self.agent_state.llm_config._to_model_settings() + ) summarization_mode_used = summarizer_config.mode if summarizer_config.mode == "all": summary, compacted_messages = await summarize_all( diff --git a/letta/helpers/converters.py b/letta/helpers/converters.py index 22e18ac6..f9befaa9 100644 --- a/letta/helpers/converters.py +++ b/letta/helpers/converters.py @@ -88,6 +88,32 @@ def deserialize_embedding_config(data: Optional[Dict]) -> Optional[EmbeddingConf 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 # -------------------------- diff --git a/letta/orm/agent.py b/letta/orm/agent.py index 744ffe43..8ff2dab6 100644 --- a/letta/orm/agent.py +++ b/letta/orm/agent.py @@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncAttrs from sqlalchemy.orm import Mapped, mapped_column, relationship 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.mixins import OrganizationMixin, ProjectMixin, TemplateEntityMixin, TemplateMixin from letta.orm.organization import Organization @@ -32,6 +32,7 @@ if TYPE_CHECKING: from letta.orm.run import Run from letta.orm.source import Source from letta.orm.tool import Tool + from letta.services.summarizer.summarizer_config import CompactionSettings 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( 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: 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 "llm_config": self.llm_config, "embedding_config": self.embedding_config, + "compaction_settings": self.compaction_settings, "project_id": self.project_id, "template_id": self.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 "llm_config": self.llm_config, "embedding_config": self.embedding_config, + "compaction_settings": self.compaction_settings, "project_id": self.project_id, "template_id": self.template_id, "base_template_id": self.base_template_id, diff --git a/letta/orm/custom_columns.py b/letta/orm/custom_columns.py index 56cbc7e5..1eecac12 100644 --- a/letta/orm/custom_columns.py +++ b/letta/orm/custom_columns.py @@ -5,6 +5,7 @@ from letta.helpers.converters import ( deserialize_agent_step_state, deserialize_approvals, deserialize_batch_request_result, + deserialize_compaction_settings, deserialize_create_batch_response, deserialize_embedding_config, deserialize_llm_config, @@ -19,6 +20,7 @@ from letta.helpers.converters import ( serialize_agent_step_state, serialize_approvals, serialize_batch_request_result, + serialize_compaction_settings, serialize_create_batch_response, serialize_embedding_config, serialize_llm_config, @@ -59,6 +61,19 @@ class EmbeddingConfigColumn(TypeDecorator): 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): """Custom SQLAlchemy column type for storing a list of ToolRules as JSON.""" diff --git a/letta/schemas/agent.py b/letta/schemas/agent.py index 561d4680..bb96cd9d 100644 --- a/letta/schemas/agent.py +++ b/letta/schemas/agent.py @@ -24,7 +24,7 @@ from letta.schemas.response_format import ResponseFormatUnion from letta.schemas.source import Source from letta.schemas.tool import Tool 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 @@ -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).") 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.") - - # TODO: add this back - # summarizer_config: Optional[SummarizerConfig] = Field(None, description="The summarizer configuration used by the agent.") + compaction_settings: Optional[CompactionSettings] = Field( + None, description="The compaction settings configuration used for compaction." + ) response_format: Optional[ResponseFormatUnion] = Field( 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).") model_settings: Optional[ModelSettingsUnion] = Field(None, description="The model settings for the agent.") - - # TODO: add this back - # summarizer_config: Optional[SummarizerConfig] = Field(None, description="The summarizer configuration used by the agent.") + compaction_settings: Optional[CompactionSettings] = Field( + None, description="The compaction settings configuration used for compaction." + ) context_window_limit: Optional[int] = Field(None, description="The context window limit used by the agent.") 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).") model_settings: Optional[ModelSettingsUnion] = Field(None, description="The model settings for the agent.") - - # TODO: add this back - # summarizer_config: Optional[SummarizerConfig] = Field(None, description="The summarizer configuration used by the agent.") + compaction_settings: Optional[CompactionSettings] = Field( + None, description="The compaction settings configuration used for compaction." + ) context_window_limit: Optional[int] = Field(None, description="The context window limit used by the agent.") reasoning: Optional[bool] = Field( diff --git a/letta/services/agent_manager.py b/letta/services/agent_manager.py index c38919f7..41597141 100644 --- a/letta/services/agent_manager.py +++ b/letta/services/agent_manager.py @@ -491,6 +491,7 @@ class AgentManager: agent_type=agent_create.agent_type, llm_config=agent_create.llm_config, embedding_config=agent_create.embedding_config, + compaction_settings=agent_create.compaction_settings, organization_id=actor.organization_id, description=agent_create.description, metadata_=agent_create.metadata, @@ -750,6 +751,7 @@ class AgentManager: "system": agent_update.system, "llm_config": agent_update.llm_config, "embedding_config": agent_update.embedding_config, + "compaction_settings": agent_update.compaction_settings, "message_ids": agent_update.message_ids, "tool_rules": agent_update.tool_rules, "description": agent_update.description, diff --git a/letta/services/summarizer/summarizer_all.py b/letta/services/summarizer/summarizer_all.py index 16df1b6a..b8d1fa2a 100644 --- a/letta/services/summarizer/summarizer_all.py +++ b/letta/services/summarizer/summarizer_all.py @@ -6,7 +6,7 @@ from letta.schemas.llm_config import LLMConfig from letta.schemas.message import Message, MessageRole from letta.schemas.user import User 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__) @@ -18,7 +18,7 @@ async def summarize_all( # LLM config for the summarizer model llm_config: LLMConfig, # Actual summarization configuration - summarizer_config: SummarizerConfig, + summarizer_config: CompactionSettings, in_context_messages: List[Message], # new_messages: List[Message], ) -> str: diff --git a/letta/services/summarizer/summarizer_config.py b/letta/services/summarizer/summarizer_config.py index 258de9f3..00635902 100644 --- a/letta/services/summarizer/summarizer_config.py +++ b/letta/services/summarizer/summarizer_config.py @@ -6,7 +6,7 @@ from letta.schemas.llm_config import LLMConfig from letta.schemas.model import ModelSettings -class SummarizerConfig(BaseModel): +class CompactionSettings(BaseModel): # 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.") 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: - """Build a default SummarizerConfig from global settings for backward compatibility. +def get_default_compaction_settings(model_settings: ModelSettings) -> CompactionSettings: + """Build a default CompactionSettings from global settings for backward compatibility. Args: llm_config: The LLMConfig to use for the summarizer model (typically the agent's llm_config). 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.prompts import gpt_summarize from letta.settings import summarizer_settings - return SummarizerConfig( + return CompactionSettings( mode="sliding_window", model_settings=model_settings, prompt=gpt_summarize.SYSTEM, diff --git a/letta/services/summarizer/summarizer_sliding_window.py b/letta/services/summarizer/summarizer_sliding_window.py index 50abaeb2..8ff85f43 100644 --- a/letta/services/summarizer/summarizer_sliding_window.py +++ b/letta/services/summarizer/summarizer_sliding_window.py @@ -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.message_manager import MessageManager 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 logger = get_logger(__name__) @@ -48,7 +48,7 @@ async def summarize_via_sliding_window( actor: User, # Actual summarization configuration llm_config: LLMConfig, - summarizer_config: SummarizerConfig, + summarizer_config: CompactionSettings, in_context_messages: List[Message], # new_messages: List[Message], ) -> Tuple[str, List[Message]]: diff --git a/tests/integration_test_summarizer.py b/tests/integration_test_summarizer.py index 23f2ab65..65af7c1b 100644 --- a/tests/integration_test_summarizer.py +++ b/tests/integration_test_summarizer.py @@ -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 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 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]) 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: - "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 new_letta_messages = await server.message_manager.create_many_messages_async(new_letta_messages, actor=actor) - # Create a custom SummarizerConfig with the desired mode - def mock_get_default_summarizer_config(model_settings): - config = get_default_summarizer_config(model_settings) + # Create a custom CompactionSettings with the desired mode + def mock_get_default_compaction_settings(model_settings): + config = get_default_compaction_settings(model_settings) # Override the mode - return SummarizerConfig( + return CompactionSettings( model_settings=config.model_settings, prompt=config.prompt, 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, ) - 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) 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. """ 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 # Create a real summarizer config using the default factory # Override sliding_window_percentage to 0.3 for this test 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 # 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.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 model_settings = ModelSettings() - summarizer_config = get_default_summarizer_config(model_settings) + summarizer_config = get_default_compaction_settings(model_settings) summarizer_config.mode = "all" # Create test messages - a simple conversation diff --git a/tests/managers/test_agent_manager.py b/tests/managers/test_agent_manager.py index 733d7e76..1bcb3c7e 100644 --- a/tests/managers/test_agent_manager.py +++ b/tests/managers/test_agent_manager.py @@ -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_config import LLMConfig 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.organization import Organization, Organization as PydanticOrganization, OrganizationUpdate 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.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.summarizer.summarizer_config import CompactionSettings from letta.settings import settings, tool_settings from letta.utils import calculate_file_defaults_based_on_context_window 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 +@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 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""" @@ -1282,6 +1369,7 @@ async def test_agent_state_schema_unchanged(server: SyncServer): from letta.schemas.source import Source from letta.schemas.tool import Tool from letta.schemas.tool_rule import ToolRule + from letta.services.summarizer.summarizer_config import CompactionSettings # Define the expected schema structure expected_schema = { @@ -1298,6 +1386,7 @@ async def test_agent_state_schema_unchanged(server: SyncServer): "agent_type": AgentType, # LLM information "llm_config": LLMConfig, + "compaction_settings": CompactionSettings, "model": str, "embedding": str, "embedding_config": EmbeddingConfig,