letta-nightly 0.11.7.dev20250916104104__py3-none-any.whl → 0.11.7.dev20250918104055__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 (63) 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 +4 -4
  5. letta/agents/agent_loop.py +2 -1
  6. letta/agents/base_agent.py +1 -1
  7. letta/agents/letta_agent.py +1 -4
  8. letta/agents/letta_agent_v2.py +5 -4
  9. letta/agents/temporal/activities/__init__.py +4 -0
  10. letta/agents/temporal/activities/example_activity.py +7 -0
  11. letta/agents/temporal/activities/prepare_messages.py +10 -0
  12. letta/agents/temporal/temporal_agent_workflow.py +56 -0
  13. letta/agents/temporal/types.py +25 -0
  14. letta/agents/voice_agent.py +3 -3
  15. letta/helpers/converters.py +8 -2
  16. letta/helpers/crypto_utils.py +144 -0
  17. letta/llm_api/llm_api_tools.py +0 -1
  18. letta/llm_api/llm_client_base.py +0 -2
  19. letta/orm/__init__.py +1 -0
  20. letta/orm/agent.py +9 -4
  21. letta/orm/job.py +3 -1
  22. letta/orm/mcp_oauth.py +6 -0
  23. letta/orm/mcp_server.py +7 -1
  24. letta/orm/sqlalchemy_base.py +2 -1
  25. letta/prompts/prompt_generator.py +4 -4
  26. letta/schemas/agent.py +14 -200
  27. letta/schemas/enums.py +15 -0
  28. letta/schemas/job.py +10 -0
  29. letta/schemas/mcp.py +146 -6
  30. letta/schemas/memory.py +216 -103
  31. letta/schemas/provider_trace.py +0 -2
  32. letta/schemas/run.py +2 -0
  33. letta/schemas/secret.py +378 -0
  34. letta/schemas/step.py +5 -1
  35. letta/schemas/tool_rule.py +34 -44
  36. letta/serialize_schemas/marshmallow_agent.py +4 -0
  37. letta/server/rest_api/routers/v1/__init__.py +2 -0
  38. letta/server/rest_api/routers/v1/agents.py +9 -4
  39. letta/server/rest_api/routers/v1/archives.py +113 -0
  40. letta/server/rest_api/routers/v1/jobs.py +7 -2
  41. letta/server/rest_api/routers/v1/runs.py +9 -1
  42. letta/server/rest_api/routers/v1/steps.py +29 -0
  43. letta/server/rest_api/routers/v1/tools.py +7 -26
  44. letta/server/server.py +2 -2
  45. letta/services/agent_manager.py +21 -15
  46. letta/services/agent_serialization_manager.py +11 -3
  47. letta/services/archive_manager.py +73 -0
  48. letta/services/helpers/agent_manager_helper.py +10 -5
  49. letta/services/job_manager.py +18 -2
  50. letta/services/mcp_manager.py +198 -82
  51. letta/services/step_manager.py +26 -0
  52. letta/services/summarizer/summarizer.py +25 -3
  53. letta/services/telemetry_manager.py +2 -0
  54. letta/services/tool_executor/composio_tool_executor.py +1 -1
  55. letta/services/tool_executor/sandbox_tool_executor.py +2 -2
  56. letta/services/tool_sandbox/base.py +135 -9
  57. letta/settings.py +2 -2
  58. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/METADATA +6 -3
  59. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/RECORD +62 -55
  60. letta/templates/template_helper.py +0 -53
  61. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/WHEEL +0 -0
  62. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/entry_points.txt +0 -0
  63. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.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
@@ -42,7 +42,7 @@ from letta.memory import summarize_messages
42
42
  from letta.orm import User
43
43
  from letta.otel.tracing import log_event, trace_method
44
44
  from letta.prompts.prompt_generator import PromptGenerator
45
- from letta.schemas.agent import AgentState, AgentStepResponse, UpdateAgent, get_prompt_template_for_agent_type
45
+ from letta.schemas.agent import AgentState, AgentStepResponse, UpdateAgent
46
46
  from letta.schemas.block import BlockUpdate
47
47
  from letta.schemas.embedding_config import EmbeddingConfig
