letta-nightly 0.8.4.dev20250615104252__py3-none-any.whl → 0.8.4.dev20250616104355__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.
Files changed (51) hide show
  1. letta/__init__.py +1 -0
  2. letta/agents/base_agent.py +12 -1
  3. letta/agents/helpers.py +5 -2
  4. letta/agents/letta_agent.py +98 -61
  5. letta/agents/voice_sleeptime_agent.py +2 -1
  6. letta/constants.py +3 -5
  7. letta/data_sources/redis_client.py +30 -10
  8. letta/functions/function_sets/files.py +4 -4
  9. letta/functions/helpers.py +6 -1
  10. letta/functions/mcp_client/types.py +95 -0
  11. letta/groups/sleeptime_multi_agent_v2.py +2 -1
  12. letta/helpers/decorators.py +91 -0
  13. letta/interfaces/anthropic_streaming_interface.py +11 -0
  14. letta/interfaces/openai_streaming_interface.py +244 -225
  15. letta/llm_api/openai_client.py +1 -1
  16. letta/local_llm/utils.py +5 -1
  17. letta/orm/enums.py +1 -0
  18. letta/orm/mcp_server.py +3 -0
  19. letta/orm/tool.py +3 -0
  20. letta/otel/metric_registry.py +12 -0
  21. letta/otel/metrics.py +16 -7
  22. letta/schemas/letta_response.py +6 -1
  23. letta/schemas/letta_stop_reason.py +22 -0
  24. letta/schemas/mcp.py +48 -6
  25. letta/schemas/openai/chat_completion_request.py +1 -1
  26. letta/schemas/openai/chat_completion_response.py +1 -1
  27. letta/schemas/pip_requirement.py +14 -0
  28. letta/schemas/sandbox_config.py +1 -19
  29. letta/schemas/tool.py +5 -0
  30. letta/server/rest_api/json_parser.py +39 -3
  31. letta/server/rest_api/routers/v1/tools.py +3 -1
  32. letta/server/rest_api/routers/v1/voice.py +2 -3
  33. letta/server/rest_api/utils.py +1 -1
  34. letta/server/server.py +11 -2
  35. letta/services/agent_manager.py +37 -29
  36. letta/services/helpers/tool_execution_helper.py +39 -9
  37. letta/services/mcp/base_client.py +13 -2
  38. letta/services/mcp/sse_client.py +8 -1
  39. letta/services/mcp/streamable_http_client.py +56 -0
  40. letta/services/mcp_manager.py +23 -9
  41. letta/services/message_manager.py +30 -3
  42. letta/services/tool_executor/files_tool_executor.py +2 -3
  43. letta/services/tool_sandbox/e2b_sandbox.py +53 -3
  44. letta/services/tool_sandbox/local_sandbox.py +3 -1
  45. letta/services/user_manager.py +22 -0
  46. letta/settings.py +3 -0
  47. {letta_nightly-0.8.4.dev20250615104252.dist-info → letta_nightly-0.8.4.dev20250616104355.dist-info}/METADATA +5 -6
  48. {letta_nightly-0.8.4.dev20250615104252.dist-info → letta_nightly-0.8.4.dev20250616104355.dist-info}/RECORD +51 -48
  49. {letta_nightly-0.8.4.dev20250615104252.dist-info → letta_nightly-0.8.4.dev20250616104355.dist-info}/LICENSE +0 -0
  50. {letta_nightly-0.8.4.dev20250615104252.dist-info → letta_nightly-0.8.4.dev20250616104355.dist-info}/WHEEL +0 -0
  51. {letta_nightly-0.8.4.dev20250615104252.dist-info → letta_nightly-0.8.4.dev20250616104355.dist-info}/entry_points.txt +0 -0
@@ -24,11 +24,22 @@ class AsyncBaseMCPClient:
24
24
  await self._initialize_connection(self.server_config)
25
25
  await self.session.initialize()
26
26
  self.initialized = True
