diff --git a/letta/services/agent_manager.py b/letta/services/agent_manager.py index d4c99dae..9a6ebc42 100644 --- a/letta/services/agent_manager.py +++ b/letta/services/agent_manager.py @@ -2007,6 +2007,26 @@ class AgentManager: @enforce_types @trace_method async def refresh_file_blocks(self, agent_state: PydanticAgentState, actor: PydanticUser) -> PydanticAgentState: + """ + Refresh the file blocks in an agent's memory with current file content. + + This method synchronizes the agent's in-memory file blocks with the actual + file content from attached sources. It respects the per-file view window + limit to prevent excessive memory usage. + + Args: + agent_state: The current agent state containing memory configuration + actor: The user performing this action (for permission checking) + + Returns: + Updated agent state with refreshed file blocks + + Important: + - File blocks are truncated based on per_file_view_window_char_limit + - None values are filtered out (files that couldn't be loaded) + - This does NOT persist changes to the database, only updates the state object + - Call this before agent interactions if files may have changed externally + """ file_blocks = await self.file_agent_manager.list_files_for_agent( agent_id=agent_state.id, per_file_view_window_char_limit=agent_state.per_file_view_window_char_limit, @@ -2056,6 +2076,28 @@ class AgentManager: @enforce_types @trace_method def append_system_message(self, agent_id: str, content: str, actor: PydanticUser): + """ + Append a system message to an agent's in-context message history. + + This method is typically used during agent initialization to add system prompts, + instructions, or context that should be treated as system-level guidance. + Unlike user messages, system messages directly influence the agent's behavior + and understanding of its role. + + Args: + agent_id: The ID of the agent to append the message to + content: The system message content (e.g., instructions, context, role definition) + actor: The user performing this action (for permission checking) + + Side Effects: + - Creates a new Message object in the database + - Updates the agent's in_context_message_ids list + - The message becomes part of the agent's permanent context window + + Note: + System messages consume tokens in the context window and cannot be + removed without rebuilding the agent's message history. + """ # get the agent agent = self.get_agent_by_id(agent_id=agent_id, actor=actor) @@ -2069,6 +2111,15 @@ class AgentManager: @enforce_types @trace_method async def append_system_message_async(self, agent_id: str, content: str, actor: PydanticUser): + """ + Async version of append_system_message. + + Append a system message to an agent's in-context message history. + See append_system_message for detailed documentation. + + This async version is preferred for high-throughput scenarios or when + called within other async operations to avoid blocking the event loop. + """ # get the agent agent = await self.get_agent_by_id_async(agent_id=agent_id, actor=actor) diff --git a/letta/services/context_window_calculator/context_window_calculator.py b/letta/services/context_window_calculator/context_window_calculator.py index c405d289..f1e4f79b 100644 --- a/letta/services/context_window_calculator/context_window_calculator.py +++ b/letta/services/context_window_calculator/context_window_calculator.py @@ -21,7 +21,26 @@ class ContextWindowCalculator: @staticmethod def extract_system_components(system_message: str) -> Tuple[str, str, str]: - """Extract system prompt, core memory, and external memory summary from system message""" + """ + Extract structured components from a formatted system message. + + Parses the system message to extract three distinct sections marked by XML-style tags: + - base_instructions: The core system prompt and agent instructions + - memory_blocks: The agent's core memory (persistent context) + - memory_metadata: Metadata about external memory systems + + Args: + system_message: A formatted system message containing XML-style section markers + + Returns: + A tuple of (system_prompt, core_memory, external_memory_summary) + Each component will be an empty string if its section is not found + + Note: + This method assumes a specific format with sections delimited by: + , , and tags. + The extraction is position-based and expects sections in this order. + """ base_start = system_message.find("") memory_blocks_start = system_message.find("") metadata_start = system_message.find("") @@ -43,7 +62,26 @@ class ContextWindowCalculator: @staticmethod def extract_summary_memory(messages: List[Any]) -> Tuple[Optional[str], int]: - """Extract summary memory if present and return starting index for real messages""" + """ + Extract summary memory from the message list if present. + + Summary memory is a special message injected at position 1 (after system message) + that contains a condensed summary of previous conversation history. This is used + when the full conversation history doesn't fit in the context window. + + Args: + messages: List of message objects to search for summary memory + + Returns: + A tuple of (summary_text, start_index) where: + - summary_text: The extracted summary content, or None if not found + - start_index: Index where actual conversation messages begin (1 or 2) + + Detection Logic: + Looks for a user message at index 1 containing the phrase + "The following is a summary of the previous" which indicates + it's a summarized conversation history rather than a real user message. + """ if ( len(messages) > 1 and messages[1].role == MessageRole.user diff --git a/letta/services/helpers/agent_manager_helper.py b/letta/services/helpers/agent_manager_helper.py index 1319175b..ad0caef8 100644 --- a/letta/services/helpers/agent_manager_helper.py +++ b/letta/services/helpers/agent_manager_helper.py @@ -156,7 +156,25 @@ def _process_tags(agent: "AgentModel", tags: List[str], replace=True): agent.tags.extend([tag for tag in new_tags if tag.tag not in existing_tags]) -def derive_system_message(agent_type: AgentType, enable_sleeptime: Optional[bool] = None, system: Optional[str] = None): +def derive_system_message(agent_type: AgentType, enable_sleeptime: Optional[bool] = None, system: Optional[str] = None) -> str: + """ + Derive the appropriate system message based on agent type and configuration. + + This function determines which system prompt template to use based on the + agent's type and whether sleeptime functionality is enabled. If a custom + system message is provided, it returns that instead. + + Args: + agent_type: The type of agent (e.g., memgpt_agent, sleeptime_agent, react_agent) + enable_sleeptime: Whether sleeptime tools should be available (affects prompt choice) + system: Optional custom system message to use instead of defaults + + Returns: + The system message string appropriate for the agent configuration + + Raises: + ValueError: If an invalid or unsupported agent type is provided + """ if system is None: # TODO: don't hardcode @@ -204,8 +222,33 @@ def compile_memory_metadata_block( memory_edit_timestamp: datetime, timezone: str, previous_message_count: int = 0, - archival_memory_size: int = 0, + archival_memory_size: Optional[int] = 0, ) -> str: + """ + Generate a memory metadata block for the agent's system prompt. + + This creates a structured metadata section that informs the agent about + the current state of its memory systems, including timing information + and memory counts. This helps the agent understand what information + is available through its tools. + + Args: + memory_edit_timestamp: When memory blocks were last modified + timezone: The timezone to use for formatting timestamps (e.g., 'America/Los_Angeles') + previous_message_count: Number of messages in recall memory (conversation history) + archival_memory_size: Number of items in archival memory (long-term storage) + + Returns: + A formatted string containing the memory metadata block with XML-style tags + + Example Output: + + - The current time is: 2024-01-15 10:30 AM PST + - Memory blocks were last modified: 2024-01-15 09:00 AM PST + - 42 previous messages between you and the user are stored in recall memory (use tools to access them) + - 156 total memories you created are stored in archival memory (use tools to access them) + + """ # Put the timestamp in the local timezone (mimicking get_local_time()) timestamp_str = format_datetime(memory_edit_timestamp, timezone) diff --git a/letta/services/tool_manager.py b/letta/services/tool_manager.py index cc699215..c798af93 100644 --- a/letta/services/tool_manager.py +++ b/letta/services/tool_manager.py @@ -404,7 +404,37 @@ class ToolManager: updated_tool_type: Optional[ToolType] = None, bypass_name_check: bool = False, ) -> PydanticTool: - """Update a tool by its ID with the given ToolUpdate object.""" + """ + Update a tool with complex validation and schema derivation logic. + + This method handles updates differently based on tool type: + - MCP tools: JSON schema is trusted, no Python source derivation + - Python/TypeScript tools: Schema derived from source code if provided + - Name conflicts are checked unless bypassed + + Args: + tool_id: The UUID of the tool to update + tool_update: Partial update data (only changed fields) + actor: User performing the update (for permissions) + updated_tool_type: Optional new tool type (e.g., converting custom to builtin) + bypass_name_check: Skip name conflict validation (use with caution) + + Returns: + Updated tool as Pydantic model + + Raises: + LettaToolNameConflictError: If new name conflicts with existing tool + NoResultFound: If tool doesn't exist or user lacks access + + Side Effects: + - Updates tool in database + - May change tool name if source code is modified + - Recomputes JSON schema from source for non-MCP tools + + Important: + When source_code is provided for Python/TypeScript tools, the name + MUST match the function name in the code, overriding any name in json_schema + """ # First, check if source code update would cause a name conflict update_data = tool_update.model_dump(to_orm=True, exclude_none=True) new_name = None @@ -570,7 +600,38 @@ class ToolManager: @enforce_types @trace_method def upsert_base_tools(self, actor: PydanticUser) -> List[PydanticTool]: - """Add default tools in base.py and multi_agent.py""" + """ + Initialize or update all built-in Letta tools for a user. + + This method scans predefined modules to discover and register all base tools + that ship with Letta. Tools are categorized by type (core, memory, multi-agent, etc.) + and tagged appropriately for filtering. + + Args: + actor: The user to create/update tools for + + Returns: + List of all base tools that were created or updated + + Tool Categories Created: + - LETTA_CORE: Basic conversation tools (send_message) + - LETTA_MEMORY_CORE: Memory management (core_memory_append/replace) + - LETTA_MULTI_AGENT_CORE: Multi-agent communication tools + - LETTA_SLEEPTIME_CORE: Sleeptime agent tools + - LETTA_VOICE_SLEEPTIME_CORE: Voice agent specific tools + - LETTA_BUILTIN: Additional built-in utilities + - LETTA_FILES_CORE: File handling tools + + Side Effects: + - Creates or updates tools in database + - Tools are marked with appropriate type and tags + - Existing custom tools with same names are NOT overwritten + + Note: + This is typically called during user initialization or system upgrade + to ensure all base tools are available. Custom tools take precedence + over base tools with the same name. + """ functions_to_schema = {} for module_name in LETTA_TOOL_MODULE_NAMES: