letta-nightly 0.7.8.dev20250502104219__py3-none-any.whl → 0.7.9.dev20250503104103__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- letta/__init__.py +2 -2
- letta/agents/helpers.py +58 -1
- letta/agents/letta_agent.py +13 -3
- letta/agents/letta_agent_batch.py +33 -17
- letta/agents/voice_agent.py +1 -2
- letta/agents/voice_sleeptime_agent.py +75 -320
- letta/functions/function_sets/multi_agent.py +1 -1
- letta/functions/function_sets/voice.py +20 -32
- letta/functions/helpers.py +7 -7
- letta/helpers/datetime_helpers.py +6 -0
- letta/helpers/message_helper.py +19 -18
- letta/jobs/scheduler.py +233 -49
- letta/llm_api/google_ai_client.py +13 -4
- letta/llm_api/google_vertex_client.py +5 -1
- letta/llm_api/openai.py +10 -2
- letta/llm_api/openai_client.py +14 -2
- letta/orm/message.py +4 -0
- letta/prompts/system/voice_sleeptime.txt +2 -3
- letta/schemas/letta_message.py +1 -0
- letta/schemas/letta_request.py +8 -1
- letta/schemas/letta_response.py +5 -0
- letta/schemas/llm_batch_job.py +6 -4
- letta/schemas/llm_config.py +9 -0
- letta/schemas/message.py +23 -2
- letta/schemas/providers.py +3 -1
- letta/server/rest_api/app.py +15 -7
- letta/server/rest_api/routers/v1/agents.py +3 -0
- letta/server/rest_api/routers/v1/messages.py +46 -1
- letta/server/rest_api/routers/v1/steps.py +1 -1
- letta/server/rest_api/utils.py +25 -6
- letta/server/server.py +11 -3
- letta/services/llm_batch_manager.py +60 -1
- letta/services/message_manager.py +1 -0
- letta/services/summarizer/summarizer.py +42 -36
- letta/settings.py +1 -0
- letta/tracing.py +5 -0
- {letta_nightly-0.7.8.dev20250502104219.dist-info → letta_nightly-0.7.9.dev20250503104103.dist-info}/METADATA +2 -2
- {letta_nightly-0.7.8.dev20250502104219.dist-info → letta_nightly-0.7.9.dev20250503104103.dist-info}/RECORD +41 -41
- {letta_nightly-0.7.8.dev20250502104219.dist-info → letta_nightly-0.7.9.dev20250503104103.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.8.dev20250502104219.dist-info → letta_nightly-0.7.9.dev20250503104103.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.8.dev20250502104219.dist-info → letta_nightly-0.7.9.dev20250503104103.dist-info}/entry_points.txt +0 -0
| @@ -1,332 +1,138 @@ | |
| 1 | 
            -
            import  | 
| 2 | 
            -
            import xml.etree.ElementTree as ET
         | 
| 3 | 
            -
            from typing import AsyncGenerator, Dict, List, Optional, Tuple, Union
         | 
| 1 | 
            +
            from typing import AsyncGenerator, List, Tuple, Union
         | 
| 4 2 |  | 
| 5 | 
            -
            import  | 
| 6 | 
            -
             | 
| 7 | 
            -
            from letta. | 
| 3 | 
            +
            from letta.agents.helpers import _create_letta_response, serialize_message_history
         | 
| 4 | 
            +
            from letta.agents.letta_agent import LettaAgent
         | 
| 5 | 
            +
            from letta.orm.enums import ToolType
         | 
| 8 6 | 
             
            from letta.schemas.agent import AgentState
         | 
| 9 7 | 
             
            from letta.schemas.block import BlockUpdate
         | 
| 10 8 | 
             
            from letta.schemas.enums import MessageStreamStatus
         | 
| 11 9 | 
             
            from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage
         | 
| 12 | 
            -
            from letta.schemas.letta_message_content import TextContent
         | 
| 13 10 | 
             
            from letta.schemas.letta_response import LettaResponse
         | 
| 14 | 
            -
            from letta.schemas.message import  | 
| 15 | 
            -
            from letta.schemas. | 
| 16 | 
            -
            from letta.schemas.usage import LettaUsageStatistics
         | 
| 11 | 
            +
            from letta.schemas.message import MessageCreate
         | 
| 12 | 
            +
            from letta.schemas.tool_rule import ChildToolRule, ContinueToolRule, InitToolRule, TerminalToolRule
         | 
| 17 13 | 
             
            from letta.schemas.user import User
         | 
| 18 | 
            -
            from letta.server.rest_api.utils import convert_in_context_letta_messages_to_openai, create_input_messages
         | 
| 19 14 | 
             
            from letta.services.agent_manager import AgentManager
         | 
| 20 15 | 
             
            from letta.services.block_manager import BlockManager
         | 
| 21 16 | 
             
            from letta.services.message_manager import MessageManager
         | 
| 22 | 
            -
            from letta. | 
| 17 | 
            +
            from letta.services.passage_manager import PassageManager
         | 
| 18 | 
            +
            from letta.services.summarizer.enums import SummarizationMode
         | 
| 19 | 
            +
            from letta.services.summarizer.summarizer import Summarizer
         | 
| 20 | 
            +
            from letta.tracing import trace_method
         | 
| 23 21 |  | 
| 24 22 |  | 
| 25 | 
            -
             | 
| 26 | 
            -
            class VoiceSleeptimeAgent(BaseAgent):
         | 
| 23 | 
            +
            class VoiceSleeptimeAgent(LettaAgent):
         | 
| 27 24 | 
             
                """
         | 
| 28 | 
            -
                A  | 
| 25 | 
            +
                A special variant of the LettaAgent that helps with offline memory computations specifically for voice.
         | 
| 29 26 | 
             
                """
         | 