27
+ except ConnectionError as e:
28
+ logger.error(f"MCP connection failed: {str(e)}")
29
+ raise e
27
30
  except Exception as e:
28
31
  logger.error(
29
- f"Connecting to MCP server failed. Please review your server config: {self.server_config.model_dump_json(indent=4)}"
32
+ f"Connecting to MCP server failed. Please review your server config: {self.server_config.model_dump_json(indent=4)}. Error: {str(e)}"
30
33
  )
31
- raise e
34
+ if hasattr(self.server_config, "server_url") and self.server_config.server_url:
35
+ server_info = f"server URL '{self.server_config.server_url}'"
36
+ elif hasattr(self.server_config, "command") and self.server_config.command:
37
+ server_info = f"command '{self.server_config.command}'"
38
+ else:
39
+ server_info = f"server '{self.server_config.server_name}'"
40
+ raise ConnectionError(
41
+ f"Failed to connect to MCP {server_info}. Please check your configuration and ensure the server is accessible."
42
+ ) from e
32
43
 
33
44
  async def _initialize_connection(self, server_config: BaseServerConfig) -> None:
34
45
  raise NotImplementedError("Subclasses must implement _initialize_connection")
@@ -14,7 +14,14 @@ logger = get_logger(__name__)
14
14
  # TODO: Get rid of Async prefix on this class name once we deprecate old sync code
15
15
  class AsyncSSEMCPClient(AsyncBaseMCPClient):
16
16
  async def _initialize_connection(self, server_config: SSEServerConfig) -> None:
17
- sse_cm = sse_client(url=server_config.server_url)
17
+ headers = {}
18
+ if server_config.custom_headers:
19
+ headers.update(server_config.custom_headers)
20
+
21
+ if server_config.auth_header and server_config.auth_token:
22
+ headers[server_config.auth_header] = server_config.auth_token
23
+
24
+ sse_cm = sse_client(url=server_config.server_url, headers=headers if headers else None)
18
25
  sse_transport = await self.exit_stack.enter_async_context(sse_cm)
19
26
  self.stdio, self.write = sse_transport
20
27
 
@@ -0,0 +1,56 @@
1
+ from mcp import ClientSession
2
+ from mcp.client.streamable_http import streamablehttp_client
3
+
4
+ from letta.functions.mcp_client.types import BaseServerConfig, StreamableHTTPServerConfig
5
+ from letta.log import get_logger
6
+ from letta.services.mcp.base_client import AsyncBaseMCPClient
7
+
8
+ logger = get_logger(__name__)
9
+
10
+
11
+ class AsyncStreamableHTTPMCPClient(AsyncBaseMCPClient):
12
+ async def _initialize_connection(self, server_config: BaseServerConfig) -> None:
13
+ if not isinstance(server_config, StreamableHTTPServerConfig):
14
+ raise ValueError("Expected StreamableHTTPServerConfig")
15
+
16
+ try:
17
+ # Prepare headers for authentication
18
+ headers = {}
19
+ if server_config.custom_headers:
20
+ headers.update(server_config.custom_headers)
21
+
22
+ # Add auth header if specified
23
+ if server_config.auth_header and server_config.auth_token:
24
+ headers[server_config.auth_header] = server_config.auth_token
25
+
26
+ # Use streamablehttp_client context manager with headers if provided
27
+ if headers:
28
+ streamable_http_cm = streamablehttp_client(server_config.server_url, headers=headers)
29
+ else:
30
+ streamable_http_cm = streamablehttp_client(server_config.server_url)
31
+ read_stream, write_stream, _ = await self.exit_stack.enter_async_context(streamable_http_cm)
32
+
33
+ # Create and enter the ClientSession context manager
34
+ session_cm = ClientSession(read_stream, write_stream)
35
+ self.session = await self.exit_stack.enter_async_context(session_cm)
36
+ except Exception as e:
37
+ # Provide more helpful error messages for specific error types
38
+ if "404" in str(e) or "Not Found" in str(e):
39
+ raise ConnectionError(
40
+ f"MCP server not found at URL: {server_config.server_url}. "
41
+ "Please verify the URL is correct and the server supports the MCP protocol."
42
+ ) from e
43
+ elif "Connection" in str(e) or "connect" in str(e).lower():
44
+ raise ConnectionError(
45
+ f"Failed to connect to MCP server at: {server_config.server_url}. "
46
+ "Please check that the server is running and accessible."
47
+ ) from e
48
+ elif "JSON" in str(e) and "validation" in str(e):
49
+ raise ConnectionError(
50
+ f"MCP server at {server_config.server_url} is not returning valid JSON-RPC responses. "
51
+ "The server may not be a proper MCP server or may be returning empty/invalid JSON. "
52
+ "Please verify this is an MCP-compatible server endpoint."
53
+ ) from e
54
+ else:
55
+ # Re-raise other exceptions with additional context
56
+ raise ConnectionError(f"Failed to initialize streamable HTTP connection to {server_config.server_url}: {str(e)}") from e
@@ -3,17 +3,18 @@ import os
3
3
  from typing import Any, Dict, List, Optional, Tuple, Union
