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.
- 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 +4 -4
- letta/agents/agent_loop.py +2 -1
- letta/agents/base_agent.py +1 -1
- letta/agents/letta_agent.py +1 -4
- letta/agents/letta_agent_v2.py +5 -4
- letta/agents/temporal/activities/__init__.py +4 -0
- letta/agents/temporal/activities/example_activity.py +7 -0
- letta/agents/temporal/activities/prepare_messages.py +10 -0
- letta/agents/temporal/temporal_agent_workflow.py +56 -0
- letta/agents/temporal/types.py +25 -0
- letta/agents/voice_agent.py +3 -3
- 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 +9 -4
- 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/prompt_generator.py +4 -4
- letta/schemas/agent.py +14 -200
- letta/schemas/enums.py +15 -0
- letta/schemas/job.py +10 -0
- letta/schemas/mcp.py +146 -6
- letta/schemas/memory.py +216 -103
- letta/schemas/provider_trace.py +0 -2
- letta/schemas/run.py +2 -0
- letta/schemas/secret.py +378 -0
- letta/schemas/step.py +5 -1
- letta/schemas/tool_rule.py +34 -44
- letta/serialize_schemas/marshmallow_agent.py +4 -0
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +9 -4
- letta/server/rest_api/routers/v1/archives.py +113 -0
- letta/server/rest_api/routers/v1/jobs.py +7 -2
- letta/server/rest_api/routers/v1/runs.py +9 -1
- letta/server/rest_api/routers/v1/steps.py +29 -0
- letta/server/rest_api/routers/v1/tools.py +7 -26
- letta/server/server.py +2 -2
- letta/services/agent_manager.py +21 -15
- letta/services/agent_serialization_manager.py +11 -3
- letta/services/archive_manager.py +73 -0
- letta/services/helpers/agent_manager_helper.py +10 -5
- letta/services/job_manager.py +18 -2
- letta/services/mcp_manager.py +198 -82
- letta/services/step_manager.py +26 -0
- letta/services/summarizer/summarizer.py +25 -3
- letta/services/telemetry_manager.py +2 -0
- letta/services/tool_executor/composio_tool_executor.py +1 -1
- letta/services/tool_executor/sandbox_tool_executor.py +2 -2
- letta/services/tool_sandbox/base.py +135 -9
- letta/settings.py +2 -2
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/METADATA +6 -3
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/RECORD +62 -55
- letta/templates/template_helper.py +0 -53
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
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
@@ -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
|
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
|
-
|
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
|
-
|
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.
|
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/agent_loop.py
CHANGED
@@ -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
|
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
|
letta/agents/base_agent.py
CHANGED
@@ -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 =
|
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)
|
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
@@ -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,
|
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 =
|
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.
|
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,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
|
letta/agents/voice_agent.py
CHANGED
@@ -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
|
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.
|
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,
|
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
@@ -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
|
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
|
-
|
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
|
-
|
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"
|
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(),
|