| 30 27 |  | 
| 31 28 | 
             
                def __init__(
         | 
| 32 29 | 
             
                    self,
         | 
| 33 30 | 
             
                    agent_id: str,
         | 
| 34 31 | 
             
                    convo_agent_state: AgentState,
         | 
| 35 | 
            -
                    openai_client: openai.AsyncClient,
         | 
| 36 32 | 
             
                    message_manager: MessageManager,
         | 
| 37 33 | 
             
                    agent_manager: AgentManager,
         | 
| 38 34 | 
             
                    block_manager: BlockManager,
         | 
| 35 | 
            +
                    passage_manager: PassageManager,
         | 
| 39 36 | 
             
                    target_block_label: str,
         | 
| 40 | 
            -
                    message_transcripts: List[str],
         | 
| 41 37 | 
             
                    actor: User,
         | 
| 42 38 | 
             
                ):
         | 
| 43 39 | 
             
                    super().__init__(
         | 
| 44 40 | 
             
                        agent_id=agent_id,
         | 
| 45 | 
            -
                        openai_client=openai_client,
         | 
| 46 41 | 
             
                        message_manager=message_manager,
         | 
| 47 42 | 
             
                        agent_manager=agent_manager,
         | 
| 43 | 
            +
                        block_manager=block_manager,
         | 
| 44 | 
            +
                        passage_manager=passage_manager,
         | 
| 48 45 | 
             
                        actor=actor,
         | 
| 49 46 | 
             
                    )
         | 
| 50 47 |  | 
| 51 48 | 
             
                    self.convo_agent_state = convo_agent_state
         | 
| 52 | 
            -
                    self.block_manager = block_manager
         | 
| 53 49 | 
             
                    self.target_block_label = target_block_label
         | 
| 54 | 
            -
                    self.message_transcripts =  | 
| 50 | 
            +
                    self.message_transcripts = []
         | 
| 51 | 
            +
                    self.summarizer = Summarizer(
         | 
| 52 | 
            +
                        mode=SummarizationMode.STATIC_MESSAGE_BUFFER,
         | 
| 53 | 
            +
                        summarizer_agent=None,
         | 
| 54 | 
            +
                        message_buffer_limit=20,
         | 
| 55 | 
            +
                        message_buffer_min=10,
         | 
| 56 | 
            +
                    )
         | 
| 55 57 |  | 
| 56 58 | 
             
                def update_message_transcript(self, message_transcripts: List[str]):
         | 
| 57 59 | 
             
                    self.message_transcripts = message_transcripts
         | 