4
4
 
5
5
  import letta.constants as constants
6
- from letta.functions.mcp_client.types import MCPServerType, MCPTool, SSEServerConfig, StdioServerConfig
6
+ from letta.functions.mcp_client.types import MCPServerType, MCPTool, SSEServerConfig, StdioServerConfig, StreamableHTTPServerConfig
7
7
  from letta.log import get_logger
8
8
  from letta.orm.errors import NoResultFound
9
9
  from letta.orm.mcp_server import MCPServer as MCPServerModel
10
- from letta.schemas.mcp import MCPServer, UpdateMCPServer, UpdateSSEMCPServer, UpdateStdioMCPServer
10
+ from letta.schemas.mcp import MCPServer, UpdateMCPServer, UpdateSSEMCPServer, UpdateStdioMCPServer, UpdateStreamableHTTPMCPServer
11
11
  from letta.schemas.tool import Tool as PydanticTool
12
12
  from letta.schemas.tool import ToolCreate
13
13
  from letta.schemas.user import User as PydanticUser
14
14
  from letta.server.db import db_registry
15
15
  from letta.services.mcp.sse_client import MCP_CONFIG_TOPLEVEL_KEY, AsyncSSEMCPClient
16
16
  from letta.services.mcp.stdio_client import AsyncStdioMCPClient
17
+ from letta.services.mcp.streamable_http_client import AsyncStreamableHTTPMCPClient
17
18
  from letta.services.tool_manager import ToolManager
18
19
  from letta.utils import enforce_types, printd
19
20
 
@@ -31,7 +32,6 @@ class MCPManager:
31
32
  @enforce_types
32
33
  async def list_mcp_server_tools(self, mcp_server_name: str, actor: PydanticUser) -> List[MCPTool]:
33
34
  """Get a list of all tools for a specific MCP server."""
34
- print("mcp_server_name", mcp_server_name)
35
35
  mcp_server_id = await self.get_mcp_server_id_by_name(mcp_server_name, actor=actor)
36
36
  mcp_config = await self.get_mcp_server_by_id_async(mcp_server_id, actor=actor)
37
37
  server_config = mcp_config.to_config()
@@ -40,12 +40,16 @@ class MCPManager:
40
40
  mcp_client = AsyncSSEMCPClient(server_config=server_config)
41
41
  elif mcp_config.server_type == MCPServerType.STDIO:
42
42
  mcp_client = AsyncStdioMCPClient(server_config=server_config)
43
+ elif mcp_config.server_type == MCPServerType.STREAMABLE_HTTP:
44
+ mcp_client = AsyncStreamableHTTPMCPClient(server_config=server_config)
45
+ else:
46
+ raise ValueError(f"Unsupported MCP server type: {mcp_config.server_type}")
43
47
  await mcp_client.connect_to_server()
44
48
 
45
49
  # list tools
46
50
  tools = await mcp_client.list_tools()
