letta-nightly 0.7.4.dev20250424184820__py3-none-any.whl → 0.7.5.dev20250425104208__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 CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.7.4"
1
+ __version__ = "0.7.5"
2
2
 
3
3
  # import clients
4
4
  from letta.client.client import LocalClient, RESTClient, create_client
letta/agent.py CHANGED
@@ -190,16 +190,15 @@ class Agent(BaseAgent):
190
190
  Returns:
191
191
  modified (bool): whether the memory was updated
192
192
  """
193
- if self.agent_state.memory.compile() != new_memory.compile():
193
+ system_message = self.message_manager.get_message_by_id(message_id=self.agent_state.message_ids[0], actor=self.user)
194
+ if new_memory.compile() not in system_message.content[0].text:
194
195
  # update the blocks (LRW) in the DB
195
196
  for label in self.agent_state.memory.list_block_labels():
196
197
  updated_value = new_memory.get_block(label).value
197
198
  if updated_value != self.agent_state.memory.get_block(label).value:
198
199
  # update the block if it's changed
199
200
  block_id = self.agent_state.memory.get_block(label).id
200
- block = self.block_manager.update_block(
201
- block_id=block_id, block_update=BlockUpdate(value=updated_value), actor=self.user
202
- )
201
+ self.block_manager.update_block(block_id=block_id, block_update=BlockUpdate(value=updated_value), actor=self.user)
203
202
 
204
203
  # refresh memory from DB (using block ids)
205
204
  self.agent_state.memory = Memory(
@@ -1233,7 +1233,10 @@ class AzureProvider(Provider):
1233
1233
  """
1234
1234
  This is hardcoded for now, since there is no API endpoints to retrieve metadata for a model.
1235
1235
  """
1236
- return AZURE_MODEL_TO_CONTEXT_LENGTH.get(model_name, 4096)
1236
+ context_window = AZURE_MODEL_TO_CONTEXT_LENGTH.get(model_name, None)
1237
+ if context_window is None:
1238
+ context_window = LLM_MAX_TOKENS.get(model_name, 4096)
1239
+ return context_window
1237
1240
 
1238
1241
 
1239
1242
  class VLLMChatCompletionsProvider(Provider):
@@ -104,6 +104,17 @@ def list_agents(
104
104
  )
105
105
 
106
106
 
107
+ @router.get("/count", response_model=int, operation_id="count_agents")
108
+ def count_agents(
109
+ server: SyncServer = Depends(get_letta_server),
110
+ actor_id: Optional[str] = Header(None, alias="user_id"),
111
+ ):
112
+ """
113
+ Get the count of all agents associated with a given user.
114
+ """
115
+ return server.agent_manager.size(actor=server.user_manager.get_user_or_default(user_id=actor_id))
116
+
117
+
107
118
  class IndentedORJSONResponse(Response):
108
119
  media_type = "application/json"
109
120
 
@@ -49,6 +49,24 @@ def list_identities(
49
49
  return identities
50
50
 
51
51
 
52
+ @router.get("/count", tags=["identities"], response_model=int, operation_id="count_identities")
53
+ def count_identities(
54
+ server: "SyncServer" = Depends(get_letta_server),
55
+ actor_id: Optional[str] = Header(None, alias="user_id"),
56
+ ):
57
+ """
58
+ Get count of all identities for a user
59
+ """
60
+ try:
61
+ return server.identity_manager.size(actor=server.user_manager.get_user_or_default(user_id=actor_id))
62
+ except NoResultFound:
63
+ return 0
64
+ except HTTPException:
65
+ raise
66
+ except Exception as e:
67
+ raise HTTPException(status_code=500, detail=f"{e}")
68
+
69
+
52
70
  @router.get("/{identity_id}", tags=["identities"], response_model=Identity, operation_id="retrieve_identity")
53
71
  def retrieve_identity(
54
72
  identity_id: str,
@@ -67,6 +67,17 @@ def list_sources(
67
67
  return server.list_all_sources(actor=actor)
68
68
 
69
69
 
70
+ @router.get("/count", response_model=int, operation_id="count_sources")
71
+ def count_sources(
72
+ server: "SyncServer" = Depends(get_letta_server),
73
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
74
+ ):
75
+ """
76
+ Count all data sources created by a user.
77
+ """
78
+ return server.source_manager.size(actor=server.user_manager.get_user_or_default(user_id=actor_id))
79
+
80
+
70
81
  @router.post("/", response_model=Source, operation_id="create_source")
71
82
  def create_source(
72
83
  source_create: SourceCreate,
@@ -80,6 +80,21 @@ def list_tools(
80
80
  raise HTTPException(status_code=500, detail=str(e))
81
81
 
82
82
 
83
+ @router.get("/count", response_model=int, operation_id="count_tools")
84
+ def count_tools(
85
+ server: SyncServer = Depends(get_letta_server),
86
+ actor_id: Optional[str] = Header(None, alias="user_id"),
87
+ ):
88
+ """
89
+ Get a count of all tools available to agents belonging to the org of the user
90
+ """
91
+ try:
92
+ return server.tool_manager.size(actor=server.user_manager.get_user_or_default(user_id=actor_id))
93
+ except Exception as e:
94
+ print(f"Error occurred: {e}")
95
+ raise HTTPException(status_code=500, detail=str(e))
96
+
97
+
83
98
  @router.post("/", response_model=Tool, operation_id="create_tool")
84
99
  def create_tool(
85
100
  request: ToolCreate = Body(...),
letta/server/server.py CHANGED
@@ -1308,14 +1308,12 @@ class SyncServer(Server):
1308
1308
  tool_execution_result = ToolExecutionSandbox(tool.name, tool_args, actor, tool_object=tool).run(
1309
1309
  agent_state=agent_state, additional_env_vars=tool_env_vars
1310
1310
  )
1311
- status = "error" if tool_execution_result.stderr else "success"
1312
- tool_return = str(tool_execution_result.stderr) if tool_execution_result.stderr else str(tool_execution_result.func_return)
1313
1311
  return ToolReturnMessage(
1314
1312
  id="null",
1315
1313
  tool_call_id="null",
1316
1314
  date=get_utc_time(),
1317
- status=status,
1318
- tool_return=tool_return,
1315
+ status=tool_execution_result.status,
1316
+ tool_return=str(tool_execution_result.func_return),
1319
1317
  stdout=tool_execution_result.stdout,
1320
1318
  stderr=tool_execution_result.stderr,
1321
1319
  )
@@ -556,6 +556,16 @@ class AgentManager:
556
556
 
557
557
  return list(session.execute(query).scalars())
558
558
 
559
+ def size(
560
+ self,
561
+ actor: PydanticUser,
562
+ ) -> int:
563
+ """
564
+ Get the total count of agents for the given user.
565
+ """
566
+ with self.session_maker() as session:
567
+ return AgentModel.size(db_session=session, actor=actor)
568
+
559
569
  @enforce_types
560
570
  def get_agent_by_id(self, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
561
571
  """Fetch an agent by its ID."""
@@ -590,15 +600,18 @@ class AgentManager:
590
600
  agents_to_delete = [agent]
591
601
  sleeptime_group_to_delete = None
592
602
 
593
- # Delete sleeptime agent and group
603
+ # Delete sleeptime agent and group (TODO this is flimsy pls fix)
594
604
  if agent.multi_agent_group:
595
605
  participant_agent_ids = agent.multi_agent_group.agent_ids
596
606
  if agent.multi_agent_group.manager_type == ManagerType.sleeptime and len(participant_agent_ids) == 1:
597
- sleeptime_agent = AgentModel.read(db_session=session, identifier=participant_agent_ids[0], actor=actor)
598
- if sleeptime_agent.agent_type == AgentType.sleeptime_agent:
599
- sleeptime_agent_group = GroupModel.read(db_session=session, identifier=agent.multi_agent_group.id, actor=actor)
600
- sleeptime_group_to_delete = sleeptime_agent_group
607
+ try:
608
+ sleeptime_agent = AgentModel.read(db_session=session, identifier=participant_agent_ids[0], actor=actor)
601
609
  agents_to_delete.append(sleeptime_agent)
610
+ except NoResultFound:
611
+ pass # agent already deleted
612
+ sleeptime_agent_group = GroupModel.read(db_session=session, identifier=agent.multi_agent_group.id, actor=actor)
613
+ sleeptime_group_to_delete = sleeptime_agent_group
614
+
602
615
  try:
603
616
  if sleeptime_group_to_delete is not None:
604
617
  session.delete(sleeptime_group_to_delete)
@@ -931,7 +944,8 @@ class AgentManager:
931
944
  modified (bool): whether the memory was updated
932
945
  """