| 58 60 |  | 
| 59 | 
            -
                async def step(self, input_messages: List[MessageCreate], max_steps: int =  | 
| 61 | 
            +
                async def step(self, input_messages: List[MessageCreate], max_steps: int = 20) -> LettaResponse:
         | 
| 60 62 | 
             
                    """
         | 
| 61 63 | 
             
                    Process the user's input message, allowing the model to call memory-related tools
         | 
| 62 64 | 
             
                    until it decides to stop and provide a final response.
         | 
| 63 65 | 
             
                    """
         | 
| 64 | 
            -
                    agent_state = self.agent_manager.get_agent_by_id( | 
| 65 | 
            -
             | 
| 66 | 
            -
                     | 
| 67 | 
            -
             | 
| 68 | 
            -
             | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
| 71 | 
            -
             | 
| 72 | 
            -
                     | 
| 73 | 
            -
             | 
| 74 | 
            -
                    # Process tool calls
         | 
| 75 | 
            -
                    tool_call = assistant_message.tool_calls[0]
         | 
| 76 | 
            -
                    function_name = tool_call.function.name
         | 
| 77 | 
            -
                    function_args = json.loads(tool_call.function.arguments)
         | 
| 78 | 
            -
             | 
| 79 | 
            -
                    if function_name == "store_memories":
         | 
| 80 | 
            -
                        print("Called store_memories")
         | 
| 81 | 
            -
                        print(function_args)
         | 
| 82 | 
            -
                        chunks = function_args.get("chunks", [])
         | 
| 83 | 
            -
                        results = [self.store_memory(agent_state=self.convo_agent_state, **chunk_args) for chunk_args in chunks]
         | 
| 84 | 
            -
             | 
| 85 | 
            -
                        aggregated_result = next((res for res, _ in results if res is not None), None)
         | 
| 86 | 
            -
                        aggregated_success = all(success for _, success in results)
         | 
| 87 | 
            -
             | 
| 88 | 
            -
                    else:
         | 
| 89 | 
            -
                        raise ValueError("Error: Unknown tool function '{function_name}'")
         | 
| 66 | 
            +
                    agent_state = self.agent_manager.get_agent_by_id(self.agent_id, actor=self.actor)
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    # Add tool rules to the agent_state specifically for this type of agent
         | 
| 69 | 
            +
                    agent_state.tool_rules = [
         | 
| 70 | 
            +
                        InitToolRule(tool_name="store_memories"),
         | 
| 71 | 
            +
                        ChildToolRule(tool_name="store_memories", children=["rethink_user_memory"]),
         | 
| 72 | 
            +
                        ContinueToolRule(tool_name="rethink_user_memory"),
         | 
| 73 | 
            +
                        TerminalToolRule(tool_name="finish_rethinking_memory"),
         | 
| 74 | 
            +
                    ]
         | 
| 90 75 |  | 
| 91 | 
            -
                     | 
| 92 | 
            -
             | 
| 93 | 
            -
                         | 
| 94 | 
            -
                        "tool_calls": [
         | 
| 95 | 
            -
                            {
         | 
| 96 | 
            -
                                "id": tool_call.id,
         | 
| 97 | 
            -
                                "type": "function",
         | 
| 98 | 
            -
                                "function": {"name": function_name, "arguments": tool_call.function.arguments},
         | 
| 99 | 
            -
                            }
         | 
| 100 | 
            -
                        ],
         | 
| 101 | 
            -
                    }
         | 
| 102 | 
            -
                    openai_messages.append(assistant_message)
         | 
| 103 | 
            -
                    in_context_messages.append(
         | 
| 104 | 
            -
                        Message.dict_to_message(
         | 
| 105 | 
            -
                            agent_id=self.agent_id,
         | 
| 106 | 
            -
                            openai_message_dict=assistant_message,
         | 
| 107 | 
            -
                            model=agent_state.llm_config.model,
         | 
| 108 | 
            -
                            name=function_name,
         | 
| 109 | 
            -
                        )
         | 
| 76 | 
            +
                    # Summarize
         | 
| 77 | 
            +
                    current_in_context_messages, new_in_context_messages = await super()._step(
         | 
| 78 | 
            +
                        agent_state=agent_state, input_messages=input_messages, max_steps=max_steps
         | 
| 110 79 | 
             
                    )
         | 
| 111 | 
            -
                     | 
| 112 | 
            -
                         | 
| 113 | 
            -
                        "tool_call_id": tool_call.id,
         | 
| 114 | 
            -
                        "content": package_function_response(was_success=aggregated_success, response_string=str(aggregated_result)),
         | 
| 115 | 
            -
                    }
         | 
| 116 | 
            -
                    openai_messages.append(tool_call_message)
         | 
| 117 | 
            -
                    in_context_messages.append(
         | 
| 118 | 
            -
                        Message.dict_to_message(
         | 
| 119 | 
            -
                            agent_id=self.agent_id,
         | 
| 120 | 
            -
                            openai_message_dict=tool_call_message,
         | 
| 121 | 
            -
                            model=agent_state.llm_config.model,
         | 
| 122 | 
            -
                            name=function_name,
         | 
| 123 | 
            -
                            tool_returns=[ToolReturn(status="success" if aggregated_success else "error")],
         | 
| 124 | 
            -
                        )
         | 
| 80 | 
            +
                    new_in_context_messages, updated = self.summarizer.summarize(
         | 
| 81 | 
            +
                        in_context_messages=current_in_context_messages, new_letta_messages=new_in_context_messages
         | 
| 125 82 | 
             
                    )
         | 
| 126 | 
            -
             | 
| 127 | 
            -
             | 
| 128 | 
            -
                    human_block_content = self.agent_manager.get_block_with_label(
         | 
| 129 | 
            -
                        agent_id=self.agent_id, block_label=self.target_block_label, actor=self.actor
         | 
| 83 | 
            +
                    self.agent_manager.set_in_context_messages(
         | 
| 84 | 
            +
                        agent_id=self.agent_id, message_ids=[m.id for m in new_in_context_messages], actor=self.actor
         | 
| 130 85 | 
             
                    )
         | 
| 131 | 
            -
                    rethink_command = f"""
         | 
| 132 | 
            -
                    Here is the current memory block created earlier:
         | 
| 133 | 
            -
             | 
| 134 | 
            -
            ### CURRENT MEMORY
         | 
| 135 | 
            -
            {human_block_content}
         | 
| 136 | 
            -
            ### END CURRENT MEMORY
         | 
| 137 | 
            -
             | 
| 138 | 
            -
            Please refine this block:
         | 
| 139 86 |  | 
| 140 | 
            -
             | 
| 141 | 
            -
            - Organize related information together (e.g., preferences, background, ongoing goals).
         | 
| 142 | 
            -
            - Add any light, supportable inferences that deepen understanding—but do not invent unsupported details.
         | 
| 87 | 
            +
                    return _create_letta_response(new_in_context_messages=new_in_context_messages, use_assistant_message=self.use_assistant_message)
         | 
| 143 88 |  | 
| 144 | 
            -
             | 
| 89 | 
            +
                @trace_method
         | 
| 90 | 
            +
                async def _execute_tool(self, tool_name: str, tool_args: dict, agent_state: AgentState) -> Tuple[str, bool]:
         | 
| 145 91 | 
             
                    """
         | 
| 146 | 
            -
                     | 
| 147 | 
            -
                    openai_messages.append(rethink_command.model_dump())
         | 
| 148 | 
            -
             | 
| 149 | 
            -
                    for _ in range(max_steps):
         | 
| 150 | 
            -
                        request = self._build_openai_request(openai_messages, agent_state, tools=self._build_sleeptime_tools())
         | 
| 151 | 
            -
                        chat_completion = await self.openai_client.chat.completions.create(**request.model_dump(exclude_unset=True))
         | 
| 152 | 
            -
                        assistant_message = chat_completion.choices[0].message
         | 
| 153 | 
            -
             | 
| 154 | 
            -
                        # Process tool calls
         | 
| 155 | 
            -
                        tool_call = assistant_message.tool_calls[0]
         | 
| 156 | 
            -
                        function_name = tool_call.function.name
         | 
| 157 | 
            -
                        function_args = json.loads(tool_call.function.arguments)
         | 
| 158 | 
            -
             | 
| 159 | 
            -
                        if function_name == "rethink_user_memory":
         | 
| 160 | 
            -
                            print("Called rethink_user_memory")
         | 
| 161 | 
            -
                            print(function_args)
         | 
| 162 | 
            -
                            result, success = self.rethink_user_memory(agent_state=agent_state, **function_args)
         | 
| 163 | 
            -
                        elif function_name == "finish_rethinking_memory":
         | 
| 164 | 
            -
                            print("Called finish_rethinking_memory")
         | 
| 165 | 
            -
                            result, success = None, True
         | 
| 166 | 
            -
                            break
         | 
| 167 | 
            -
                        else:
         | 
| 168 | 
            -
                            print(f"Error: Unknown tool function '{function_name}'")
         | 
| 169 | 
            -
                            raise ValueError(f"Error: Unknown tool function '{function_name}'", False)
         | 
| 170 | 
            -
                        assistant_message = {
         | 
| 171 | 
            -
                            "role": "assistant",
         | 
| 172 | 
            -
                            "content": assistant_message.content,
         | 
| 173 | 
            -
                            "tool_calls": [
         | 
| 174 | 
            -
                                {
         | 
| 175 | 
            -
                                    "id": tool_call.id,
         | 
| 176 | 
            -
                                    "type": "function",
         | 
| 177 | 
            -
                                    "function": {"name": function_name, "arguments": tool_call.function.arguments},
         | 
| 178 | 
            -
                                }
         | 
| 179 | 
            -
                            ],
         | 
| 180 | 
            -
                        }
         | 
| 181 | 
            -
                        openai_messages.append(assistant_message)
         | 
| 182 | 
            -
                        in_context_messages.append(
         | 
| 183 | 
            -
                            Message.dict_to_message(
         | 
| 184 | 
            -
                                agent_id=self.agent_id,
         | 
| 185 | 
            -
                                openai_message_dict=assistant_message,
         | 
| 186 | 
            -
                                model=agent_state.llm_config.model,
         | 
| 187 | 
            -
                                name=function_name,
         | 
| 188 | 
            -
                            )
         | 
| 189 | 
            -
                        )
         | 
| 190 | 
            -
                        tool_call_message = {
         | 
| 191 | 
            -
                            "role": "tool",
         | 
| 192 | 
            -
                            "tool_call_id": tool_call.id,
         | 
| 193 | 
            -
                            "content": package_function_response(was_success=success, response_string=str(result)),
         | 
| 194 | 
            -
                        }
         | 
| 195 | 
            -
                        openai_messages.append(tool_call_message)
         | 
| 196 | 
            -
                        in_context_messages.append(
         | 
| 197 | 
            -
                            Message.dict_to_message(
         | 
| 198 | 
            -
                                agent_id=self.agent_id,
         | 
| 199 | 
            -
                                openai_message_dict=tool_call_message,
         | 
| 200 | 
            -
                                model=agent_state.llm_config.model,
         | 
| 201 | 
            -
                                name=function_name,
         | 
| 202 | 
            -
                                tool_returns=[ToolReturn(status="success" if success else "error")],
         | 
| 203 | 
            -
                            )
         | 
| 204 | 
            -
                        )
         | 
| 205 | 
            -
             | 
| 206 | 
            -
                    # Actually save the memory:
         | 
| 207 | 
            -
                    target_block = agent_state.memory.get_block(self.target_block_label)
         | 
| 208 | 
            -
                    self.block_manager.update_block(block_id=target_block.id, block_update=BlockUpdate(value=target_block.value), actor=self.actor)
         | 
| 209 | 
            -
             | 
| 210 | 
            -
                    self.message_manager.create_many_messages(pydantic_msgs=in_context_messages, actor=self.actor)
         | 
| 211 | 
            -
                    return LettaResponse(messages=[msg for m in in_context_messages for msg in m.to_letta_messages()], usage=LettaUsageStatistics())
         | 
| 212 | 
            -
             | 
| 213 | 
            -
                def _format_messages_llm_friendly(self):
         | 
| 214 | 
            -
                    messages = self.message_manager.list_messages_for_agent(agent_id=self.agent_id, actor=self.actor)
         | 
| 215 | 
            -
             | 
| 216 | 
            -
                    llm_friendly_messages = [f"{m.role}: {m.content[0].text}" for m in messages if m.content and isinstance(m.content[0], TextContent)]
         | 
| 217 | 
            -
                    return "\n".join(llm_friendly_messages)
         | 
| 218 | 
            -
             | 
| 219 | 
            -
                def _build_openai_request(self, openai_messages: List[Dict], agent_state: AgentState, tools: List[Tool]) -> ChatCompletionRequest:
         | 
| 220 | 
            -
                    openai_request = ChatCompletionRequest(
         | 
| 221 | 
            -
                        model=agent_state.llm_config.model,  # TODO: Separate config for summarizer?
         | 
| 222 | 
            -
                        messages=openai_messages,
         | 
| 223 | 
            -
                        tools=tools,
         | 
| 224 | 
            -
                        tool_choice="required",
         | 
| 225 | 
            -
                        user=self.actor.id,
         | 
| 226 | 
            -
                        max_completion_tokens=agent_state.llm_config.max_tokens,
         | 
| 227 | 
            -
                        temperature=agent_state.llm_config.temperature,
         | 
| 228 | 
            -
                        stream=False,
         | 
| 229 | 
            -
                    )
         | 
| 230 | 
            -
                    return openai_request
         | 
| 231 | 
            -
             | 
| 232 | 
            -
                def _build_store_memory_tool_schemas(self) -> List[Tool]:
         | 
| 233 | 
            -
                    """
         | 
| 234 | 
            -
                    Build the schemas for the three memory-related tools.
         | 
| 92 | 
            +
                    Executes a tool and returns (result, success_flag).
         | 
| 235 93 | 
             
                    """
         | 
| 236 | 
            -
                     | 
| 237 | 
            -
             | 
| 238 | 
            -
             | 
| 239 | 
            -
             | 
| 240 | 
            -
                                "name": "store_memories",
         | 
| 241 | 
            -
                                "description": "Archive coherent chunks of dialogue that will be evicted, preserving raw lines and a brief contextual description.",
         | 
| 242 | 
            -
                                "parameters": {
         | 
| 243 | 
            -
                                    "type": "object",
         | 
| 244 | 
            -
                                    "properties": {
         | 
| 245 | 
            -
                                        "chunks": {
         | 
| 246 | 
            -
                                            "type": "array",
         | 
| 247 | 
            -
                                            "items": {
         | 
| 248 | 
            -
                                                "type": "object",
         | 
| 249 | 
            -
                                                "properties": {
         | 
| 250 | 
            -
                                                    "start_index": {"type": "integer", "description": "Index of first line in original history."},
         | 
| 251 | 
            -
                                                    "end_index": {"type": "integer", "description": "Index of last line in original history."},
         | 
| 252 | 
            -
                                                    "context": {
         | 
| 253 | 
            -
                                                        "type": "string",
         | 
| 254 | 
            -
                                                        "description": "A high-level description providing context for why this chunk matters.",
         | 
| 255 | 
            -
                                                    },
         | 
| 256 | 
            -
                                                },
         | 
| 257 | 
            -
                                                "required": ["start_index", "end_index", "context"],
         | 
| 258 | 
            -
                                            },
         | 
| 259 | 
            -
                                        }
         | 
| 260 | 
            -
                                    },
         | 
| 261 | 
            -
                                    "required": ["chunks"],
         | 
| 262 | 
            -
                                    "additionalProperties": False,
         | 
| 263 | 
            -
                                },
         | 
| 264 | 
            -
                            },
         | 
| 265 | 
            -
                        ),
         | 
| 266 | 
            -
                    ]
         | 