47
- # TODO: change to pydantic tools
48
51
 
52
+ # TODO: change to pydantic tools
49
53
  await mcp_client.cleanup()
50
54
 
51
55
  return tools
@@ -55,7 +59,6 @@ class MCPManager:
55
59
  self, mcp_server_name: str, tool_name: str, tool_args: Optional[Dict[str, Any]], actor: PydanticUser
56
60
  ) -> Tuple[str, bool]:
57
61
  """Call a specific tool from a specific MCP server."""
58
-
59
62
  from letta.settings import tool_settings
60
63
 
61
64
  if not tool_settings.mcp_read_from_config:
@@ -75,6 +78,10 @@ class MCPManager:
75
78
  mcp_client = AsyncSSEMCPClient(server_config=server_config)
76
79
  elif isinstance(server_config, StdioServerConfig):
77
80
  mcp_client = AsyncStdioMCPClient(server_config=server_config)
81
+ elif isinstance(server_config, StreamableHTTPServerConfig):
82
+ mcp_client = AsyncStreamableHTTPMCPClient(server_config=server_config)
83
+ else:
84
+ raise ValueError(f"Unsupported server config type: {type(server_config)}")
78
85
  await mcp_client.connect_to_server()
79
86
 
80
87
  # call tool
@@ -114,7 +121,6 @@ class MCPManager:
114
121
  async def create_or_update_mcp_server(self, pydantic_mcp_server: MCPServer, actor: PydanticUser) -> MCPServer:
115
122
  """Create a new tool based on the ToolCreate schema."""
116
123
  mcp_server_id = await self.get_mcp_server_id_by_name(mcp_server_name=pydantic_mcp_server.server_name, actor=actor)
117
- print("FOUND SERVER", mcp_server_id, pydantic_mcp_server.server_name)
118
124
  if mcp_server_id:
119
125
  # Put to dict and remove fields that should not be reset
120
126
  update_data = pydantic_mcp_server.model_dump(exclude_unset=True, exclude_none=True)
@@ -122,11 +128,16 @@ class MCPManager:
122
128
  # If there's anything to update (can only update the configs, not the name)
123
129
  if update_data:
124
130
  if pydantic_mcp_server.server_type == MCPServerType.SSE:
125
- update_request = UpdateSSEMCPServer(server_url=pydantic_mcp_server.server_url)
131
+ update_request = UpdateSSEMCPServer(server_url=pydantic_mcp_server.server_url, token=pydantic_mcp_server.token)
126
132
  elif pydantic_mcp_server.server_type == MCPServerType.STDIO:
127
133
  update_request = UpdateStdioMCPServer(stdio_config=pydantic_mcp_server.stdio_config)
134
+ elif pydantic_mcp_server.server_type == MCPServerType.STREAMABLE_HTTP:
135
+ update_request = UpdateStreamableHTTPMCPServer(
136
+ server_url=pydantic_mcp_server.server_url, token=pydantic_mcp_server.token
137
+ )
138
+ else:
139
+ raise ValueError(f"Unsupported server type: {pydantic_mcp_server.server_type}")
128
140
  mcp_server = await self.update_mcp_server_by_id(mcp_server_id, update_request, actor)
129
- print("RETURN", mcp_server)
130
141
  else:
