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.
Files changed (99) hide show
  1. letta/__init__.py +10 -2
  2. letta/adapters/letta_llm_request_adapter.py +0 -1
  3. letta/adapters/letta_llm_stream_adapter.py +0 -1
  4. letta/agent.py +1 -1
  5. letta/agents/letta_agent.py +1 -4
  6. letta/agents/letta_agent_v2.py +2 -1
  7. letta/agents/voice_agent.py +1 -1
  8. letta/functions/function_sets/multi_agent.py +1 -1
  9. letta/functions/helpers.py +1 -1
  10. letta/helpers/converters.py +8 -2
  11. letta/helpers/crypto_utils.py +144 -0
  12. letta/llm_api/llm_api_tools.py +0 -1
  13. letta/llm_api/llm_client_base.py +0 -2
  14. letta/orm/__init__.py +1 -0
  15. letta/orm/agent.py +5 -1
  16. letta/orm/job.py +3 -1
  17. letta/orm/mcp_oauth.py +6 -0
  18. letta/orm/mcp_server.py +7 -1
  19. letta/orm/sqlalchemy_base.py +2 -1
  20. letta/prompts/gpt_system.py +13 -15
  21. letta/prompts/system_prompts/__init__.py +27 -0
  22. letta/prompts/{system/memgpt_chat.txt → system_prompts/memgpt_chat.py} +2 -0
  23. letta/prompts/{system/memgpt_generate_tool.txt → system_prompts/memgpt_generate_tool.py} +4 -2
  24. letta/prompts/{system/memgpt_v2_chat.txt → system_prompts/memgpt_v2_chat.py} +2 -0
  25. letta/prompts/{system/react.txt → system_prompts/react.py} +2 -0
  26. letta/prompts/{system/sleeptime_doc_ingest.txt → system_prompts/sleeptime_doc_ingest.py} +2 -0
  27. letta/prompts/{system/sleeptime_v2.txt → system_prompts/sleeptime_v2.py} +2 -0
  28. letta/prompts/{system/summary_system_prompt.txt → system_prompts/summary_system_prompt.py} +2 -0
  29. letta/prompts/{system/voice_chat.txt → system_prompts/voice_chat.py} +2 -0
  30. letta/prompts/{system/voice_sleeptime.txt → system_prompts/voice_sleeptime.py} +2 -0
  31. letta/prompts/{system/workflow.txt → system_prompts/workflow.py} +2 -0
  32. letta/schemas/agent.py +10 -7
  33. letta/schemas/job.py +10 -0
  34. letta/schemas/mcp.py +146 -6
  35. letta/schemas/provider_trace.py +0 -2
  36. letta/schemas/run.py +2 -0
  37. letta/schemas/secret.py +378 -0
  38. letta/serialize_schemas/marshmallow_agent.py +4 -0
  39. letta/server/rest_api/dependencies.py +37 -0
  40. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +4 -3
  41. letta/server/rest_api/routers/v1/__init__.py +2 -0
  42. letta/server/rest_api/routers/v1/agents.py +115 -107
  43. letta/server/rest_api/routers/v1/archives.py +113 -0
  44. letta/server/rest_api/routers/v1/blocks.py +44 -20
  45. letta/server/rest_api/routers/v1/embeddings.py +3 -3
  46. letta/server/rest_api/routers/v1/folders.py +107 -47
  47. letta/server/rest_api/routers/v1/groups.py +52 -32
  48. letta/server/rest_api/routers/v1/identities.py +110 -21
  49. letta/server/rest_api/routers/v1/internal_templates.py +28 -13
  50. letta/server/rest_api/routers/v1/jobs.py +19 -14
  51. letta/server/rest_api/routers/v1/llms.py +6 -8
  52. letta/server/rest_api/routers/v1/messages.py +14 -14
  53. letta/server/rest_api/routers/v1/organizations.py +1 -1
  54. letta/server/rest_api/routers/v1/providers.py +40 -16
  55. letta/server/rest_api/routers/v1/runs.py +28 -20
  56. letta/server/rest_api/routers/v1/sandbox_configs.py +25 -25
  57. letta/server/rest_api/routers/v1/sources.py +44 -45
  58. letta/server/rest_api/routers/v1/steps.py +27 -25
  59. letta/server/rest_api/routers/v1/tags.py +11 -7
  60. letta/server/rest_api/routers/v1/telemetry.py +11 -6
  61. letta/server/rest_api/routers/v1/tools.py +78 -80
  62. letta/server/rest_api/routers/v1/users.py +1 -1
  63. letta/server/rest_api/routers/v1/voice.py +6 -5
  64. letta/server/rest_api/utils.py +1 -18
  65. letta/services/agent_manager.py +17 -9
  66. letta/services/agent_serialization_manager.py +11 -3
  67. letta/services/archive_manager.py +73 -0
  68. letta/services/file_manager.py +6 -0
  69. letta/services/group_manager.py +2 -1
  70. letta/services/helpers/agent_manager_helper.py +6 -1
  71. letta/services/identity_manager.py +67 -0
  72. letta/services/job_manager.py +18 -2
  73. letta/services/mcp_manager.py +198 -82
  74. letta/services/provider_manager.py +14 -1
  75. letta/services/source_manager.py +11 -1
  76. letta/services/telemetry_manager.py +2 -0
  77. letta/services/tool_executor/composio_tool_executor.py +1 -1
  78. letta/services/tool_manager.py +46 -9
  79. letta/services/tool_sandbox/base.py +2 -3
  80. letta/utils.py +4 -2
  81. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/METADATA +5 -2
  82. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/RECORD +85 -94
  83. letta/prompts/system/memgpt_base.txt +0 -54
  84. letta/prompts/system/memgpt_chat_compressed.txt +0 -13
  85. letta/prompts/system/memgpt_chat_fstring.txt +0 -51
  86. letta/prompts/system/memgpt_convo_only.txt +0 -12
  87. letta/prompts/system/memgpt_doc.txt +0 -50
  88. letta/prompts/system/memgpt_gpt35_extralong.txt +0 -53
  89. letta/prompts/system/memgpt_intuitive_knowledge.txt +0 -31
  90. letta/prompts/system/memgpt_memory_only.txt +0 -29
  91. letta/prompts/system/memgpt_modified_chat.txt +0 -23
  92. letta/prompts/system/memgpt_modified_o1.txt +0 -31
  93. letta/prompts/system/memgpt_offline_memory.txt +0 -23
  94. letta/prompts/system/memgpt_offline_memory_chat.txt +0 -35
  95. letta/prompts/system/memgpt_sleeptime_chat.txt +0 -52
  96. letta/prompts/system/sleeptime.txt +0 -37
  97. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/WHEEL +0 -0
  98. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/entry_points.txt +0 -0
  99. {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
- from letta.orm import sqlite_functions
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",
@@ -164,7 +164,6 @@ class LettaLLMStreamAdapter(LettaLLMAdapter):
164
164
  },