933
946
  agent_state = self.get_agent_by_id(agent_id=agent_id, actor=actor)
934
- if agent_state.memory.compile() != new_memory.compile():
947
+ system_message = self.message_manager.get_message_by_id(message_id=agent_state.message_ids[0], actor=actor)
948
+ if new_memory.compile() not in system_message.content[0].text:
935
949
  # update the blocks (LRW) in the DB
936
950
  for label in agent_state.memory.list_block_labels():
937
951
  updated_value = new_memory.get_block(label).value
@@ -190,6 +190,17 @@ class IdentityManager:
190
190
  session.delete(identity)
191
191
  session.commit()
192
192
 
193
+ @enforce_types
194
+ def size(
195
+ self,
196
+ actor: PydanticUser,
197
+ ) -> int:
198
+ """
199
+ Get the total count of identities for the given user.
200
+ """
201
+ with self.session_maker() as session:
202
+ return IdentityModel.size(db_session=session, actor=actor)
203
+
193
204
  def _process_relationship(
194
205
  self,
195
206
  session: Session,
File without changes
@@ -0,0 +1,67 @@
1
+ from contextlib import AsyncExitStack
2
+ from typing import Optional, Tuple
3
+
4
+ from mcp import ClientSession
5
+ from mcp import Tool as MCPTool
6
+ from mcp.types import TextContent
7
+
8
+ from letta.functions.mcp_client.types import BaseServerConfig
9
+ from letta.log import get_logger
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ # TODO: Get rid of Async prefix on this class name once we deprecate old sync code
15
+ class AsyncBaseMCPClient:
16
+ def __init__(self, server_config: BaseServerConfig):
17
+ self.server_config = server_config
18
+ self.exit_stack = AsyncExitStack()
19
+ self.session: Optional[ClientSession] = None
20
+ self.initialized = False
21
+
22
+ async def connect_to_server(self):
23
+ try:
24
+ await self._initialize_connection(self.server_config)
25
+ await self.session.initialize()
26
+ self.initialized = True
27
+ except Exception as e:
28
+ logger.error(
29
+ f"Connecting to MCP server failed. Please review your server config: {self.server_config.model_dump_json(indent=4)}"
30
+ )
31
+ raise e
32
+
33
+ async def _initialize_connection(self, exit_stack: AsyncExitStack[bool | None], server_config: BaseServerConfig) -> None:
34
+ raise NotImplementedError("Subclasses must implement _initialize_connection")
35
+
36
+ async def list_tools(self) -> list[MCPTool]:
37
+ self._check_initialized()
38
+ response = await self.session.list_tools()
39
+ return response.tools
40
+
41
+ async def execute_tool(self, tool_name: str, tool_args: dict) -> Tuple[str, bool]:
42
+ self._check_initialized()
43
+ result = await self.session.call_tool(tool_name, tool_args)
44
+ parsed_content = []
45
+ for content_piece in result.content:
46
+ if isinstance(content_piece, TextContent):
47
+ parsed_content.append(content_piece.text)
48
+ print("parsed_content (text)", parsed_content)
49
+ else:
50
+ parsed_content.append(str(content_piece))
51
+ print("parsed_content (other)", parsed_content)
52
+ if len(parsed_content) > 0:
53
+ final_content = " ".join(parsed_content)
54
+ else:
55
+ # TODO move hardcoding to constants
56
+ final_content = "Empty response from tool"
57
+
58
+ return final_content, result.isError
59
+
60
+ def _check_initialized(self):
61
+ if not self.initialized:
62
+ logger.error("MCPClient has not been initialized")
63
+ raise RuntimeError("MCPClient has not been initialized")
64
+
65
+ async def cleanup(self):
66
+ """Clean up resources"""
67
+ await self.exit_stack.aclose()
@@ -0,0 +1,25 @@
1
+ from contextlib import AsyncExitStack
2
+
3
+ from mcp import ClientSession
4
+ from mcp.client.sse import sse_client
5
+
6
+ from letta.functions.mcp_client.types import SSEServerConfig
7
+ from letta.log import get_logger
8
+ from letta.services.mcp.base_client import AsyncBaseMCPClient
9
+
10
+ # see: https://modelcontextprotocol.io/quickstart/user
11
+ MCP_CONFIG_TOPLEVEL_KEY = "mcpServers"
12
+
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ # TODO: Get rid of Async prefix on this class name once we deprecate old sync code
17
+ class AsyncSSEMCPClient(AsyncBaseMCPClient):
18
+ async def _initialize_connection(self, exit_stack: AsyncExitStack[bool | None], server_config: SSEServerConfig) -> None:
19
+ sse_cm = sse_client(url=server_config.server_url)
20
+ sse_transport = await exit_stack.enter_async_context(sse_cm)
21
+ self.stdio, self.write = sse_transport
22
+
23
+ # Create and enter the ClientSession context manager
24
+ session_cm = ClientSession(self.stdio, self.write)
25
+ self.session = await exit_stack.enter_async_context(session_cm)
@@ -0,0 +1,19 @@
1
+ from contextlib import AsyncExitStack
2
+
3
+ from mcp import ClientSession, StdioServerParameters
4
+ from mcp.client.stdio import stdio_client
5
+
6
+ from letta.functions.mcp_client.types import StdioServerConfig
7
+ from letta.log import get_logger
8
+ from letta.services.mcp.base_client import AsyncBaseMCPClient
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ # TODO: Get rid of Async prefix on this class name once we deprecate old sync code
14
+ class AsyncStdioMCPClient(AsyncBaseMCPClient):
15
+ async def _initialize_connection(self, exit_stack: AsyncExitStack[bool | None], server_config: StdioServerConfig) -> None:
16
+ server_params = StdioServerParameters(command=server_config.command, args=server_config.args)
17
+ stdio_transport = await exit_stack.enter_async_context(stdio_client(server_params))
18
+ self.stdio, self.write = stdio_transport
19
+ self.session = await exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
@@ -0,0 +1,48 @@
1
+ from enum import Enum
2
+ from typing import List, Optional
3
+
4
+ from mcp import Tool
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class MCPTool(Tool):
9
+ """A simple wrapper around MCP's tool definition (to avoid conflict with our own)"""
10
+
11
+
12
+ class MCPServerType(str, Enum):
13
+ SSE = "sse"
14
+ STDIO = "stdio"
15
+
16
+
17
+ class BaseServerConfig(BaseModel):
18
+ server_name: str = Field(..., description="The name of the server")
19
+ type: MCPServerType
20
+
21
+
22
+ class SSEServerConfig(BaseServerConfig):
23
+ type: MCPServerType = MCPServerType.SSE
24
+ server_url: str = Field(..., description="The URL of the server (MCP SSE client will connect to this URL)")
25
+
26
+ def to_dict(self) -> dict:
27
+ values = {
28
+ "transport": "sse",
29
+ "url": self.server_url,
30
+ }
31
+ return values
32
+
33
+
34
+ class StdioServerConfig(BaseServerConfig):
35
+ type: MCPServerType = MCPServerType.STDIO
36
+ command: str = Field(..., description="The command to run (MCP 'local' client will run this command)")
37
+ args: List[str] = Field(..., description="The arguments to pass to the command")
38
+ env: Optional[dict[str, str]] = Field(None, description="Environment variables to set")
39
+
40
+ def to_dict(self) -> dict:
41
+ values = {
42
+ "transport": "stdio",
43
+ "command": self.command,
44
+ "args": self.args,
45
+ }
46
+ if self.env is not None:
47
+ values["env"] = self.env
48
+ return values
@@ -77,6 +77,17 @@ class SourceManager:
77
77
  )
