letta-nightly 0.12.1.dev20251024104217__py3-none-any.whl → 0.13.0.dev20251024223017__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.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +2 -3
- letta/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/simple_llm_request_adapter.py +8 -5
- letta/adapters/simple_llm_stream_adapter.py +22 -6
- letta/agents/agent_loop.py +10 -3
- letta/agents/base_agent.py +4 -1
- letta/agents/helpers.py +41 -9
- letta/agents/letta_agent.py +11 -10
- letta/agents/letta_agent_v2.py +47 -37
- letta/agents/letta_agent_v3.py +395 -300
- letta/agents/voice_agent.py +8 -6
- letta/agents/voice_sleeptime_agent.py +3 -3
- letta/constants.py +30 -7
- letta/errors.py +20 -0
- letta/functions/function_sets/base.py +55 -3
- letta/functions/mcp_client/types.py +33 -57
- letta/functions/schema_generator.py +135 -23
- letta/groups/sleeptime_multi_agent_v3.py +6 -11
- letta/groups/sleeptime_multi_agent_v4.py +227 -0
- letta/helpers/converters.py +78 -4
- letta/helpers/crypto_utils.py +6 -2
- letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py +9 -11
- letta/interfaces/anthropic_streaming_interface.py +3 -4
- letta/interfaces/gemini_streaming_interface.py +4 -6
- letta/interfaces/openai_streaming_interface.py +63 -28
- letta/llm_api/anthropic_client.py +7 -4
- letta/llm_api/deepseek_client.py +6 -4
- letta/llm_api/google_ai_client.py +3 -12
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/helpers.py +90 -61
- letta/llm_api/llm_api_tools.py +4 -1
- letta/llm_api/openai.py +12 -12
- letta/llm_api/openai_client.py +53 -16
- letta/local_llm/constants.py +4 -3
- letta/local_llm/json_parser.py +5 -2
- letta/local_llm/utils.py +2 -3
- letta/log.py +171 -7
- letta/orm/agent.py +43 -9
- letta/orm/archive.py +4 -0
- letta/orm/custom_columns.py +15 -0
- letta/orm/identity.py +11 -11
- letta/orm/mcp_server.py +9 -0
- letta/orm/message.py +6 -1
- letta/orm/run_metrics.py +7 -2
- letta/orm/sqlalchemy_base.py +2 -2
- letta/orm/tool.py +3 -0
- letta/otel/tracing.py +2 -0
- letta/prompts/prompt_generator.py +7 -2
- letta/schemas/agent.py +41 -10
- letta/schemas/agent_file.py +3 -0
- letta/schemas/archive.py +4 -2
- letta/schemas/block.py +2 -1
- letta/schemas/enums.py +36 -3
- letta/schemas/file.py +3 -3
- letta/schemas/folder.py +2 -1
- letta/schemas/group.py +2 -1
- letta/schemas/identity.py +18 -9
- letta/schemas/job.py +3 -1
- letta/schemas/letta_message.py +71 -12
- letta/schemas/letta_request.py +7 -3
- letta/schemas/letta_stop_reason.py +0 -25
- letta/schemas/llm_config.py +8 -2
- letta/schemas/mcp.py +80 -83
- letta/schemas/mcp_server.py +349 -0
- letta/schemas/memory.py +20 -8
- letta/schemas/message.py +212 -67
- letta/schemas/providers/anthropic.py +13 -6
- letta/schemas/providers/azure.py +6 -4
- letta/schemas/providers/base.py +8 -4
- letta/schemas/providers/bedrock.py +6 -2
- letta/schemas/providers/cerebras.py +7 -3
- letta/schemas/providers/deepseek.py +2 -1
- letta/schemas/providers/google_gemini.py +15 -6
- letta/schemas/providers/groq.py +2 -1
- letta/schemas/providers/lmstudio.py +9 -6
- letta/schemas/providers/mistral.py +2 -1
- letta/schemas/providers/openai.py +7 -2
- letta/schemas/providers/together.py +9 -3
- letta/schemas/providers/xai.py +7 -3
- letta/schemas/run.py +7 -2
- letta/schemas/run_metrics.py +2 -1
- letta/schemas/sandbox_config.py +2 -2
- letta/schemas/secret.py +3 -158
- letta/schemas/source.py +2 -2
- letta/schemas/step.py +2 -2
- letta/schemas/tool.py +24 -1
- letta/schemas/usage.py +0 -1
- letta/server/rest_api/app.py +123 -7
- letta/server/rest_api/dependencies.py +3 -0
- letta/server/rest_api/interface.py +7 -4
- letta/server/rest_api/redis_stream_manager.py +16 -1
- letta/server/rest_api/routers/v1/__init__.py +7 -0
- letta/server/rest_api/routers/v1/agents.py +332 -322
- letta/server/rest_api/routers/v1/archives.py +127 -40
- letta/server/rest_api/routers/v1/blocks.py +54 -6
- letta/server/rest_api/routers/v1/chat_completions.py +146 -0
- letta/server/rest_api/routers/v1/folders.py +27 -35
- letta/server/rest_api/routers/v1/groups.py +23 -35
- letta/server/rest_api/routers/v1/identities.py +24 -10
- letta/server/rest_api/routers/v1/internal_runs.py +107 -0
- letta/server/rest_api/routers/v1/internal_templates.py +162 -179
- letta/server/rest_api/routers/v1/jobs.py +15 -27
- letta/server/rest_api/routers/v1/mcp_servers.py +309 -0
- letta/server/rest_api/routers/v1/messages.py +23 -34
- letta/server/rest_api/routers/v1/organizations.py +6 -27
- letta/server/rest_api/routers/v1/providers.py +35 -62
- letta/server/rest_api/routers/v1/runs.py +30 -43
- letta/server/rest_api/routers/v1/sandbox_configs.py +6 -4
- letta/server/rest_api/routers/v1/sources.py +26 -42
- letta/server/rest_api/routers/v1/steps.py +16 -29
- letta/server/rest_api/routers/v1/tools.py +17 -13
- letta/server/rest_api/routers/v1/users.py +5 -17
- letta/server/rest_api/routers/v1/voice.py +18 -27
- letta/server/rest_api/streaming_response.py +5 -2
- letta/server/rest_api/utils.py +187 -25
- letta/server/server.py +27 -22
- letta/server/ws_api/server.py +5 -4
- letta/services/agent_manager.py +148 -26
- letta/services/agent_serialization_manager.py +6 -1
- letta/services/archive_manager.py +168 -15
- letta/services/block_manager.py +14 -4
- letta/services/file_manager.py +33 -29
- letta/services/group_manager.py +10 -0
- letta/services/helpers/agent_manager_helper.py +65 -11
- letta/services/identity_manager.py +105 -4
- letta/services/job_manager.py +11 -1
- letta/services/mcp/base_client.py +2 -2
- letta/services/mcp/oauth_utils.py +33 -8
- letta/services/mcp_manager.py +174 -78
- letta/services/mcp_server_manager.py +1331 -0
- letta/services/message_manager.py +109 -4
- letta/services/organization_manager.py +4 -4
- letta/services/passage_manager.py +9 -25
- letta/services/provider_manager.py +91 -15
- letta/services/run_manager.py +72 -15
- letta/services/sandbox_config_manager.py +45 -3
- letta/services/source_manager.py +15 -8
- letta/services/step_manager.py +24 -1
- letta/services/streaming_service.py +581 -0
- letta/services/summarizer/summarizer.py +1 -1
- letta/services/tool_executor/core_tool_executor.py +111 -0
- letta/services/tool_executor/files_tool_executor.py +5 -3
- letta/services/tool_executor/sandbox_tool_executor.py +2 -2
- letta/services/tool_executor/tool_execution_manager.py +1 -1
- letta/services/tool_manager.py +10 -3
- letta/services/tool_sandbox/base.py +61 -1
- letta/services/tool_sandbox/local_sandbox.py +1 -3
- letta/services/user_manager.py +2 -2
- letta/settings.py +49 -5
- letta/system.py +14 -5
- letta/utils.py +73 -1
- letta/validators.py +105 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/METADATA +4 -2
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/RECORD +157 -151
- letta/schemas/letta_ping.py +0 -28
- letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/WHEEL +0 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from letta.functions.mcp_client.types import (
|
|
8
|
+
MCP_AUTH_HEADER_AUTHORIZATION,
|
|
9
|
+
MCP_AUTH_TOKEN_BEARER_PREFIX,
|
|
10
|
+
MCPServerType,
|
|
11
|
+
SSEServerConfig,
|
|
12
|
+
StdioServerConfig,
|
|
13
|
+
StreamableHTTPServerConfig,
|
|
14
|
+
)
|
|
15
|
+
from letta.orm.mcp_oauth import OAuthSessionStatus
|
|
16
|
+
from letta.schemas.letta_base import LettaBase
|
|
17
|
+
from letta.schemas.secret import Secret
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseMCPServer(LettaBase):
|
|
21
|
+
__id_prefix__ = "mcp_server"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Create Schemas (for POST requests)
|
|
25
|
+
class CreateStdioMCPServer(StdioServerConfig):
|
|
26
|
+
"""Create a new Stdio MCP server"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CreateSSEMCPServer(SSEServerConfig):
|
|
30
|
+
"""Create a new SSE MCP server"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CreateStreamableHTTPMCPServer(StreamableHTTPServerConfig):
|
|
34
|
+
"""Create a new Streamable HTTP MCP server"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
CreateMCPServerUnion = Union[CreateStdioMCPServer, CreateSSEMCPServer, CreateStreamableHTTPMCPServer]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StdioMCPServer(CreateStdioMCPServer):
|
|
41
|
+
"""A Stdio MCP server"""
|
|
42
|
+
|
|
43
|
+
id: str = BaseMCPServer.generate_id_field()
|
|
44
|
+
type: MCPServerType = MCPServerType.STDIO
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SSEMCPServer(CreateSSEMCPServer):
|
|
48
|
+
"""An SSE MCP server"""
|
|
49
|
+
|
|
50
|
+
id: str = BaseMCPServer.generate_id_field()
|
|
51
|
+
type: MCPServerType = MCPServerType.SSE
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class StreamableHTTPMCPServer(CreateStreamableHTTPMCPServer):
|
|
55
|
+
"""A Streamable HTTP MCP server"""
|
|
56
|
+
|
|
57
|
+
id: str = BaseMCPServer.generate_id_field()
|
|
58
|
+
type: MCPServerType = MCPServerType.STREAMABLE_HTTP
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
MCPServerUnion = Union[StdioMCPServer, SSEMCPServer, StreamableHTTPMCPServer]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Update Schemas (for PATCH requests) - same shape as Create/Config, but all fields optional.
|
|
65
|
+
# We exclude fields that aren't persisted on the server model to avoid invalid ORM assignments.
|
|
66
|
+
class UpdateStdioMCPServer(LettaBase):
|
|
67
|
+
"""Update schema for Stdio MCP server - all fields optional"""
|
|
68
|
+
|
|
69
|
+
server_name: Optional[str] = Field(None, description="The name of the MCP server")
|
|
70
|
+
command: Optional[str] = Field(None, description="The command to run the MCP server")
|
|
71
|
+
args: Optional[List[str]] = Field(None, description="The arguments to pass to the command")
|
|
72
|
+
env: Optional[Dict[str, str]] = Field(None, description="Environment variables to set")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class UpdateSSEMCPServer(LettaBase):
|
|
76
|
+
"""Update schema for SSE MCP server - all fields optional"""
|
|
77
|
+
|
|
78
|
+
server_name: Optional[str] = Field(None, description="The name of the MCP server")
|
|
79
|
+
server_url: Optional[str] = Field(None, description="The URL of the SSE MCP server")
|
|
80
|
+
# Accept both `auth_token` (API surface) and `token` (internal ORM naming)
|
|
81
|
+
auth_token: Optional[str] = Field(None, description="The authentication token or API key value")
|
|
82
|
+
token: Optional[str] = Field(None, description="The authentication token (internal)")
|
|
83
|
+
auth_header: Optional[str] = Field(None, description="The name of the authentication header (e.g., 'Authorization')")
|
|
84
|
+
custom_headers: Optional[Dict[str, str]] = Field(None, description="Custom headers to send with requests")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class UpdateStreamableHTTPMCPServer(LettaBase):
|
|
88
|
+
"""Update schema for Streamable HTTP MCP server - all fields optional"""
|
|
89
|
+
|
|
90
|
+
server_name: Optional[str] = Field(None, description="The name of the MCP server")
|
|
91
|
+
server_url: Optional[str] = Field(None, description="The URL of the Streamable HTTP MCP server")
|
|
92
|
+
# Accept both `auth_token` (API surface) and `token` (internal ORM naming)
|
|
93
|
+
auth_token: Optional[str] = Field(None, description="The authentication token or API key value")
|
|
94
|
+
token: Optional[str] = Field(None, description="The authentication token (internal)")
|
|
95
|
+
auth_header: Optional[str] = Field(None, description="The name of the authentication header (e.g., 'Authorization')")
|
|
96
|
+
custom_headers: Optional[Dict[str, str]] = Field(None, description="Custom headers to send with requests")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
UpdateMCPServerUnion = Union[UpdateStdioMCPServer, UpdateSSEMCPServer, UpdateStreamableHTTPMCPServer]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# OAuth-related schemas
|
|
103
|
+
class BaseMCPOAuth(LettaBase):
|
|
104
|
+
__id_prefix__ = "mcp-oauth"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class MCPOAuthSession(BaseMCPOAuth):
|
|
108
|
+
"""OAuth session for MCP server authentication."""
|
|
109
|
+
|
|
110
|
+
id: str = BaseMCPOAuth.generate_id_field()
|
|
111
|
+
state: str = Field(..., description="OAuth state parameter")
|
|
112
|
+
server_id: Optional[str] = Field(None, description="MCP server ID")
|
|
113
|
+
server_url: str = Field(..., description="MCP server URL")
|
|
114
|
+
server_name: str = Field(..., description="MCP server display name")
|
|
115
|
+
|
|
116
|
+
# User and organization context
|
|
117
|
+
user_id: Optional[str] = Field(None, description="User ID associated with the session")
|
|
118
|
+
organization_id: str = Field(..., description="Organization ID associated with the session")
|
|
119
|
+
|
|
120
|
+
# OAuth flow data
|
|
121
|
+
authorization_url: Optional[str] = Field(None, description="OAuth authorization URL")
|
|
122
|
+
authorization_code: Optional[str] = Field(None, description="OAuth authorization code")
|
|
123
|
+
|
|
124
|
+
# Encrypted authorization code (for internal use)
|
|
125
|
+
authorization_code_enc: Secret | None = Field(None, description="Encrypted OAuth authorization code as Secret object")
|
|
126
|
+
|
|
127
|
+
# Token data
|
|
128
|
+
access_token: Optional[str] = Field(None, description="OAuth access token")
|
|
129
|
+
refresh_token: Optional[str] = Field(None, description="OAuth refresh token")
|
|
130
|
+
token_type: str = Field(default="Bearer", description="Token type")
|
|
131
|
+
expires_at: Optional[datetime] = Field(None, description="Token expiry time")
|
|
132
|
+
scope: Optional[str] = Field(None, description="OAuth scope")
|
|
133
|
+
|
|
134
|
+
# Encrypted token fields (for internal use)
|
|
135
|
+
access_token_enc: Secret | None = Field(None, description="Encrypted OAuth access token as Secret object")
|
|
136
|
+
refresh_token_enc: Secret | None = Field(None, description="Encrypted OAuth refresh token as Secret object")
|
|
137
|
+
|
|
138
|
+
# Client configuration
|
|
139
|
+
client_id: Optional[str] = Field(None, description="OAuth client ID")
|
|
140
|
+
client_secret: Optional[str] = Field(None, description="OAuth client secret")
|
|
141
|
+
redirect_uri: Optional[str] = Field(None, description="OAuth redirect URI")
|
|
142
|
+
|
|
143
|
+
# Encrypted client secret (for internal use)
|
|
144
|
+
client_secret_enc: Secret | None = Field(None, description="Encrypted OAuth client secret as Secret object")
|
|
145
|
+
|
|
146
|
+
# Session state
|
|
147
|
+
status: OAuthSessionStatus = Field(default=OAuthSessionStatus.PENDING, description="Session status")
|
|
148
|
+
|
|
149
|
+
# Timestamps
|
|
150
|
+
created_at: datetime = Field(default_factory=datetime.now, description="Session creation time")
|
|
151
|
+
updated_at: datetime = Field(default_factory=datetime.now, description="Last update time")
|
|
152
|
+
|
|
153
|
+
def get_access_token_secret(self) -> Secret:
|
|
154
|
+
"""Get the access token as a Secret object, preferring encrypted over plaintext."""
|
|
155
|
+
if self.access_token_enc is not None:
|
|
156
|
+
return self.access_token_enc
|
|
157
|
+
return Secret.from_db(None, self.access_token)
|
|
158
|
+
|
|
159
|
+
def get_refresh_token_secret(self) -> Secret:
|
|
160
|
+
"""Get the refresh token as a Secret object, preferring encrypted over plaintext."""
|
|
161
|
+
if self.refresh_token_enc is not None:
|
|
162
|
+
return self.refresh_token_enc
|
|
163
|
+
return Secret.from_db(None, self.refresh_token)
|
|
164
|
+
|
|
165
|
+
def get_client_secret_secret(self) -> Secret:
|
|
166
|
+
"""Get the client secret as a Secret object, preferring encrypted over plaintext."""
|
|
167
|
+
if self.client_secret_enc is not None:
|
|
168
|
+
return self.client_secret_enc
|
|
169
|
+
return Secret.from_db(None, self.client_secret)
|
|
170
|
+
|
|
171
|
+
def get_authorization_code_secret(self) -> Secret:
|
|
172
|
+
"""Get the authorization code as a Secret object, preferring encrypted over plaintext."""
|
|
173
|
+
if self.authorization_code_enc is not None:
|
|
174
|
+
return self.authorization_code_enc
|
|
175
|
+
return Secret.from_db(None, self.authorization_code)
|
|
176
|
+
|
|
177
|
+
def set_access_token_secret(self, secret: Secret) -> None:
|
|
178
|
+
"""Set access token from a Secret object."""
|
|
179
|
+
self.access_token_enc = secret
|
|
180
|
+
secret_dict = secret.to_dict()
|
|
181
|
+
if not secret.was_encrypted:
|
|
182
|
+
self.access_token = secret_dict["plaintext"]
|
|
183
|
+
else:
|
|
184
|
+
self.access_token = None
|
|
185
|
+
|
|
186
|
+
def set_refresh_token_secret(self, secret: Secret) -> None:
|
|
187
|
+
"""Set refresh token from a Secret object."""
|
|
188
|
+
self.refresh_token_enc = secret
|
|
189
|
+
secret_dict = secret.to_dict()
|
|
190
|
+
if not secret.was_encrypted:
|
|
191
|
+
self.refresh_token = secret_dict["plaintext"]
|
|
192
|
+
else:
|
|
193
|
+
self.refresh_token = None
|
|
194
|
+
|
|
195
|
+
def set_client_secret_secret(self, secret: Secret) -> None:
|
|
196
|
+
"""Set client secret from a Secret object."""
|
|
197
|
+
self.client_secret_enc = secret
|
|
198
|
+
secret_dict = secret.to_dict()
|
|
199
|
+
if not secret.was_encrypted:
|
|
200
|
+
self.client_secret = secret_dict["plaintext"]
|
|
201
|
+
else:
|
|
202
|
+
self.client_secret = None
|
|
203
|
+
|
|
204
|
+
def set_authorization_code_secret(self, secret: Secret) -> None:
|
|
205
|
+
"""Set authorization code from a Secret object."""
|
|
206
|
+
self.authorization_code_enc = secret
|
|
207
|
+
secret_dict = secret.to_dict()
|
|
208
|
+
if not secret.was_encrypted:
|
|
209
|
+
self.authorization_code = secret_dict["plaintext"]
|
|
210
|
+
else:
|
|
211
|
+
self.authorization_code = None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class MCPOAuthSessionCreate(BaseMCPOAuth):
|
|
215
|
+
"""Create a new OAuth session."""
|
|
216
|
+
|
|
217
|
+
server_url: str = Field(..., description="MCP server URL")
|
|
218
|
+
server_name: str = Field(..., description="MCP server display name")
|
|
219
|
+
user_id: Optional[str] = Field(None, description="User ID associated with the session")
|
|
220
|
+
organization_id: str = Field(..., description="Organization ID associated with the session")
|
|
221
|
+
state: Optional[str] = Field(None, description="OAuth state parameter")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class MCPOAuthSessionUpdate(BaseMCPOAuth):
|
|
225
|
+
"""Update an existing OAuth session."""
|
|
226
|
+
|
|
227
|
+
authorization_url: Optional[str] = Field(None, description="OAuth authorization URL")
|
|
228
|
+
authorization_code: Optional[str] = Field(None, description="OAuth authorization code")
|
|
229
|
+
access_token: Optional[str] = Field(None, description="OAuth access token")
|
|
230
|
+
refresh_token: Optional[str] = Field(None, description="OAuth refresh token")
|
|
231
|
+
token_type: Optional[str] = Field(None, description="Token type")
|
|
232
|
+
expires_at: Optional[datetime] = Field(None, description="Token expiry time")
|
|
233
|
+
scope: Optional[str] = Field(None, description="OAuth scope")
|
|
234
|
+
client_id: Optional[str] = Field(None, description="OAuth client ID")
|
|
235
|
+
client_secret: Optional[str] = Field(None, description="OAuth client secret")
|
|
236
|
+
redirect_uri: Optional[str] = Field(None, description="OAuth redirect URI")
|
|
237
|
+
status: Optional[OAuthSessionStatus] = Field(None, description="Session status")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class MCPServerResyncResult(LettaBase):
|
|
241
|
+
"""Result of resyncing MCP server tools."""
|
|
242
|
+
|
|
243
|
+
deleted: List[str] = Field(default_factory=list, description="List of deleted tool names")
|
|
244
|
+
updated: List[str] = Field(default_factory=list, description="List of updated tool names")
|
|
245
|
+
added: List[str] = Field(default_factory=list, description="List of added tool names")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class MCPToolExecuteRequest(LettaBase):
|
|
249
|
+
"""Request to execute an MCP tool by IDs."""
|
|
250
|
+
|
|
251
|
+
args: Dict[str, Any] = Field(default_factory=dict, description="Arguments to pass to the MCP tool")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def convert_generic_to_union(server) -> MCPServerUnion:
|
|
255
|
+
"""
|
|
256
|
+
Convert a generic MCPServer (from letta.schemas.mcp) to the appropriate MCPServerUnion type
|
|
257
|
+
based on the server_type field.
|
|
258
|
+
|
|
259
|
+
This is used to convert internal MCPServer representations to the API response types.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
server: A GenericMCPServer instance from letta.schemas.mcp
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
The appropriate MCPServerUnion type (StdioMCPServer, SSEMCPServer, or StreamableHTTPMCPServer)
|
|
266
|
+
"""
|
|
267
|
+
# Import here to avoid circular dependency
|
|
268
|
+
from letta.schemas.mcp import MCPServer as GenericMCPServer
|
|
269
|
+
|
|
270
|
+
if not isinstance(server, GenericMCPServer):
|
|
271
|
+
raise TypeError(f"Expected GenericMCPServer, got {type(server)}")
|
|
272
|
+
|
|
273
|
+
if server.server_type == MCPServerType.STDIO:
|
|
274
|
+
return StdioMCPServer(
|
|
275
|
+
id=server.id,
|
|
276
|
+
server_name=server.server_name,
|
|
277
|
+
type=MCPServerType.STDIO,
|
|
278
|
+
command=server.stdio_config.command if server.stdio_config else None,
|
|
279
|
+
args=server.stdio_config.args if server.stdio_config else None,
|
|
280
|
+
env=server.stdio_config.env if server.stdio_config else None,
|
|
281
|
+
)
|
|
282
|
+
elif server.server_type == MCPServerType.SSE:
|
|
283
|
+
return SSEMCPServer(
|
|
284
|
+
id=server.id,
|
|
285
|
+
server_name=server.server_name,
|
|
286
|
+
type=MCPServerType.SSE,
|
|
287
|
+
server_url=server.server_url,
|
|
288
|
+
auth_header="Authorization" if server.token else None,
|
|
289
|
+
auth_token=f"Bearer {server.token}" if server.token else None,
|
|
290
|
+
custom_headers=server.custom_headers,
|
|
291
|
+
)
|
|
292
|
+
elif server.server_type == MCPServerType.STREAMABLE_HTTP:
|
|
293
|
+
return StreamableHTTPMCPServer(
|
|
294
|
+
id=server.id,
|
|
295
|
+
server_name=server.server_name,
|
|
296
|
+
type=MCPServerType.STREAMABLE_HTTP,
|
|
297
|
+
server_url=server.server_url,
|
|
298
|
+
auth_header="Authorization" if server.token else None,
|
|
299
|
+
auth_token=f"Bearer {server.token}" if server.token else None,
|
|
300
|
+
custom_headers=server.custom_headers,
|
|
301
|
+
)
|
|
302
|
+
else:
|
|
303
|
+
raise ValueError(f"Unknown server type: {server.server_type}")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def convert_update_to_internal(request: Union[UpdateStdioMCPServer, UpdateSSEMCPServer, UpdateStreamableHTTPMCPServer]):
|
|
307
|
+
"""Convert external API update models to internal UpdateMCPServer union used by the manager.
|
|
308
|
+
|
|
309
|
+
- Flattens stdio fields into StdioServerConfig inside UpdateStdioMCPServer
|
|
310
|
+
- Maps `auth_token` to `token` for HTTP-based transports
|
|
311
|
+
- Ignores `auth_header` at update time (header is derived from token)
|
|
312
|
+
"""
|
|
313
|
+
# Local import to avoid circulars
|
|
314
|
+
from letta.functions.mcp_client.types import MCPServerType as MCPType, StdioServerConfig as StdioCfg
|
|
315
|
+
from letta.schemas.mcp import (
|
|
316
|
+
UpdateSSEMCPServer as InternalUpdateSSE,
|
|
317
|
+
UpdateStdioMCPServer as InternalUpdateStdio,
|
|
318
|
+
UpdateStreamableHTTPMCPServer as InternalUpdateHTTP,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if isinstance(request, UpdateStdioMCPServer):
|
|
322
|
+
stdio_cfg = None
|
|
323
|
+
# Only build stdio_config if command and args are explicitly provided to avoid overwriting existing config
|
|
324
|
+
if request.command is not None and request.args is not None:
|
|
325
|
+
stdio_cfg = StdioCfg(
|
|
326
|
+
server_name=request.server_name or "",
|
|
327
|
+
type=MCPType.STDIO,
|
|
328
|
+
command=request.command,
|
|
329
|
+
args=request.args,
|
|
330
|
+
env=request.env,
|
|
331
|
+
)
|
|
332
|
+
kwargs: dict = {}
|
|
333
|
+
if request.server_name is not None:
|
|
334
|
+
kwargs["server_name"] = request.server_name
|
|
335
|
+
if stdio_cfg is not None:
|
|
336
|
+
kwargs["stdio_config"] = stdio_cfg
|
|
337
|
+
return InternalUpdateStdio(**kwargs)
|
|
338
|
+
elif isinstance(request, UpdateSSEMCPServer):
|
|
339
|
+
token_value = request.auth_token or request.token
|
|
340
|
+
return InternalUpdateSSE(
|
|
341
|
+
server_name=request.server_name, server_url=request.server_url, token=token_value, custom_headers=request.custom_headers
|
|
342
|
+
)
|
|
343
|
+
elif isinstance(request, UpdateStreamableHTTPMCPServer):
|
|
344
|
+
token_value = request.auth_token or request.token
|
|
345
|
+
return InternalUpdateHTTP(
|
|
346
|
+
server_name=request.server_name, server_url=request.server_url, auth_token=token_value, custom_headers=request.custom_headers
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
raise TypeError(f"Unsupported update request type: {type(request)}")
|
letta/schemas/memory.py
CHANGED
|
@@ -4,6 +4,10 @@ from datetime import datetime
|
|
|
4
4
|
from io import StringIO
|
|
5
5
|
from typing import TYPE_CHECKING, List, Optional, Union
|
|
6
6
|
|
|
7
|
+
from letta.log import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
7
11
|
from openai.types.beta.function_tool import FunctionTool as OpenAITool
|
|
8
12
|
from pydantic import BaseModel, Field, field_validator
|
|
9
13
|
|
|
@@ -153,11 +157,11 @@ class Memory(BaseModel, validate_assignment=True):
|
|
|
153
157
|
s.write(f"\n- chars_current={len(value)}")
|
|
154
158
|
s.write(f"\n- chars_limit={limit}\n")
|
|
155
159
|
s.write("</metadata>\n")
|
|
160
|
+
s.write(f"<warning>\n{CORE_MEMORY_LINE_NUMBER_WARNING}\n</warning>\n")
|
|
156
161
|
s.write("<value>\n")
|
|
157
|
-
s.write(f"{CORE_MEMORY_LINE_NUMBER_WARNING}\n")
|
|
158
162
|
if value:
|
|
159
163
|
for i, line in enumerate(value.split("\n"), start=1):
|
|
160
|
-
s.write(f"
|
|
164
|
+
s.write(f"{i}→ {line}\n")
|
|
161
165
|
s.write("</value>\n")
|
|
162
166
|
s.write(f"</{label}>\n")
|
|
163
167
|
if idx != len(self.blocks) - 1:
|
|
@@ -264,14 +268,21 @@ class Memory(BaseModel, validate_assignment=True):
|
|
|
264
268
|
s.write("</directory>\n")
|
|
265
269
|
s.write("</directories>")
|
|
266
270
|
|
|
267
|
-
def compile(self, tool_usage_rules=None, sources=None, max_files_open=None) -> str:
|
|
271
|
+
def compile(self, tool_usage_rules=None, sources=None, max_files_open=None, llm_config=None) -> str:
|
|
268
272
|
"""Efficiently render memory, tool rules, and sources into a prompt string."""
|
|
269
273
|
s = StringIO()
|
|
270
274
|
|
|
271
275
|
raw_type = self.agent_type.value if hasattr(self.agent_type, "value") else (self.agent_type or "")
|
|
272
276
|
norm_type = raw_type.lower()
|
|
273
277
|
is_react = norm_type in ("react_agent", "workflow_agent")
|
|
274
|
-
|
|
278
|
+
|
|
279
|
+
# Check if we should use line numbers based on both agent type and model provider
|
|
280
|
+
is_line_numbered = False # Default to no line numbers
|
|
281
|
+
if llm_config and hasattr(llm_config, "model_endpoint_type"):
|
|
282
|
+
is_anthropic = llm_config.model_endpoint_type == "anthropic"
|
|
283
|
+
is_line_numbered_agent_type = norm_type in ("sleeptime_agent", "memgpt_v2_agent", "letta_v1_agent")
|
|
284
|
+
# Only use line numbers for specific agent types AND Anthropic models
|
|
285
|
+
is_line_numbered = is_line_numbered_agent_type and is_anthropic
|
|
275
286
|
|
|
276
287
|
# Memory blocks (not for react/workflow). Always include wrapper for preview/tests.
|
|
277
288
|
if not is_react:
|
|
@@ -297,22 +308,23 @@ class Memory(BaseModel, validate_assignment=True):
|
|
|
297
308
|
return s.getvalue()
|
|
298
309
|
|
|
299
310
|
@trace_method
|
|
300
|
-
async def compile_async(self, tool_usage_rules=None, sources=None, max_files_open=None) -> str:
|
|
311
|
+
async def compile_async(self, tool_usage_rules=None, sources=None, max_files_open=None, llm_config=None) -> str:
|
|
301
312
|
"""Async version that offloads to a thread for CPU-bound string building."""
|
|
302
313
|
return await asyncio.to_thread(
|
|
303
314
|
self.compile,
|
|
304
315
|
tool_usage_rules=tool_usage_rules,
|
|
305
316
|
sources=sources,
|
|
306
317
|
max_files_open=max_files_open,
|
|
318
|
+
llm_config=llm_config,
|
|
307
319
|
)
|
|
308
320
|
|
|
309
321
|
@trace_method
|
|
310
|
-
async def compile_in_thread_async(self, tool_usage_rules=None, sources=None, max_files_open=None) -> str:
|
|
322
|
+
async def compile_in_thread_async(self, tool_usage_rules=None, sources=None, max_files_open=None, llm_config=None) -> str:
|
|
311
323
|
"""Deprecated: use compile() instead."""
|
|
312
324
|
import warnings
|
|
313
325
|
|
|
314
|
-
|
|
315
|
-
return self.compile(tool_usage_rules=tool_usage_rules, sources=sources, max_files_open=max_files_open)
|
|
326
|
+
logger.warning("compile_in_thread_async is deprecated; use compile()", stacklevel=2)
|
|
327
|
+
return self.compile(tool_usage_rules=tool_usage_rules, sources=sources, max_files_open=max_files_open, llm_config=llm_config)
|
|
316
328
|
|
|
317
329
|
def list_block_labels(self) -> List[str]:
|
|
318
330
|
"""Return a list of the block names held inside the memory object"""
|