letta-nightly 0.8.4.dev20250615104252__py3-none-any.whl → 0.8.4.dev20250615221417__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 -0
- letta/agents/base_agent.py +12 -1
- letta/agents/helpers.py +5 -2
- letta/agents/letta_agent.py +98 -61
- letta/agents/voice_sleeptime_agent.py +2 -1
- letta/constants.py +3 -5
- letta/data_sources/redis_client.py +30 -10
- letta/functions/function_sets/files.py +4 -4
- letta/functions/helpers.py +6 -1
- letta/functions/mcp_client/types.py +95 -0
- letta/groups/sleeptime_multi_agent_v2.py +2 -1
- letta/helpers/decorators.py +91 -0
- letta/interfaces/anthropic_streaming_interface.py +11 -0
- letta/interfaces/openai_streaming_interface.py +244 -225
- letta/llm_api/openai_client.py +1 -1
- letta/local_llm/utils.py +5 -1
- letta/orm/enums.py +1 -0
- letta/orm/mcp_server.py +3 -0
- letta/orm/tool.py +3 -0
- letta/otel/metric_registry.py +12 -0
- letta/otel/metrics.py +16 -7
- letta/schemas/letta_response.py +6 -1
- letta/schemas/letta_stop_reason.py +22 -0
- letta/schemas/mcp.py +48 -6
- letta/schemas/openai/chat_completion_request.py +1 -1
- letta/schemas/openai/chat_completion_response.py +1 -1
- letta/schemas/pip_requirement.py +14 -0
- letta/schemas/sandbox_config.py +1 -19
- letta/schemas/tool.py +5 -0
- letta/server/rest_api/json_parser.py +39 -3
- letta/server/rest_api/routers/v1/tools.py +3 -1
- letta/server/rest_api/routers/v1/voice.py +2 -3
- letta/server/rest_api/utils.py +1 -1
- letta/server/server.py +11 -2
- letta/services/agent_manager.py +37 -29
- letta/services/helpers/tool_execution_helper.py +39 -9
- letta/services/mcp/base_client.py +13 -2
- letta/services/mcp/sse_client.py +8 -1
- letta/services/mcp/streamable_http_client.py +56 -0
- letta/services/mcp_manager.py +23 -9
- letta/services/message_manager.py +30 -3
- letta/services/tool_executor/files_tool_executor.py +2 -3
- letta/services/tool_sandbox/e2b_sandbox.py +53 -3
- letta/services/tool_sandbox/local_sandbox.py +3 -1
- letta/services/user_manager.py +22 -0
- letta/settings.py +3 -0
- {letta_nightly-0.8.4.dev20250615104252.dist-info → letta_nightly-0.8.4.dev20250615221417.dist-info}/METADATA +5 -6
- {letta_nightly-0.8.4.dev20250615104252.dist-info → letta_nightly-0.8.4.dev20250615221417.dist-info}/RECORD +51 -48
- {letta_nightly-0.8.4.dev20250615104252.dist-info → letta_nightly-0.8.4.dev20250615221417.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.4.dev20250615104252.dist-info → letta_nightly-0.8.4.dev20250615221417.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.4.dev20250615104252.dist-info → letta_nightly-0.8.4.dev20250615221417.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
|
-
|
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")
|
letta/services/mcp/sse_client.py
CHANGED
@@ -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
|
-
|
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
|
letta/services/mcp_manager.py
CHANGED
@@ -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
|
-
#
|
648
|
+
# commit once
|
622
649
|
await session.commit()
|
623
650
|
|
624
|
-
#
|
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 "
|
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
|
-
|
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
|
-
"
|
220
|
+
"tool_pip_install_started",
|
195
221
|
{
|
196
222
|
"sandbox_id": sbx.sandbox_id,
|
197
|
-
"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(
|
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
|
letta/services/user_manager.py
CHANGED
@@ -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.
|
3
|
+
Version: 0.8.4.dev20250615221417
|
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.
|
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.
|
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
|
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"
|