165
165
  },
166
166
  step_id=step_id, # Use original step_id for telemetry
167
- organization_id=actor.organization_id,
168
167
  ),
169
168
  ),
170
169
  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.tool_exec_environment_variables:
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
@@ -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.tool_exec_environment_variables}
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,
@@ -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.tool_exec_environment_variables}
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()
@@ -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.tool_exec_environment_variables}
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.utils import get_letta_server
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:
@@ -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.utils import get_letta_server
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
 
@@ -44,8 +44,14 @@ from letta.schemas.tool_rule import (
44
44
  )
45
45
  from letta.settings import DatabaseChoice, settings
46
46
 
47
- if settings.database_engine == DatabaseChoice.SQLITE:
48
- import sqlite_vec
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)
@@ -235,7 +235,6 @@ def create(
235
235
  request_json=prepare_openai_payload(data),
236
236
  response_json=response.model_json_schema(),
237
237
  step_id=step_id,
238
- organization_id=actor.organization_id,
239
238
  ),
240
239
  )
241
240
 
@@ -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"
@@ -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(),
@@ -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
- filename = f"{key}.txt"
8
- file_path = os.path.join(os.path.dirname(__file__), "system", filename)
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
- # first look in prompts/system/*.txt
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
- # try looking in ~/.letta/system_prompts/*.txt
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
+ """