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
letta/schemas/letta_message.py
CHANGED
|
@@ -17,6 +17,34 @@ from letta.schemas.letta_message_content import (
|
|
|
17
17
|
# ---------------------------
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class MessageReturnType(str, Enum):
|
|
21
|
+
approval = "approval"
|
|
22
|
+
tool = "tool"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MessageReturn(BaseModel):
|
|
26
|
+
type: MessageReturnType = Field(..., description="The message type to be created.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ApprovalReturn(MessageReturn):
|
|
30
|
+
type: Literal[MessageReturnType.approval] = Field(default=MessageReturnType.approval, description="The message type to be created.")
|
|
31
|
+
tool_call_id: str = Field(..., description="The ID of the tool call that corresponds to this approval")
|
|
32
|
+
approve: bool = Field(..., description="Whether the tool has been approved")
|
|
33
|
+
reason: Optional[str] = Field(None, description="An optional explanation for the provided approval status")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ToolReturn(MessageReturn):
|
|
37
|
+
type: Literal[MessageReturnType.tool] = Field(default=MessageReturnType.tool, description="The message type to be created.")
|
|
38
|
+
tool_return: str
|
|
39
|
+
status: Literal["success", "error"]
|
|
40
|
+
tool_call_id: str
|
|
41
|
+
stdout: Optional[List[str]] = None
|
|
42
|
+
stderr: Optional[List[str]] = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
LettaMessageReturnUnion = Annotated[Union[ApprovalReturn, ToolReturn], Field(discriminator="type")]
|
|
46
|
+
|
|
47
|
+
|
|
20
48
|
class MessageType(str, Enum):
|
|
21
49
|
system_message = "system_message"
|
|
22
50
|
user_message = "user_message"
|
|
@@ -233,14 +261,6 @@ class ToolCallMessage(LettaMessage):
|
|
|
233
261
|
return v
|
|
234
262
|
|
|
235
263
|
|
|
236
|
-
class ToolReturn(BaseModel):
|
|
237
|
-
tool_return: str
|
|
238
|
-
status: Literal["success", "error"]
|
|
239
|
-
tool_call_id: str
|
|
240
|
-
stdout: Optional[List[str]] = None
|
|
241
|
-
stderr: Optional[List[str]] = None
|
|
242
|
-
|
|
243
|
-
|
|
244
264
|
class ToolReturnMessage(LettaMessage):
|
|
245
265
|
"""
|
|
246
266
|
A message representing the return value of a tool call (generated by Letta executing the requested tool).
|
|
@@ -282,7 +302,12 @@ class ApprovalRequestMessage(LettaMessage):
|
|
|
282
302
|
message_type: Literal[MessageType.approval_request_message] = Field(
|
|
283
303
|
default=MessageType.approval_request_message, description="The type of the message."
|
|
284
304
|
)
|
|
285
|
-
tool_call: Union[ToolCall, ToolCallDelta] = Field(
|
|
305
|
+
tool_call: Union[ToolCall, ToolCallDelta] = Field(
|
|
306
|
+
..., description="The tool call that has been requested by the llm to run", deprecated=True
|
|
307
|
+
)
|
|
308
|
+
tool_calls: Optional[Union[List[ToolCall], ToolCallDelta]] = Field(
|
|
309
|
+
None, description="The tool calls that have been requested by the llm to run, which are pending approval"
|
|
310
|
+
)
|
|
286
311
|
|
|
287
312
|
|
|
288
313
|
class ApprovalResponseMessage(LettaMessage):
|
|
@@ -301,9 +326,10 @@ class ApprovalResponseMessage(LettaMessage):
|
|
|
301
326
|
message_type: Literal[MessageType.approval_response_message] = Field(
|
|
302
327
|
default=MessageType.approval_response_message, description="The type of the message."
|
|
303
328
|
)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
329
|
+
approvals: Optional[List[LettaMessageReturnUnion]] = Field(default=None, description="The list of approval responses")
|
|
330
|
+
approve: Optional[bool] = Field(None, description="Whether the tool has been approved", deprecated=True)
|
|
331
|
+
approval_request_id: Optional[str] = Field(None, description="The message ID of the approval request", deprecated=True)
|
|
332
|
+
reason: Optional[str] = Field(None, description="An optional explanation for the provided approval status", deprecated=True)
|
|
307
333
|
|
|
308
334
|
|
|
309
335
|
class AssistantMessage(LettaMessage):
|
|
@@ -327,6 +353,21 @@ class AssistantMessage(LettaMessage):
|
|
|
327
353
|
)
|
|
328
354
|
|
|
329
355
|
|
|
356
|
+
class LettaPing(LettaMessage):
|
|
357
|
+
"""
|
|
358
|
+
A ping message used as a keepalive to prevent SSE streams from timing out during long running requests.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
id (str): The ID of the message
|
|
362
|
+
date (datetime): The date the message was created in ISO format
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
message_type: Literal["ping"] = Field(
|
|
366
|
+
"ping",
|
|
367
|
+
description="The type of the message. Ping messages are a keep-alive to prevent SSE streams from timing out during long running requests.",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
330
371
|
# NOTE: use Pydantic's discriminated unions feature: https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions
|
|
331
372
|
LettaMessageUnion = Annotated[
|
|
332
373
|
Union[
|
|
@@ -374,6 +415,24 @@ def create_letta_message_union_schema():
|
|
|
374
415
|
}
|
|
375
416
|
|
|
376
417
|
|
|
418
|
+
def create_letta_ping_schema():
|
|
419
|
+
return {
|
|
420
|
+
"properties": {
|
|
421
|
+
"message_type": {
|
|
422
|
+
"type": "string",
|
|
423
|
+
"const": "ping",
|
|
424
|
+
"title": "Message Type",
|
|
425
|
+
"description": "The type of the message.",
|
|
426
|
+
"default": "ping",
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
"type": "object",
|
|
430
|
+
"required": ["message_type"],
|
|
431
|
+
"title": "LettaPing",
|
|
432
|
+
"description": "Ping messages are a keep-alive to prevent SSE streams from timing out during long running requests.",
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
|
|
377
436
|
# --------------------------
|
|
378
437
|
# Message Update API Schemas
|
|
379
438
|
# --------------------------
|
letta/schemas/letta_request.py
CHANGED
|
@@ -15,15 +15,18 @@ class LettaRequest(BaseModel):
|
|
|
15
15
|
)
|
|
16
16
|
use_assistant_message: bool = Field(
|
|
17
17
|
default=True,
|
|
18
|
-
description="Whether the server should parse specific tool call arguments (default `send_message`) as `AssistantMessage` objects.",
|
|
18
|
+
description="Whether the server should parse specific tool call arguments (default `send_message`) as `AssistantMessage` objects. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.",
|
|
19
|
+
deprecated=True,
|
|
19
20
|
)
|
|
20
21
|
assistant_message_tool_name: str = Field(
|
|
21
22
|
default=DEFAULT_MESSAGE_TOOL,
|
|
22
|
-
description="The name of the designated message tool.",
|
|
23
|
+
description="The name of the designated message tool. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.",
|
|
24
|
+
deprecated=True,
|
|
23
25
|
)
|
|
24
26
|
assistant_message_tool_kwarg: str = Field(
|
|
25
27
|
default=DEFAULT_MESSAGE_TOOL_KWARG,
|
|
26
|
-
description="The name of the message argument in the designated message tool.",
|
|
28
|
+
description="The name of the message argument in the designated message tool. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.",
|
|
29
|
+
deprecated=True,
|
|
27
30
|
)
|
|
28
31
|
|
|
29
32
|
# filter to only return specific message types
|
|
@@ -34,6 +37,7 @@ class LettaRequest(BaseModel):
|
|
|
34
37
|
enable_thinking: str = Field(
|
|
35
38
|
default=True,
|
|
36
39
|
description="If set to True, enables reasoning before responses or tool calls from the agent.",
|
|
40
|
+
deprecated=True,
|
|
37
41
|
)
|
|
38
42
|
|
|
39
43
|
@field_validator("messages", mode="before")
|
|
@@ -48,28 +48,3 @@ class LettaStopReason(BaseModel):
|
|
|
48
48
|
|
|
49
49
|
message_type: Literal["stop_reason"] = Field("stop_reason", description="The type of the message.")
|
|
50
50
|
stop_reason: StopReasonType = Field(..., description="The reason why execution stopped.")
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def create_letta_ping_schema():
|
|
54
|
-
return {
|
|
55
|
-
"properties": {
|
|
56
|
-
"message_type": {
|
|
57
|
-
"type": "string",
|
|
58
|
-
"const": "ping",
|
|
59
|
-
"title": "Message Type",
|
|
60
|
-
"description": "The type of the message.",
|
|
61
|
-
"default": "ping",
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
"type": "object",
|
|
65
|
-
"required": ["message_type"],
|
|
66
|
-
"title": "LettaPing",
|
|
67
|
-
"description": "Ping messages are a keep-alive to prevent SSE streams from timing out during long running requests.",
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class LettaPing(BaseModel):
|
|
72
|
-
message_type: Literal["ping"] = Field(
|
|
73
|
-
"ping",
|
|
74
|
-
description="The type of the message. Ping messages are a keep-alive to prevent SSE streams from timing out during long running requests.",
|
|
75
|
-
)
|
letta/schemas/llm_config.py
CHANGED
|
@@ -13,6 +13,8 @@ class LLMConfig(BaseModel):
|
|
|
13
13
|
"""Configuration for Language Model (LLM) connection and generation parameters."""
|
|
14
14
|
|
|
15
15
|
model: str = Field(..., description="LLM model name. ")
|
|
16
|
+
display_name: Optional[str] = Field(None, description="A human-friendly display name for the model.")
|
|
17
|
+
|
|
16
18
|
model_endpoint_type: Literal[
|
|
17
19
|
"openai",
|
|
18
20
|
"anthropic",
|
|
@@ -78,6 +80,7 @@ class LLMConfig(BaseModel):
|
|
|
78
80
|
|
|
79
81
|
# FIXME hack to silence pydantic protected namespace warning
|
|
80
82
|
model_config = ConfigDict(protected_namespaces=())
|
|
83
|
+
parallel_tool_calls: Optional[bool] = Field(False, description="If set to True, enables parallel tool calling. Defaults to False.")
|
|
81
84
|
|
|
82
85
|
@model_validator(mode="before")
|
|
83
86
|
@classmethod
|
|
@@ -151,7 +154,10 @@ class LLMConfig(BaseModel):
|
|
|
151
154
|
values["put_inner_thoughts_in_kwargs"] = False
|
|
152
155
|
|
|
153
156
|
if values.get("model_endpoint_type") == "anthropic" and (
|
|
154
|
-
model.startswith("claude-3-7-sonnet")
|
|
157
|
+
model.startswith("claude-3-7-sonnet")
|
|
158
|
+
or model.startswith("claude-sonnet-4")
|
|
159
|
+
or model.startswith("claude-opus-4")
|
|
160
|
+
or model.startswith("claude-haiku-4-5")
|
|
155
161
|
):
|
|
156
162
|
values["put_inner_thoughts_in_kwargs"] = False
|
|
157
163
|
|
|
@@ -239,7 +245,7 @@ class LLMConfig(BaseModel):
|
|
|
239
245
|
config.model.startswith("claude-opus-4")
|
|
240
246
|
or config.model.startswith("claude-sonnet-4")
|
|
241
247
|
or config.model.startswith("claude-3-7-sonnet")
|
|
242
|
-
or config.model.startswith("claude-4-5
|
|
248
|
+
or config.model.startswith("claude-haiku-4-5")
|
|
243
249
|
)
|
|
244
250
|
|
|
245
251
|
@classmethod
|
letta/schemas/mcp.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from datetime import datetime
|
|
2
3
|
from typing import Any, Dict, List, Optional, Union
|
|
3
4
|
|
|
@@ -13,7 +14,7 @@ from letta.functions.mcp_client.types import (
|
|
|
13
14
|
)
|
|
14
15
|
from letta.orm.mcp_oauth import OAuthSessionStatus
|
|
15
16
|
from letta.schemas.letta_base import LettaBase
|
|
16
|
-
from letta.schemas.secret import Secret
|
|
17
|
+
from letta.schemas.secret import Secret
|
|
17
18
|
from letta.settings import settings
|
|
18
19
|
|
|
19
20
|
|
|
@@ -31,8 +32,8 @@ class MCPServer(BaseMCPServer):
|
|
|
31
32
|
token: Optional[str] = Field(None, description="The access token or API key for the MCP server (used for authentication)")
|
|
32
33
|
custom_headers: Optional[Dict[str, str]] = Field(None, description="Custom authentication headers as key-value pairs")
|
|
33
34
|
|
|
34
|
-
token_enc:
|
|
35
|
-
custom_headers_enc:
|
|
35
|
+
token_enc: Secret | None = Field(None, description="Encrypted token as Secret object")
|
|
36
|
+
custom_headers_enc: Secret | None = Field(None, description="Encrypted custom headers as Secret object")
|
|
36
37
|
|
|
37
38
|
# stdio config
|
|
38
39
|
stdio_config: Optional[StdioServerConfig] = Field(
|
|
@@ -48,55 +49,55 @@ class MCPServer(BaseMCPServer):
|
|
|
48
49
|
|
|
49
50
|
def get_token_secret(self) -> Secret:
|
|
50
51
|
"""Get the token as a Secret object, preferring encrypted over plaintext."""
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
if self.token_enc is not None:
|
|
53
|
+
return self.token_enc
|
|
54
|
+
return Secret.from_db(None, self.token)
|
|
55
|
+
|
|
56
|
+
def get_custom_headers_secret(self) -> Secret:
|
|
57
|
+
"""Get custom headers as a Secret object (stores JSON string), preferring encrypted over plaintext."""
|
|
58
|
+
if self.custom_headers_enc is not None:
|
|
59
|
+
return self.custom_headers_enc
|
|
60
|
+
# Fallback: convert plaintext dict to JSON string and wrap in Secret
|
|
61
|
+
if self.custom_headers is not None:
|
|
62
|
+
json_str = json.dumps(self.custom_headers)
|
|
63
|
+
return Secret.from_plaintext(json_str)
|
|
64
|
+
return Secret.from_plaintext(None)
|
|
65
|
+
|
|
66
|
+
def get_custom_headers_dict(self) -> Optional[Dict[str, str]]:
|
|
67
|
+
"""Get custom headers as a plaintext dictionary."""
|
|
68
|
+
secret = self.get_custom_headers_secret()
|
|
69
|
+
json_str = secret.get_plaintext()
|
|
70
|
+
if json_str:
|
|
71
|
+
try:
|
|
72
|
+
return json.loads(json_str)
|
|
73
|
+
except (json.JSONDecodeError, TypeError):
|
|
74
|
+
return None
|
|
75
|
+
return None
|
|
56
76
|
|
|
57
77
|
def set_token_secret(self, secret: Secret) -> None:
|
|
58
78
|
"""Set token from a Secret object, updating both encrypted and plaintext fields."""
|
|
79
|
+
self.token_enc = secret
|
|
59
80
|
secret_dict = secret.to_dict()
|
|
60
|
-
self.token_enc = secret_dict["encrypted"]
|
|
61
81
|
# Only set plaintext during migration phase
|
|
62
|
-
if not secret.
|
|
82
|
+
if not secret.was_encrypted:
|
|
63
83
|
self.token = secret_dict["plaintext"]
|
|
64
84
|
else:
|
|
65
85
|
self.token = None
|
|
66
86
|
|
|
67
|
-
def set_custom_headers_secret(self, secret:
|
|
68
|
-
"""Set custom headers from a
|
|
87
|
+
def set_custom_headers_secret(self, secret: Secret) -> None:
|
|
88
|
+
"""Set custom headers from a Secret object (containing JSON string), updating both fields."""
|
|
89
|
+
self.custom_headers_enc = secret
|
|
69
90
|
secret_dict = secret.to_dict()
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if not secret.
|
|
73
|
-
|
|
91
|
+
# Parse JSON string to dict for plaintext field
|
|
92
|
+
json_str = secret_dict.get("plaintext")
|
|
93
|
+
if json_str and not secret.was_encrypted:
|
|
94
|
+
try:
|
|
95
|
+
self.custom_headers = json.loads(json_str)
|
|
96
|
+
except (json.JSONDecodeError, TypeError):
|
|
97
|
+
self.custom_headers = None
|
|
74
98
|
else:
|
|
75
99
|
self.custom_headers = None
|
|
76
100
|
|
|
77
|
-
def model_dump(self, to_orm: bool = False, **kwargs):
|
|
78
|
-
"""Override model_dump to handle encryption when saving to database."""
|
|
79
|
-
data = super().model_dump(to_orm=to_orm, **kwargs)
|
|
80
|
-
|
|
81
|
-
if to_orm and settings.encryption_key:
|
|
82
|
-
# Encrypt token if present
|
|
83
|
-
if self.token is not None:
|
|
84
|
-
token_secret = Secret.from_plaintext(self.token)
|
|
85
|
-
secret_dict = token_secret.to_dict()
|
|
86
|
-
data["token_enc"] = secret_dict["encrypted"]
|
|
87
|
-
# Keep plaintext for dual-write during migration
|
|
88
|
-
data["token"] = secret_dict["plaintext"]
|
|
89
|
-
|
|
90
|
-
# Encrypt custom headers if present
|
|
91
|
-
if self.custom_headers is not None:
|
|
92
|
-
headers_secret = SecretDict.from_plaintext(self.custom_headers)
|
|
93
|
-
secret_dict = headers_secret.to_dict()
|
|
94
|
-
data["custom_headers_enc"] = secret_dict["encrypted"]
|
|
95
|
-
# Keep plaintext for dual-write during migration
|
|
96
|
-
data["custom_headers"] = secret_dict["plaintext"]
|
|
97
|
-
|
|
98
|
-
return data
|
|
99
|
-
|
|
100
101
|
def to_config(
|
|
101
102
|
self,
|
|
102
103
|
environment_variables: Optional[Dict[str, str]] = None,
|
|
@@ -106,8 +107,8 @@ class MCPServer(BaseMCPServer):
|
|
|
106
107
|
token_secret = self.get_token_secret()
|
|
107
108
|
token_plaintext = token_secret.get_plaintext()
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
headers_plaintext =
|
|
110
|
+
# Get custom headers as dict
|
|
111
|
+
headers_plaintext = self.get_custom_headers_dict()
|
|
111
112
|
|
|
112
113
|
if self.server_type == MCPServerType.SSE:
|
|
113
114
|
config = SSEServerConfig(
|
|
@@ -147,6 +148,7 @@ class MCPServer(BaseMCPServer):
|
|
|
147
148
|
class UpdateSSEMCPServer(LettaBase):
|
|
148
149
|
"""Update an SSE MCP server"""
|
|
149
150
|
|
|
151
|
+
server_name: Optional[str] = Field(None, description="The name of the MCP server")
|
|
150
152
|
server_url: Optional[str] = Field(None, description="The URL of the server (MCP SSE client will connect to this URL)")
|
|
151
153
|
token: Optional[str] = Field(None, description="The access token or API key for the MCP server (used for SSE authentication)")
|
|
152
154
|
custom_headers: Optional[Dict[str, str]] = Field(None, description="Custom authentication headers as key-value pairs")
|
|
@@ -155,6 +157,7 @@ class UpdateSSEMCPServer(LettaBase):
|
|
|
155
157
|
class UpdateStdioMCPServer(LettaBase):
|
|
156
158
|
"""Update a Stdio MCP server"""
|
|
157
159
|
|
|
160
|
+
server_name: Optional[str] = Field(None, description="The name of the MCP server")
|
|
158
161
|
stdio_config: Optional[StdioServerConfig] = Field(
|
|
159
162
|
None, description="The configuration for the server (MCP 'local' client will run this command)"
|
|
160
163
|
)
|
|
@@ -163,6 +166,7 @@ class UpdateStdioMCPServer(LettaBase):
|
|
|
163
166
|
class UpdateStreamableHTTPMCPServer(LettaBase):
|
|
164
167
|
"""Update a Streamable HTTP MCP server"""
|
|
165
168
|
|
|
169
|
+
server_name: Optional[str] = Field(None, description="The name of the MCP server")
|
|
166
170
|
server_url: Optional[str] = Field(None, description="The URL path for the streamable HTTP server (e.g., 'example/mcp')")
|
|
167
171
|
auth_header: Optional[str] = Field(None, description="The name of the authentication header (e.g., 'Authorization')")
|
|
168
172
|
auth_token: Optional[str] = Field(None, description="The authentication token or API key value")
|
|
@@ -194,6 +198,9 @@ class MCPOAuthSession(BaseMCPOAuth):
|
|
|
194
198
|
authorization_url: Optional[str] = Field(None, description="OAuth authorization URL")
|
|
195
199
|
authorization_code: Optional[str] = Field(None, description="OAuth authorization code")
|
|
196
200
|
|
|
201
|
+
# Encrypted authorization code (for internal use)
|
|
202
|
+
authorization_code_enc: Secret | None = Field(None, description="Encrypted OAuth authorization code as Secret object")
|
|
203
|
+
|
|
197
204
|
# Token data
|
|
198
205
|
access_token: Optional[str] = Field(None, description="OAuth access token")
|
|
199
206
|
refresh_token: Optional[str] = Field(None, description="OAuth refresh token")
|
|
@@ -202,8 +209,8 @@ class MCPOAuthSession(BaseMCPOAuth):
|
|
|
202
209
|
scope: Optional[str] = Field(None, description="OAuth scope")
|
|
203
210
|
|
|
204
211
|
# Encrypted token fields (for internal use)
|
|
205
|
-
access_token_enc:
|
|
206
|
-
refresh_token_enc:
|
|
212
|
+
access_token_enc: Secret | None = Field(None, description="Encrypted OAuth access token as Secret object")
|
|
213
|
+
refresh_token_enc: Secret | None = Field(None, description="Encrypted OAuth refresh token as Secret object")
|
|
207
214
|
|
|
208
215
|
# Client configuration
|
|
209
216
|
client_id: Optional[str] = Field(None, description="OAuth client ID")
|
|
@@ -211,7 +218,7 @@ class MCPOAuthSession(BaseMCPOAuth):
|
|
|
211
218
|
redirect_uri: Optional[str] = Field(None, description="OAuth redirect URI")
|
|
212
219
|
|
|
213
220
|
# Encrypted client secret (for internal use)
|
|
214
|
-
client_secret_enc:
|
|
221
|
+
client_secret_enc: Secret | None = Field(None, description="Encrypted OAuth client secret as Secret object")
|
|
215
222
|
|
|
216
223
|
# Session state
|
|
217
224
|
status: OAuthSessionStatus = Field(default=OAuthSessionStatus.PENDING, description="Session status")
|
|
@@ -222,73 +229,63 @@ class MCPOAuthSession(BaseMCPOAuth):
|
|
|
222
229
|
|
|
223
230
|
def get_access_token_secret(self) -> Secret:
|
|
224
231
|
"""Get the access token as a Secret object, preferring encrypted over plaintext."""
|
|
225
|
-
|
|
232
|
+
if self.access_token_enc is not None:
|
|
233
|
+
return self.access_token_enc
|
|
234
|
+
return Secret.from_db(None, self.access_token)
|
|
226
235
|
|
|
227
236
|
def get_refresh_token_secret(self) -> Secret:
|
|
228
237
|
"""Get the refresh token as a Secret object, preferring encrypted over plaintext."""
|
|
229
|
-
|
|
238
|
+
if self.refresh_token_enc is not None:
|
|
239
|
+
return self.refresh_token_enc
|
|
240
|
+
return Secret.from_db(None, self.refresh_token)
|
|
230
241
|
|
|
231
242
|
def get_client_secret_secret(self) -> Secret:
|
|
232
243
|
"""Get the client secret as a Secret object, preferring encrypted over plaintext."""
|
|
233
|
-
|
|
244
|
+
if self.client_secret_enc is not None:
|
|
245
|
+
return self.client_secret_enc
|
|
246
|
+
return Secret.from_db(None, self.client_secret)
|
|
247
|
+
|
|
248
|
+
def get_authorization_code_secret(self) -> Secret:
|
|
249
|
+
"""Get the authorization code as a Secret object, preferring encrypted over plaintext."""
|
|
250
|
+
if self.authorization_code_enc is not None:
|
|
251
|
+
return self.authorization_code_enc
|
|
252
|
+
return Secret.from_db(None, self.authorization_code)
|
|
234
253
|
|
|
235
254
|
def set_access_token_secret(self, secret: Secret) -> None:
|
|
236
255
|
"""Set access token from a Secret object."""
|
|
256
|
+
self.access_token_enc = secret
|
|
237
257
|
secret_dict = secret.to_dict()
|
|
238
|
-
|
|
239
|
-
if not secret._was_encrypted:
|
|
258
|
+
if not secret.was_encrypted:
|
|
240
259
|
self.access_token = secret_dict["plaintext"]
|
|
241
260
|
else:
|
|
242
261
|
self.access_token = None
|
|
243
262
|
|
|
244
263
|
def set_refresh_token_secret(self, secret: Secret) -> None:
|
|
245
264
|
"""Set refresh token from a Secret object."""
|
|
265
|
+
self.refresh_token_enc = secret
|
|
246
266
|
secret_dict = secret.to_dict()
|
|
247
|
-
|
|
248
|
-
if not secret._was_encrypted:
|
|
267
|
+
if not secret.was_encrypted:
|
|
249
268
|
self.refresh_token = secret_dict["plaintext"]
|
|
250
269
|
else:
|
|
251
270
|
self.refresh_token = None
|
|
252
271
|
|
|
253
272
|
def set_client_secret_secret(self, secret: Secret) -> None:
|
|
254
273
|
"""Set client secret from a Secret object."""
|
|
274
|
+
self.client_secret_enc = secret
|
|
255
275
|
secret_dict = secret.to_dict()
|
|
256
|
-
|
|
257
|
-
if not secret._was_encrypted:
|
|
276
|
+
if not secret.was_encrypted:
|
|
258
277
|
self.client_secret = secret_dict["plaintext"]
|
|
259
278
|
else:
|
|
260
279
|
self.client_secret = None
|
|
261
280
|
|
|
262
|
-
def
|
|
263
|
-
"""
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
secret_dict = token_secret.to_dict()
|
|
271
|
-
data["access_token_enc"] = secret_dict["encrypted"]
|
|
272
|
-
# Keep plaintext for dual-write during migration
|
|
273
|
-
data["access_token"] = secret_dict["plaintext"]
|
|
274
|
-
|
|
275
|
-
# Encrypt refresh token if present
|
|
276
|
-
if self.refresh_token is not None:
|
|
277
|
-
token_secret = Secret.from_plaintext(self.refresh_token)
|
|
278
|
-
secret_dict = token_secret.to_dict()
|
|
279
|
-
data["refresh_token_enc"] = secret_dict["encrypted"]
|
|
280
|
-
# Keep plaintext for dual-write during migration
|
|
281
|
-
data["refresh_token"] = secret_dict["plaintext"]
|
|
282
|
-
|
|
283
|
-
# Encrypt client secret if present
|
|
284
|
-
if self.client_secret is not None:
|
|
285
|
-
secret = Secret.from_plaintext(self.client_secret)
|
|
286
|
-
secret_dict = secret.to_dict()
|
|
287
|
-
data["client_secret_enc"] = secret_dict["encrypted"]
|
|
288
|
-
# Keep plaintext for dual-write during migration
|
|
289
|
-
data["client_secret"] = secret_dict["plaintext"]
|
|
290
|
-
|
|
291
|
-
return data
|
|
281
|
+
def set_authorization_code_secret(self, secret: Secret) -> None:
|
|
282
|
+
"""Set authorization code from a Secret object."""
|
|
283
|
+
self.authorization_code_enc = secret
|
|
284
|
+
secret_dict = secret.to_dict()
|
|
285
|
+
if not secret.was_encrypted:
|
|
286
|
+
self.authorization_code = secret_dict["plaintext"]
|
|
287
|
+
else:
|
|
288
|
+
self.authorization_code = None
|
|
292
289
|
|
|
293
290
|
|
|
294
291
|
class MCPOAuthSessionCreate(BaseMCPOAuth):
|