| 267 | 
            -
             | 
| 268 | 
            -
                    return tools
         | 
| 94 | 
            +
                    # Special memory case
         | 
| 95 | 
            +
                    target_tool = next((x for x in agent_state.tools if x.name == tool_name), None)
         | 
| 96 | 
            +
                    if not target_tool:
         | 
| 97 | 
            +
                        return f"Tool not found: {tool_name}", False
         | 
| 269 98 |  | 
| 270 | 
            -
             | 
| 271 | 
            -
             | 
| 272 | 
            -
             | 
| 273 | 
            -
             | 
| 274 | 
            -
                             | 
| 275 | 
            -
             | 
| 276 | 
            -
             | 
| 277 | 
            -
             | 
| 278 | 
            -
             | 
| 279 | 
            -
             | 
| 280 | 
            -
             | 
| 281 | 
            -
             | 
| 282 | 
            -
             | 
| 283 | 
            -
             | 
| 284 | 
            -
             | 
| 285 | 
            -
             | 
| 286 | 
            -
             | 
| 287 | 
            -
             | 
| 288 | 
            -
                                                "The new memory with information integrated from the memory block. "
         | 
| 289 | 
            -
                                                "If there is no new information, then this should be the same as the "
         | 
| 290 | 
            -
                                                "content in the source block."
         | 