131
142
  printd(
132
143
  f"`create_or_update_mcp_server` was called with user_id={actor.id}, organization_id={actor.organization_id}, name={pydantic_mcp_server.server_name}, but found existing mcp server with nothing to update."
@@ -229,7 +240,7 @@ class MCPManager:
229
240
  except NoResultFound:
230
241
  raise ValueError(f"MCP server with id {mcp_server_id} not found.")
231
242
 
232
- def read_mcp_config(self) -> dict[str, Union[SSEServerConfig, StdioServerConfig]]:
243
+ def read_mcp_config(self) -> dict[str, Union[SSEServerConfig, StdioServerConfig, StreamableHTTPServerConfig]]:
233
244
  mcp_server_list = {}
234
245
 
235
246
  # Attempt to read from ~/.letta/mcp_config.json
@@ -260,6 +271,9 @@ class MCPManager:
260
271
  server_params = SSEServerConfig(
261
272
  server_name=server_name,
262
273
  server_url=server_params_raw["url"],
274
+ auth_header=server_params_raw.get("auth_header", None),
275
+ auth_token=server_params_raw.get("auth_token", None),
276
+ headers=server_params_raw.get("headers", None),
263
277
  )
264
278
  mcp_server_list[server_name] = server_params
265
279
  except Exception as e:
@@ -603,10 +603,11 @@ class MessageManager:
603
603
 
604
604
  @enforce_types
605
605
  @trace_method
606
- async def delete_all_messages_for_agent_async(self, agent_id: str, actor: PydanticUser) -> int:
606
+ async def delete_all_messages_for_agent_async(self, agent_id: str, actor: PydanticUser, exclude_ids: Optional[List[str]] = None) -> int:
607
607
  """
608
608
  Efficiently deletes all messages associated with a given agent_id,
609
609
  while enforcing permission checks and avoiding any ORM‑level loads.
610
+ Optionally excludes specific message IDs from deletion.
610
611
  """
611
612
  async with db_registry.async_session() as session:
612
613
  # 1) verify the agent exists and the actor has access
@@ -616,10 +617,36 @@ class MessageManager:
616
617
  stmt = (
617
618
  delete(MessageModel).where(MessageModel.agent_id == agent_id).where(MessageModel.organization_id == actor.organization_id)
618
619
  )
620
+
621
+ # 3) exclude specific message IDs if provided
622
+ if exclude_ids:
623
+ stmt = stmt.where(~MessageModel.id.in_(exclude_ids))
624
+
625
+ result = await session.execute(stmt)
626
+
627
+ # 4) commit once
628
+ await session.commit()
629
+
630
+ # 5) return the number of rows deleted
631
+ return result.rowcount
632
+
633
+ @enforce_types
634
+ @trace_method
635
+ async def delete_messages_by_ids_async(self, message_ids: List[str], actor: PydanticUser) -> int:
636
+ """
637
+ Efficiently deletes messages by their specific IDs,
638
+ while enforcing permission checks.
639
+ """
640
+ if not message_ids:
641
+ return 0
642
+
643
+ async with db_registry.async_session() as session:
644
+ # issue a CORE DELETE against the mapped class for specific message IDs
645
+ stmt = delete(MessageModel).where(MessageModel.id.in_(message_ids)).where(MessageModel.organization_id == actor.organization_id)
619
646
  result = await session.execute(stmt)
620
647
 
621
- # 3) commit once
648
+ # commit once
622
649
  await session.commit()
623
650
 
624
- # 4) return the number of rows deleted
651
+ # return the number of rows deleted
625
652
  return result.rowcount
@@ -126,15 +126,14 @@ class LettaFileToolExecutor(ToolExecutor):
126
126
  await self.files_agents_manager.update_file_agent_by_id(
127
127
  agent_id=agent_state.id, file_id=file_id, actor=self.actor, is_open=True, visible_content=visible_content
128
128
  )
129
-
130
- return "Success"
129
+ return f"Successfully opened file {file_name}, lines {start} to {end} are now visible in memory block <{file_name}>"
131
130
 
132
131
  async def close_file(self, agent_state: AgentState, file_name: str) -> str:
133
132
  """Stub for close_file tool."""
134
133
  await self.files_agents_manager.update_file_agent_by_name(
135
134
  agent_id=agent_state.id, file_name=file_name, actor=self.actor, is_open=False
136
135
  )
137
- return "Success"
136
+ return f"Successfully closed file {file_name}, use function calls to re-open file"
138
137
 
139
138
  def _validate_regex_pattern(self, pattern: str) -> None:
140
139
  """Validate regex pattern to prevent catastrophic backtracking."""
@@ -1,5 +1,6 @@
1
1
  from typing import TYPE_CHECKING, Any, Dict, Optional
2
2
 
3
+ from e2b.sandbox.commands.command_handle import CommandExitException
3
4
  from e2b_code_interpreter import AsyncSandbox
4
5
 
5
6
  from letta.log import get_logger
@@ -189,14 +190,63 @@ class AsyncToolSandboxE2B(AsyncToolSandboxBase):
189
190
  "package": package,
190
191
  },