78
78
  return [source.to_pydantic() for source in sources]
79
79
 
80
+ @enforce_types
81
+ def size(
82
+ self,
83
+ actor: PydanticUser,
84
+ ) -> int:
85
+ """
86
+ Get the total count of sources for the given user.
87
+ """
88
+ with self.session_maker() as session:
89
+ return SourceModel.size(db_session=session, actor=actor)
90
+
80
91
  @enforce_types
81
92
  def list_attached_agents(self, source_id: str, actor: Optional[PydanticUser] = None) -> List[PydanticAgentState]:
82
93
  """
@@ -1,10 +1,12 @@
1
1
  import ast
2
2
  import base64
3
+ import io
3
4
  import os
4
5
  import pickle
5
6
  import subprocess
6
7
  import sys
7
8
  import tempfile
9
+ import traceback
8
10
  import uuid
9
11
  from typing import Any, Dict, Optional
10
12
 
@@ -117,98 +119,108 @@ class ToolExecutionSandbox:
117
119
 
118
120
  @trace_method
119
121
  def run_local_dir_sandbox(
120
- self,
121
- agent_state: Optional[AgentState] = None,
122
- additional_env_vars: Optional[Dict] = None,
122
+ self, agent_state: Optional[AgentState] = None, additional_env_vars: Optional[Dict] = None
123
123
  ) -> ToolExecutionResult:
124
- sbx_config = self.sandbox_config_manager.get_or_create_default_sandbox_config(
125
- sandbox_type=SandboxType.LOCAL,
126
- actor=self.user,
127
- )
124
+ sbx_config = self.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.LOCAL, actor=self.user)
128
125
  local_configs = sbx_config.get_local_config()
129
- sandbox_dir = os.path.expanduser(local_configs.sandbox_dir)
130
- venv_path = os.path.join(sandbox_dir, local_configs.venv_name)
131
126
 
132
- # Aggregate environment variables
127
+ # Get environment variables for the sandbox
133
128
  env = os.environ.copy()
134
- env.update(self.sandbox_config_manager.get_sandbox_env_vars_as_dict(sandbox_config_id=sbx_config.id, actor=self.user, limit=100))
129
+ env_vars = self.sandbox_config_manager.get_sandbox_env_vars_as_dict(sandbox_config_id=sbx_config.id, actor=self.user, limit=100)
130
+ env.update(env_vars)
131
+
132
+ # Get environment variables for this agent specifically
135
133
  if agent_state:
136
134
  env.update(agent_state.get_agent_env_vars_as_dict())
135
+
136
+ # Finally, get any that are passed explicitly into the `run` function call
137
137
  if additional_env_vars:
138
138
  env.update(additional_env_vars)
139
139
 
140
- # Ensure sandbox dir exists
141
- if not os.path.exists(sandbox_dir):
142
- logger.warning(f"Sandbox directory does not exist, creating: {sandbox_dir}")
143
- os.makedirs(sandbox_dir)
140
+ # Safety checks
141
+ if not os.path.exists(local_configs.sandbox_dir) or not os.path.isdir(local_configs.sandbox_dir):
142
+ logger.warning(f"Sandbox directory does not exist, creating: {local_configs.sandbox_dir}")
143
+ os.makedirs(local_configs.sandbox_dir)
144
+
145
+ # Write the code to a temp file in the sandbox_dir
146
+ with tempfile.NamedTemporaryFile(mode="w", dir=local_configs.sandbox_dir, suffix=".py", delete=False) as temp_file:
147
+ if local_configs.force_create_venv:
148
+ # If using venv, we need to wrap with special string markers to separate out the output and the stdout (since it is all in stdout)
149
+ code = self.generate_execution_script(agent_state=agent_state, wrap_print_with_markers=True)
150
+ else:
151
+ code = self.generate_execution_script(agent_state=agent_state)
144
152
 
145
- # Write the code to a temp file
146
- with tempfile.NamedTemporaryFile(mode="w", dir=sandbox_dir, suffix=".py", delete=False) as temp_file:
147
- code = self.generate_execution_script(agent_state=agent_state, wrap_print_with_markers=True)
148
153
  temp_file.write(code)
149
154
  temp_file.flush()
150
155
  temp_file_path = temp_file.name
151
-
152
156
  try:
153
- # Decide whether to use venv
154
- use_venv = os.path.isdir(venv_path)
155
-
156
- if self.force_recreate_venv or (not use_venv and local_configs.force_create_venv):
157
- logger.warning(f"Virtual environment not found at {venv_path}. Creating one...")
158
- log_event(name="start create_venv_for_local_sandbox", attributes={"venv_path": venv_path})
159
- create_venv_for_local_sandbox(
160
- sandbox_dir_path=sandbox_dir,
161
- venv_path=venv_path,
162
- env=env,
163
- force_recreate=self.force_recreate_venv,
164
- )
165
- log_event(name="finish create_venv_for_local_sandbox")
166
- use_venv = True
167
-
168
- if use_venv:
169
- log_event(name="start install_pip_requirements_for_sandbox", attributes={"local_configs": local_configs.model_dump_json()})
170
- install_pip_requirements_for_sandbox(local_configs, env=env)
171
- log_event(name="finish install_pip_requirements_for_sandbox", attributes={"local_configs": local_configs.model_dump_json()})
172
-
173
- python_executable = find_python_executable(local_configs)
174
- if not os.path.isfile(python_executable):
175
- logger.warning(
176
- f"Python executable not found at expected venv path: {python_executable}. Falling back to system Python."
177
- )
178
- python_executable = sys.executable
179
- else:
180
- env = dict(env)
181
- env["VIRTUAL_ENV"] = venv_path
182
- env["PATH"] = os.path.join(venv_path, "bin") + ":" + env.get("PATH", "")
157
+ if local_configs.force_create_venv:
158
+ return self.run_local_dir_sandbox_venv(sbx_config, env, temp_file_path)
183
159
  else:
184
- python_executable = sys.executable
160
+ return self.run_local_dir_sandbox_directly(sbx_config, env, temp_file_path)
161
+ except Exception as e:
162
+ logger.error(f"Executing tool {self.tool_name} has an unexpected error: {e}")
163
+ logger.error(f"Logging out tool {self.tool_name} auto-generated code for debugging: \n\n{code}")
164
+ raise e
165
+ finally:
166
+ # Clean up the temp file
167
+ os.remove(temp_file_path)
168
+
169
+ @trace_method
170
+ def run_local_dir_sandbox_venv(
171
+ self,
172
+ sbx_config: SandboxConfig,
173
+ env: Dict[str, str],
174
+ temp_file_path: str,
175
+ ) -> ToolExecutionResult:
176
+ local_configs = sbx_config.get_local_config()
177
+ sandbox_dir = os.path.expanduser(local_configs.sandbox_dir) # Expand tilde
178
+ venv_path = os.path.join(sandbox_dir, local_configs.venv_name)
179
+
180
+ # Recreate venv if required
181
+ if self.force_recreate_venv or not os.path.isdir(venv_path):
182
+ logger.warning(f"Virtual environment directory does not exist at: {venv_path}, creating one now...")
183
+ log_event(name="start create_venv_for_local_sandbox", attributes={"venv_path": venv_path})
184
+ create_venv_for_local_sandbox(
185
+ sandbox_dir_path=sandbox_dir, venv_path=venv_path, env=env, force_recreate=self.force_recreate_venv
186
+ )
187
+ log_event(name="finish create_venv_for_local_sandbox")
188
+
189
+ log_event(name="start install_pip_requirements_for_sandbox", attributes={"local_configs": local_configs.model_dump_json()})
190
+ install_pip_requirements_for_sandbox(local_configs, env=env)
191
+ log_event(name="finish install_pip_requirements_for_sandbox", attributes={"local_configs": local_configs.model_dump_json()})
192
+
193
+ # Ensure Python executable exists
194
+ python_executable = find_python_executable(local_configs)
195
+ if not os.path.isfile(python_executable):
196
+ raise FileNotFoundError(f"Python executable not found in virtual environment: {python_executable}")
185
197
 
186
- env["PYTHONWARNINGS"] = "ignore"
198
+ # Set up environment variables
199
+ env["VIRTUAL_ENV"] = venv_path
200
+ env["PATH"] = os.path.join(venv_path, "bin") + ":" + env["PATH"]
201
+ env["PYTHONWARNINGS"] = "ignore"
187
202
 
203
+ # Execute the code
204
+ try:
188
205
  log_event(name="start subprocess")
189
206
  result = subprocess.run(
190
- [python_executable, temp_file_path],
191
- env=env,
192
- cwd=sandbox_dir,
193
- timeout=60,
194
- capture_output=True,
195
- text=True,
207
+ [python_executable, temp_file_path], env=env, cwd=sandbox_dir, timeout=60, capture_output=True, text=True, check=True
196
208
  )
197
209
  log_event(name="finish subprocess")
198
210
  func_result, stdout = self.parse_out_function_results_markers(result.stdout)
199
- func_return, parsed_agent_state = self.parse_best_effort(func_result)
211
+ func_return, agent_state = self.parse_best_effort(func_result)
200
212
 
201
213
  return ToolExecutionResult(
202
214
  status="success",
203
215
  func_return=func_return,
204
- agent_state=parsed_agent_state,
216
+ agent_state=agent_state,
205
217
  stdout=[stdout] if stdout else [],
206
218
  stderr=[result.stderr] if result.stderr else [],
207
219
  sandbox_config_fingerprint=sbx_config.fingerprint(),
208
220
  )
209
221
 
210
222
  except subprocess.CalledProcessError as e:
211
- logger.error(f"Tool execution failed: {e}")
223
+ logger.error(f"Executing tool {self.tool_name} has process error: {e}")
212
224
  func_return = get_friendly_error_msg(
213
225
  function_name=self.tool_name,
214
226
  exception_name=type(e).__name__,
@@ -228,11 +240,72 @@ class ToolExecutionSandbox:
228
240
 
229
241
  except Exception as e:
230
242
  logger.error(f"Executing tool {self.tool_name} has an unexpected error: {e}")
231
- logger.error(f"Generated script:\n{code}")
232
243
  raise e
233
244
 
234
- finally:
235
- os.remove(temp_file_path)
245
+ @trace_method
246
+ def run_local_dir_sandbox_directly(
247
+ self,
248
+ sbx_config: SandboxConfig,
249
+ env: Dict[str, str],
250
+ temp_file_path: str,
251
+ ) -> ToolExecutionResult:
252
+ status = "success"
253
+ func_return, agent_state, stderr = None, None, None
254
+
255
+ old_stdout = sys.stdout
256
+ old_stderr = sys.stderr
257
+ captured_stdout, captured_stderr = io.StringIO(), io.StringIO()
258
+
259
+ sys.stdout = captured_stdout
260
+ sys.stderr = captured_stderr
261
+
262
+ try:
263
+ with self.temporary_env_vars(env):
264
+
265
+ # Read and compile the Python script
266
+ with open(temp_file_path, "r", encoding="utf-8") as f:
267
+ source = f.read()
268
+ code_obj = compile(source, temp_file_path, "exec")
269
+
270
+ # Provide a dict for globals.
271
+ globals_dict = dict(env) # or {}
272
+ # If you need to mimic `__main__` behavior:
273
+ globals_dict["__name__"] = "__main__"
274
+ globals_dict["__file__"] = temp_file_path
275
+
276
+ # Execute the compiled code
277
+ log_event(name="start exec", attributes={"temp_file_path": temp_file_path})
278
+ exec(code_obj, globals_dict)
279
+ log_event(name="finish exec", attributes={"temp_file_path": temp_file_path})
280
+
281
+ # Get result from the global dict
282
+ func_result = globals_dict.get(self.LOCAL_SANDBOX_RESULT_VAR_NAME)
283
+ func_return, agent_state = self.parse_best_effort(func_result)
284
+
285
+ except Exception as e:
286
+ func_return = get_friendly_error_msg(
287
+ function_name=self.tool_name,
288
+ exception_name=type(e).__name__,
289
+ exception_message=str(e),
290
+ )
291
+ traceback.print_exc(file=sys.stderr)
292
+ status = "error"
293
+
294
+ # Restore stdout/stderr
295
+ sys.stdout = old_stdout
296
+ sys.stderr = old_stderr
297
+
298
+ stdout_output = [captured_stdout.getvalue()] if captured_stdout.getvalue() else []
299
+ stderr_output = [captured_stderr.getvalue()] if captured_stderr.getvalue() else []
300
+
301
+ return ToolExecutionResult(
302
+ status=status,
303
+ func_return=func_return,
304
+ agent_state=agent_state,
305
+ stdout=stdout_output,
306
+ stderr=stderr_output,
307
+ sandbox_config_fingerprint=sbx_config.fingerprint(),
308
+ )
236
309
 
237
310
  def parse_out_function_results_markers(self, text: str):
238
311
  if self.LOCAL_SANDBOX_RESULT_START_MARKER not in text:
@@ -145,6 +145,17 @@ class ToolManager:
145
145
 
146
146
  return results
147
147
 
148
+ @enforce_types
149
+ def size(
150
+ self,
151
+ actor: PydanticUser,
152
+ ) -> int:
153
+ """
154
+ Get the total count of tools for the given user.
155
+ """
156
+ with self.session_maker() as session:
157
+ return ToolModel.size(db_session=session, actor=actor)
158
+
148
159
  @enforce_types
149
160
  def update_tool_by_id(self, tool_id: str, tool_update: ToolUpdate, actor: PydanticUser) -> PydanticTool:
150
161
  """Update a tool by its ID with the given ToolUpdate object."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: letta-nightly