| 291 | 
            -
                                            ),
         | 
| 292 | 
            -
                                        },
         | 
| 293 | 
            -
                                    },
         | 
| 294 | 
            -
                                    "required": ["new_memory"],
         | 
| 295 | 
            -
                                    "additionalProperties": False,
         | 
| 296 | 
            -
                                },
         | 
| 297 | 
            -
                            },
         | 
| 298 | 
            -
                        ),
         | 
| 299 | 
            -
                        Tool(
         | 
| 300 | 
            -
                            type="function",
         | 
| 301 | 
            -
                            function={
         | 
| 302 | 
            -
                                "name": "finish_rethinking_memory",
         | 
| 303 | 
            -
                                "description": ("This function is called when the agent is done rethinking the memory."),
         | 
| 304 | 
            -
                                "parameters": {
         | 
| 305 | 
            -
                                    "type": "object",
         | 
| 306 | 
            -
                                    "properties": {},
         | 
| 307 | 
            -
                                    "required": [],
         | 
| 308 | 
            -
                                    "additionalProperties": False,
         | 
| 309 | 
            -
                                },
         | 
| 310 | 
            -
                            },
         | 
| 311 | 
            -
                        ),
         | 
| 312 | 
            -
                    ]
         | 
| 313 | 
            -
             | 
| 314 | 
            -
                    return tools
         | 
| 99 | 
            +
                    try:
         | 
| 100 | 
            +
                        if target_tool.name == "rethink_user_memory" and target_tool.tool_type == ToolType.LETTA_VOICE_SLEEPTIME_CORE:
         | 
| 101 | 
            +
                            return self.rethink_user_memory(agent_state=agent_state, **tool_args)
         | 
| 102 | 
            +
                        elif target_tool.name == "finish_rethinking_memory" and target_tool.tool_type == ToolType.LETTA_VOICE_SLEEPTIME_CORE:
         | 
| 103 | 
            +
                            return "", True
         | 
| 104 | 
            +
                        elif target_tool.name == "store_memories" and target_tool.tool_type == ToolType.LETTA_VOICE_SLEEPTIME_CORE:
         | 
| 105 | 
            +
                            chunks = tool_args.get("chunks", [])
         | 
| 106 | 
            +
                            results = [self.store_memory(agent_state=self.convo_agent_state, **chunk_args) for chunk_args in chunks]
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                            aggregated_result = next((res for res, _ in results if res is not None), None)
         | 
| 109 | 
            +
                            aggregated_success = all(success for _, success in results)
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                            return aggregated_result, aggregated_success  # Note that here we store to the convo agent's archival memory
         | 
| 112 | 
            +
                        else:
         | 
| 113 | 
            +
                            result = f"Voice sleeptime agent tried invoking invalid tool with type {target_tool.tool_type}: {target_tool}"
         | 
| 114 | 
            +
                            return result, False
         | 
| 115 | 
            +
                    except Exception as e:
         | 
| 116 | 
            +
                        return f"Failed to call tool. Error: {e}", False
         | 