191
192
  )
192
- await sbx.commands.run(f"pip install {package}")
193
+ try:
194
+ await sbx.commands.run(f"pip install {package}")
195
+ log_event(
196
+ "e2b_pip_install_finished",
197
+ {
198
+ "sandbox_id": sbx.sandbox_id,
199
+ "package": package,
200
+ },
201
+ )
202
+ except CommandExitException as e:
203
+ error_msg = f"Failed to install sandbox pip requirement '{package}' in E2B sandbox. This may be due to package version incompatibility with the E2B environment. Error: {e}"
204
+ logger.error(error_msg)
205
+ log_event(
206
+ "e2b_pip_install_failed",
207
+ {
208
+ "sandbox_id": sbx.sandbox_id,
209
+ "package": package,
210
+ "error": str(e),
211
+ },
212
+ )
213
+ raise RuntimeError(error_msg) from e
214
+
215
+ # Install tool-specific pip requirements
216
+ if self.tool and self.tool.pip_requirements:
217
+ for pip_requirement in self.tool.pip_requirements:
218
+ package_str = str(pip_requirement)
193
219
  log_event(
194
- "e2b_pip_install_finished",
220
+ "tool_pip_install_started",
195
221
  {
196
222
  "sandbox_id": sbx.sandbox_id,
197
- "package": package,
223
+ "package": package_str,
224
+ "tool_name": self.tool.name,
198
225
  },
199
226
  )
227
+ try:
228
+ await sbx.commands.run(f"pip install {package_str}")
229
+ log_event(
230
+ "tool_pip_install_finished",
231
+ {
232
+ "sandbox_id": sbx.sandbox_id,
233
+ "package": package_str,
234
+ "tool_name": self.tool.name,
235
+ },
236
+ )
237
+ except CommandExitException as e:
238
+ error_msg = f"Failed to install tool pip requirement '{package_str}' for tool '{self.tool.name}' in E2B sandbox. This may be due to package version incompatibility with the E2B environment. Consider updating the package version or removing the version constraint. Error: {e}"
239
+ logger.error(error_msg)
240
+ log_event(
241
+ "tool_pip_install_failed",
242
+ {
243
+ "sandbox_id": sbx.sandbox_id,
244
+ "package": package_str,
245
+ "tool_name": self.tool.name,
246
+ "error": str(e),
247
+ },
248
+ )
249
+ raise RuntimeError(error_msg) from e
200
250
 
201
251
  return sbx
202
252
 
@@ -175,7 +175,9 @@ class AsyncToolSandboxLocal(AsyncToolSandboxBase):
175
175
  log_event(name="finish create_venv_for_local_sandbox")
176
176
 
177
177
  log_event(name="start install_pip_requirements_for_sandbox", attributes={"local_configs": local_configs.model_dump_json()})
178
- await asyncio.to_thread(install_pip_requirements_for_sandbox, local_configs, upgrade=True, user_install_if_no_venv=False, env=env)
178
+ await asyncio.to_thread(
179
+ install_pip_requirements_for_sandbox, local_configs, upgrade=True, user_install_if_no_venv=False, env=env, tool=self.tool
180
+ )
179
181
  log_event(name="finish install_pip_requirements_for_sandbox", attributes={"local_configs": local_configs.model_dump_json()})
180
182
 
181
183
  @trace_method
@@ -3,6 +3,9 @@ from typing import List, Optional
3
3
  from sqlalchemy import select, text
4
4
 
5
5
  from letta.constants import DEFAULT_ORG_ID