3
- Version: 0.7.4.dev20250424184820
3
+ Version: 0.7.5.dev20250425104208
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  License: Apache License
6
6
  Author: Letta Team
@@ -1,6 +1,6 @@
1
- letta/__init__.py,sha256=syDK9U6HGmZVN3Corz6hxHnV1EA-JVL--ta6nFa6z5s,917
1
+ letta/__init__.py,sha256=bAqRCwvutm5LP1C2uuuUcODc0u_yqtCtac7CL9bi61k,917
2
2
  letta/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
3
- letta/agent.py,sha256=YmAkpFXHwZ0UhoatyIE-ZrV-kQDwg7f4A1o4cymoI-Y,72170
3
+ letta/agent.py,sha256=VCTvaT0skG1uj7s76GVEMKge9Q0ranjajlQtpjST4iw,72242
4
4
  letta/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  letta/agents/base_agent.py,sha256=yjB1Yz6L-9hTFinqkvyf6c-8dDX9O4u8VCrKrvA4G3s,2348
6
6
  letta/agents/ephemeral_agent.py,sha256=el-SUF_16vv_7OouIR-6z0pAE9Yc0PLibygvfCKwqfo,2736
@@ -221,7 +221,7 @@ letta/schemas/openai/embedding_response.py,sha256=WKIZpXab1Av7v6sxKG8feW3ZtpQUNo
221
221
  letta/schemas/openai/openai.py,sha256=Hilo5BiLAGabzxCwnwfzK5QrWqwYD8epaEKFa4Pwndk,7970
