letta-nightly 0.7.6.dev20250430104233__py3-none-any.whl → 0.7.8.dev20250501064110__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 +1 -1
- letta/agent.py +8 -12
- letta/agents/exceptions.py +6 -0
- letta/agents/helpers.py +1 -1
- letta/agents/letta_agent.py +48 -35
- letta/agents/letta_agent_batch.py +6 -2
- letta/agents/voice_agent.py +41 -59
- letta/agents/{ephemeral_memory_agent.py → voice_sleeptime_agent.py} +106 -129
- letta/client/client.py +3 -3
- letta/constants.py +18 -2
- letta/functions/composio_helpers.py +100 -0
- letta/functions/function_sets/base.py +0 -10
- letta/functions/function_sets/voice.py +92 -0
- letta/functions/functions.py +4 -2
- letta/functions/helpers.py +19 -101
- letta/groups/helpers.py +1 -0
- letta/groups/sleeptime_multi_agent.py +5 -1
- letta/helpers/message_helper.py +21 -4
- letta/helpers/tool_execution_helper.py +1 -1
- letta/interfaces/anthropic_streaming_interface.py +165 -158
- letta/interfaces/openai_chat_completions_streaming_interface.py +1 -1
- letta/llm_api/anthropic.py +15 -10
- letta/llm_api/anthropic_client.py +5 -1
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/llm_api_tools.py +7 -0
- letta/llm_api/llm_client.py +12 -2
- letta/llm_api/llm_client_base.py +4 -0
- letta/llm_api/openai.py +9 -3
- letta/llm_api/openai_client.py +18 -4
- letta/memory.py +3 -1
- letta/orm/enums.py +1 -0
- letta/orm/group.py +2 -0
- letta/orm/provider.py +10 -0
- letta/personas/examples/voice_memory_persona.txt +5 -0
- letta/prompts/system/voice_chat.txt +29 -0
- letta/prompts/system/voice_sleeptime.txt +74 -0
- letta/schemas/agent.py +14 -2
- letta/schemas/enums.py +11 -0
- letta/schemas/group.py +37 -2
- letta/schemas/llm_config.py +1 -0
- letta/schemas/llm_config_overrides.py +2 -2
- letta/schemas/message.py +4 -3
- letta/schemas/providers.py +75 -213
- letta/schemas/tool.py +8 -12
- letta/server/rest_api/app.py +12 -0
- letta/server/rest_api/chat_completions_interface.py +1 -1
- letta/server/rest_api/interface.py +8 -10
- letta/server/rest_api/{optimistic_json_parser.py → json_parser.py} +62 -26
- letta/server/rest_api/routers/v1/agents.py +1 -1
- letta/server/rest_api/routers/v1/embeddings.py +4 -3
- letta/server/rest_api/routers/v1/llms.py +4 -3
- letta/server/rest_api/routers/v1/providers.py +4 -1
- letta/server/rest_api/routers/v1/voice.py +0 -2
- letta/server/rest_api/utils.py +22 -33
- letta/server/server.py +91 -37
- letta/services/agent_manager.py +14 -7
- letta/services/group_manager.py +61 -0
- letta/services/helpers/agent_manager_helper.py +69 -12
- letta/services/message_manager.py +2 -2
- letta/services/passage_manager.py +13 -4
- letta/services/provider_manager.py +25 -14
- letta/services/summarizer/summarizer.py +20 -15
- letta/services/tool_executor/tool_execution_manager.py +1 -1
- letta/services/tool_executor/tool_executor.py +3 -3
- letta/services/tool_manager.py +32 -7
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/METADATA +4 -5
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/RECORD +70 -64
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/entry_points.txt +0 -0
| @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            import json
         | 
| 2 2 | 
             
            import xml.etree.ElementTree as ET
         | 
| 3 | 
            -
            from typing import AsyncGenerator, Dict, List, Tuple, Union
         | 
| 3 | 
            +
            from typing import AsyncGenerator, Dict, List, Optional, Tuple, Union
         | 
| 4 4 |  | 
| 5 5 | 
             
            import openai
         | 
| 6 6 |  | 
| @@ -11,17 +11,19 @@ from letta.schemas.enums import MessageStreamStatus | |
| 11 11 | 
             
            from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage
         | 
| 12 12 | 
             
            from letta.schemas.letta_message_content import TextContent
         | 
| 13 13 | 
             
            from letta.schemas.letta_response import LettaResponse
         | 
| 14 | 
            -
            from letta.schemas.message import MessageCreate
         | 
| 15 | 
            -
            from letta.schemas.openai.chat_completion_request import ChatCompletionRequest,  | 
| 14 | 
            +
            from letta.schemas.message import Message, MessageCreate, ToolReturn
         | 
| 15 | 
            +
            from letta.schemas.openai.chat_completion_request import ChatCompletionRequest, Tool, UserMessage
         | 
| 16 16 | 
             
            from letta.schemas.usage import LettaUsageStatistics
         | 
| 17 17 | 
             
            from letta.schemas.user import User
         | 
| 18 18 | 
             
            from letta.server.rest_api.utils import convert_in_context_letta_messages_to_openai, create_input_messages
         | 
| 19 19 | 
             
            from letta.services.agent_manager import AgentManager
         | 
| 20 20 | 
             
            from letta.services.block_manager import BlockManager
         | 
| 21 21 | 
             
            from letta.services.message_manager import MessageManager
         | 
| 22 | 
            +
            from letta.system import package_function_response
         | 