6
+ from letta.data_sources.redis_client import get_redis_client
7
+ from letta.helpers.decorators import async_redis_cache
8
+ from letta.log import get_logger
6
9
  from letta.orm.errors import NoResultFound
7
10
  from letta.orm.organization import Organization as OrganizationModel
8
11
  from letta.orm.user import User as UserModel
@@ -13,6 +16,8 @@ from letta.server.db import db_registry
13
16
  from letta.utils import enforce_types
14
17
  from letta.settings import settings
15
18
 
19
+ logger = get_logger(__name__)
20
+
16
21
 
17
22
  class UserManager:
18
23
  """Manager class to handle business logic related to Users."""
@@ -59,6 +64,7 @@ class UserManager:
59
64
  # If it doesn't exist, make it
60
65
  actor = UserModel(id=self.DEFAULT_USER_ID, name=self.DEFAULT_USER_NAME, organization_id=org_id)
61
66
  await actor.create_async(session)
67
+ await self._invalidate_actor_cache(self.DEFAULT_USER_ID)
62
68
 
63
69
  return actor.to_pydantic()
64
70
 
@@ -78,6 +84,7 @@ class UserManager:
78
84
  async with db_registry.async_session() as session:
79
85
  new_user = UserModel(**pydantic_user.model_dump(to_orm=True))
80
86
  await new_user.create_async(session)
87
+ await self._invalidate_actor_cache(new_user.id)
81
88
  return new_user.to_pydantic()
82
89
 
83
90
  @enforce_types
@@ -112,6 +119,7 @@ class UserManager:
112
119
 
113
120
  # Commit the updated user
114
121
  await existing_user.update_async(session)
122
+ await self._invalidate_actor_cache(user_update.id)
115
123
  return existing_user.to_pydantic()
116
124
 
117
125
  @enforce_types
@@ -133,6 +141,7 @@ class UserManager:
133
141
  # Delete from user table
134
142
  user = await UserModel.read_async(db_session=session, identifier=user_id)
135
143
  await user.hard_delete_async(session)
144
+ await self._invalidate_actor_cache(user_id)
136
145
 
137
146
  @enforce_types
138
147
  @trace_method
@@ -144,6 +153,7 @@ class UserManager:
144
153
 
145
154
  @enforce_types
146
155
  @trace_method
156
+ @async_redis_cache(key_func=lambda self, actor_id: f"actor_id:{actor_id}", model_class=PydanticUser)
147
157
  async def get_actor_by_id_async(self, actor_id: str) -> PydanticUser:
148
158
  """Fetch a user by ID asynchronously."""
149
159
  async with db_registry.async_session() as session:
@@ -228,3 +238,15 @@ class UserManager:
228
238
  limit=limit,
229
239
  )
230
240
  return [user.to_pydantic() for user in users]
241
+
242
+ async def _invalidate_actor_cache(self, actor_id: str) -> bool:
243
+ """Invalidates the actor cache on CRUD operations.
244
+ TODO (cliandy): see notes on redis cache decorator
245
+ """
246
+ try:
247
+ redis_client = await get_redis_client()
248
+ cache_key = self.get_actor_by_id_async.cache_key_func(self, actor_id)
249
+ return (await redis_client.delete(cache_key)) > 0
250
+ except Exception as e:
251
+ logger.error(f"Failed to invalidate cache: {e}")
252
+ return False
letta/settings.py CHANGED
@@ -205,6 +205,9 @@ class Settings(BaseSettings):
205
205
 
206
206
  # telemetry logging
207
207
  otel_exporter_otlp_endpoint: Optional[str] = None # otel default: "http://localhost:4317"
208
+ otel_preferred_temporality: Optional[int] = Field(
209
+ default=1, ge=0, le=2, description="Exported metric temporality. {0: UNSPECIFIED, 1: DELTA, 2: CUMULATIVE}"
210
+ )
208
211
  disable_tracing: bool = False
209
212
  llm_api_logging: bool = True