222
222
  letta/schemas/organization.py,sha256=TXrHN4IBQnX-mWvRuCOH57XZSLYCVOY0wWm2_UzDQIA,1279
223
223
  letta/schemas/passage.py,sha256=RG0vkaewEu4a_NAZM-FVyMammHjqpPP0RDYAdu27g6A,3723
224
- letta/schemas/providers.py,sha256=lUz9QvMm_-wUUJZ5OGRsefsora0Y_55s3xQwnzL8gOw,51643
224
+ letta/schemas/providers.py,sha256=6TyGvmfAT-bgmYblfjheQ3Ki4gGxVCFfC5Lt8D3X4qc,51784
225
225
  letta/schemas/response_format.py,sha256=pXNsjbtpA3Tf8HsDyIa40CSmoUbVR_7n2WOfQaX4aFs,2204
226
226
  letta/schemas/run.py,sha256=SRqPRziINIiPunjOhE_NlbnQYgxTvqmbauni_yfBQRA,2085
227
227
  letta/schemas/sandbox_config.py,sha256=Uu_GDofZasycarUVjXB1tL5-X1pLXSsrCB_7XycRIuM,6011
@@ -258,11 +258,11 @@ letta/server/rest_api/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
258
258
  letta/server/rest_api/routers/openai/chat_completions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