| 22 23 |  | 
| 23 24 |  | 
| 24 | 
            -
             | 
| 25 | 
            +
            # TODO: Move this to the new Letta Agent loop
         | 
| 26 | 
            +
            class VoiceSleeptimeAgent(BaseAgent):
         | 
| 25 27 | 
             
                """
         | 
| 26 28 | 
             
                A stateless agent that helps with offline memory computations.
         | 
| 27 29 | 
             
                """
         | 
| @@ -29,6 +31,7 @@ class EphemeralMemoryAgent(BaseAgent): | |
| 29 31 | 
             
                def __init__(
         | 
| 30 32 | 
             
                    self,
         | 
| 31 33 | 
             
                    agent_id: str,
         | 
| 34 | 
            +
                    convo_agent_state: AgentState,
         | 
| 32 35 | 
             
                    openai_client: openai.AsyncClient,
         | 
| 33 36 | 
             
                    message_manager: MessageManager,
         | 
| 34 37 | 
             
                    agent_manager: AgentManager,
         | 
| @@ -45,6 +48,7 @@ class EphemeralMemoryAgent(BaseAgent): | |
| 45 48 | 
             
                        actor=actor,
         | 
| 46 49 | 
             
                    )
         | 
| 47 50 |  | 
| 51 | 
            +
                    self.convo_agent_state = convo_agent_state
         | 
| 48 52 | 
             
                    self.block_manager = block_manager
         | 
| 49 53 | 
             
                    self.target_block_label = target_block_label
         | 
| 50 54 | 
             
                    self.message_transcripts = message_transcripts
         | 
| @@ -62,9 +66,7 @@ class EphemeralMemoryAgent(BaseAgent): | |
| 62 66 | 
             
                    openai_messages = convert_in_context_letta_messages_to_openai(in_context_messages, exclude_system_messages=True)
         | 
| 63 67 |  | 
| 64 68 | 
             
                    # 1. Store memories
         | 
| 65 | 
            -
                    request = self._build_openai_request(
         | 
| 66 | 
            -
                        openai_messages, agent_state, tools=self._build_store_memory_tool_schemas(), system=self._get_memory_store_system_prompt()
         | 
| 67 | 
            -
                    )
         | 
| 69 | 
            +
                    request = self._build_openai_request(openai_messages, agent_state, tools=self._build_store_memory_tool_schemas())
         | 
| 68 70 |  | 
| 69 71 | 
             
                    chat_completion = await self.openai_client.chat.completions.create(**request.model_dump(exclude_unset=True))
         | 
| 70 72 | 
             
                    assistant_message = chat_completion.choices[0].message
         | 
| @@ -74,29 +76,53 @@ class EphemeralMemoryAgent(BaseAgent): | |
| 74 76 | 
             
                    function_name = tool_call.function.name
         | 
| 75 77 | 
             
                    function_args = json.loads(tool_call.function.arguments)
         | 
| 76 78 |  | 
| 77 | 
            -
                    if function_name == " | 
| 78 | 
            -
                        print("Called  | 
| 79 | 
            +
                    if function_name == "store_memories":
         | 
| 80 | 
            +
                        print("Called store_memories")
         | 
| 79 81 | 
             
                        print(function_args)
         | 
| 80 | 
            -
                         | 
| 81 | 
            -
             | 
| 82 | 
            -
             | 
| 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 | 
            +
             | 
| 83 88 | 
             
                    else:
         | 
| 84 89 | 
             
                        raise ValueError("Error: Unknown tool function '{function_name}'")
         | 
| 85 90 |  | 
| 86 | 
            -
                     | 
| 87 | 
            -
                         | 
| 88 | 
            -
             | 
| 89 | 
            -
             | 
| 90 | 
            -
                             | 
| 91 | 
            -
                                 | 
| 92 | 
            -
             | 
| 93 | 
            -
             | 
| 94 | 
            -
             | 
| 95 | 
            -
             | 
| 96 | 
            -
             | 
| 97 | 
            -
             | 
| 91 | 
            +
                    assistant_message = {
         | 
| 92 | 
            +
                        "role": "assistant",
         | 
| 93 | 
            +
                        "content": assistant_message.content,
         | 
| 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 | 
            +
                        )
         | 
| 110 | 
            +
                    )
         | 
| 111 | 
            +
                    tool_call_message = {
         | 
| 112 | 
            +
                        "role": "tool",
         | 
| 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 | 
            +
                        )
         | 
| 98 125 | 
             
                    )
         | 
| 99 | 
            -
                    openai_messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": str(result)})
         | 
| 100 126 |  | 
| 101 127 | 
             
                    # 2. Execute rethink block memory loop
         | 