210
213
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: letta-nightly
3
- Version: 0.8.4.dev20250615104252
3
+ Version: 0.8.4.dev20250616104355
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  License: Apache License
6
6
  Author: Letta Team
@@ -20,7 +20,6 @@ Provides-Extra: experimental
20
20
  Provides-Extra: external-tools
21
21
  Provides-Extra: google
22
22
  Provides-Extra: postgres
23
- Provides-Extra: qdrant
24
23
  Provides-Extra: redis
25
24
  Provides-Extra: server
26
25
  Provides-Extra: tests
@@ -56,13 +55,13 @@ Requires-Dist: isort (>=5.13.2,<6.0.0) ; extra == "dev" or extra == "all"
56
55
  Requires-Dist: jinja2 (>=3.1.5,<4.0.0)
57
56
  Requires-Dist: langchain (>=0.3.7,<0.4.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
58
57
  Requires-Dist: langchain-community (>=0.3.7,<0.4.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
59
- Requires-Dist: letta_client (>=0.1.153,<0.2.0)
58
+ Requires-Dist: letta_client (>=0.1.155,<0.2.0)
60
59
  Requires-Dist: llama-index (>=0.12.2,<0.13.0)
61
60
  Requires-Dist: llama-index-embeddings-openai (>=0.3.1,<0.4.0)
62
61
  Requires-Dist: locust (>=2.31.5,<3.0.0) ; extra == "dev" or extra == "desktop" or extra == "all"
63
62
  Requires-Dist: marshmallow-sqlalchemy (>=1.4.1,<2.0.0)
64
63
  Requires-Dist: matplotlib (>=3.10.1,<4.0.0)
65
- Requires-Dist: mcp (>=1.3.0,<2.0.0)
64
+ Requires-Dist: mcp[cli] (>=1.9.4,<2.0.0)
66
65
  Requires-Dist: mistralai (>=1.8.1,<2.0.0)
67
66
  Requires-Dist: nltk (>=3.8.1,<4.0.0)
68
67
  Requires-Dist: numpy (>=1.26.2,<2.0.0)
@@ -89,7 +88,6 @@ Requires-Dist: python-box (>=7.1.1,<8.0.0)
89
88
  Requires-Dist: python-multipart (>=0.0.19,<0.0.20)
90
89
  Requires-Dist: pytz (>=2023.3.post1,<2024.0)
91
90
  Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
92
- Requires-Dist: qdrant-client (>=1.9.1,<2.0.0) ; extra == "qdrant"
93
91
  Requires-Dist: questionary (>=2.0.1,<3.0.0)
94
92
  Requires-Dist: redis (>=6.2.0,<7.0.0) ; extra == "redis" or extra == "all"
95
93
  Requires-Dist: rich (>=13.9.4,<14.0.0)
@@ -99,9 +97,10 @@ Requires-Dist: sqlalchemy-json (>=0.7.0,<0.8.0)
99
97
  Requires-Dist: sqlalchemy-utils (>=0.41.2,<0.42.0)
100
98
  Requires-Dist: sqlalchemy[asyncio] (>=2.0.41,<3.0.0)
101
99
  Requires-Dist: sqlmodel (>=0.0.16,<0.0.17)
100
+ Requires-Dist: structlog (>=25.4.0,<26.0.0)
102
101
  Requires-Dist: tavily-python (>=0.7.2,<0.8.0)
103
102
  Requires-Dist: tqdm (>=4.66.1,<5.0.0)
104
- Requires-Dist: typer[all] (>=0.9.0,<0.10.0)
103
+ Requires-Dist: typer (>=0.15.2,<0.16.0)
105
104
  Requires-Dist: uvicorn (>=0.24.0.post1,<0.25.0) ; extra == "server" or extra == "desktop" or extra == "all"
106
105
  Requires-Dist: uvloop (>=0.21.0,<0.22.0) ; extra == "experimental" or extra == "all"
107
106
  Requires-Dist: wikipedia (>=1.4.0,<2.0.0) ; extra == "external-tools" or extra == "tests" or extra == "desktop" or extra == "all"