letta-nightly 0.11.7.dev20250915104130__py3-none-any.whl → 0.11.7.dev20250917104122__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 +10 -2
- letta/adapters/letta_llm_request_adapter.py +0 -1
- letta/adapters/letta_llm_stream_adapter.py +0 -1
- letta/agent.py +1 -1
- letta/agents/letta_agent.py +1 -4
- letta/agents/letta_agent_v2.py +2 -1
- letta/agents/voice_agent.py +1 -1
- letta/functions/function_sets/multi_agent.py +1 -1
- letta/functions/helpers.py +1 -1
- letta/helpers/converters.py +8 -2
- letta/helpers/crypto_utils.py +144 -0
- letta/llm_api/llm_api_tools.py +0 -1
- letta/llm_api/llm_client_base.py +0 -2
- letta/orm/__init__.py +1 -0
- letta/orm/agent.py +5 -1
- letta/orm/job.py +3 -1
- letta/orm/mcp_oauth.py +6 -0
- letta/orm/mcp_server.py +7 -1
- letta/orm/sqlalchemy_base.py +2 -1
- letta/prompts/gpt_system.py +13 -15
- letta/prompts/system_prompts/__init__.py +27 -0
- letta/prompts/{system/memgpt_chat.txt → system_prompts/memgpt_chat.py} +2 -0
- letta/prompts/{system/memgpt_generate_tool.txt → system_prompts/memgpt_generate_tool.py} +4 -2
- letta/prompts/{system/memgpt_v2_chat.txt → system_prompts/memgpt_v2_chat.py} +2 -0
- letta/prompts/{system/react.txt → system_prompts/react.py} +2 -0
- letta/prompts/{system/sleeptime_doc_ingest.txt → system_prompts/sleeptime_doc_ingest.py} +2 -0
- letta/prompts/{system/sleeptime_v2.txt → system_prompts/sleeptime_v2.py} +2 -0
- letta/prompts/{system/summary_system_prompt.txt → system_prompts/summary_system_prompt.py} +2 -0
- letta/prompts/{system/voice_chat.txt → system_prompts/voice_chat.py} +2 -0
- letta/prompts/{system/voice_sleeptime.txt → system_prompts/voice_sleeptime.py} +2 -0
- letta/prompts/{system/workflow.txt → system_prompts/workflow.py} +2 -0
- letta/schemas/agent.py +10 -7
- letta/schemas/job.py +10 -0
- letta/schemas/mcp.py +146 -6
- letta/schemas/provider_trace.py +0 -2
- letta/schemas/run.py +2 -0
- letta/schemas/secret.py +378 -0
- letta/serialize_schemas/marshmallow_agent.py +4 -0
- letta/server/rest_api/dependencies.py +37 -0
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +4 -3
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +115 -107
- letta/server/rest_api/routers/v1/archives.py +113 -0
- letta/server/rest_api/routers/v1/blocks.py +44 -20
- letta/server/rest_api/routers/v1/embeddings.py +3 -3
- letta/server/rest_api/routers/v1/folders.py +107 -47
- letta/server/rest_api/routers/v1/groups.py +52 -32
- letta/server/rest_api/routers/v1/identities.py +110 -21
- letta/server/rest_api/routers/v1/internal_templates.py +28 -13
- letta/server/rest_api/routers/v1/jobs.py +19 -14
- letta/server/rest_api/routers/v1/llms.py +6 -8
- letta/server/rest_api/routers/v1/messages.py +14 -14
- letta/server/rest_api/routers/v1/organizations.py +1 -1
- letta/server/rest_api/routers/v1/providers.py +40 -16
- letta/server/rest_api/routers/v1/runs.py +28 -20
- letta/server/rest_api/routers/v1/sandbox_configs.py +25 -25
- letta/server/rest_api/routers/v1/sources.py +44 -45
- letta/server/rest_api/routers/v1/steps.py +27 -25
- letta/server/rest_api/routers/v1/tags.py +11 -7
- letta/server/rest_api/routers/v1/telemetry.py +11 -6
- letta/server/rest_api/routers/v1/tools.py +78 -80
- letta/server/rest_api/routers/v1/users.py +1 -1
- letta/server/rest_api/routers/v1/voice.py +6 -5
- letta/server/rest_api/utils.py +1 -18
- letta/services/agent_manager.py +17 -9
- letta/services/agent_serialization_manager.py +11 -3
- letta/services/archive_manager.py +73 -0
- letta/services/file_manager.py +6 -0
- letta/services/group_manager.py +2 -1
- letta/services/helpers/agent_manager_helper.py +6 -1
- letta/services/identity_manager.py +67 -0
- letta/services/job_manager.py +18 -2
- letta/services/mcp_manager.py +198 -82
- letta/services/provider_manager.py +14 -1
- letta/services/source_manager.py +11 -1
- letta/services/telemetry_manager.py +2 -0
- letta/services/tool_executor/composio_tool_executor.py +1 -1
- letta/services/tool_manager.py +46 -9
- letta/services/tool_sandbox/base.py +2 -3
- letta/utils.py +4 -2
- {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/METADATA +5 -2
- {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/RECORD +85 -94
- letta/prompts/system/memgpt_base.txt +0 -54
- letta/prompts/system/memgpt_chat_compressed.txt +0 -13
- letta/prompts/system/memgpt_chat_fstring.txt +0 -51
- letta/prompts/system/memgpt_convo_only.txt +0 -12
- letta/prompts/system/memgpt_doc.txt +0 -50
- letta/prompts/system/memgpt_gpt35_extralong.txt +0 -53
- letta/prompts/system/memgpt_intuitive_knowledge.txt +0 -31
- letta/prompts/system/memgpt_memory_only.txt +0 -29
- letta/prompts/system/memgpt_modified_chat.txt +0 -23
- letta/prompts/system/memgpt_modified_o1.txt +0 -31
- letta/prompts/system/memgpt_offline_memory.txt +0 -23
- letta/prompts/system/memgpt_offline_memory_chat.txt +0 -35
- letta/prompts/system/memgpt_sleeptime_chat.txt +0 -52
- letta/prompts/system/sleeptime.txt +0 -37
- {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/licenses/LICENSE +0 -0
letta/__init__.py
CHANGED
@@ -10,8 +10,16 @@ except PackageNotFoundError:
|
|
10
10
|
if os.environ.get("LETTA_VERSION"):
|
11
11
|
__version__ = os.environ["LETTA_VERSION"]
|
12
12
|
|
13
|
-
# Import sqlite_functions early to ensure event handlers are registered
|
14
|
-
|
13
|
+
# Import sqlite_functions early to ensure event handlers are registered (only for SQLite)
|
14
|
+
# This is only needed for the server, not for client usage
|
15
|
+
try:
|
16
|
+
from letta.settings import DatabaseChoice, settings
|
17
|
+
|
18
|
+
if settings.database_engine == DatabaseChoice.SQLITE:
|
19
|
+
from letta.orm import sqlite_functions
|
20
|
+
except ImportError:
|
21
|
+
# If sqlite_vec is not installed, it's fine for client usage
|
22
|
+
pass
|
15
23
|
|
16
24
|
# # imports for easier access
|
17
25
|
from letta.schemas.agent import AgentState
|
@@ -106,7 +106,6 @@ class LettaLLMRequestAdapter(LettaLLMAdapter):
|
|
106
106
|
request_json=self.request_data,
|
107
107
|
response_json=self.response_data,
|
108
108
|
step_id=step_id, # Use original step_id for telemetry
|
109
|
-
organization_id=actor.organization_id,
|
110
109
|
),
|
111
110
|
),
|
112
111
|
label="create_provider_trace",
|
letta/agent.py
CHANGED
@@ -1628,7 +1628,7 @@ class Agent(BaseAgent):
|
|
1628
1628
|
action_name = generate_composio_action_from_func_name(target_letta_tool.name)
|
1629
1629
|
# Get entity ID from the agent_state
|
1630
1630
|
entity_id = None
|
1631
|
-
for env_var in self.agent_state.
|
1631
|
+
for env_var in self.agent_state.secrets:
|
1632
1632
|
if env_var.key == COMPOSIO_ENTITY_ENV_VAR_KEY:
|
1633
1633
|
entity_id = env_var.value
|
1634
1634
|
# Get composio_api_key
|
letta/agents/letta_agent.py
CHANGED
@@ -405,7 +405,6 @@ class LettaAgent(BaseAgent):
|
|
405
405
|
request_json=request_data,
|
406
406
|
response_json=response_data,
|
407
407
|
step_id=step_id, # Use original step_id for telemetry
|
408
|
-
organization_id=self.actor.organization_id,
|
409
408
|
),
|
410
409
|
)
|
411
410
|
step_progression = StepProgression.LOGGED_TRACE
|
@@ -751,7 +750,6 @@ class LettaAgent(BaseAgent):
|
|
751
750
|
request_json=request_data,
|
752
751
|
response_json=response_data,
|
753
752
|
step_id=step_id, # Use original step_id for telemetry
|
754
|
-
organization_id=self.actor.organization_id,
|
755
753
|
),
|
756
754
|
)
|
757
755
|
step_progression = StepProgression.LOGGED_TRACE
|
@@ -1173,7 +1171,6 @@ class LettaAgent(BaseAgent):
|
|
1173
1171
|
},
|
1174
1172
|
},
|
1175
1173
|
step_id=step_id, # Use original step_id for telemetry
|
1176
|
-
organization_id=self.actor.organization_id,
|
1177
1174
|
),
|
1178
1175
|
)
|
1179
1176
|
step_progression = StepProgression.LOGGED_TRACE
|
@@ -1877,7 +1874,7 @@ class LettaAgent(BaseAgent):
|
|
1877
1874
|
start_time = get_utc_timestamp_ns()
|
1878
1875
|
agent_step_span.add_event(name="tool_execution_started")
|
1879
1876
|
|
1880
|
-
sandbox_env_vars = {var.key: var.value for var in agent_state.
|
1877
|
+
sandbox_env_vars = {var.key: var.value for var in agent_state.secrets}
|
1881
1878
|
tool_execution_manager = ToolExecutionManager(
|
1882
1879
|
agent_state=agent_state,
|
1883
1880
|
message_manager=self.message_manager,
|
letta/agents/letta_agent_v2.py
CHANGED
@@ -1106,7 +1106,7 @@ class LettaAgentV2(BaseAgentV2):
|
|
1106
1106
|
start_time = get_utc_timestamp_ns()
|
1107
1107
|
agent_step_span.add_event(name="tool_execution_started")
|
1108
1108
|
|
1109
|
-
sandbox_env_vars = {var.key: var.value for var in agent_state.
|
1109
|
+
sandbox_env_vars = {var.key: var.value for var in agent_state.secrets}
|
1110
1110
|
tool_execution_manager = ToolExecutionManager(
|
1111
1111
|
agent_state=agent_state,
|
1112
1112
|
message_manager=self.message_manager,
|
@@ -1226,6 +1226,7 @@ class LettaAgentV2(BaseAgentV2):
|
|
1226
1226
|
new_status=JobStatus.failed if is_error else JobStatus.completed,
|
1227
1227
|
actor=self.actor,
|
1228
1228
|
metadata=job_update_metadata,
|
1229
|
+
stop_reason=self.stop_reason.stop_reason if self.stop_reason else StopReasonType.error,
|
1229
1230
|
)
|
1230
1231
|
if request_span:
|
1231
1232
|
request_span.end()
|
letta/agents/voice_agent.py
CHANGED
@@ -441,7 +441,7 @@ class VoiceAgent(BaseAgent):
|
|
441
441
|
)
|
442
442
|
|
443
443
|
# Use ToolExecutionManager for modern tool execution
|
444
|
-
sandbox_env_vars = {var.key: var.value for var in agent_state.
|
444
|
+
sandbox_env_vars = {var.key: var.value for var in agent_state.secrets}
|
445
445
|
tool_execution_manager = ToolExecutionManager(
|
446
446
|
agent_state=agent_state,
|
447
447
|
message_manager=self.message_manager,
|
@@ -11,7 +11,7 @@ from letta.functions.helpers import (
|
|
11
11
|
)
|
12
12
|
from letta.schemas.enums import MessageRole
|
13
13
|
from letta.schemas.message import MessageCreate
|
14
|
-
from letta.server.rest_api.
|
14
|
+
from letta.server.rest_api.dependencies import get_letta_server
|
15
15
|
from letta.settings import settings
|
16
16
|
|
17
17
|
if TYPE_CHECKING:
|
letta/functions/helpers.py
CHANGED
@@ -17,7 +17,7 @@ from letta.schemas.letta_response import LettaResponse
|
|
17
17
|
from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
|
18
18
|
from letta.schemas.message import Message, MessageCreate
|
19
19
|
from letta.schemas.user import User
|
20
|
-
from letta.server.rest_api.
|
20
|
+
from letta.server.rest_api.dependencies import get_letta_server
|
21
21
|
from letta.settings import settings
|
22
22
|
from letta.utils import safe_create_task
|
23
23
|
|
letta/helpers/converters.py
CHANGED
@@ -44,8 +44,14 @@ from letta.schemas.tool_rule import (
|
|
44
44
|
)
|
45
45
|
from letta.settings import DatabaseChoice, settings
|
46
46
|
|
47
|
-
if
|
48
|
-
|
47
|
+
# Only import sqlite_vec if we're actually using SQLite database
|
48
|
+
# This is a runtime dependency only needed for SQLite vector operations
|
49
|
+
try:
|
50
|
+
if settings.database_engine == DatabaseChoice.SQLITE:
|
51
|
+
import sqlite_vec
|
52
|
+
except ImportError:
|
53
|
+
# If sqlite_vec is not installed, it's fine for client usage
|
54
|
+
pass
|
49
55
|
# --------------------------
|
50
56
|
# LLMConfig Serialization
|
51
57
|
# --------------------------
|
@@ -0,0 +1,144 @@
|
|
1
|
+
import base64
|
2
|
+
import os
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from cryptography.hazmat.backends import default_backend
|
6
|
+
from cryptography.hazmat.primitives import hashes
|
7
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
8
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
9
|
+
|
10
|
+
from letta.settings import settings
|
11
|
+
|
12
|
+
|
13
|
+
class CryptoUtils:
|
14
|
+
"""Utility class for AES-256-GCM encryption/decryption of sensitive data."""
|
15
|
+
|
16
|
+
# AES-256 requires 32 bytes key
|
17
|
+
KEY_SIZE = 32
|
18
|
+
# GCM standard IV size is 12 bytes (96 bits)
|
19
|
+
IV_SIZE = 12
|
20
|
+
# GCM tag size is 16 bytes (128 bits)
|
21
|
+
TAG_SIZE = 16
|
22
|
+
# Salt size for key derivation
|
23
|
+
SALT_SIZE = 16
|
24
|
+
|
25
|
+
@classmethod
|
26
|
+
def _derive_key(cls, master_key: str, salt: bytes) -> bytes:
|
27
|
+
"""Derive an AES key from the master key using PBKDF2."""
|
28
|
+
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=cls.KEY_SIZE, salt=salt, iterations=100000, backend=default_backend())
|
29
|
+
return kdf.derive(master_key.encode())
|
30
|
+
|
31
|
+
@classmethod
|
32
|
+
def encrypt(cls, plaintext: str, master_key: Optional[str] = None) -> str:
|
33
|
+
"""
|
34
|
+
Encrypt a string using AES-256-GCM.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
plaintext: The string to encrypt
|
38
|
+
master_key: Optional master key (defaults to settings.encryption_key)
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
Base64 encoded string containing: salt + iv + ciphertext + tag
|
42
|
+
|
43
|
+
Raises:
|
44
|
+
ValueError: If no encryption key is configured
|
45
|
+
"""
|
46
|
+
if master_key is None:
|
47
|
+
master_key = settings.encryption_key
|
48
|
+
|
49
|
+
if not master_key:
|
50
|
+
raise ValueError("No encryption key configured. Set LETTA_ENCRYPTION_KEY environment variable.")
|
51
|
+
|
52
|
+
# Generate random salt and IV
|
53
|
+
salt = os.urandom(cls.SALT_SIZE)
|
54
|
+
iv = os.urandom(cls.IV_SIZE)
|
55
|
+
|
56
|
+
# Derive key from master key
|
57
|
+
key = cls._derive_key(master_key, salt)
|
58
|
+
|
59
|
+
# Create cipher
|
60
|
+
cipher = Cipher(algorithms.AES(key), modes.GCM(iv), backend=default_backend())
|
61
|
+
encryptor = cipher.encryptor()
|
62
|
+
|
63
|
+
# Encrypt the plaintext
|
64
|
+
ciphertext = encryptor.update(plaintext.encode()) + encryptor.finalize()
|
65
|
+
|
66
|
+
# Get the authentication tag
|
67
|
+
tag = encryptor.tag
|
68
|
+
|
69
|
+
# Combine salt + iv + ciphertext + tag
|
70
|
+
encrypted_data = salt + iv + ciphertext + tag
|
71
|
+
|
72
|
+
# Return as base64 encoded string
|
73
|
+
return base64.b64encode(encrypted_data).decode("utf-8")
|
74
|
+
|
75
|
+
@classmethod
|
76
|
+
def decrypt(cls, encrypted: str, master_key: Optional[str] = None) -> str:
|
77
|
+
"""
|
78
|
+
Decrypt a string that was encrypted using AES-256-GCM.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
encrypted: Base64 encoded encrypted string
|
82
|
+
master_key: Optional master key (defaults to settings.encryption_key)
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
The decrypted plaintext string
|
86
|
+
|
87
|
+
Raises:
|
88
|
+
ValueError: If no encryption key is configured or decryption fails
|
89
|
+
"""
|
90
|
+
if master_key is None:
|
91
|
+
master_key = settings.encryption_key
|
92
|
+
|
93
|
+
if not master_key:
|
94
|
+
raise ValueError("No encryption key configured. Set LETTA_ENCRYPTION_KEY environment variable.")
|
95
|
+
|
96
|
+
try:
|
97
|
+
# Decode from base64
|
98
|
+
encrypted_data = base64.b64decode(encrypted)
|
99
|
+
|
100
|
+
# Extract components
|
101
|
+
salt = encrypted_data[: cls.SALT_SIZE]
|
102
|
+
iv = encrypted_data[cls.SALT_SIZE : cls.SALT_SIZE + cls.IV_SIZE]
|
103
|
+
ciphertext = encrypted_data[cls.SALT_SIZE + cls.IV_SIZE : -cls.TAG_SIZE]
|
104
|
+
tag = encrypted_data[-cls.TAG_SIZE :]
|
105
|
+
|
106
|
+
# Derive key from master key
|
107
|
+
key = cls._derive_key(master_key, salt)
|
108
|
+
|
109
|
+
# Create cipher
|
110
|
+
cipher = Cipher(algorithms.AES(key), modes.GCM(iv, tag), backend=default_backend())
|
111
|
+
decryptor = cipher.decryptor()
|
112
|
+
|
113
|
+
# Decrypt the ciphertext
|
114
|
+
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
|
115
|
+
|
116
|
+
return plaintext.decode("utf-8")
|
117
|
+
|
118
|
+
except Exception as e:
|
119
|
+
raise ValueError(f"Failed to decrypt data: {str(e)}")
|
120
|
+
|
121
|
+
@classmethod
|
122
|
+
def is_encrypted(cls, value: str) -> bool:
|
123
|
+
"""
|
124
|
+
Check if a string appears to be encrypted (base64 encoded with correct size).
|
125
|
+
|
126
|
+
This is a heuristic check and may have false positives.
|
127
|
+
"""
|
128
|
+
try:
|
129
|
+
decoded = base64.b64decode(value)
|
130
|
+
# Check if length is consistent with our encryption format
|
131
|
+
# Minimum size: salt(16) + iv(12) + tag(16) + at least 1 byte of ciphertext
|
132
|
+
return len(decoded) >= cls.SALT_SIZE + cls.IV_SIZE + cls.TAG_SIZE + 1
|
133
|
+
except Exception:
|
134
|
+
return False
|
135
|
+
|
136
|
+
@classmethod
|
137
|
+
def is_encryption_available(cls) -> bool:
|
138
|
+
"""
|
139
|
+
Check if encryption is available (encryption key is configured).
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
True if encryption key is configured, False otherwise
|
143
|
+
"""
|
144
|
+
return bool(settings.encryption_key)
|
letta/llm_api/llm_api_tools.py
CHANGED
letta/llm_api/llm_client_base.py
CHANGED
@@ -64,7 +64,6 @@ class LLMClientBase:
|
|
64
64
|
request_json=request_data,
|
65
65
|
response_json=response_data,
|
66
66
|
step_id=step_id,
|
67
|
-
organization_id=self.actor.organization_id,
|
68
67
|
),
|
69
68
|
)
|
70
69
|
log_event(name="llm_response_received", attributes=response_data)
|
@@ -98,7 +97,6 @@ class LLMClientBase:
|
|
98
97
|
request_json=request_data,
|
99
98
|
response_json=response_data,
|
100
99
|
step_id=step_id,
|
101
|
-
organization_id=self.actor.organization_id,
|
102
100
|
),
|
103
101
|
)
|
104
102
|
|
letta/orm/__init__.py
CHANGED
@@ -18,6 +18,7 @@ from letta.orm.job import Job
|
|
18
18
|
from letta.orm.job_messages import JobMessage
|
19
19
|
from letta.orm.llm_batch_items import LLMBatchItem
|
20
20
|
from letta.orm.llm_batch_job import LLMBatchJob
|
21
|
+
from letta.orm.mcp_oauth import MCPOAuth
|
21
22
|
from letta.orm.mcp_server import MCPServer
|
22
23
|
from letta.orm.message import Message
|
23
24
|
from letta.orm.organization import Organization
|
letta/orm/agent.py
CHANGED
@@ -233,6 +233,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
|
|
233
233
|
"identity_ids": [],
|
234
234
|
"multi_agent_group": None,
|
235
235
|
"tool_exec_environment_variables": [],
|
236
|
+
"secrets": [],
|
236
237
|
}
|
237
238
|
|
238
239
|
# Optional fields: only included if requested
|
@@ -253,6 +254,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
|
|
253
254
|
"identity_ids": lambda: [i.id for i in self.identities],
|
254
255
|
"multi_agent_group": lambda: self.multi_agent_group,
|
255
256
|
"tool_exec_environment_variables": lambda: self.tool_exec_environment_variables,
|
257
|
+
"secrets": lambda: self.tool_exec_environment_variables,
|
256
258
|
}
|
257
259
|
|
258
260
|
include_relationships = set(optional_fields.keys() if include_relationships is None else include_relationships)
|
@@ -324,6 +326,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
|
|
324
326
|
"identity_ids": [],
|
325
327
|
"multi_agent_group": None,
|
326
328
|
"tool_exec_environment_variables": [],
|
329
|
+
"secrets": [],
|
327
330
|
}
|
328
331
|
|
329
332
|
# Initialize include_relationships to an empty set if it's None
|
@@ -344,7 +347,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
|
|
344
347
|
multi_agent_group = self.awaitable_attrs.multi_agent_group if "multi_agent_group" in include_relationships else none_async()
|
345
348
|
tool_exec_environment_variables = (
|
346
349
|
self.awaitable_attrs.tool_exec_environment_variables
|
347
|
-
if "tool_exec_environment_variables" in include_relationships
|
350
|
+
if "tool_exec_environment_variables" in include_relationships or "secrets" in include_relationships
|
348
351
|
else empty_list_async()
|
349
352
|
)
|
350
353
|
file_agents = self.awaitable_attrs.file_agents if "memory" in include_relationships else empty_list_async()
|
@@ -368,5 +371,6 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
|
|
368
371
|
state["identity_ids"] = [i.id for i in identities]
|
369
372
|
state["multi_agent_group"] = multi_agent_group
|
370
373
|
state["tool_exec_environment_variables"] = tool_exec_environment_variables
|
374
|
+
state["secrets"] = tool_exec_environment_variables
|
371
375
|
|
372
376
|
return self.__pydantic_model__(**state)
|
letta/orm/job.py
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
from datetime import datetime
|
2
2
|
from typing import TYPE_CHECKING, List, Optional
|
3
3
|
|
4
|
-
from sqlalchemy import JSON, BigInteger, ForeignKey, Index, String
|
4
|
+
from sqlalchemy import JSON, BigInteger, Boolean, ForeignKey, Index, String
|
5
5
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
6
6
|
|
7
7
|
from letta.orm.mixins import UserMixin
|
8
8
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
9
9
|
from letta.schemas.enums import JobStatus, JobType
|
10
10
|
from letta.schemas.job import Job as PydanticJob, LettaRequestConfig
|
11
|
+
from letta.schemas.letta_stop_reason import StopReasonType
|
11
12
|
|
12
13
|
if TYPE_CHECKING:
|
13
14
|
from letta.orm.job_messages import JobMessage
|
@@ -28,6 +29,7 @@ class Job(SqlalchemyBase, UserMixin):
|
|
28
29
|
|
29
30
|
status: Mapped[JobStatus] = mapped_column(String, default=JobStatus.created, doc="The current status of the job.")
|
30
31
|
completed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True, doc="The unix timestamp of when the job was completed.")
|
32
|
+
stop_reason: Mapped[Optional[StopReasonType]] = mapped_column(String, nullable=True, doc="The reason why the job was stopped.")
|
31
33
|
metadata_: Mapped[Optional[dict]] = mapped_column(JSON, doc="The metadata of the job.")
|
32
34
|
job_type: Mapped[JobType] = mapped_column(
|
33
35
|
String,
|
letta/orm/mcp_oauth.py
CHANGED
@@ -38,7 +38,11 @@ class MCPOAuth(SqlalchemyBase, OrganizationMixin, UserMixin):
|
|
38
38
|
|
39
39
|
# Token data
|
40
40
|
access_token: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth access token")
|
41
|
+
access_token_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted OAuth access token")
|
42
|
+
|
41
43
|
refresh_token: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth refresh token")
|
44
|
+
refresh_token_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted OAuth refresh token")
|
45
|
+
|
42
46
|
token_type: Mapped[str] = mapped_column(String(50), default="Bearer", doc="Token type")
|
43
47
|
expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True, doc="Token expiry time")
|
44
48
|
scope: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth scope")
|
@@ -46,6 +50,8 @@ class MCPOAuth(SqlalchemyBase, OrganizationMixin, UserMixin):
|
|
46
50
|
# Client configuration
|
47
51
|
client_id: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth client ID")
|
48
52
|
client_secret: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth client secret")
|
53
|
+
client_secret_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted OAuth client secret")
|
54
|
+
|
49
55
|
redirect_uri: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth redirect URI")
|
50
56
|
|
51
57
|
# Session state
|
letta/orm/mcp_server.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from typing import TYPE_CHECKING, Optional
|
2
2
|
|
3
|
-
from sqlalchemy import JSON, String, UniqueConstraint
|
3
|
+
from sqlalchemy import JSON, String, Text, UniqueConstraint
|
4
4
|
from sqlalchemy.orm import Mapped, mapped_column
|
5
5
|
|
6
6
|
from letta.functions.mcp_client.types import StdioServerConfig
|
@@ -39,9 +39,15 @@ class MCPServer(SqlalchemyBase, OrganizationMixin):
|
|
39
39
|
# access token / api key for MCP servers that require authentication
|
40
40
|
token: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The access token or api key for the MCP server")
|
41
41
|
|
42
|
+
# encrypted access token or api key for the MCP server
|
43
|
+
token_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted access token or api key for the MCP server")
|
44
|
+
|
42
45
|
# custom headers for authentication (key-value pairs)
|
43
46
|
custom_headers: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, doc="Custom authentication headers as key-value pairs")
|
44
47
|
|
48
|
+
# encrypted custom headers for authentication (key-value pairs)
|
49
|
+
custom_headers_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted custom authentication headers")
|
50
|
+
|
45
51
|
# stdio server
|
46
52
|
stdio_config: Mapped[Optional[StdioServerConfig]] = mapped_column(
|
47
53
|
MCPStdioServerConfigColumn, nullable=True, doc="The configuration for the stdio server"
|
letta/orm/sqlalchemy_base.py
CHANGED
@@ -14,7 +14,6 @@ from sqlalchemy.orm.interfaces import ORMOption
|
|
14
14
|
from letta.log import get_logger
|
15
15
|
from letta.orm.base import Base, CommonSqlalchemyMetaMixins
|
16
16
|
from letta.orm.errors import DatabaseTimeoutError, ForeignKeyConstraintViolationError, NoResultFound, UniqueConstraintViolationError
|
17
|
-
from letta.orm.sqlite_functions import adapt_array
|
18
17
|
from letta.settings import DatabaseChoice
|
19
18
|
|
20
19
|
if TYPE_CHECKING:
|
@@ -401,6 +400,8 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
401
400
|
query = query.order_by(cls.embedding.cosine_distance(query_embedding).asc())
|
402
401
|
else:
|
403
402
|
# SQLite with custom vector type
|
403
|
+
from letta.orm.sqlite_functions import adapt_array
|
404
|
+
|
404
405
|
query_embedding_binary = adapt_array(query_embedding)
|
405
406
|
query = query.order_by(
|
406
407
|
func.cosine_distance(cls.embedding, query_embedding_binary).asc(),
|
letta/prompts/gpt_system.py
CHANGED
@@ -1,26 +1,24 @@
|
|
1
1
|
import os
|
2
2
|
|
3
3
|
from letta.constants import LETTA_DIR
|
4
|
+
from letta.prompts.system_prompts import SYSTEM_PROMPTS
|
4
5
|
|
5
6
|
|
6
7
|
def get_system_text(key):
|
7
|
-
|
8
|
-
|
8
|
+
# first try to get from python constants (no file I/O)
|
9
|
+
if key in SYSTEM_PROMPTS:
|
10
|
+
return SYSTEM_PROMPTS[key].strip()
|
9
11
|
|
10
|
-
#
|
12
|
+
# fallback to user custom prompts in ~/.letta/system_prompts/*.txt
|
13
|
+
filename = f"{key}.txt"
|
14
|
+
user_system_prompts_dir = os.path.join(LETTA_DIR, "system_prompts")
|
15
|
+
# create directory if it doesn't exist
|
16
|
+
if not os.path.exists(user_system_prompts_dir):
|
17
|
+
os.makedirs(user_system_prompts_dir)
|
18
|
+
# look inside for a matching system prompt
|
19
|
+
file_path = os.path.join(user_system_prompts_dir, filename)
|
11
20
|
if os.path.exists(file_path):
|
12
21
|
with open(file_path, "r", encoding="utf-8") as file:
|
13
22
|
return file.read().strip()
|
14
23
|
else:
|
15
|
-
|
16
|
-
user_system_prompts_dir = os.path.join(LETTA_DIR, "system_prompts")
|
17
|
-
# create directory if it doesn't exist
|
18
|
-
if not os.path.exists(user_system_prompts_dir):
|
19
|
-
os.makedirs(user_system_prompts_dir)
|
20
|
-
# look inside for a matching system prompt
|
21
|
-
file_path = os.path.join(user_system_prompts_dir, filename)
|
22
|
-
if os.path.exists(file_path):
|
23
|
-
with open(file_path, "r", encoding="utf-8") as file:
|
24
|
-
return file.read().strip()
|
25
|
-
else:
|
26
|
-
raise FileNotFoundError(f"No file found for key {key}, path={file_path}")
|
24
|
+
raise FileNotFoundError(f"No system prompt found for key '{key}'")
|
@@ -0,0 +1,27 @@
|
|
1
|
+
from letta.prompts.system_prompts import (
|
2
|
+
memgpt_chat,
|
3
|
+
memgpt_generate_tool,
|
4
|
+
memgpt_v2_chat,
|
5
|
+
react,
|
6
|
+
sleeptime_doc_ingest,
|
7
|
+
sleeptime_v2,
|
8
|
+
summary_system_prompt,
|
9
|
+
voice_chat,
|
10
|
+
voice_sleeptime,
|
11
|
+
workflow,
|
12
|
+
)
|
13
|
+
|
14
|
+
SYSTEM_PROMPTS = {
|
15
|
+
"voice_chat": voice_chat.PROMPT,
|
16
|
+
"voice_sleeptime": voice_sleeptime.PROMPT,
|
17
|
+
"memgpt_v2_chat": memgpt_v2_chat.PROMPT,
|
18
|
+
"sleeptime_v2": sleeptime_v2.PROMPT,
|
19
|
+
"react": react.PROMPT,
|
20
|
+
"workflow": workflow.PROMPT,
|
21
|
+
"memgpt_chat": memgpt_chat.PROMPT,
|
22
|
+
"sleeptime_doc_ingest": sleeptime_doc_ingest.PROMPT,
|
23
|
+
"summary_system_prompt": summary_system_prompt.PROMPT,
|
24
|
+
"memgpt_generate_tool": memgpt_generate_tool.PROMPT,
|
25
|
+
}
|
26
|
+
|
27
|
+
__all__ = ["SYSTEM_PROMPTS"]
|
@@ -1,3 +1,4 @@
|
|
1
|
+
PROMPT = r"""
|
1
2
|
You are Letta, the latest version of Limnal Corporation's digital companion, developed in 2023.
|
2
3
|
Your task is to converse with a user from the perspective of your persona.
|
3
4
|
|
@@ -56,3 +57,4 @@ There is no function to search your core memory because it is always visible in
|
|
56
57
|
|
57
58
|
Base instructions finished.
|
58
59
|
From now on, you are going to act as your persona.
|
60
|
+
"""
|
@@ -1,3 +1,4 @@
|
|
1
|
+
PROMPT = r"""
|
1
2
|
<base_instructions>
|
2
3
|
You are Letta, the latest version of Limnal Corporation's digital companion, developed in 2025.
|
3
4
|
You are a memory-augmented agent with a memory system consisting of memory blocks. Your primary task is to generate tools for the user to use in their interactions with you.
|
@@ -64,7 +65,7 @@ A docstring must always be generated and formatted correctly as part of any gene
|
|
64
65
|
Example:
|
65
66
|
```python
|
66
67
|
def get_price(coin_ids: str, vs_currencies: str, reverse: bool) -> list:
|
67
|
-
"""
|
68
|
+
\"\"\"
|
68
69
|
Fetch prices from CoinGecko.
|
69
70
|
|
70
71
|
Args:
|
@@ -74,7 +75,7 @@ def get_price(coin_ids: str, vs_currencies: str, reverse: bool) -> list:
|
|
74
75
|
|
75
76
|
Returns:
|
76
77
|
list: the prices in the target currency, in the same order as the coin_ids if reverse is False, otherwise in the reverse order
|
77
|
-
"""
|
78
|
+
\"\"\"
|
78
79
|
...
|
79
80
|
```
|
80
81
|
</tool_docstring>
|
@@ -137,3 +138,4 @@ Example:
|
|
137
138
|
|
138
139
|
Base instructions finished.
|
139
140
|
</base_instructions>
|
141
|
+
"""
|
@@ -1,3 +1,4 @@
|
|
1
|
+
PROMPT = r"""
|
1
2
|
<base_instructions>
|
2
3
|
You are Letta, the latest version of Limnal Corporation's digital companion, developed in 2025.
|
3
4
|
You are a memory-augmented agent with a memory system consisting of memory blocks.
|
@@ -70,3 +71,4 @@ Maintain only those files relevant to the user’s current interaction.
|
|
70
71
|
|
71
72
|
Base instructions finished.
|
72
73
|
</base_instructions>
|
74
|
+
"""
|
@@ -1,3 +1,4 @@
|
|
1
|
+
PROMPT = r"""
|
1
2
|
<base_instructions>
|
2
3
|
You are Letta ReAct agent, the latest version of Limnal Corporation's digital AI agent, developed in 2025.
|
3
4
|
You are an AI agent that can be equipped with various tools which you can execute.
|
@@ -17,3 +18,4 @@ You should use your inner monologue to plan actions or think privately.
|
|
17
18
|
|
18
19
|
Base instructions finished.
|
19
20
|
</base_instructions>
|
21
|
+
"""
|
@@ -1,3 +1,4 @@
|
|
1
|
+
PROMPT = r"""
|
1
2
|
You are Letta-Sleeptime-Doc-Ingest, the latest version of Limnal Corporation's memory management system, developed in 2025.
|
2
3
|
|
3
4
|
You run in the background, organizing and maintaining the memories of an agent assistant who chats with the user.
|
@@ -33,3 +34,4 @@ Line numbers:
|
|
33
34
|
Line numbers are shown to you when viewing the memory blocks to help you make precise edits when needed. The line numbers are for viewing only, do NOT under any circumstances actually include the line numbers when using your memory editing tools, or they will not work properly.
|
34
35
|
|
35
36
|
You will be sent external context about the interaction, and your goal is to summarize the context and store it in the right memory blocks.
|
37
|
+
"""
|
@@ -1,3 +1,4 @@
|
|
1
|
+
PROMPT = r"""
|
1
2
|
<base_instructions>
|
2
3
|
You are Letta-Sleeptime-Memory, the latest version of Limnal Corporation's memory management system, developed in 2025.
|
3
4
|
|
@@ -26,3 +27,4 @@ Not every observation warrants a memory edit, be selective in your memory editin
|
|
26
27
|
Line numbers:
|
27
28
|
Line numbers are shown to you when viewing the memory blocks to help you make precise edits when needed. The line numbers are for viewing only, do NOT under any circumstances actually include the line numbers when using your memory editing tools, or they will not work properly.
|
28
29
|
</base_instructions>
|
30
|
+
"""
|
@@ -1,3 +1,4 @@
|
|
1
|
+
PROMPT = r"""
|
1
2
|
You are a memory-recall assistant that preserves conversational context as messages exit the AI's context window.
|
2
3
|
|
3
4
|
<core_function>
|
@@ -60,3 +61,4 @@ Apply to: high-level discussions, philosophical topics, general preferences
|
|
60
61
|
<critical_reminder>
|
61
62
|
Your notes are the sole record of evicted messages. Every word should enable future continuity.
|
62
63
|
</critical_reminder>
|
64
|
+
"""
|
@@ -1,3 +1,4 @@
|
|
1
|
+
PROMPT = r"""
|
1
2
|
You are the single LLM turn in a low-latency voice assistant pipeline (STT ➜ LLM ➜ TTS).
|
2
3
|
Your goals, in priority order, are:
|
3
4
|
|
@@ -27,3 +28,4 @@ Tone.
|
|
27
28
|
|
28
29
|
The memory of the conversation so far below contains enduring facts and user preferences produced by the system.
|
29
30
|
Treat it as reliable ground-truth context. If the user references information that should appear here but does not, follow guidelines and consider `search_memory`.
|
31
|
+
"""
|
@@ -1,3 +1,4 @@
|
|
1
|
+
PROMPT = r"""
|
1
2
|
You are Letta-Sleeptime-Memory, the latest version of Limnal Corporation's memory management system (developed 2025). You operate asynchronously to maintain the memories of a chat agent interacting with a user.
|
2
3
|
|
3
4
|
Your current task involves a two-phase process executed sequentially:
|
@@ -71,3 +72,4 @@ After the `store_memories` tool call is processed, consider the current content
|
|
71
72
|
|
72
73
|
Output Requirements:
|
73
74
|
- You MUST ONLY output tool calls in the specified sequence: First `store_memories` (once), then one or more `rethink_user_memory` calls, and finally `finish_rethinking_memory` (once).
|
75
|
+
"""
|
@@ -1,3 +1,4 @@
|
|
1
|
+
PROMPT = r"""
|
1
2
|
<base_instructions>
|
2
3
|
You are Letta workflow agent, the latest version of Limnal Corporation's digital AI agent, developed in 2025.
|
3
4
|
You are an AI agent that is capable of running one or more tools in a sequence to accomplish a task.
|
@@ -13,3 +14,4 @@ You should use your inner monologue to plan actions or think privately.
|
|
13
14
|
|
14
15
|
Base instructions finished.
|
15
16
|
</base_instructions>
|
17
|
+
"""
|