259
259
  letta/server/rest_api/routers/openai/chat_completions/chat_completions.py,sha256=Ksh_F6hJDOrafY2Gxz7NQdQZXBNeX-3-w4mVEQgMhrQ,5327
260
260
  letta/server/rest_api/routers/v1/__init__.py,sha256=M-L-Ls4rQaVyQvMjFNEu3NhcRLCvsbOKI-l7F4JxifQ,1557
261
- letta/server/rest_api/routers/v1/agents.py,sha256=Tn6IXvC0Pd03q62AleKHbQm1nv06734sYA-pelYcrPo,34289
261
+ letta/server/rest_api/routers/v1/agents.py,sha256=3mbPzEKBCzJmpUBUmJrs1WbmkIGTjEJ32HNUb1Lo5_M,34676
262
262
  letta/server/rest_api/routers/v1/blocks.py,sha256=Sefvon0jLvlNh0oAzntUcDZptnutuJOf-2Wcad_45Dg,4169
263
263
  letta/server/rest_api/routers/v1/groups.py,sha256=sLXkw8kgf9fhaQwb-n0SVbyzH6-e1kdzNuqGbdvPPgo,10890
264
264
  letta/server/rest_api/routers/v1/health.py,sha256=MoOjkydhGcJXTiuJrKIB0etVXiRMdTa51S8RQ8-50DQ,399
265
- letta/server/rest_api/routers/v1/identities.py,sha256=iH1OAOHHu1DTrQBSJpU0DfpXrPHjbpETKrRLUepDh7w,6877
265
+ letta/server/rest_api/routers/v1/identities.py,sha256=fvp-0cwvb4iX1fUGPkL--9nq8YD3tIE47kYRxUgOlp4,7462
266
266
  letta/server/rest_api/routers/v1/jobs.py,sha256=4oeJfI2odNGubU_g7WSORJhn_usFsbRaD-qm86rve1E,2746
267
267
  letta/server/rest_api/routers/v1/llms.py,sha256=lYp5URXtZk1yu_Pe-p1Wq1uQ0qeb6aWtx78rXSB7N_E,881