48
48
  from letta.schemas.enums import MessageRole, ProviderType, StepStatus, ToolType
@@ -221,7 +221,7 @@ class Agent(BaseAgent):
221
221
  self.agent_state.memory = Memory(
222
222
  blocks=[self.block_manager.get_block_by_id(block.id, actor=self.user) for block in self.agent_state.memory.get_blocks()],
223
223
  file_blocks=self.agent_state.memory.file_blocks,
224
- prompt_template=get_prompt_template_for_agent_type(self.agent_state.agent_type),
224
+ agent_type=self.agent_state.agent_type,
225
225
  )
226
226
 
227
227
  # NOTE: don't do this since re-buildin the memory is handled at the start of the step
@@ -880,7 +880,7 @@ class Agent(BaseAgent):
880
880
  current_persisted_memory = Memory(
881
881
  blocks=[self.block_manager.get_block_by_id(block.id, actor=self.user) for block in self.agent_state.memory.get_blocks()],
882
882
  file_blocks=self.agent_state.memory.file_blocks,
883
- prompt_template=get_prompt_template_for_agent_type(self.agent_state.agent_type),
883
+ agent_type=self.agent_state.agent_type,
884
884
  ) # read blocks from DB
885
885
  self.update_memory_if_changed(current_persisted_memory)
886
886
 
@@ -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
@@ -3,7 +3,8 @@ from typing import TYPE_CHECKING
3
3
  from letta.agents.base_agent_v2 import BaseAgentV2
4
4
  from letta.agents.letta_agent_v2 import LettaAgentV2
5
5
  from letta.groups.sleeptime_multi_agent_v3 import SleeptimeMultiAgentV3
6
- from letta.schemas.agent import AgentState, AgentType
6
+ from letta.schemas.agent import AgentState
7
+ from letta.schemas.enums import AgentType
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  from letta.orm import User
@@ -139,7 +139,7 @@ class BaseAgent(ABC):
139
139
  curr_dynamic_section = extract_dynamic_section(curr_system_message_text)
140
140
 
141
141
  # generate just the memory string with current state for comparison