| 102 128 | 
             
                    human_block_content = self.agent_manager.get_block_with_label(
         | 
| @@ -115,15 +141,13 @@ Please refine this block: | |
| 115 141 | 
             
            - Organize related information together (e.g., preferences, background, ongoing goals).
         | 
| 116 142 | 
             
            - Add any light, supportable inferences that deepen understanding—but do not invent unsupported details.
         | 
| 117 143 |  | 
| 118 | 
            -
            Use ` | 
| 144 | 
            +
            Use `rethink_user_memory(new_memory)` as many times as you need to iteratively improve the text. When it’s fully polished and complete, call `finish_rethinking_memory()`.
         | 
| 119 145 | 
             
                    """
         | 
| 120 146 | 
             
                    rethink_command = UserMessage(content=rethink_command)
         | 
| 121 147 | 
             
                    openai_messages.append(rethink_command.model_dump())
         | 
| 122 148 |  | 
| 123 149 | 
             
                    for _ in range(max_steps):
         | 
| 124 | 
            -
                        request = self._build_openai_request(
         | 
| 125 | 
            -
                            openai_messages, agent_state, tools=self._build_sleeptime_tools(), system=self._get_rethink_memory_system_prompt()
         | 
| 126 | 
            -
                        )
         | 
| 150 | 
            +
                        request = self._build_openai_request(openai_messages, agent_state, tools=self._build_sleeptime_tools())
         | 
| 127 151 | 
             
                        chat_completion = await self.openai_client.chat.completions.create(**request.model_dump(exclude_unset=True))
         | 
| 128 152 | 
             
                        assistant_message = chat_completion.choices[0].message
         | 
| 129 153 |  | 
| @@ -132,35 +156,59 @@ Use `rethink_memory(new_memory)` as many times as you need to iteratively improv | |
| 132 156 | 
             
                        function_name = tool_call.function.name
         | 
| 133 157 | 
             
                        function_args = json.loads(tool_call.function.arguments)
         | 
| 134 158 |  | 
| 135 | 
            -
                        if function_name == " | 
| 136 | 
            -
                            print("Called  | 
| 159 | 
            +
                        if function_name == "rethink_user_memory":
         | 
| 160 | 
            +
                            print("Called rethink_user_memory")
         | 
| 137 161 | 
             
                            print(function_args)
         | 
| 138 | 
            -
                            result = self. | 
| 162 | 
            +
                            result, success = self.rethink_user_memory(agent_state=agent_state, **function_args)
         | 
| 139 163 | 
             
                        elif function_name == "finish_rethinking_memory":
         | 
| 140 164 | 
             
                            print("Called finish_rethinking_memory")
         | 
| 165 | 
            +
                            result, success = None, True
         | 
| 141 166 | 
             
                            break
         | 
| 142 167 | 
             
                        else:
         | 
| 143 | 
            -
                             | 
| 144 | 
            -
             | 
| 145 | 
            -
             | 
| 146 | 
            -
             | 
| 147 | 
            -
             | 
| 148 | 
            -
             | 
| 149 | 
            -
             | 
| 150 | 
            -
             | 
| 151 | 
            -
             | 
| 152 | 
            -
             | 
| 153 | 
            -
             | 
| 154 | 
            -
             | 
| 155 | 
            -
             | 
| 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 | 
            +
                            )
         | 
| 156 204 | 
             
                        )
         | 
| 157 | 
            -
                        openai_messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": str(result)})
         | 
| 158 205 |  | 
| 159 206 | 
             
                    # Actually save the memory:
         | 
| 160 207 | 
             
                    target_block = agent_state.memory.get_block(self.target_block_label)
         | 
| 161 208 | 
             
                    self.block_manager.update_block(block_id=target_block.id, block_update=BlockUpdate(value=target_block.value), actor=self.actor)
         | 
| 162 209 |  | 
| 163 | 
            -
                     | 
| 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())
         | 
| 164 212 |  | 
| 165 213 | 
             
                def _format_messages_llm_friendly(self):
         | 
| 166 214 | 
             
                    messages = self.message_manager.list_messages_for_agent(agent_id=self.agent_id, actor=self.actor)
         | 
| @@ -168,13 +216,10 @@ Use `rethink_memory(new_memory)` as many times as you need to iteratively improv | |
| 168 216 | 
             
                    llm_friendly_messages = [f"{m.role}: {m.content[0].text}" for m in messages if m.content and isinstance(m.content[0], TextContent)]
         | 
| 169 217 | 
             
                    return "\n".join(llm_friendly_messages)
         | 
| 170 218 |  | 
| 171 | 
            -
                def _build_openai_request(
         | 
| 172 | 
            -
                    self, openai_messages: List[Dict], agent_state: AgentState, tools: List[Tool], system: str
         | 
| 173 | 
            -
                ) -> ChatCompletionRequest:
         | 
| 174 | 
            -
                    system_message = SystemMessage(role="system", content=system)
         | 
| 219 | 
            +
                def _build_openai_request(self, openai_messages: List[Dict], agent_state: AgentState, tools: List[Tool]) -> ChatCompletionRequest:
         | 
| 175 220 | 
             
                    openai_request = ChatCompletionRequest(
         | 
| 176 | 
            -
                        model= | 
| 177 | 
            -
                        messages= | 
| 221 | 
            +
                        model=agent_state.llm_config.model,  # TODO: Separate config for summarizer?
         | 
| 222 | 
            +
                        messages=openai_messages,
         | 
| 178 223 | 
             
                        tools=tools,
         | 
| 179 224 | 
             
                        tool_choice="required",
         | 
| 180 225 | 
             
                        user=self.actor.id,
         | 
| @@ -192,7 +237,7 @@ Use `rethink_memory(new_memory)` as many times as you need to iteratively improv | |
| 192 237 | 
             
                        Tool(
         | 
| 193 238 | 
             
                            type="function",
         | 
| 194 239 | 
             
                            function={
         | 
| 195 | 
            -
                                "name": " | 
| 240 | 
            +
                                "name": "store_memories",
         | 
| 196 241 | 
             
                                "description": "Archive coherent chunks of dialogue that will be evicted, preserving raw lines and a brief contextual description.",
         | 
| 197 242 | 
             
                                "parameters": {
         | 
| 198 243 | 
             
                                    "type": "object",
         | 
| @@ -227,7 +272,7 @@ Use `rethink_memory(new_memory)` as many times as you need to iteratively improv | |
| 227 272 | 
             
                        Tool(
         | 
| 228 273 | 
             
                            type="function",
         | 
| 229 274 | 
             
                            function={
         | 
| 230 | 
            -
                                "name": " | 
| 275 | 
            +
                                "name": "rethink_user_memory",
         | 
| 231 276 | 
             
                                "description": (
         | 
| 232 277 | 
             
                                    "Rewrite memory block for the main agent, new_memory should contain all current "
         | 
| 233 278 | 
             
                                    "information from the block that is not outdated or inconsistent, integrating any "
         | 
| @@ -268,14 +313,14 @@ Use `rethink_memory(new_memory)` as many times as you need to iteratively improv | |
| 268 313 |  | 
| 269 314 | 
             
                    return tools
         | 
| 270 315 |  | 
| 271 | 
            -
                def  | 
| 316 | 
            +
                def rethink_user_memory(self, new_memory: str, agent_state: AgentState) -> Tuple[Optional[str], bool]:
         | 
| 272 317 | 
             
                    if agent_state.memory.get_block(self.target_block_label) is None:
         | 
| 273 318 | 
             
                        agent_state.memory.create_block(label=self.target_block_label, value=new_memory)
         | 
| 274 319 |  | 
| 275 320 | 
             
                    agent_state.memory.update_block_value(label=self.target_block_label, value=new_memory)
         | 
| 276 | 
            -
                    return  | 
| 321 | 
            +
                    return None, True
         | 
| 277 322 |  | 
| 278 | 
            -
                def store_memory(self, start_index: int, end_index: int, context: str, agent_state: AgentState) -> str:
         | 
| 323 | 
            +
                def store_memory(self, start_index: int, end_index: int, context: str, agent_state: AgentState) -> Tuple[Optional[str], bool]:
         | 
| 279 324 | 
             
                    """
         | 
| 280 325 | 
             
                    Store a memory.
         | 
| 281 326 | 
             
                    """
         | 
| @@ -290,9 +335,9 @@ Use `rethink_memory(new_memory)` as many times as you need to iteratively improv | |
| 290 335 | 
             
                        )
         | 
| 291 336 | 
             
                        self.agent_manager.rebuild_system_prompt(agent_id=agent_state.id, actor=self.actor, force=True)
         | 
| 292 337 |  | 
| 293 | 
            -
                        return  | 
| 338 | 
            +
                        return None, True
         | 
| 294 339 | 
             
                    except Exception as e:
         | 
| 295 | 
            -
                        return f"Failed to store memory given start_index {start_index} and end_index {end_index}: {e}"
         | 
| 340 | 
            +
                        return f"Failed to store memory given start_index {start_index} and end_index {end_index}: {e}", False
         | 
| 296 341 |  | 
| 297 342 | 
             
                def serialize(self, messages: List[str], context: str) -> str:
         | 
| 298 343 | 
             
                    """
         | 
| @@ -351,72 +396,4 @@ Use `rethink_memory(new_memory)` as many times as you need to iteratively improv | |
| 351 396 | 
             
                    """
         | 
| 352 397 | 
             
                    This agent is synchronous-only. If called in an async context, raise an error.
         | 
| 353 398 | 
             
                    """
         | 
| 354 | 
            -
                    raise NotImplementedError(" | 
| 355 | 
            -
             | 
| 356 | 
            -
                # TODO: Move these to independent text files
         | 
| 357 | 
            -
                def _get_memory_store_system_prompt(self) -> str:
         | 
| 358 | 
            -
                    return """
         | 
| 359 | 
            -
            You are a memory-recall assistant working asynchronously alongside a main chat agent that retains only a portion of the message history in its context window.
         | 
| 360 | 
            -
             | 
| 361 | 
            -
            When given a full transcript with lines marked (Older) or (Newer), you should:
         | 
| 362 | 
            -
            1. Segment the (Older) portion into coherent chunks by topic, instruction, or preference.
         | 
| 363 | 
            -
            2. For each chunk, produce only:
         | 
| 364 | 
            -
               - start_index: the first line’s index
         | 
| 365 | 
            -
               - end_index:   the last line’s index
         | 
| 366 | 
            -
               - context: a blurb explaining why this chunk matters
         | 
| 367 | 
            -
             | 
| 368 | 
            -
            Return exactly one JSON tool call to `store_memory`, consider this miniature example:
         | 
| 369 | 
            -
             | 
| 370 | 
            -
            ---
         | 
| 371 | 
            -
             | 
| 372 | 
            -
            (Older)
         | 
| 373 | 
            -
            0. user: Okay. Got it. Keep your answers shorter, please.
         | 
| 374 | 
            -
            1. assistant: Sure thing! I’ll keep it brief. What would you like to know?
         | 
| 375 | 
            -
            2. user: I like basketball.
         | 
| 376 | 
            -
            3. assistant: That's great! Do you have a favorite team or player?
         | 
| 377 | 
            -
             | 
| 378 | 
            -
            (Newer)
         | 
| 379 | 
            -
            4. user: Yeah. I like basketball.
         | 
| 380 | 
            -
            5. assistant: Awesome! What do you enjoy most about basketball?
         | 
| 381 | 
            -
             | 
| 382 | 
            -
            ---
         | 
| 383 | 
            -
             | 
| 384 | 
            -
            Example output:
         | 
| 385 | 
            -
             | 
| 386 | 
            -
            ```json
         | 
| 387 | 
            -
            {
         | 
| 388 | 
            -
              "name": "store_memory",
         | 
| 389 | 
            -
              "arguments": {
         | 
| 390 | 
            -
                "chunks": [
         | 
| 391 | 
            -
                  {
         | 
| 392 | 
            -
                    "start_index": 0,
         | 
| 393 | 
            -
                    "end_index": 1,
         | 
| 394 | 
            -
                    "context": "User explicitly asked the assistant to keep responses concise."
         | 
| 395 | 
            -
                  },
         | 
| 396 | 
            -
                  {
         | 
| 397 | 
            -
                    "start_index": 2,
         | 
| 398 | 
            -
                    "end_index": 3,
         | 
| 399 | 
            -
                    "context": "User enjoys basketball and prompted follow-up about their favorite team or player."
         | 
| 400 | 
            -
                  }
         | 
| 401 | 
            -
                ]
         | 
| 402 | 
            -
              }
         | 
| 403 | 
            -
            }
         | 
| 404 | 
            -
            ```
         | 
| 405 | 
            -
                """
         | 
| 406 | 
            -
             | 
| 407 | 
            -
                def _get_rethink_memory_system_prompt(self) -> str:
         | 
| 408 | 
            -
                    return """
         | 
| 409 | 
            -
            SYSTEM
         | 
| 410 | 
            -
            You are a Memory-Updater agent. Your job is to iteratively refine the given memory block until it’s concise, organized, and complete.
         | 
| 411 | 
            -
             | 
| 412 | 
            -
            Instructions:
         | 
| 413 | 
            -
            - Call `rethink_memory(new_memory: string)` as many times as you like. Each call should submit a fully revised version of the block so far.
         | 
| 414 | 
            -
            - When you’re fully satisfied, call `finish_rethinking_memory()`.
         | 
| 415 | 
            -
            - Don’t output anything else—only the JSON for these tool calls.
         | 
| 416 | 
            -
             | 
| 417 | 
            -
            Goals:
         | 
| 418 | 
            -
            - Merge in new facts and remove contradictions.
         | 
| 419 | 
            -
            - Group related details (preferences, biography, goals).
         | 
| 420 | 
            -
            - Draw light, supportable inferences without inventing facts.
         | 
| 421 | 
            -
            - Preserve every critical piece of information.
         | 
| 422 | 
            -
                """
         | 
| 399 | 
            +
                    raise NotImplementedError("VoiceSleeptimeAgent does not support async step.")
         | 
    
        letta/client/client.py
    CHANGED
    
    | @@ -1031,7 +1031,7 @@ class RESTClient(AbstractClient): | |
| 1031 1031 | 
             
                        #     messages = []
         | 
| 1032 1032 | 
             
                        #     for m in response.messages:
         | 
| 1033 1033 | 
             
                        #         assert isinstance(m, Message)
         | 
| 1034 | 
            -
                        #         messages += m. | 
| 1034 | 
            +
                        #         messages += m.to_letta_messages()
         | 
| 1035 1035 | 
             
                        #     response.messages = messages
         | 
| 1036 1036 |  | 
| 1037 1037 | 
             
                        return response
         | 
| @@ -2725,14 +2725,14 @@ class LocalClient(AbstractClient): | |
| 2725 2725 | 
             
                    #    assert isinstance(m, Message), f"Expected Message object, got {type(m)}"
         | 
| 2726 2726 | 
             
                    # letta_messages = []
         | 
| 2727 2727 | 
             
                    # for m in messages:
         | 
| 2728 | 
            -
                    #    letta_messages += m. | 
| 2728 | 
            +
                    #    letta_messages += m.to_letta_messages()
         | 
| 2729 2729 | 
             
                    # return LettaResponse(messages=letta_messages, usage=usage)
         | 
| 2730 2730 |  | 
| 2731 2731 | 
             
                    # format messages
         | 
| 2732 2732 | 
             
                    messages = self.interface.to_list()
         | 
| 2733 2733 | 
             
                    letta_messages = []
         | 
| 2734 2734 | 
             
                    for m in messages:
         | 
| 2735 | 
            -
                        letta_messages += m. | 
| 2735 | 
            +
                        letta_messages += m.to_letta_messages()
         | 
| 2736 2736 |  | 
| 2737 2737 | 
             
                    return LettaResponse(messages=letta_messages, usage=usage)
         | 
| 2738 2738 |  | 
    
        letta/constants.py
    CHANGED
    
    | @@ -4,7 +4,7 @@ from logging import CRITICAL, DEBUG, ERROR, INFO, NOTSET, WARN, WARNING | |
| 4 4 | 
             
            LETTA_DIR = os.path.join(os.path.expanduser("~"), ".letta")
         | 
| 5 5 | 
             
            LETTA_TOOL_EXECUTION_DIR = os.path.join(LETTA_DIR, "tool_execution_dir")
         | 
| 6 6 |  | 
| 7 | 
            -
            LETTA_MODEL_ENDPOINT = "https://inference. | 
| 7 | 
            +
            LETTA_MODEL_ENDPOINT = "https://inference.letta.com"
         | 
| 8 8 |  | 
| 9 9 | 
             
            ADMIN_PREFIX = "/v1/admin"
         | 
| 10 10 | 
             
            API_PREFIX = "/v1"
         | 
| @@ -18,6 +18,8 @@ MCP_TOOL_TAG_NAME_PREFIX = "mcp"  # full format, mcp:server_name | |
| 18 18 |  | 
| 19 19 | 
             
            LETTA_CORE_TOOL_MODULE_NAME = "letta.functions.function_sets.base"
         | 
| 20 20 | 
             
            LETTA_MULTI_AGENT_TOOL_MODULE_NAME = "letta.functions.function_sets.multi_agent"
         | 
| 21 | 
            +
            LETTA_VOICE_TOOL_MODULE_NAME = "letta.functions.function_sets.voice"
         | 
| 22 | 
            +
             | 
| 21 23 |  | 
| 22 24 | 
             
            # String in the error message for when the context window is too large
         | 
| 23 25 | 
             
            # Example full message:
         | 
| @@ -33,6 +35,10 @@ TOOL_CALL_ID_MAX_LEN = 29 | |
| 33 35 | 
             
            # minimum context window size
         | 
| 34 36 | 
             
            MIN_CONTEXT_WINDOW = 4096
         | 
| 35 37 |  | 
| 38 | 
            +
            # Voice Sleeptime message buffer lengths
         | 
| 39 | 
            +
            DEFAULT_MAX_MESSAGE_BUFFER_LENGTH = 30
         | 
| 40 | 
            +
            DEFAULT_MIN_MESSAGE_BUFFER_LENGTH = 15
         | 
| 41 | 
            +
             | 
| 36 42 | 
             
            # embeddings
         | 
| 37 43 | 
             
            MAX_EMBEDDING_DIM = 4096  # maximum supported embeding size - do NOT change or else DBs will need to be reset
         | 
| 38 44 | 
             
            DEFAULT_EMBEDDING_CHUNK_SIZE = 300
         | 
| @@ -67,10 +73,20 @@ BASE_SLEEPTIME_TOOLS = [ | |
| 67 73 | 
             
                # "archival_memory_search",
         | 
| 68 74 | 
             
                # "conversation_search",
         | 
| 69 75 | 
             
            ]
         | 
| 76 | 
            +
            # Base tools for the voice agent
         | 
| 77 | 
            +
            BASE_VOICE_SLEEPTIME_CHAT_TOOLS = [SEND_MESSAGE_TOOL_NAME, "search_memory"]
         | 
| 78 | 
            +
            # Base memory tools for sleeptime agent
         | 
| 79 | 
            +
            BASE_VOICE_SLEEPTIME_TOOLS = [
         | 
| 80 | 
            +
                "store_memories",
         | 
| 81 | 
            +
                "rethink_user_memory",
         | 
| 82 | 
            +
                "finish_rethinking_memory",
         | 
| 83 | 
            +
            ]
         | 
| 70 84 | 
             
            # Multi agent tools
         | 
| 71 85 | 
             
            MULTI_AGENT_TOOLS = ["send_message_to_agent_and_wait_for_reply", "send_message_to_agents_matching_tags", "send_message_to_agent_async"]
         | 
| 72 86 | 
             
            # Set of all built-in Letta tools
         | 
| 73 | 
            -
            LETTA_TOOL_SET = set( | 
| 87 | 
            +
            LETTA_TOOL_SET = set(
         | 
| 88 | 
            +
                BASE_TOOLS + BASE_MEMORY_TOOLS + MULTI_AGENT_TOOLS + BASE_SLEEPTIME_TOOLS + BASE_VOICE_SLEEPTIME_TOOLS + BASE_VOICE_SLEEPTIME_CHAT_TOOLS
         | 
| 89 | 
            +
            )
         | 
| 74 90 |  | 
| 75 91 | 
             
            # The name of the tool used to send message to the user
         | 
| 76 92 | 
             
            # May not be relevant in cases where the agent has multiple ways to message to user (send_imessage, send_discord_mesasge, ...)
         | 
| @@ -0,0 +1,100 @@ | |
| 1 | 
            +
            import asyncio
         | 
| 2 | 
            +
            import os
         | 
| 3 | 
            +
            from typing import Any, Optional
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            from composio import ComposioToolSet
         | 
| 6 | 
            +
            from composio.constants import DEFAULT_ENTITY_ID
         | 
| 7 | 
            +
            from composio.exceptions import (
         | 
| 8 | 
            +
                ApiKeyNotProvidedError,
         | 
| 9 | 
            +
                ComposioSDKError,
         | 
| 10 | 
            +
                ConnectedAccountNotFoundError,
         | 
| 11 | 
            +
                EnumMetadataNotFound,
         | 
| 12 | 
            +
                EnumStringNotFound,
         | 
| 13 | 
            +
            )
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            from letta.constants import COMPOSIO_ENTITY_ENV_VAR_KEY
         | 
| 16 | 
            +
             | 
| 17 | 
            +
             | 
| 18 | 
            +
            # TODO: This is kind of hacky, as this is used to search up the action later on composio's side
         | 
| 19 | 
            +
            # TODO: So be very careful changing/removing these pair of functions
         | 
| 20 | 
            +
            def _generate_func_name_from_composio_action(action_name: str) -> str:
         | 
| 21 | 
            +
                """
         | 
| 22 | 
            +
                Generates the composio function name from the composio action.
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                Args:
         | 
| 25 | 
            +
                    action_name: The composio action name
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                Returns:
         | 
| 28 | 
            +
                    function name
         | 
| 29 | 
            +
                """
         | 
| 30 | 
            +
                return action_name.lower()
         | 
| 31 | 
            +
             | 
| 32 | 
            +
             | 
| 33 | 
            +
            def generate_composio_action_from_func_name(func_name: str) -> str:
         | 
| 34 | 
            +
                """
         | 
| 35 | 
            +
                Generates the composio action from the composio function name.
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                Args:
         | 
| 38 | 
            +
                    func_name: The composio function name
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                Returns:
         | 
| 41 | 
            +
                    composio action name
         | 
| 42 | 
            +
                """
         | 
| 43 | 
            +
                return func_name.upper()
         | 
| 44 | 
            +
             | 
| 45 | 
            +
             | 
| 46 | 
            +
            def generate_composio_tool_wrapper(action_name: str) -> tuple[str, str]:
         | 
| 47 | 
            +
                # Generate func name
         | 
| 48 | 
            +
                func_name = _generate_func_name_from_composio_action(action_name)
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                wrapper_function_str = f"""\
         | 
| 51 | 
            +
            def {func_name}(**kwargs):
         | 
| 52 | 
            +
                raise RuntimeError("Something went wrong - we should never be using the persisted source code for Composio. Please reach out to Letta team")
         | 
| 53 | 
            +
            """
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                # Compile safety check
         | 
| 56 | 
            +
                _assert_code_gen_compilable(wrapper_function_str.strip())
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                return func_name, wrapper_function_str.strip()
         | 
| 59 | 
            +
             | 
| 60 | 
            +
             | 
| 61 | 
            +
            async def execute_composio_action_async(
         | 
| 62 | 
            +
                action_name: str, args: dict, api_key: Optional[str] = None, entity_id: Optional[str] = None
         | 
| 63 | 
            +
            ) -> tuple[str, str]:
         | 
| 64 | 
            +
                try:
         | 
| 65 | 
            +
                    loop = asyncio.get_running_loop()
         | 
| 66 | 
            +
                    return await loop.run_in_executor(None, execute_composio_action, action_name, args, api_key, entity_id)
         | 
| 67 | 
            +
                except Exception as e:
         | 
| 68 | 
            +
                    raise RuntimeError(f"Error in execute_composio_action_async: {e}") from e
         | 
| 69 | 
            +
             | 
| 70 | 
            +
             | 
| 71 | 
            +
            def execute_composio_action(action_name: str, args: dict, api_key: Optional[str] = None, entity_id: Optional[str] = None) -> Any:
         | 
| 72 | 
            +
                entity_id = entity_id or os.getenv(COMPOSIO_ENTITY_ENV_VAR_KEY, DEFAULT_ENTITY_ID)
         | 
| 73 | 
            +
                try:
         | 
| 74 | 
            +
                    composio_toolset = ComposioToolSet(api_key=api_key, entity_id=entity_id, lock=False)
         | 
| 75 | 
            +
                    response = composio_toolset.execute_action(action=action_name, params=args)
         | 
| 76 | 
            +
                except ApiKeyNotProvidedError:
         | 
| 77 | 
            +
                    raise RuntimeError(
         | 
| 78 | 
            +
                        f"Composio API key is missing for action '{action_name}'. "
         | 
| 79 | 
            +
                        "Please set the sandbox environment variables either through the ADE or the API."
         | 
| 80 | 
            +
                    )
         | 
| 81 | 
            +
                except ConnectedAccountNotFoundError:
         | 
| 82 | 
            +
                    raise RuntimeError(f"No connected account was found for action '{action_name}'. " "Please link an account and try again.")
         | 
| 83 | 
            +
                except EnumStringNotFound as e:
         | 
| 84 | 
            +
                    raise RuntimeError(f"Invalid value provided for action '{action_name}': " + str(e) + ". Please check the action parameters.")
         | 
| 85 | 
            +
                except EnumMetadataNotFound as e:
         | 
| 86 | 
            +
                    raise RuntimeError(f"Invalid value provided for action '{action_name}': " + str(e) + ". Please check the action parameters.")
         | 
| 87 | 
            +
                except ComposioSDKError as e:
         | 
| 88 | 
            +
                    raise RuntimeError(f"An unexpected error occurred in Composio SDK while executing action '{action_name}': " + str(e))
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                if "error" in response and response["error"]:
         | 
| 91 | 
            +
                    raise RuntimeError(f"Error while executing action '{action_name}': " + str(response["error"]))
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                return response.get("data")
         | 
| 94 | 
            +
             | 
| 95 | 
            +
             | 
| 96 | 
            +
            def _assert_code_gen_compilable(code_str):
         | 
| 97 | 
            +
                try:
         | 
| 98 | 
            +
                    compile(code_str, "<string>", "exec")
         | 
| 99 | 
            +
                except SyntaxError as e:
         | 
| 100 | 
            +
                    print(f"Syntax error in code: {e}")
         | 
| @@ -186,16 +186,6 @@ def rethink_memory(agent_state: "AgentState", new_memory: str, target_block_labe | |
| 186 186 | 
             
                return None
         | 
| 187 187 |  | 
| 188 188 |  | 
| 189 | 
            -
            def finish_rethinking_memory(agent_state: "AgentState") -> None:  # type: ignore
         | 
| 190 | 
            -
                """
         | 
| 191 | 
            -
                This function is called when the agent is done rethinking the memory.
         | 
| 192 | 
            -
             | 
| 193 | 
            -
                Returns:
         | 
| 194 | 
            -
                    Optional[str]: None is always returned as this function does not produce a response.
         | 
| 195 | 
            -
                """
         | 
| 196 | 
            -
                return None
         | 
| 197 | 
            -
             | 
| 198 | 
            -
             | 
| 199 189 | 
             
            ## Attempted v2 of sleep-time function set, meant to work better across all types
         | 
| 200 190 |  | 
| 201 191 | 
             
            SNIPPET_LINES: int = 4
         | 
| @@ -0,0 +1,92 @@ | |
| 1 | 
            +
            ## Voice chat + sleeptime tools
         | 
| 2 | 
            +
            from typing import List, Optional
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            from pydantic import BaseModel, Field
         | 
| 5 | 
            +
             | 
| 6 | 
            +
             | 
| 7 | 
            +
            def rethink_user_memory(agent_state: "AgentState", new_memory: str) -> None:
         | 
| 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.
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                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.
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                Returns:
         | 
| 20 | 
            +
                    None: None is always returned as this function does not produce a response.
         | 
| 21 | 
            +
                """
         | 
| 22 | 
            +
                # This is implemented directly in the agent loop
         | 
| 23 | 
            +
                return None
         | 
| 24 | 
            +
             | 
| 25 | 
            +
             | 
| 26 | 
            +
            def finish_rethinking_memory(agent_state: "AgentState") -> None:  # type: ignore
         | 
| 27 | 
            +
                """
         | 
| 28 | 
            +
                This function is called when the agent is done rethinking the memory.
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                Returns:
         | 
| 31 | 
            +
                    Optional[str]: None is always returned as this function does not produce a response.
         | 
| 32 | 
            +
                """
         | 
| 33 | 
            +
                return None
         | 
| 34 | 
            +
             | 
| 35 | 
            +
             | 
| 36 | 
            +
            class MemoryChunk(BaseModel):
         | 
| 37 | 
            +
                start_index: int = Field(..., description="Index of the first line in the original conversation history.")
         | 
| 38 | 
            +
                end_index: int = Field(..., description="Index of the last line in the original conversation history.")
         | 
| 39 | 
            +
                context: str = Field(..., description="A concise, high-level note explaining why this chunk matters.")
         | 
| 40 | 
            +
             | 
| 41 | 
            +
             | 
| 42 | 
            +
            def store_memories(agent_state: "AgentState", chunks: List[MemoryChunk]) -> None:
         | 
| 43 | 
            +
                """
         | 
| 44 | 
            +
                Archive coherent chunks of dialogue that will be evicted, preserving raw lines
         | 
| 45 | 
            +
                and a brief contextual description.
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                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 | 
            +
                    chunks (List[MemoryChunk]):
         | 
| 52 | 
            +
                        A list of MemoryChunk models, each representing a segment to archive:
         | 
| 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.
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                Returns:
         | 
| 59 | 
            +
                    None
         | 
| 60 | 
            +
                """
         | 
| 61 | 
            +
                # This is implemented directly in the agent loop
         | 
| 62 | 
            +
                return None
         | 
| 63 | 
            +
             | 
| 64 | 
            +
             | 
| 65 | 
            +
            def search_memory(
         | 
| 66 | 
            +
                agent_state: "AgentState",
         | 
| 67 | 
            +
                convo_keyword_queries: Optional[List[str]],
         | 
| 68 | 
            +
                start_minutes_ago: Optional[int],
         | 
| 69 | 
            +
                end_minutes_ago: Optional[int],
         | 
| 70 | 
            +
            ) -> Optional[str]:
         | 
| 71 | 
            +
                """
         | 
| 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.
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                Args:
         | 
| 77 | 
            +
                    agent_state (AgentState): The current state of the agent, including its
         | 
| 78 | 
            +
                        memory stores and context.
         | 
| 79 | 
            +
                    convo_keyword_queries (Optional[List[str]]): Extra keywords or identifiers
         | 
| 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.
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                Returns:
         | 
| 88 | 
            +
                    Optional[str]: A formatted string of matching memory entries, or None if no
         | 
| 89 | 
            +
                        relevant memories are found.
         | 
| 90 | 
            +
                """
         | 
| 91 | 
            +
                # This is implemented directly in the agent loop
         | 
| 92 | 
            +
                return None
         | 
    
        letta/functions/functions.py
    CHANGED
    
    | @@ -1,8 +1,9 @@ | |
| 1 1 | 
             
            import importlib
         | 
| 2 2 | 
             
            import inspect
         | 
| 3 | 
            +
            from collections.abc import Callable
         | 
| 3 4 | 
             
            from textwrap import dedent  # remove indentation
         | 
| 4 5 | 
             
            from types import ModuleType
         | 
| 5 | 
            -
            from typing import Dict, List, Literal, Optional
         | 
| 6 | 
            +
            from typing import Any, Dict, List, Literal, Optional
         | 
| 6 7 |  | 
| 7 8 | 
             
            from letta.errors import LettaToolCreateError
         | 
| 8 9 | 
             
            from letta.functions.schema_generator import generate_schema
         | 
| @@ -66,7 +67,8 @@ def parse_source_code(func) -> str: | |
| 66 67 | 
             
                return source_code
         | 
| 67 68 |  | 
| 68 69 |  | 
| 69 | 
            -
             | 
| 70 | 
            +
            # TODO (cliandy) refactor below two funcs
         | 
| 71 | 
            +
            def get_function_from_module(module_name: str, function_name: str) -> Callable[..., Any]:
         | 
| 70 72 | 
             
                """
         | 
| 71 73 | 
             
                Dynamically imports a function from a specified module.
         | 
| 72 74 |  |