268
268
  letta/server/rest_api/routers/v1/messages.py,sha256=3hfaiiCBiWr-wub_uzr3Vyh4IBTZYwGqeDG-2h6Xlus,5753
@@ -270,15 +270,15 @@ letta/server/rest_api/routers/v1/organizations.py,sha256=r7rj-cA3shgAgM0b2JCMqjY
270
270
  letta/server/rest_api/routers/v1/providers.py,sha256=MVfAUvXj_2jx8XFwSigM-8CuCfEATW60h8J5UEmAhp0,3146
271
271
  letta/server/rest_api/routers/v1/runs.py,sha256=9nuJRjBtRgZPq3CiCEUA_3S2xPHFP5DsJxIenH5OO34,8847
272
272
  letta/server/rest_api/routers/v1/sandbox_configs.py,sha256=9hqnnMwJ3wCwO-Bezu3Xl8i3TDSIuInw3gSeHaKUXfE,8526
273
- letta/server/rest_api/routers/v1/sources.py,sha256=igrT2w2fkwMDhw8LZMjRug00YG5mYg08C6ZLpRHeYq4,10270
273
+ letta/server/rest_api/routers/v1/sources.py,sha256=U9cf7DlqKAvXgNnmyOvr2XLBHiJbimFCQw__gV__HV8,10709
274
274
  letta/server/rest_api/routers/v1/steps.py,sha256=DVVwaxLNbNAgWpr2oQkrNjdS-wi0bP8kVJZUO-hiaf8,3275
275
275
  letta/server/rest_api/routers/v1/tags.py,sha256=coydgvL6-9cuG2Hy5Ea7QY3inhTHlsf69w0tcZenBus,880
276
- letta/server/rest_api/routers/v1/tools.py,sha256=k_4kSFMsK2WNSmNMclMdu3GrNo-JDqtQxelxG8OfuxQ,18279
276
+ letta/server/rest_api/routers/v1/tools.py,sha256=vDDhG25vQOz3y12XN_dayh4OweW1rjaLHxZJuJgZkQA,18821
277
277
  letta/server/rest_api/routers/v1/users.py,sha256=G5DBHSkPfBgVHN2Wkm-rVYiLQAudwQczIq2Z3YLdbVo,2277
278
278
  letta/server/rest_api/routers/v1/voice.py,sha256=0lerWjrKLkt4gXLhZl1cIcgstOz9Q2HZwc67L58BCXE,2451
279
279
  letta/server/rest_api/static_files.py,sha256=NG8sN4Z5EJ8JVQdj19tkFa9iQ1kBPTab9f_CUxd_u4Q,3143
280
280
  letta/server/rest_api/utils.py,sha256=OKUWg7u4vmROVweqRrs83bQvS958bZAoR_bUFDwwqsc,14861
281
- letta/server/server.py,sha256=lEoFawsNHHa2VI-ipFuYqToi7OgJrVYOLUlNd_CYHqA,79889
281
+ letta/server/server.py,sha256=XcZ_vrQkG2CVr_mgCKVAV6EMVjuR4lQVtU3FTAUJIIc,79726
282
282
  letta/server/startup.sh,sha256=MRXh1RKbS5lyA7XAsk7O6Q4LEKOqnv5B-dwe0SnTHeQ,2514
283
283
  letta/server/static_files/assets/index-048c9598.js,sha256=mR16XppvselwKCcNgONs4L7kZEVa4OEERm4lNZYtLSk,146819
284
284
  letta/server/static_files/assets/index-0e31b727.css,sha256=SBbja96uiQVLDhDOroHgM6NSl7tS4lpJRCREgSS_hA8,7672
@@ -292,30 +292,35 @@ letta/server/ws_api/interface.py,sha256=TWl9vkcMCnLsUtgsuENZ-ku2oMDA-OUTzLh_yNRo
292
292
  letta/server/ws_api/protocol.py,sha256=5mDgpfNZn_kNwHnpt5Dsuw8gdNH298sgxTGed3etzYg,1836
293
293
  letta/server/ws_api/server.py,sha256=cBSzf-V4zT1bL_0i54OTI3cMXhTIIxqjSRF8pYjk7fg,5835
294
294
  letta/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
295
- letta/services/agent_manager.py,sha256=I893THGSJrwQt-NynuY2rUJxTOGVCetIlXdkEZALc7w,71123
295
+ letta/services/agent_manager.py,sha256=9BRBbAf0LXftDzke8OBzVgzOmP1FLCnsRSQggbNxeOU,71581
296
296
  letta/services/block_manager.py,sha256=rphbpGBIEDFvCJ5GJt6A1OfbURGFQZ3WardljELQyAc,15225
297
297
  letta/services/group_manager.py,sha256=z1woWRRnjkWrGG_auSicXr2bJaJ653JV6PYl2N_AUfw,12224
298
298
  letta/services/helpers/agent_manager_helper.py,sha256=57ARg5TcmE_JdrWmhz5uaNHAt1NGlJ3wQH1tP2XOiAs,17825
299
299
  letta/services/helpers/tool_execution_helper.py,sha256=E8knHJUP_EKmAzwKx-z-rd5EKjU-oroyPzyElUmq83o,6968
300
- letta/services/identity_manager.py,sha256=PqnUnM3OGCRnd5NPjGonwYBnYAQK8rpiissdNYUouGU,9389
300
+ letta/services/identity_manager.py,sha256=sjHTCPbLYRDyWCJ3qjcuKZqWqzDoEuslRsDVKQtBraE,9683
301
301
  letta/services/job_manager.py,sha256=Z9cSD-IqpiOIWE_UOTBUEIxGITrJ10JT1G3ske-0yIY,17166
302
302
  letta/services/llm_batch_manager.py,sha256=jm6MY91hrZdcPOsimSd7Ns3mf0J4SwKV2_ShWXhD02k,16819
