letta-nightly 0.11.7.dev20250916104104__py3-none-any.whl → 0.11.7.dev20250917104122__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- letta/__init__.py +10 -2
- letta/adapters/letta_llm_request_adapter.py +0 -1
- letta/adapters/letta_llm_stream_adapter.py +0 -1
- letta/agent.py +1 -1
- letta/agents/letta_agent.py +1 -4
- letta/agents/letta_agent_v2.py +2 -1
- letta/agents/voice_agent.py +1 -1
- letta/helpers/converters.py +8 -2
- letta/helpers/crypto_utils.py +144 -0
- letta/llm_api/llm_api_tools.py +0 -1
- letta/llm_api/llm_client_base.py +0 -2
- letta/orm/__init__.py +1 -0
- letta/orm/agent.py +5 -1
- letta/orm/job.py +3 -1
- letta/orm/mcp_oauth.py +6 -0
- letta/orm/mcp_server.py +7 -1
- letta/orm/sqlalchemy_base.py +2 -1
- letta/schemas/agent.py +10 -7
- letta/schemas/job.py +10 -0
- letta/schemas/mcp.py +146 -6
- letta/schemas/provider_trace.py +0 -2
- letta/schemas/run.py +2 -0
- letta/schemas/secret.py +378 -0
- letta/serialize_schemas/marshmallow_agent.py +4 -0
- letta/server/rest_api/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/tools.py +7 -26
- letta/services/agent_manager.py +17 -9
- letta/services/agent_serialization_manager.py +11 -3
- letta/services/archive_manager.py +73 -0
- letta/services/helpers/agent_manager_helper.py +6 -1
- letta/services/job_manager.py +18 -2
- letta/services/mcp_manager.py +198 -82
- letta/services/telemetry_manager.py +2 -0
- letta/services/tool_executor/composio_tool_executor.py +1 -1
- letta/services/tool_sandbox/base.py +2 -3
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/METADATA +5 -2
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/RECORD +44 -41
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/licenses/LICENSE +0 -0
letta/__init__.py
CHANGED
@@ -10,8 +10,16 @@ except PackageNotFoundError:
|
|
10
10
|
if os.environ.get("LETTA_VERSION"):
|
11
11
|
__version__ = os.environ["LETTA_VERSION"]
|
12
12
|
|
13
|
-
# Import sqlite_functions early to ensure event handlers are registered
|
14
|
-
|
13
|
+
# Import sqlite_functions early to ensure event handlers are registered (only for SQLite)
|
14
|
+
# This is only needed for the server, not for client usage
|
15
|
+
try:
|
16
|
+
from letta.settings import DatabaseChoice, settings
|
17
|
+
|
18
|
+
if settings.database_engine == DatabaseChoice.SQLITE:
|
19
|
+
from letta.orm import sqlite_functions
|
20
|
+
except ImportError:
|
21
|
+
# If sqlite_vec is not installed, it's fine for client usage
|
22
|
+
pass
|
15
23
|
|
16
24
|
# # imports for easier access
|
17
25
|
from letta.schemas.agent import AgentState
|
@@ -106,7 +106,6 @@ class LettaLLMRequestAdapter(LettaLLMAdapter):
|
|
106
106
|
request_json=self.request_data,
|
107
107
|
response_json=self.response_data,
|
108
108
|
step_id=step_id, # Use original step_id for telemetry
|
109
|
-
organization_id=actor.organization_id,
|
110
109
|
),
|
111
110
|
),
|
112
111
|
label="create_provider_trace",
|
letta/agent.py
CHANGED
@@ -1628,7 +1628,7 @@ class Agent(BaseAgent):
|
|
1628
1628
|
action_name = generate_composio_action_from_func_name(target_letta_tool.name)
|
1629
1629
|
# Get entity ID from the agent_state
|
1630
1630
|
entity_id = None
|
1631
|
-
for env_var in self.agent_state.
|
1631
|
+
for env_var in self.agent_state.secrets:
|
1632
1632
|
if env_var.key == COMPOSIO_ENTITY_ENV_VAR_KEY:
|
1633
1633
|
entity_id = env_var.value
|
1634
1634
|
# Get composio_api_key
|
letta/agents/letta_agent.py
CHANGED
@@ -405,7 +405,6 @@ class LettaAgent(BaseAgent):
|
|
405
405
|
request_json=request_data,
|
406
406
|
response_json=response_data,
|
407
407
|
step_id=step_id, # Use original step_id for telemetry
|
408
|
-
organization_id=self.actor.organization_id,
|
409
408
|
),
|
410
409
|
)
|
411
410
|
step_progression = StepProgression.LOGGED_TRACE
|
@@ -751,7 +750,6 @@ class LettaAgent(BaseAgent):
|
|
751
750
|
request_json=request_data,
|
752
751
|
response_json=response_data,
|
753
752
|
step_id=step_id, # Use original step_id for telemetry
|
754
|
-
organization_id=self.actor.organization_id,
|
755
753
|
),
|
756
754
|
)
|
757
755
|
step_progression = StepProgression.LOGGED_TRACE
|
@@ -1173,7 +1171,6 @@ class LettaAgent(BaseAgent):
|
|
1173
1171
|
},
|
1174
1172
|
},
|
1175
1173
|
step_id=step_id, # Use original step_id for telemetry
|
1176
|
-
organization_id=self.actor.organization_id,
|
1177
1174
|
),
|
1178
1175
|
)
|
1179
1176
|
step_progression = StepProgression.LOGGED_TRACE
|
@@ -1877,7 +1874,7 @@ class LettaAgent(BaseAgent):
|
|
1877
1874
|
start_time = get_utc_timestamp_ns()
|
1878
1875
|
agent_step_span.add_event(name="tool_execution_started")
|
1879
1876
|
|
1880
|
-
sandbox_env_vars = {var.key: var.value for var in agent_state.
|
1877
|
+
sandbox_env_vars = {var.key: var.value for var in agent_state.secrets}
|
1881
1878
|
tool_execution_manager = ToolExecutionManager(
|
1882
1879
|
agent_state=agent_state,
|
1883
1880
|
message_manager=self.message_manager,
|
letta/agents/letta_agent_v2.py
CHANGED
@@ -1106,7 +1106,7 @@ class LettaAgentV2(BaseAgentV2):
|
|
1106
1106
|
start_time = get_utc_timestamp_ns()
|
1107
1107
|
agent_step_span.add_event(name="tool_execution_started")
|
1108
1108
|
|
1109
|
-
sandbox_env_vars = {var.key: var.value for var in agent_state.
|
1109
|
+
sandbox_env_vars = {var.key: var.value for var in agent_state.secrets}
|
1110
1110
|
tool_execution_manager = ToolExecutionManager(
|
1111
1111
|
agent_state=agent_state,
|
1112
1112
|
message_manager=self.message_manager,
|
@@ -1226,6 +1226,7 @@ class LettaAgentV2(BaseAgentV2):
|
|
1226
1226
|
new_status=JobStatus.failed if is_error else JobStatus.completed,
|
1227
1227
|
actor=self.actor,
|
1228
1228
|
metadata=job_update_metadata,
|
1229
|
+
stop_reason=self.stop_reason.stop_reason if self.stop_reason else StopReasonType.error,
|
1229
1230
|
)
|
1230
1231
|
if request_span:
|
1231
1232
|
request_span.end()
|
letta/agents/voice_agent.py
CHANGED
@@ -441,7 +441,7 @@ class VoiceAgent(BaseAgent):
|
|
441
441
|
)
|
442
442
|
|
443
443
|
# Use ToolExecutionManager for modern tool execution
|
444
|
-
sandbox_env_vars = {var.key: var.value for var in agent_state.
|
444
|
+
sandbox_env_vars = {var.key: var.value for var in agent_state.secrets}
|
445
445
|
tool_execution_manager = ToolExecutionManager(
|
446
446
|
agent_state=agent_state,
|
447
447
|
message_manager=self.message_manager,
|
letta/helpers/converters.py
CHANGED
@@ -44,8 +44,14 @@ from letta.schemas.tool_rule import (
|
|
44
44
|
)
|
45
45
|
from letta.settings import DatabaseChoice, settings
|
46
46
|
|
47
|
-
if
|
48
|
-
|
47
|
+
# Only import sqlite_vec if we're actually using SQLite database
|
48
|
+
# This is a runtime dependency only needed for SQLite vector operations
|
49
|
+
try:
|
50
|
+
if settings.database_engine == DatabaseChoice.SQLITE:
|
51
|
+
import sqlite_vec
|
52
|
+
except ImportError:
|
53
|
+
# If sqlite_vec is not installed, it's fine for client usage
|
54
|
+
pass
|
49
55
|
# --------------------------
|
50
56
|
# LLMConfig Serialization
|
51
57
|
# --------------------------
|
@@ -0,0 +1,144 @@
|
|
1
|
+
import base64
|
2
|
+
import os
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from cryptography.hazmat.backends import default_backend
|
6
|
+
from cryptography.hazmat.primitives import hashes
|
7
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
8
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
9
|
+
|
10
|
+
from letta.settings import settings
|
11
|
+
|
12
|
+
|
13
|
+
class CryptoUtils:
|
14
|
+
"""Utility class for AES-256-GCM encryption/decryption of sensitive data."""
|
15
|
+
|
16
|
+
# AES-256 requires 32 bytes key
|
17
|
+
KEY_SIZE = 32
|
18
|
+
# GCM standard IV size is 12 bytes (96 bits)
|
19
|
+
IV_SIZE = 12
|
20
|
+
# GCM tag size is 16 bytes (128 bits)
|
21
|
+
TAG_SIZE = 16
|
22
|
+
# Salt size for key derivation
|
23
|
+
SALT_SIZE = 16
|
24
|
+
|
25
|
+
@classmethod
|
26
|
+
def _derive_key(cls, master_key: str, salt: bytes) -> bytes:
|
27
|
+
"""Derive an AES key from the master key using PBKDF2."""
|
28
|
+
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=cls.KEY_SIZE, salt=salt, iterations=100000, backend=default_backend())
|
29
|
+
return kdf.derive(master_key.encode())
|
30
|
+
|
31
|
+
@classmethod
|
32
|
+
def encrypt(cls, plaintext: str, master_key: Optional[str] = None) -> str:
|
33
|
+
"""
|
34
|
+
Encrypt a string using AES-256-GCM.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
plaintext: The string to encrypt
|
38
|
+
master_key: Optional master key (defaults to settings.encryption_key)
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
Base64 encoded string containing: salt + iv + ciphertext + tag
|
42
|
+
|
43
|
+
Raises:
|
44
|
+
ValueError: If no encryption key is configured
|
45
|
+
"""
|
46
|
+
if master_key is None:
|
47
|
+
master_key = settings.encryption_key
|
48
|
+
|
49
|
+
if not master_key:
|
50
|
+
raise ValueError("No encryption key configured. Set LETTA_ENCRYPTION_KEY environment variable.")
|
51
|
+
|
52
|
+
# Generate random salt and IV
|
53
|
+
salt = os.urandom(cls.SALT_SIZE)
|
54
|
+
iv = os.urandom(cls.IV_SIZE)
|
55
|
+
|
56
|
+
# Derive key from master key
|
57
|
+
key = cls._derive_key(master_key, salt)
|
58
|
+
|
59
|
+
# Create cipher
|
60
|
+
cipher = Cipher(algorithms.AES(key), modes.GCM(iv), backend=default_backend())
|
61
|
+
encryptor = cipher.encryptor()
|
62
|
+
|
63
|
+
# Encrypt the plaintext
|
64
|
+
ciphertext = encryptor.update(plaintext.encode()) + encryptor.finalize()
|
65
|
+
|
66
|
+
# Get the authentication tag
|
67
|
+
tag = encryptor.tag
|
68
|
+
|
69
|
+
# Combine salt + iv + ciphertext + tag
|
70
|
+
encrypted_data = salt + iv + ciphertext + tag
|
71
|
+
|
72
|
+
# Return as base64 encoded string
|
73
|
+
return base64.b64encode(encrypted_data).decode("utf-8")
|
74
|
+
|
75
|
+
@classmethod
|
76
|
+
def decrypt(cls, encrypted: str, master_key: Optional[str] = None) -> str:
|
77
|
+
"""
|
78
|
+
Decrypt a string that was encrypted using AES-256-GCM.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
encrypted: Base64 encoded encrypted string
|
82
|
+
master_key: Optional master key (defaults to settings.encryption_key)
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
The decrypted plaintext string
|
86
|
+
|
87
|
+
Raises:
|
88
|
+
ValueError: If no encryption key is configured or decryption fails
|
89
|
+
"""
|
90
|
+
if master_key is None:
|
91
|
+
master_key = settings.encryption_key
|
92
|
+
|
93
|
+
if not master_key:
|
94
|
+
raise ValueError("No encryption key configured. Set LETTA_ENCRYPTION_KEY environment variable.")
|
95
|
+
|
96
|
+
try:
|
97
|
+
# Decode from base64
|
98
|
+
encrypted_data = base64.b64decode(encrypted)
|
99
|
+
|
100
|
+
# Extract components
|
101
|
+
salt = encrypted_data[: cls.SALT_SIZE]
|
102
|
+
iv = encrypted_data[cls.SALT_SIZE : cls.SALT_SIZE + cls.IV_SIZE]
|
103
|
+
ciphertext = encrypted_data[cls.SALT_SIZE + cls.IV_SIZE : -cls.TAG_SIZE]
|
104
|
+
tag = encrypted_data[-cls.TAG_SIZE :]
|
105
|
+
|
106
|
+
# Derive key from master key
|
107
|
+
key = cls._derive_key(master_key, salt)
|
108
|
+
|
109
|
+
# Create cipher
|
110
|
+
cipher = Cipher(algorithms.AES(key), modes.GCM(iv, tag), backend=default_backend())
|
111
|
+
decryptor = cipher.decryptor()
|
112
|
+
|
113
|
+
# Decrypt the ciphertext
|
114
|
+
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
|
115
|
+
|
116
|
+
return plaintext.decode("utf-8")
|
117
|
+
|
118
|
+
except Exception as e:
|
119
|
+
raise ValueError(f"Failed to decrypt data: {str(e)}")
|
120
|
+
|
121
|
+
@classmethod
|
122
|
+
def is_encrypted(cls, value: str) -> bool:
|
123
|
+
"""
|
124
|
+
Check if a string appears to be encrypted (base64 encoded with correct size).
|
125
|
+
|
126
|
+
This is a heuristic check and may have false positives.
|
127
|
+
"""
|
128
|
+
try:
|
129
|
+
decoded = base64.b64decode(value)
|
130
|
+
# Check if length is consistent with our encryption format
|
131
|
+
# Minimum size: salt(16) + iv(12) + tag(16) + at least 1 byte of ciphertext
|
132
|
+
return len(decoded) >= cls.SALT_SIZE + cls.IV_SIZE + cls.TAG_SIZE + 1
|
133
|
+
except Exception:
|
134
|
+
return False
|
135
|
+
|
136
|
+
@classmethod
|
137
|
+
def is_encryption_available(cls) -> bool:
|
138
|
+
"""
|
139
|
+
Check if encryption is available (encryption key is configured).
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
True if encryption key is configured, False otherwise
|
143
|
+
"""
|
144
|
+
return bool(settings.encryption_key)
|
letta/llm_api/llm_api_tools.py
CHANGED
letta/llm_api/llm_client_base.py
CHANGED
@@ -64,7 +64,6 @@ class LLMClientBase:
|
|
64
64
|
request_json=request_data,
|
65
65
|
response_json=response_data,
|
66
66
|
step_id=step_id,
|
67
|
-
organization_id=self.actor.organization_id,
|
68
67
|
),
|
69
68
|
)
|
70
69
|
log_event(name="llm_response_received", attributes=response_data)
|
@@ -98,7 +97,6 @@ class LLMClientBase:
|
|
98
97
|
request_json=request_data,
|
99
98
|
response_json=response_data,
|
100
99
|
step_id=step_id,
|
101
|
-
organization_id=self.actor.organization_id,
|
102
100
|
),
|
103
101
|
)
|
104
102
|
|
letta/orm/__init__.py
CHANGED
@@ -18,6 +18,7 @@ from letta.orm.job import Job
|
|
18
18
|
from letta.orm.job_messages import JobMessage
|
19
19
|
from letta.orm.llm_batch_items import LLMBatchItem
|
20
20
|
from letta.orm.llm_batch_job import LLMBatchJob
|
21
|
+
from letta.orm.mcp_oauth import MCPOAuth
|
21
22
|
from letta.orm.mcp_server import MCPServer
|
22
23
|
from letta.orm.message import Message
|
23
24
|
from letta.orm.organization import Organization
|
letta/orm/agent.py
CHANGED
@@ -233,6 +233,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
|
|
233
233
|
"identity_ids": [],
|
234
234
|
"multi_agent_group": None,
|
235
235
|
"tool_exec_environment_variables": [],
|
236
|
+
"secrets": [],
|
236
237
|
}
|
237
238
|
|
238
239
|
# Optional fields: only included if requested
|
@@ -253,6 +254,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
|
|
253
254
|
"identity_ids": lambda: [i.id for i in self.identities],
|
254
255
|
"multi_agent_group": lambda: self.multi_agent_group,
|
255
256
|
"tool_exec_environment_variables": lambda: self.tool_exec_environment_variables,
|
257
|
+
"secrets": lambda: self.tool_exec_environment_variables,
|
256
258
|
}
|
257
259
|
|
258
260
|
include_relationships = set(optional_fields.keys() if include_relationships is None else include_relationships)
|
@@ -324,6 +326,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
|
|
324
326
|
"identity_ids": [],
|
325
327
|
"multi_agent_group": None,
|
326
328
|
"tool_exec_environment_variables": [],
|
329
|
+
"secrets": [],
|
327
330
|
}
|
328
331
|
|
329
332
|
# Initialize include_relationships to an empty set if it's None
|
@@ -344,7 +347,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
|
|
344
347
|
multi_agent_group = self.awaitable_attrs.multi_agent_group if "multi_agent_group" in include_relationships else none_async()
|
345
348
|
tool_exec_environment_variables = (
|
346
349
|
self.awaitable_attrs.tool_exec_environment_variables
|
347
|
-
if "tool_exec_environment_variables" in include_relationships
|
350
|
+
if "tool_exec_environment_variables" in include_relationships or "secrets" in include_relationships
|
348
351
|
else empty_list_async()
|
349
352
|
)
|
350
353
|
file_agents = self.awaitable_attrs.file_agents if "memory" in include_relationships else empty_list_async()
|
@@ -368,5 +371,6 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
|
|
368
371
|
state["identity_ids"] = [i.id for i in identities]
|
369
372
|
state["multi_agent_group"] = multi_agent_group
|
370
373
|
state["tool_exec_environment_variables"] = tool_exec_environment_variables
|
374
|
+
state["secrets"] = tool_exec_environment_variables
|
371
375
|
|
372
376
|
return self.__pydantic_model__(**state)
|
letta/orm/job.py
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
from datetime import datetime
|
2
2
|
from typing import TYPE_CHECKING, List, Optional
|
3
3
|
|
4
|
-
from sqlalchemy import JSON, BigInteger, ForeignKey, Index, String
|
4
|
+
from sqlalchemy import JSON, BigInteger, Boolean, ForeignKey, Index, String
|
5
5
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
6
6
|
|
7
7
|
from letta.orm.mixins import UserMixin
|
8
8
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
9
9
|
from letta.schemas.enums import JobStatus, JobType
|
10
10
|
from letta.schemas.job import Job as PydanticJob, LettaRequestConfig
|
11
|
+
from letta.schemas.letta_stop_reason import StopReasonType
|
11
12
|
|
12
13
|
if TYPE_CHECKING:
|
13
14
|
from letta.orm.job_messages import JobMessage
|
@@ -28,6 +29,7 @@ class Job(SqlalchemyBase, UserMixin):
|
|
28
29
|
|
29
30
|
status: Mapped[JobStatus] = mapped_column(String, default=JobStatus.created, doc="The current status of the job.")
|
30
31
|
completed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True, doc="The unix timestamp of when the job was completed.")
|
32
|
+
stop_reason: Mapped[Optional[StopReasonType]] = mapped_column(String, nullable=True, doc="The reason why the job was stopped.")
|
31
33
|
metadata_: Mapped[Optional[dict]] = mapped_column(JSON, doc="The metadata of the job.")
|
32
34
|
job_type: Mapped[JobType] = mapped_column(
|
33
35
|
String,
|
letta/orm/mcp_oauth.py
CHANGED
@@ -38,7 +38,11 @@ class MCPOAuth(SqlalchemyBase, OrganizationMixin, UserMixin):
|
|
38
38
|
|
39
39
|
# Token data
|
40
40
|
access_token: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth access token")
|
41
|
+
access_token_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted OAuth access token")
|
42
|
+
|
41
43
|
refresh_token: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth refresh token")
|
44
|
+
refresh_token_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted OAuth refresh token")
|
45
|
+
|
42
46
|
token_type: Mapped[str] = mapped_column(String(50), default="Bearer", doc="Token type")
|
43
47
|
expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True, doc="Token expiry time")
|
44
48
|
scope: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth scope")
|
@@ -46,6 +50,8 @@ class MCPOAuth(SqlalchemyBase, OrganizationMixin, UserMixin):
|
|
46
50
|
# Client configuration
|
47
51
|
client_id: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth client ID")
|
48
52
|
client_secret: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth client secret")
|
53
|
+
client_secret_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted OAuth client secret")
|
54
|
+
|
49
55
|
redirect_uri: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth redirect URI")
|
50
56
|
|
51
57
|
# Session state
|
letta/orm/mcp_server.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from typing import TYPE_CHECKING, Optional
|
2
2
|
|
3
|
-
from sqlalchemy import JSON, String, UniqueConstraint
|
3
|
+
from sqlalchemy import JSON, String, Text, UniqueConstraint
|
4
4
|
from sqlalchemy.orm import Mapped, mapped_column
|
5
5
|
|
6
6
|
from letta.functions.mcp_client.types import StdioServerConfig
|
@@ -39,9 +39,15 @@ class MCPServer(SqlalchemyBase, OrganizationMixin):
|
|
39
39
|
# access token / api key for MCP servers that require authentication
|
40
40
|
token: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The access token or api key for the MCP server")
|
41
41
|
|
42
|
+
# encrypted access token or api key for the MCP server
|
43
|
+
token_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted access token or api key for the MCP server")
|
44
|
+
|
42
45
|
# custom headers for authentication (key-value pairs)
|
43
46
|
custom_headers: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, doc="Custom authentication headers as key-value pairs")
|
44
47
|
|
48
|
+
# encrypted custom headers for authentication (key-value pairs)
|
49
|
+
custom_headers_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted custom authentication headers")
|
50
|
+
|
45
51
|
# stdio server
|
46
52
|
stdio_config: Mapped[Optional[StdioServerConfig]] = mapped_column(
|
47
53
|
MCPStdioServerConfigColumn, nullable=True, doc="The configuration for the stdio server"
|
letta/orm/sqlalchemy_base.py
CHANGED
@@ -14,7 +14,6 @@ from sqlalchemy.orm.interfaces import ORMOption
|
|
14
14
|
from letta.log import get_logger
|
15
15
|
from letta.orm.base import Base, CommonSqlalchemyMetaMixins
|
16
16
|
from letta.orm.errors import DatabaseTimeoutError, ForeignKeyConstraintViolationError, NoResultFound, UniqueConstraintViolationError
|
17
|
-
from letta.orm.sqlite_functions import adapt_array
|
18
17
|
from letta.settings import DatabaseChoice
|
19
18
|
|
20
19
|
if TYPE_CHECKING:
|
@@ -401,6 +400,8 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
401
400
|
query = query.order_by(cls.embedding.cosine_distance(query_embedding).asc())
|
402
401
|
else:
|
403
402
|
# SQLite with custom vector type
|
403
|
+
from letta.orm.sqlite_functions import adapt_array
|
404
|
+
|
404
405
|
query_embedding_binary = adapt_array(query_embedding)
|
405
406
|
query = query.order_by(
|
406
407
|
func.cosine_distance(cls.embedding, query_embedding_binary).asc(),
|
letta/schemas/agent.py
CHANGED
@@ -86,6 +86,11 @@ class AgentState(OrmMetadataBase, validate_assignment=True):
|
|
86
86
|
sources: List[Source] = Field(..., description="The sources used by the agent.")
|
87
87
|
tags: List[str] = Field(..., description="The tags associated with the agent.")
|
88
88
|
tool_exec_environment_variables: List[AgentEnvironmentVariable] = Field(
|
89
|
+
default_factory=list,
|
90
|
+
description="Deprecated: use `secrets` field instead.",
|
91
|
+
deprecated=True,
|
92
|
+
)
|
93
|
+
secrets: List[AgentEnvironmentVariable] = Field(
|
89
94
|
default_factory=list, description="The environment variables for tool execution specific to this agent."
|
90
95
|
)
|
91
96
|
project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
|
@@ -133,7 +138,7 @@ class AgentState(OrmMetadataBase, validate_assignment=True):
|
|
133
138
|
def get_agent_env_vars_as_dict(self) -> Dict[str, str]:
|
134
139
|
# Get environment variables for this agent specifically
|
135
140
|
per_agent_env_vars = {}
|
136
|
-
for agent_env_var_obj in self.
|
141
|
+
for agent_env_var_obj in self.secrets:
|
137
142
|
per_agent_env_vars[agent_env_var_obj.key] = agent_env_var_obj.value
|
138
143
|
return per_agent_env_vars
|
139
144
|
|
@@ -222,9 +227,8 @@ class CreateAgent(BaseModel, validate_assignment=True): #
|
|
222
227
|
deprecated=True,
|
223
228
|
description="Deprecated: Project should now be passed via the X-Project header instead of in the request body. If using the sdk, this can be done via the new x_project field below.",
|
224
229
|
)
|
225
|
-
tool_exec_environment_variables: Optional[Dict[str, str]] = Field(
|
226
|
-
|
227
|
-
)
|
230
|
+
tool_exec_environment_variables: Optional[Dict[str, str]] = Field(None, description="Deprecated: use `secrets` field instead.")
|
231
|
+
secrets: Optional[Dict[str, str]] = Field(None, description="The environment variables for tool execution specific to this agent.")
|
228
232
|
memory_variables: Optional[Dict[str, str]] = Field(None, description="The variables that should be set for the agent.")
|
229
233
|
project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
|
230
234
|
template_id: Optional[str] = Field(None, description="The id of the template the agent belongs to.")
|
@@ -328,9 +332,8 @@ class UpdateAgent(BaseModel):
|
|
328
332
|
message_ids: Optional[List[str]] = Field(None, description="The ids of the messages in the agent's in-context memory.")
|
329
333
|
description: Optional[str] = Field(None, description="The description of the agent.")
|
330
334
|
metadata: Optional[Dict] = Field(None, description="The metadata of the agent.")
|
331
|
-
tool_exec_environment_variables: Optional[Dict[str, str]] = Field(
|
332
|
-
|
333
|
-
)
|
335
|
+
tool_exec_environment_variables: Optional[Dict[str, str]] = Field(None, description="Deprecated: use `secrets` field instead")
|
336
|
+
secrets: Optional[Dict[str, str]] = Field(None, description="The environment variables for tool execution specific to this agent.")
|
334
337
|
project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
|
335
338
|
template_id: Optional[str] = Field(None, description="The id of the template the agent belongs to.")
|
336
339
|
base_template_id: Optional[str] = Field(None, description="The base template id of the agent.")
|
letta/schemas/job.py
CHANGED
@@ -8,16 +8,26 @@ from letta.helpers.datetime_helpers import get_utc_time
|
|
8
8
|
from letta.schemas.enums import JobStatus, JobType
|
9
9
|
from letta.schemas.letta_base import OrmMetadataBase
|
10
10
|
from letta.schemas.letta_message import MessageType
|
11
|
+
from letta.schemas.letta_stop_reason import StopReasonType
|
11
12
|
|
12
13
|
|
13
14
|
class JobBase(OrmMetadataBase):
|
14
15
|
__id_prefix__ = "job"
|
15
16
|
status: JobStatus = Field(default=JobStatus.created, description="The status of the job.")
|
16
17
|
created_at: datetime = Field(default_factory=get_utc_time, description="The unix timestamp of when the job was created.")
|
18
|
+
|
19
|
+
# completion related
|
17
20
|
completed_at: Optional[datetime] = Field(None, description="The unix timestamp of when the job was completed.")
|
21
|
+
stop_reason: Optional[StopReasonType] = Field(None, description="The reason why the job was stopped.")
|
22
|
+
|
23
|
+
# metadata
|
18
24
|
metadata: Optional[dict] = Field(None, validation_alias="metadata_", description="The metadata of the job.")
|
19
25
|
job_type: JobType = Field(default=JobType.JOB, description="The type of the job.")
|
20
26
|
|
27
|
+
## TODO: Run-specific fields
|
28
|
+
# background: Optional[bool] = Field(None, description="Whether the job was created in background mode.")
|
29
|
+
# agent_id: Optional[str] = Field(None, description="The agent associated with this job/run.")
|
30
|
+
|
21
31
|
callback_url: Optional[str] = Field(None, description="If set, POST to this URL when the job completes.")
|
22
32
|
callback_sent_at: Optional[datetime] = Field(None, description="Timestamp when the callback was last attempted.")
|
23
33
|
callback_status_code: Optional[int] = Field(None, description="HTTP status code returned by the callback endpoint.")
|