| 315 117 |  | 
| 316 | 
            -
                def rethink_user_memory(self, new_memory: str, agent_state: AgentState) -> Tuple[ | 
| 118 | 
            +
                def rethink_user_memory(self, new_memory: str, agent_state: AgentState) -> Tuple[str, bool]:
         | 
| 317 119 | 
             
                    if agent_state.memory.get_block(self.target_block_label) is None:
         | 
| 318 120 | 
             
                        agent_state.memory.create_block(label=self.target_block_label, value=new_memory)
         | 
| 319 121 |  | 
| 320 122 | 
             
                    agent_state.memory.update_block_value(label=self.target_block_label, value=new_memory)
         | 
| 321 | 
            -
                    return None, True
         | 
| 322 123 |  | 
| 323 | 
            -
             | 
| 124 | 
            +
                    target_block = agent_state.memory.get_block(self.target_block_label)
         | 
| 125 | 
            +
                    self.block_manager.update_block(block_id=target_block.id, block_update=BlockUpdate(value=target_block.value), actor=self.actor)
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                    return "", True
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                def store_memory(self, start_index: int, end_index: int, context: str, agent_state: AgentState) -> Tuple[str, bool]:
         | 
| 324 130 | 
             
                    """
         | 
| 325 131 | 
             
                    Store a memory.
         | 
| 326 132 | 
             
                    """
         | 
| 327 133 | 
             
                    try:
         | 
| 328 134 | 
             
                        messages = self.message_transcripts[start_index : end_index + 1]
         | 
| 329 | 
            -
                        memory =  | 
| 135 | 
            +
                        memory = serialize_message_history(messages, context)
         | 
| 330 136 | 
             
                        self.agent_manager.passage_manager.insert_passage(
         | 
| 331 137 | 
             
                            agent_state=agent_state,
         | 
| 332 138 | 
             
                            agent_id=agent_state.id,
         | 
| @@ -335,63 +141,12 @@ Use `rethink_user_memory(new_memory)` as many times as you need to iteratively i | |
| 335 141 | 
             
                        )
         | 
| 336 142 | 
             
                        self.agent_manager.rebuild_system_prompt(agent_id=agent_state.id, actor=self.actor, force=True)
         | 
| 337 143 |  | 
| 338 | 
            -
                        return  | 
| 144 | 
            +
                        return "", True
         | 
| 339 145 | 
             
                    except Exception as e:
         | 
| 340 146 | 
             
                        return f"Failed to store memory given start_index {start_index} and end_index {end_index}: {e}", False
         | 
| 341 147 |  | 
| 342 | 
            -
                def serialize(self, messages: List[str], context: str) -> str:
         | 
| 343 | 
            -
                    """
         | 
| 344 | 
            -
                    Produce an XML document like:
         | 
| 345 | 
            -
             | 
| 346 | 
            -
                    <memory>
         | 
| 347 | 
            -
                      <messages>
         | 
| 348 | 
            -
                        <message>…</message>
         | 
| 349 | 
            -
                        <message>…</message>
         | 
| 350 | 
            -
                        …
         | 
| 351 | 
            -
                      </messages>
         | 
| 352 | 
            -
                      <context>…</context>
         | 
| 353 | 
            -
                    </memory>
         | 
| 354 | 
            -
                    """
         | 
| 355 | 
            -
                    root = ET.Element("memory")
         | 
| 356 | 
            -
             | 
| 357 | 
            -
                    msgs_el = ET.SubElement(root, "messages")
         | 
| 358 | 
            -
                    for msg in messages:
         | 
| 359 | 
            -
                        m = ET.SubElement(msgs_el, "message")
         | 
| 360 | 
            -
                        m.text = msg
         | 
| 361 | 
            -
             | 
| 362 | 
            -
                    sum_el = ET.SubElement(root, "context")
         | 
| 363 | 
            -
                    sum_el.text = context
         | 
| 364 | 
            -
             | 
| 365 | 
            -
                    # ET.tostring will escape reserved chars for you
         | 
| 366 | 
            -
                    return ET.tostring(root, encoding="unicode")
         | 
| 367 | 
            -
             | 
| 368 | 
            -
                def deserialize(self, xml_str: str) -> Tuple[List[str], str]:
         | 
| 369 | 
            -
                    """
         | 
| 370 | 
            -
                    Parse the XML back into (messages, context). Raises ValueError if tags are missing.
         | 
| 371 | 
            -
                    """
         | 
| 372 | 
            -
                    try:
         | 
| 373 | 
            -
                        root = ET.fromstring(xml_str)
         | 
| 374 | 
            -
                    except ET.ParseError as e:
         | 
| 375 | 
            -
                        raise ValueError(f"Invalid XML: {e}")
         | 
| 376 | 
            -
             | 
| 377 | 
            -
                    msgs_el = root.find("messages")
         | 
| 378 | 
            -
                    if msgs_el is None:
         | 
| 379 | 
            -
                        raise ValueError("Missing <messages> section")
         | 
| 380 | 
            -
             | 
| 381 | 
            -
                    messages = []
         | 
| 382 | 
            -
                    for m in msgs_el.findall("message"):
         | 
| 383 | 
            -
                        # .text may be None if empty, so coerce to empty string
         | 
| 384 | 
            -
                        messages.append(m.text or "")
         | 
| 385 | 
            -
             | 
| 386 | 
            -
                    sum_el = root.find("context")
         | 
| 387 | 
            -
                    if sum_el is None:
         | 
| 388 | 
            -
                        raise ValueError("Missing <context> section")
         | 
| 389 | 
            -
                    context = sum_el.text or ""
         | 
| 390 | 
            -
             | 
| 391 | 
            -
                    return messages, context
         | 
| 392 | 
            -
             | 
| 393 148 | 
             
                async def step_stream(
         | 
| 394 | 
            -
                    self, input_messages: List[MessageCreate], max_steps: int = 10
         | 
| 149 | 
            +
                    self, input_messages: List[MessageCreate], max_steps: int = 10, use_assistant_message: bool = False
         | 
| 395 150 | 
             
                ) -> AsyncGenerator[Union[LettaMessage, LegacyLettaMessage, MessageStreamStatus], None]:
         | 
| 396 151 | 
             
                    """
         | 
| 397 152 | 
             
                    This agent is synchronous-only. If called in an async context, raise an error.
         | 
| @@ -68,7 +68,7 @@ def send_message_to_agent_async(self: "Agent", message: str, other_agent_id: str | |
| 68 68 | 
             
                    messages=messages,
         | 
| 69 69 | 
             
                    other_agent_id=other_agent_id,
         | 
| 70 70 | 
             
                    log_prefix="[send_message_to_agent_async]",
         | 
| 71 | 
            -
                    use_retries=False,  # or True if you want to use  | 
| 71 | 
            +
                    use_retries=False,  # or True if you want to use _async_send_message_with_retries
         | 
| 72 72 | 
             
                )
         | 
| 73 73 |  | 
| 74 74 | 
             
                # Immediately return to caller
         | 
| @@ -6,15 +6,10 @@ from pydantic import BaseModel, Field | |
| 6 6 |  | 
| 7 7 | 
             
            def rethink_user_memory(agent_state: "AgentState", new_memory: str) -> None:
         | 
| 8 8 | 
             
                """
         | 
| 9 | 
            -
                Rewrite memory block for the main agent, new_memory should contain all current
         | 
| 10 | 
            -
                information from the block that is not outdated or inconsistent, integrating any
         | 
| 11 | 
            -
                new information, resulting in a new memory block that is organized, readable, and
         | 
| 12 | 
            -
                comprehensive.
         | 
| 9 | 
            +
                Rewrite memory block for the main agent, new_memory should contain all current information from the block that is not outdated or inconsistent, integrating any new information, resulting in a new memory block that is organized, readable, and comprehensive.
         | 
| 13 10 |  | 
| 14 11 | 
             
                Args:
         | 
| 15 | 
            -
                    new_memory (str): The new memory with information integrated from the memory block.
         | 
| 16 | 
            -
                                      If there is no new information, then this should be the same as
         | 
| 17 | 
            -
                                      the content in the source block.
         | 
| 12 | 
            +
                    new_memory (str): The new memory with information integrated from the memory block. If there is no new information, then this should be the same as the content in the source block.
         | 
| 18 13 |  | 
| 19 14 | 
             
                Returns:
         | 
| 20 15 | 
             
                    None: None is always returned as this function does not produce a response.
         | 
| @@ -34,26 +29,27 @@ def finish_rethinking_memory(agent_state: "AgentState") -> None:  # type: ignore | |
| 34 29 |  | 
| 35 30 |  | 
| 36 31 | 
             
            class MemoryChunk(BaseModel):
         | 
| 37 | 
            -
                start_index: int = Field( | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 32 | 
            +
                start_index: int = Field(
         | 
| 33 | 
            +
                    ...,
         | 
| 34 | 
            +
                    description="Zero-based index of the first evicted line in this chunk.",
         | 
| 35 | 
            +
                )
         | 
| 36 | 
            +
                end_index: int = Field(
         | 
| 37 | 
            +
                    ...,
         | 
| 38 | 
            +
                    description="Zero-based index of the last evicted line (inclusive).",
         | 
| 39 | 
            +
                )
         | 
| 40 | 
            +
                context: str = Field(
         | 
| 41 | 
            +
                    ...,
         | 
| 42 | 
            +
                    description="1-3 sentence paraphrase capturing key facts/details, user preferences, or goals that this chunk reveals—written for future retrieval.",
         | 
| 43 | 
            +
                )
         | 
| 40 44 |  | 
| 41 45 |  | 
| 42 46 | 
             
            def store_memories(agent_state: "AgentState", chunks: List[MemoryChunk]) -> None:
         | 
| 43 47 | 
             
                """
         | 
| 44 | 
            -
                 | 
| 45 | 
            -
                and a brief contextual description.
         | 
| 48 | 
            +
                Persist dialogue that is about to fall out of the agent’s context window.
         | 
| 46 49 |  | 
| 47 50 | 
             
                Args:
         | 
| 48 | 
            -
                    agent_state (AgentState):
         | 
| 49 | 
            -
                        The agent’s current memory state, exposing both its in-session history
         | 
| 50 | 
            -
                        and the archival memory API.
         | 
| 51 51 | 
             
                    chunks (List[MemoryChunk]):
         | 
| 52 | 
            -
                         | 
| 53 | 
            -
                          • start_index (int): Index of the first line in the original history.
         | 
| 54 | 
            -
                          • end_index   (int): Index of the last line in the original history.
         | 
| 55 | 
            -
                          • context     (str): A concise, high-level description of why this chunk
         | 
| 56 | 
            -
                                             matters and what it contains.
         | 
| 52 | 
            +
                        Each chunk pinpoints a contiguous block of **evicted** lines and provides a short, forward-looking synopsis (`context`) that will be embedded for future semantic lookup.
         | 
| 57 53 |  | 
| 58 54 | 
             
                Returns:
         | 
| 59 55 | 
             
                    None
         | 
| @@ -69,20 +65,12 @@ def search_memory( | |
| 69 65 | 
             
                end_minutes_ago: Optional[int],
         | 
| 70 66 | 
             
            ) -> Optional[str]:
         | 
| 71 67 | 
             
                """
         | 
| 72 | 
            -
                Look in long-term or earlier-conversation memory only when the user asks about
         | 
| 73 | 
            -
                something missing from the visible context. The user’s latest utterance is sent
         | 
| 74 | 
            -
                automatically as the main query.
         | 
| 68 | 
            +
                Look in long-term or earlier-conversation memory only when the user asks about something missing from the visible context. The user’s latest utterance is sent automatically as the main query.
         | 
| 75 69 |  | 
| 76 70 | 
             
                Args:
         | 
| 77 | 
            -
                     | 
| 78 | 
            -
             | 
| 79 | 
            -
                     | 
| 80 | 
            -
                        (e.g., order ID, place name) to refine the search when the request is vague.
         | 
| 81 | 
            -
                        Set to None if the user’s utterance is already specific.
         | 
| 82 | 
            -
                    start_minutes_ago (Optional[int]): Newer bound of the time window for results,
         | 
| 83 | 
            -
                        specified in minutes ago. Set to None if no lower time bound is needed.
         | 
| 84 | 
            -
                    end_minutes_ago (Optional[int]): Older bound of the time window for results,
         | 
| 85 | 
            -
                        specified in minutes ago. Set to None if no upper time bound is needed.
         | 
| 71 | 
            +
                    convo_keyword_queries (Optional[List[str]]): Extra keywords (e.g., order ID, place name). Use *null* if not appropriate for the latest user message.
         | 
| 72 | 
            +
                    start_minutes_ago (Optional[int]): Newer bound of the time window for results, specified in minutes ago. Use *null* if no lower time bound is needed.
         | 
| 73 | 
            +
                    end_minutes_ago (Optional[int]): Older bound of the time window, in minutes ago. Use *null* if no upper bound is needed.
         | 
| 86 74 |  | 
| 87 75 | 
             
                Returns:
         | 
| 88 76 | 
             
                    Optional[str]: A formatted string of matching memory entries, or None if no
         | 
    
        letta/functions/helpers.py
    CHANGED
    
    | @@ -231,7 +231,7 @@ async def async_execute_send_message_to_agent( | |
| 231 231 | 
             
                """
         | 
| 232 232 | 
             
                Async helper to:
         | 
| 233 233 | 
             
                  1) validate the target agent exists & is in the same org,
         | 
| 234 | 
            -
                  2) send a message via  | 
| 234 | 
            +
                  2) send a message via _async_send_message_with_retries.
         | 
| 235 235 | 
             
                """
         | 
| 236 236 | 
             
                server = get_letta_server()
         | 
| 237 237 |  | 
| @@ -242,7 +242,7 @@ async def async_execute_send_message_to_agent( | |
| 242 242 | 
             
                    raise ValueError(f"Target agent {other_agent_id} either does not exist or is not in org " f"({sender_agent.user.organization_id}).")
         | 
| 243 243 |  | 
| 244 244 | 
             
                # 2. Use your async retry logic
         | 
| 245 | 
            -
                return await  | 
| 245 | 
            +
                return await _async_send_message_with_retries(
         | 
| 246 246 | 
             
                    server=server,
         | 
| 247 247 | 
             
                    sender_agent=sender_agent,
         | 
| 248 248 | 
             
                    target_agent_id=other_agent_id,
         | 
| @@ -304,7 +304,7 @@ async def _async_send_message_with_retries( | |
| 304 304 | 
             
                timeout: int,
         | 
| 305 305 | 
             
                logging_prefix: Optional[str] = None,
         | 
| 306 306 | 
             
            ) -> str:
         | 
| 307 | 
            -
                logging_prefix = logging_prefix or "[ | 
| 307 | 
            +
                logging_prefix = logging_prefix or "[_async_send_message_with_retries]"
         | 
| 308 308 |  | 
| 309 309 | 
             
                for attempt in range(1, max_retries + 1):
         | 
| 310 310 | 
             
                    try:
         | 
| @@ -363,7 +363,7 @@ def fire_and_forget_send_to_agent( | |
| 363 363 | 
             
                    messages (List[MessageCreate]): The messages to send.
         | 
| 364 364 | 
             
                    other_agent_id (str): The ID of the target agent.
         | 
| 365 365 | 
             
                    log_prefix (str): Prefix for logging.
         | 
| 366 | 
            -
                    use_retries (bool): If True, uses  | 
| 366 | 
            +
                    use_retries (bool): If True, uses _async_send_message_with_retries;
         | 
| 367 367 | 
             
                                        if False, calls server.send_message_to_agent directly.
         | 
| 368 368 | 
             
                """
         | 
| 369 369 | 
             
                server = get_letta_server()
         | 
| @@ -381,7 +381,7 @@ def fire_and_forget_send_to_agent( | |
| 381 381 | 
             
                async def background_task():
         | 
| 382 382 | 
             
                    try:
         | 
| 383 383 | 
             
                        if use_retries:
         | 
| 384 | 
            -
                            result = await  | 
| 384 | 
            +
                            result = await _async_send_message_with_retries(
         | 
| 385 385 | 
             
                                server=server,
         | 
| 386 386 | 
             
                                sender_agent=sender_agent,
         | 
| 387 387 | 
             
                                target_agent_id=other_agent_id,
         | 
| @@ -434,7 +434,7 @@ async def _send_message_to_agents_matching_tags_async( | |
| 434 434 | 
             
                sender_agent: "Agent", server: "SyncServer", messages: List[MessageCreate], matching_agents: List["AgentState"]
         | 
| 435 435 | 
             
            ) -> List[str]:
         | 
| 436 436 | 
             
                async def _send_single(agent_state):
         | 
| 437 | 
            -
                    return await  | 
| 437 | 
            +
                    return await _async_send_message_with_retries(
         | 
| 438 438 | 
             
                        server=server,
         | 
| 439 439 | 
             
                        sender_agent=sender_agent,
         | 
| 440 440 | 
             
                        target_agent_id=agent_state.id,
         | 
| @@ -475,7 +475,7 @@ async def _send_message_to_all_agents_in_group_async(sender_agent: "Agent", mess | |
| 475 475 |  | 
| 476 476 | 
             
                async def _send_single(agent_state):
         | 
| 477 477 | 
             
                    async with sem:
         | 
| 478 | 
            -
                        return await  | 
| 478 | 
            +
                        return await _async_send_message_with_retries(
         | 
| 479 479 | 
             
                            server=server,
         | 
| 480 480 | 
             
                            sender_agent=sender_agent,
         | 
| 481 481 | 
             
                            target_agent_id=agent_state.id,
         | 
| @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            import re
         | 
| 2 | 
            +
            import time
         | 
| 2 3 | 
             
            from datetime import datetime, timedelta, timezone
         | 
| 3 4 | 
             
            from time import strftime
         | 
| 4 5 |  | 
| @@ -77,6 +78,11 @@ def get_utc_time_int() -> int: | |
| 77 78 | 
             
                return int(get_utc_time().timestamp())
         | 
| 78 79 |  | 
| 79 80 |  | 
| 81 | 
            +
            def get_utc_timestamp_ns() -> int:
         | 
| 82 | 
            +
                """Get the current UTC time in nanoseconds"""
         | 
| 83 | 
            +
                return int(time.time_ns())
         | 
| 84 | 
            +
             | 
| 85 | 
            +
             | 
| 80 86 | 
             
            def timestamp_to_datetime(timestamp_seconds: int) -> datetime:
         | 
| 81 87 | 
             
                """Convert Unix timestamp in seconds to UTC datetime object"""
         | 
| 82 88 | 
             
                return datetime.fromtimestamp(timestamp_seconds, tz=timezone.utc)
         |