142
- curr_memory_str = await agent_state.memory.compile_in_thread_async(
142
+ curr_memory_str = agent_state.memory.compile(
143
143
  tool_usage_rules=tool_constraint_block, sources=agent_state.sources, max_files_open=agent_state.max_files_open
144
144
  )
145
145
  new_dynamic_section = extract_dynamic_section(curr_memory_str)
@@ -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,
@@ -29,8 +29,8 @@ from letta.local_llm.constants import INNER_THOUGHTS_KWARG
29
29
  from letta.log import get_logger
30
30
  from letta.otel.tracing import log_event, trace_method, tracer
31
31
  from letta.prompts.prompt_generator import PromptGenerator
32
- from letta.schemas.agent import AgentState, AgentType, UpdateAgent
33
- from letta.schemas.enums import JobStatus, MessageRole, MessageStreamStatus, StepStatus
32
+ from letta.schemas.agent import AgentState, UpdateAgent
33
+ from letta.schemas.enums import AgentType, JobStatus, MessageRole, MessageStreamStatus, StepStatus
34
34
  from letta.schemas.letta_message import LettaMessage, MessageType
35
35
  from letta.schemas.letta_message_content import OmittedReasoningContent, ReasoningContent, RedactedReasoningContent, TextContent
36
36
  from letta.schemas.letta_response import LettaResponse
@@ -679,7 +679,7 @@ class LettaAgentV2(BaseAgentV2):
679
679
  curr_dynamic_section = extract_dynamic_section(curr_system_message_text)
680
680
 
681
681
  # generate just the memory string with current state for comparison
682
- curr_memory_str = await agent_state.memory.compile_in_thread_async(
682
+ curr_memory_str = agent_state.memory.compile(
683
683
  tool_usage_rules=tool_constraint_block, sources=agent_state.sources, max_files_open=agent_state.max_files_open
684
684
  )
685
685
  new_dynamic_section = extract_dynamic_section(curr_memory_str)
@@ -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()
@@ -0,0 +1,4 @@
1
+ from .core_activities import your_activity
2
+ from .processing_activities import process_activity_result
3
+
4
+ __all__ = ["prepare_messages", "example_activity"]
@@ -0,0 +1,7 @@
1
+ from temporalio import activity
2
+ from ..types import PreparedMessages
3
+
4
+ @activity.defn(name="example_activity")
5
+ async def example_activity(input_: PreparedMessages) -> str:
6
+ # Process the result from the previous activity
7
+ pass
@@ -0,0 +1,10 @@
1
+ from temporalio import activity
2
+ from ..types import WorkflowInputParams, PreparedMessages
3
+
4
+ @activity.defn(name="prepare_messages")
5
+ async def prepare_messages(input_: WorkflowInputParams) -> PreparedMessages:
6
+ # TODO
7
+ return PreparedMessages(
8
+ in_context_messages=[],
9
+ input_messages_to_persist=[],
10
+ )
@@ -0,0 +1,56 @@
1
+ from dataclasses import dataclass
2
+ from datetime import timedelta
3
+ from temporalio import workflow
4
+
5
+ from ...schemas.letta_stop_reason import StopReasonType
6
+ from ...schemas.usage import LettaUsageStatistics
7
+
8
+ # Import activity, passing it through the sandbox without reloading the module
9
+ with workflow.unsafe.imports_passed_through():
10
+ from .activities import prepare_messages, example_activity
11
+ from .types import WorkflowInputParams, FinalResult
12
+
13
+ @workflow.defn
14
+ class TemporalAgentWorkflow:
15
+ @workflow.run
16
+ async def run(self, params: WorkflowInputParams) -> FinalResult:
17
+ messages = await workflow.execute_activity(
18
+ prepare_messages, params, start_to_close_timeout=timedelta(seconds=5)
19
+ )
20
+ result = FinalResult(
21
+ stop_reason=StopReasonType.end_turn,
22
+ usage=LettaUsageStatistics(),
23
+ )
24
+
25
+ for i in range(params.max_steps):
26
+ _ = await workflow.execute_activity(
27
+ example_activity, messages, start_to_close_timeout=timedelta(seconds=5)
28
+ )
29
+ # self._maybe_get_approval_messages
30
+ # if approval
31
+ # parse tool params from approval message
32
+ # else
33
+ # self._check_run_cancellation
34
+ # self._refresh_messages
35
+
36
+ # try:
37
+ # self.llm_client.build_request_data
38
+ # self.llm_client.request_async
39
+ # self.llm_client.convert_response_to_chat_completion
40
+ # except ContextWindowExceededError:
41
+ # self.summarize_conversation_history
42
+ # self.llm_client.build_request_data
43
+ # self.llm_client.request_async
44
+ # self.llm_client.convert_response_to_chat_completion
45
+
46
+ # self._update_global_usage_stats
47
+ # parse tool call args
48
+ # self._handle_ai_response <-- this needs to be broken up into individual pieces
49
+ # self.agent_manager.update_message_ids_async
50
+ # convert message to letta message and return
51
+ pass
52
+
53
+ # self.summarize_conversation_history
54
+ # put together final result
55
+
56
+ return result
@@ -0,0 +1,25 @@
1
+ from dataclasses import dataclass
2
+ from typing import List
3
+ from letta.schemas.agent import AgentState
4
+ from letta.schemas.letta_stop_reason import StopReasonType
5
+ from letta.schemas.message import Message, MessageCreate
6
+ from letta.schemas.usage import LettaUsageStatistics
7
+ from letta.schemas.user import User
8
+
9
+ @dataclass
10
+ class WorkflowInputParams:
11
+ agent_state: AgentState
12
+ messages: list[MessageCreate]
13
+ actor: User
14
+ max_steps: int = 50
15
+
16
+ @dataclass
17
+ class PreparedMessages:
18
+ in_context_messages: List[Message]
19
+ input_messages_to_persist: List[Message]
20
+
21
+
22
+ @dataclass
23
+ class FinalResult:
24
+ stop_reason: StopReasonType
25
+ usage: LettaUsageStatistics
@@ -14,8 +14,8 @@ from letta.helpers.tool_execution_helper import add_pre_execution_message, enabl
14
14
  from letta.interfaces.openai_chat_completions_streaming_interface import OpenAIChatCompletionsStreamingInterface
15
15
  from letta.log import get_logger
16
16
  from letta.prompts.prompt_generator import PromptGenerator
17
- from letta.schemas.agent import AgentState, AgentType
18
- from letta.schemas.enums import MessageRole, ToolType
17
+ from letta.schemas.agent import AgentState
18
+ from letta.schemas.enums import AgentType, MessageRole, ToolType
19
19
  from letta.schemas.letta_response import LettaResponse
20
20
  from letta.schemas.message import Message, MessageCreate
21
21
  from letta.schemas.openai.chat_completion_request import (
@@ -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,
@@ -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
@@ -13,8 +13,9 @@ from letta.orm.identity import Identity
13
13
  from letta.orm.mixins import OrganizationMixin, ProjectMixin, TemplateEntityMixin, TemplateMixin
14
14
  from letta.orm.organization import Organization
15
15
  from letta.orm.sqlalchemy_base import SqlalchemyBase
16
- from letta.schemas.agent import AgentState as PydanticAgentState, AgentType, get_prompt_template_for_agent_type
16
+ from letta.schemas.agent import AgentState as PydanticAgentState
17
17
  from letta.schemas.embedding_config import EmbeddingConfig
18
+ from letta.schemas.enums import AgentType
18
19
  from letta.schemas.llm_config import LLMConfig
19
20
  from letta.schemas.memory import Memory
20
21
  from letta.schemas.response_format import ResponseFormatUnion
@@ -233,6 +234,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
233
234
  "identity_ids": [],
234
235
  "multi_agent_group": None,
235
236
  "tool_exec_environment_variables": [],
237
+ "secrets": [],
236
238
  }
237
239
 
238
240
  # Optional fields: only included if requested
@@ -248,11 +250,12 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
248
250
  if (block := b.to_pydantic_block(per_file_view_window_char_limit=self._get_per_file_view_window_char_limit()))
249
251
  is not None
250
252
  ],
251
- prompt_template=get_prompt_template_for_agent_type(self.agent_type),
253
+ agent_type=self.agent_type,
252
254
  ),