303
+ letta/services/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
304
+ letta/services/mcp/base_client.py,sha256=YoRb9eKKTGaLxaMVtuH5UcC74iXyWlcyYbC5xOeGr4k,2571
305
+ letta/services/mcp/sse_client.py,sha256=Vj0AgaadgMnpFQOWkSoPfeOI00ZvURMf3TIU7fv_DN8,1012
306
+ letta/services/mcp/stdio_client.py,sha256=wdPzTqSRkibjt9pXhwi0Nul_z_cTAPim-OHjLc__yBE,925
307
+ letta/services/mcp/types.py,sha256=nmcnQn2EpxXzXg5_pWPsHZobfxO6OucaUgz1bVvam7o,1411
303
308
  letta/services/message_manager.py,sha256=iRFFu7WP9GBtGKrQp5Igiqp_wonSfRKZ_Ran5X6SZZA,17946
304
309
  letta/services/organization_manager.py,sha256=Ax0KmPSc_YYsYaxeld9gc7ST-J6DemHQ542DD7l7AWA,3989
305
310
  letta/services/passage_manager.py,sha256=KY18gHTbx8ROBsOeR7ZAefTMGZwzbxYqOjbadqVFiyQ,9121
306
311
  letta/services/per_agent_lock_manager.py,sha256=porM0cKKANQ1FvcGXOO_qM7ARk5Fgi1HVEAhXsAg9-4,546
307
312
  letta/services/provider_manager.py,sha256=_gEBW0tYIf2vJEGGYxk-nvogrFI9sjFl_97MSL5WC2s,3759
308
313
  letta/services/sandbox_config_manager.py,sha256=ATgZNWNpkdIQDUPy4ABsguHQba2PZf51-c4Ji60MzLE,13361
309
- letta/services/source_manager.py,sha256=SE24AiPPhpvZMGDD047H3_ZDD7OM4zHbTW1JXjPEv7U,7672
314
+ letta/services/source_manager.py,sha256=yW88wAJoeAWtbg0FxifE352jhgOTKNiG7K-IPKXKnvg,7961
310
315
  letta/services/step_manager.py,sha256=B64iYn6Dt9yRKsSJ5vLxWQR2t-apvPLfUZyzrUsJTpI,5335
311
316
  letta/services/summarizer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
312
317
  letta/services/summarizer/enums.py,sha256=szzPX2OBRRJEZsBTGYQThrNz02ELFqhuLwvOR7ozi7A,208
313
318
  letta/services/summarizer/summarizer.py,sha256=4rbbzcB_lY4-3ybT8HMxM8OskLC38YCs9n5h0NhWRcY,4988
314
319
  letta/services/tool_executor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
315
320
  letta/services/tool_executor/tool_execution_manager.py,sha256=RcmnwPougb8AxIwKdC4N9ZxTvOQqJyjI6CaVHF2HBi4,4505
316
- letta/services/tool_executor/tool_execution_sandbox.py,sha256=EMQC25mYf7OxsUNPHFNkbcUZHZvf4QaeO94xenQJv4Y,21450
321
+ letta/services/tool_executor/tool_execution_sandbox.py,sha256=hu-SVqfRalJGcXRKTpbYkqgX-DZH3Uky_eD_vh0kx6s,24704
317
322
  letta/services/tool_executor/tool_executor.py,sha256=keWIzQuwqSzcC6kWcbTY_SfclhKtOT5CZNE-r3OBWNk,27372
318
- letta/services/tool_manager.py,sha256=a5PyLqYDd1H3snWdQGxl-dscMrBHlXcwg6U4T-tc_IM,10276
323
+ letta/services/tool_manager.py,sha256=lFnQkgBYXAXl9q_8_TqJX_UZtLTATXmPliCdm8_7K8M,10561
319
324
  letta/services/tool_sandbox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
320
325
  letta/services/tool_sandbox/base.py,sha256=pUnPFkEg9I5ktMuT4AOOxbTnTmZTGcTA2phLe1H1EdY,8306
321
326
  letta/services/tool_sandbox/e2b_sandbox.py,sha256=umsXfolzM_j67izswECDdVfnlcm03wLpMoZtS6SZ0sc,6147
@@ -327,8 +332,8 @@ letta/streaming_utils.py,sha256=jLqFTVhUL76FeOuYk8TaRQHmPTf3HSRc2EoJwxJNK6U,1194
327
332
  letta/system.py,sha256=dnOrS2FlRMwijQnOvfrky0Lg8wEw-FUq2zzfAJOUSKA,8477
328
333
  letta/tracing.py,sha256=RstWXpfWVF77nmb_ISORVWd9IQw2Ky3de40k_S70yKI,8258
329
334
  letta/utils.py,sha256=IZFvtj9WYcrxUbkoUUYGDxMYQYdn5SgfqsvnARGsAzc,32245
330
- letta_nightly-0.7.4.dev20250424184820.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
331
- letta_nightly-0.7.4.dev20250424184820.dist-info/METADATA,sha256=VuX6foyFknSyK3rzhVvx8iY8IY4yHwnfo_yIpjEFi8I,22282
332
- letta_nightly-0.7.4.dev20250424184820.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
333
- letta_nightly-0.7.4.dev20250424184820.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
334
- letta_nightly-0.7.4.dev20250424184820.dist-info/RECORD,,
335
+ letta_nightly-0.7.5.dev20250425104208.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
336
+ letta_nightly-0.7.5.dev20250425104208.dist-info/METADATA,sha256=WeqMXkmCYUka4wxlVOUi9CB9eVUtVbf2m3oXQ-4Ik6w,22282
337
+ letta_nightly-0.7.5.dev20250425104208.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
338
+ letta_nightly-0.7.5.dev20250425104208.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
339
+ letta_nightly-0.7.5.dev20250425104208.dist-info/RECORD,,