253
255
  "identity_ids": lambda: [i.id for i in self.identities],
254
256
  "multi_agent_group": lambda: self.multi_agent_group,
255
257
  "tool_exec_environment_variables": lambda: self.tool_exec_environment_variables,
258
+ "secrets": lambda: self.tool_exec_environment_variables,
256
259
  }
257
260
 
258
261
  include_relationships = set(optional_fields.keys() if include_relationships is None else include_relationships)
@@ -324,6 +327,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
324
327
  "identity_ids": [],
325
328
  "multi_agent_group": None,
326
329
  "tool_exec_environment_variables": [],
330
+ "secrets": [],
327
331
  }
328
332
 
329
333
  # Initialize include_relationships to an empty set if it's None
@@ -344,7 +348,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
344
348
  multi_agent_group = self.awaitable_attrs.multi_agent_group if "multi_agent_group" in include_relationships else none_async()
345
349
  tool_exec_environment_variables = (
346
350
  self.awaitable_attrs.tool_exec_environment_variables
347
- if "tool_exec_environment_variables" in include_relationships
351
+ if "tool_exec_environment_variables" in include_relationships or "secrets" in include_relationships
348
352
  else empty_list_async()
349
353
  )
350
354
  file_agents = self.awaitable_attrs.file_agents if "memory" in include_relationships else empty_list_async()
@@ -363,10 +367,11 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
363
367
  for b in file_agents
364
368
  if (block := b.to_pydantic_block(per_file_view_window_char_limit=self._get_per_file_view_window_char_limit())) is not None
365
369
  ],
366
- prompt_template=get_prompt_template_for_agent_type(self.agent_type),
370
+ agent_type=self.agent_type,
367
371
  )
368
372
  state["identity_ids"] = [i.id for i in identities]
369
373
  state["multi_agent_group"] = multi_agent_group
370
374
  state["tool_exec_environment_variables"] = tool_exec_environment_variables
375
+ state["secrets"] = tool_exec_environment_variables
371
376
 
372
377
  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(),