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/services/job_manager.py
CHANGED
@@ -18,6 +18,7 @@ from letta.otel.tracing import log_event, trace_method
|
|
18
18
|
from letta.schemas.enums import JobStatus, JobType, MessageRole
|
19
19
|
from letta.schemas.job import BatchJob as PydanticBatchJob, Job as PydanticJob, JobUpdate, LettaRequestConfig
|
20
20
|
from letta.schemas.letta_message import LettaMessage
|
21
|
+
from letta.schemas.letta_stop_reason import StopReasonType
|
21
22
|
from letta.schemas.message import Message as PydanticMessage
|
22
23
|
from letta.schemas.run import Run as PydanticRun
|
23
24
|
from letta.schemas.step import Step as PydanticStep
|
@@ -207,7 +208,12 @@ class JobManager:
|
|
207
208
|
@enforce_types
|
208
209
|
@trace_method
|
209
210
|
async def safe_update_job_status_async(
|
210
|
-
self,
|
211
|
+
self,
|
212
|
+
job_id: str,
|
213
|
+
new_status: JobStatus,
|
214
|
+
actor: PydanticUser,
|
215
|
+
stop_reason: Optional[StopReasonType] = None,
|
216
|
+
metadata: Optional[dict] = None,
|
211
217
|
) -> bool:
|
212
218
|
"""
|
213
219
|
Safely update job status with state transition guards.
|
@@ -217,7 +223,7 @@ class JobManager:
|
|
217
223
|
True if update was successful, False if update was skipped due to invalid transition
|
218
224
|
"""
|
219
225
|
try:
|
220
|
-
job_update_builder = partial(JobUpdate, status=new_status)
|
226
|
+
job_update_builder = partial(JobUpdate, status=new_status, stop_reason=stop_reason)
|
221
227
|
|
222
228
|
# If metadata is provided, merge it with existing metadata
|
223
229
|
if metadata:
|
@@ -268,6 +274,7 @@ class JobManager:
|
|
268
274
|
statuses: Optional[List[JobStatus]] = None,
|
269
275
|
job_type: JobType = JobType.JOB,
|
270
276
|
ascending: bool = True,
|
277
|
+
stop_reason: Optional[StopReasonType] = None,
|
271
278
|
) -> List[PydanticJob]:
|
272
279
|
"""List all jobs with optional pagination and status filter."""
|
273
280
|
with db_registry.session() as session:
|
@@ -277,6 +284,10 @@ class JobManager:
|
|
277
284
|
if statuses:
|
278
285
|
filter_kwargs["status"] = statuses
|
279
286
|
|
287
|
+
# Add stop_reason filter if provided
|
288
|
+
if stop_reason is not None:
|
289
|
+
filter_kwargs["stop_reason"] = stop_reason
|
290
|
+
|
280
291
|
jobs = JobModel.list(
|
281
292
|
db_session=session,
|
282
293
|
before=before,
|
@@ -299,6 +310,7 @@ class JobManager:
|
|
299
310
|
job_type: JobType = JobType.JOB,
|
300
311
|
ascending: bool = True,
|
301
312
|
source_id: Optional[str] = None,
|
313
|
+
stop_reason: Optional[StopReasonType] = None,
|
302
314
|
) -> List[PydanticJob]:
|
303
315
|
"""List all jobs with optional pagination and status filter."""
|
304
316
|
from sqlalchemy import and_, or_, select
|
@@ -317,6 +329,10 @@ class JobManager:
|
|
317
329
|
column = column.op("->>")("source_id")
|
318
330
|
query = query.where(column == source_id)
|
319
331
|
|
332
|
+
# add stop_reason filter if provided
|
333
|
+
if stop_reason is not None:
|
334
|
+
query = query.where(JobModel.stop_reason == stop_reason)
|
335
|
+
|
320
336
|
# handle cursor-based pagination
|
321
337
|
if before or after:
|
322
338
|
# get cursor objects
|
letta/services/mcp_manager.py
CHANGED
@@ -35,6 +35,7 @@ from letta.schemas.mcp import (
|
|
35
35
|
UpdateStdioMCPServer,
|
36
36
|
UpdateStreamableHTTPMCPServer,
|
37
37
|
)
|
38
|
+
from letta.schemas.secret import Secret, SecretDict
|
38
39
|
from letta.schemas.tool import Tool as PydanticTool, ToolCreate, ToolUpdate
|
39
40
|
from letta.schemas.user import User as PydanticUser
|
40
41
|
from letta.server.db import db_registry
|
@@ -354,6 +355,69 @@ class MCPManager:
|
|
354
355
|
logger.error(f"Failed to create MCP server: {e}")
|
355
356
|
raise
|
356
357
|
|
358
|
+
@enforce_types
|
359
|
+
async def create_mcp_server_from_config(
|
360
|
+
self, server_config: Union[StdioServerConfig, SSEServerConfig, StreamableHTTPServerConfig], actor: PydanticUser
|
361
|
+
) -> MCPServer:
|
362
|
+
"""
|
363
|
+
Create an MCP server from a config object, handling encryption of sensitive fields.
|
364
|
+
|
365
|
+
This method converts the server config to an MCPServer model and encrypts
|
366
|
+
sensitive fields like tokens and custom headers.
|
367
|
+
"""
|
368
|
+
# Create base MCPServer object
|
369
|
+
if isinstance(server_config, StdioServerConfig):
|
370
|
+
mcp_server = MCPServer(server_name=server_config.server_name, server_type=server_config.type, stdio_config=server_config)
|
371
|
+
elif isinstance(server_config, SSEServerConfig):
|
372
|
+
mcp_server = MCPServer(
|
373
|
+
server_name=server_config.server_name,
|
374
|
+
server_type=server_config.type,
|
375
|
+
server_url=server_config.server_url,
|
376
|
+
)
|
377
|
+
# Encrypt sensitive fields
|
378
|
+
token = server_config.resolve_token()
|
379
|
+
if token:
|
380
|
+
token_secret = Secret.from_plaintext(token)
|
381
|
+
mcp_server.set_token_secret(token_secret)
|
382
|
+
if server_config.custom_headers:
|
383
|
+
headers_secret = SecretDict.from_plaintext(server_config.custom_headers)
|
384
|
+
mcp_server.set_custom_headers_secret(headers_secret)
|
385
|
+
|
386
|
+
elif isinstance(server_config, StreamableHTTPServerConfig):
|
387
|
+
mcp_server = MCPServer(
|
388
|
+
server_name=server_config.server_name,
|
389
|
+
server_type=server_config.type,
|
390
|
+
server_url=server_config.server_url,
|
391
|
+
)
|
392
|
+
# Encrypt sensitive fields
|
393
|
+
token = server_config.resolve_token()
|
394
|
+
if token:
|
395
|
+
token_secret = Secret.from_plaintext(token)
|
396
|
+
mcp_server.set_token_secret(token_secret)
|
397
|
+
if server_config.custom_headers:
|
398
|
+
headers_secret = SecretDict.from_plaintext(server_config.custom_headers)
|
399
|
+
mcp_server.set_custom_headers_secret(headers_secret)
|
400
|
+
else:
|
401
|
+
raise ValueError(f"Unsupported server config type: {type(server_config)}")
|
402
|
+
|
403
|
+
return mcp_server
|
404
|
+
|
405
|
+
@enforce_types
|
406
|
+
async def create_mcp_server_from_config_with_tools(
|
407
|
+
self, server_config: Union[StdioServerConfig, SSEServerConfig, StreamableHTTPServerConfig], actor: PydanticUser
|
408
|
+
) -> MCPServer:
|
409
|
+
"""
|
410
|
+
Create an MCP server from a config object and optimistically sync its tools.
|
411
|
+
|
412
|
+
This method handles encryption of sensitive fields and then creates the server
|
413
|
+
with automatic tool synchronization.
|
414
|
+
"""
|
415
|
+
# Convert config to MCPServer with encryption
|
416
|
+
mcp_server = await self.create_mcp_server_from_config(server_config, actor)
|
417
|
+
|
418
|
+
# Create the server with tools
|
419
|
+
return await self.create_mcp_server_with_tools(mcp_server, actor)
|
420
|
+
|
357
421
|
@enforce_types
|
358
422
|
async def create_mcp_server_with_tools(self, pydantic_mcp_server: MCPServer, actor: PydanticUser) -> MCPServer:
|
359
423
|
"""
|
@@ -420,10 +484,33 @@ class MCPManager:
|
|
420
484
|
# Update tool attributes with only the fields that were explicitly set
|
421
485
|
update_data = mcp_server_update.model_dump(to_orm=True, exclude_unset=True)
|
422
486
|
|
423
|
-
#
|
424
|
-
if update_data
|
425
|
-
|
426
|
-
|
487
|
+
# Handle encryption for token if provided
|
488
|
+
if "token" in update_data and update_data["token"] is not None:
|
489
|
+
token_secret = Secret.from_plaintext(update_data["token"])
|
490
|
+
secret_dict = token_secret.to_dict()
|
491
|
+
update_data["token_enc"] = secret_dict["encrypted"]
|
492
|
+
# During migration phase, also update plaintext
|
493
|
+
if not token_secret._was_encrypted:
|
494
|
+
update_data["token"] = secret_dict["plaintext"]
|
495
|
+
else:
|
496
|
+
update_data["token"] = None
|
497
|
+
|
498
|
+
# Handle encryption for custom_headers if provided
|
499
|
+
if "custom_headers" in update_data:
|
500
|
+
if update_data["custom_headers"] is not None:
|
501
|
+
headers_secret = SecretDict.from_plaintext(update_data["custom_headers"])
|
502
|
+
secret_dict = headers_secret.to_dict()
|
503
|
+
update_data["custom_headers_enc"] = secret_dict["encrypted"]
|
504
|
+
# During migration phase, also update plaintext
|
505
|
+
if not headers_secret._was_encrypted:
|
506
|
+
update_data["custom_headers"] = secret_dict["plaintext"]
|
507
|
+
else:
|
508
|
+
update_data["custom_headers"] = None
|
509
|
+
else:
|
510
|
+
# Ensure custom_headers None is stored as SQL NULL, not JSON null
|
511
|
+
update_data.pop("custom_headers", None)
|
512
|
+
setattr(mcp_server, "custom_headers", null())
|
513
|
+
setattr(mcp_server, "custom_headers_enc", None)
|
427
514
|
|
428
515
|
for key, value in update_data.items():
|
429
516
|
setattr(mcp_server, key, value)
|
@@ -664,6 +751,64 @@ class MCPManager:
|
|
664
751
|
raise ValueError(f"Unsupported server config type: {type(server_config)}")
|
665
752
|
|
666
753
|
# OAuth-related methods
|
754
|
+
def _oauth_orm_to_pydantic(self, oauth_session: MCPOAuth) -> MCPOAuthSession:
|
755
|
+
"""
|
756
|
+
Convert OAuth ORM model to Pydantic model, handling decryption of sensitive fields.
|
757
|
+
"""
|
758
|
+
from letta.settings import settings
|
759
|
+
|
760
|
+
# Get decrypted values using the dual-read approach
|
761
|
+
# Secret.from_db() will automatically use settings.encryption_key if available
|
762
|
+
access_token = None
|
763
|
+
if oauth_session.access_token_enc or oauth_session.access_token:
|
764
|
+
if settings.encryption_key:
|
765
|
+
secret = Secret.from_db(oauth_session.access_token_enc, oauth_session.access_token)
|
766
|
+
access_token = secret.get_plaintext()
|
767
|
+
else:
|
768
|
+
# No encryption key, use plaintext if available
|
769
|
+
access_token = oauth_session.access_token
|
770
|
+
|
771
|
+
refresh_token = None
|
772
|
+
if oauth_session.refresh_token_enc or oauth_session.refresh_token:
|
773
|
+
if settings.encryption_key:
|
774
|
+
secret = Secret.from_db(oauth_session.refresh_token_enc, oauth_session.refresh_token)
|
775
|
+
refresh_token = secret.get_plaintext()
|
776
|
+
else:
|
777
|
+
# No encryption key, use plaintext if available
|
778
|
+
refresh_token = oauth_session.refresh_token
|
779
|
+
|
780
|
+
client_secret = None
|
781
|
+
if oauth_session.client_secret_enc or oauth_session.client_secret:
|
782
|
+
if settings.encryption_key:
|
783
|
+
secret = Secret.from_db(oauth_session.client_secret_enc, oauth_session.client_secret)
|
784
|
+
client_secret = secret.get_plaintext()
|
785
|
+
else:
|
786
|
+
# No encryption key, use plaintext if available
|
787
|
+
client_secret = oauth_session.client_secret
|
788
|
+
|
789
|
+
return MCPOAuthSession(
|
790
|
+
id=oauth_session.id,
|
791
|
+
state=oauth_session.state,
|
792
|
+
server_id=oauth_session.server_id,
|
793
|
+
server_url=oauth_session.server_url,
|
794
|
+
server_name=oauth_session.server_name,
|
795
|
+
user_id=oauth_session.user_id,
|
796
|
+
organization_id=oauth_session.organization_id,
|
797
|
+
authorization_url=oauth_session.authorization_url,
|
798
|
+
authorization_code=oauth_session.authorization_code,
|
799
|
+
access_token=access_token,
|
800
|
+
refresh_token=refresh_token,
|
801
|
+
token_type=oauth_session.token_type,
|
802
|
+
expires_at=oauth_session.expires_at,
|
803
|
+
scope=oauth_session.scope,
|
804
|
+
client_id=oauth_session.client_id,
|
805
|
+
client_secret=client_secret,
|
806
|
+
redirect_uri=oauth_session.redirect_uri,
|
807
|
+
status=oauth_session.status,
|
808
|
+
created_at=oauth_session.created_at,
|
809
|
+
updated_at=oauth_session.updated_at,
|
810
|
+
)
|
811
|
+
|
667
812
|
@enforce_types
|
668
813
|
async def create_oauth_session(self, session_create: MCPOAuthSessionCreate, actor: PydanticUser) -> MCPOAuthSession:
|
669
814
|
"""Create a new OAuth session for MCP server authentication."""
|
@@ -682,18 +827,8 @@ class MCPManager:
|
|
682
827
|
)
|
683
828
|
oauth_session = await oauth_session.create_async(session, actor=actor)
|
684
829
|
|
685
|
-
# Convert to Pydantic model
|
686
|
-
return
|
687
|
-
id=oauth_session.id,
|
688
|
-
state=oauth_session.state,
|
689
|
-
server_url=oauth_session.server_url,
|
690
|
-
server_name=oauth_session.server_name,
|
691
|
-
user_id=oauth_session.user_id,
|
692
|
-
organization_id=oauth_session.organization_id,
|
693
|
-
status=oauth_session.status,
|
694
|
-
created_at=oauth_session.created_at,
|
695
|
-
updated_at=oauth_session.updated_at,
|
696
|
-
)
|
830
|
+
# Convert to Pydantic model - note: new sessions won't have tokens yet
|
831
|
+
return self._oauth_orm_to_pydantic(oauth_session)
|
697
832
|
|
698
833
|
@enforce_types
|
699
834
|
async def get_oauth_session_by_id(self, session_id: str, actor: PydanticUser) -> Optional[MCPOAuthSession]:
|
@@ -701,27 +836,7 @@ class MCPManager:
|
|
701
836
|
async with db_registry.async_session() as session:
|
702
837
|
try:
|
703
838
|
oauth_session = await MCPOAuth.read_async(db_session=session, identifier=session_id, actor=actor)
|
704
|
-
return
|
705
|
-
id=oauth_session.id,
|
706
|
-
state=oauth_session.state,
|
707
|
-
server_url=oauth_session.server_url,
|
708
|
-
server_name=oauth_session.server_name,
|
709
|
-
user_id=oauth_session.user_id,
|
710
|
-
organization_id=oauth_session.organization_id,
|
711
|
-
authorization_url=oauth_session.authorization_url,
|
712
|
-
authorization_code=oauth_session.authorization_code,
|
713
|
-
access_token=oauth_session.access_token,
|
714
|
-
refresh_token=oauth_session.refresh_token,
|
715
|
-
token_type=oauth_session.token_type,
|
716
|
-
expires_at=oauth_session.expires_at,
|
717
|
-
scope=oauth_session.scope,
|
718
|
-
client_id=oauth_session.client_id,
|
719
|
-
client_secret=oauth_session.client_secret,
|
720
|
-
redirect_uri=oauth_session.redirect_uri,
|
721
|
-
status=oauth_session.status,
|
722
|
-
created_at=oauth_session.created_at,
|
723
|
-
updated_at=oauth_session.updated_at,
|
724
|
-
)
|
839
|
+
return self._oauth_orm_to_pydantic(oauth_session)
|
725
840
|
except NoResultFound:
|
726
841
|
return None
|
727
842
|
|
@@ -747,27 +862,7 @@ class MCPManager:
|
|
747
862
|
if not oauth_session:
|
748
863
|
return None
|
749
864
|
|
750
|
-
return
|
751
|
-
id=oauth_session.id,
|
752
|
-
state=oauth_session.state,
|
753
|
-
server_url=oauth_session.server_url,
|
754
|
-
server_name=oauth_session.server_name,
|
755
|
-
user_id=oauth_session.user_id,
|
756
|
-
organization_id=oauth_session.organization_id,
|
757
|
-
authorization_url=oauth_session.authorization_url,
|
758
|
-
authorization_code=oauth_session.authorization_code,
|
759
|
-
access_token=oauth_session.access_token,
|
760
|
-
refresh_token=oauth_session.refresh_token,
|
761
|
-
token_type=oauth_session.token_type,
|
762
|
-
expires_at=oauth_session.expires_at,
|
763
|
-
scope=oauth_session.scope,
|
764
|
-
client_id=oauth_session.client_id,
|
765
|
-
client_secret=oauth_session.client_secret,
|
766
|
-
redirect_uri=oauth_session.redirect_uri,
|
767
|
-
status=oauth_session.status,
|
768
|
-
created_at=oauth_session.created_at,
|
769
|
-
updated_at=oauth_session.updated_at,
|
770
|
-
)
|
865
|
+
return self._oauth_orm_to_pydantic(oauth_session)
|
771
866
|
|
772
867
|
@enforce_types
|
773
868
|
async def update_oauth_session(self, session_id: str, session_update: MCPOAuthSessionUpdate, actor: PydanticUser) -> MCPOAuthSession:
|
@@ -780,10 +875,37 @@ class MCPManager:
|
|
780
875
|
oauth_session.authorization_url = session_update.authorization_url
|
781
876
|
if session_update.authorization_code is not None:
|
782
877
|
oauth_session.authorization_code = session_update.authorization_code
|
878
|
+
|
879
|
+
# Handle encryption for access_token
|
783
880
|
if session_update.access_token is not None:
|
784
|
-
|
881
|
+
from letta.settings import settings
|
882
|
+
|
883
|
+
if settings.encryption_key:
|
884
|
+
token_secret = Secret.from_plaintext(session_update.access_token)
|
885
|
+
secret_dict = token_secret.to_dict()
|
886
|
+
oauth_session.access_token_enc = secret_dict["encrypted"]
|
887
|
+
# During migration phase, also update plaintext
|
888
|
+
oauth_session.access_token = secret_dict["plaintext"] if not token_secret._was_encrypted else None
|
889
|
+
else:
|
890
|
+
# No encryption, store plaintext
|
891
|
+
oauth_session.access_token = session_update.access_token
|
892
|
+
oauth_session.access_token_enc = None
|
893
|
+
|
894
|
+
# Handle encryption for refresh_token
|
785
895
|
if session_update.refresh_token is not None:
|
786
|
-
|
896
|
+
from letta.settings import settings
|
897
|
+
|
898
|
+
if settings.encryption_key:
|
899
|
+
token_secret = Secret.from_plaintext(session_update.refresh_token)
|
900
|
+
secret_dict = token_secret.to_dict()
|
901
|
+
oauth_session.refresh_token_enc = secret_dict["encrypted"]
|
902
|
+
# During migration phase, also update plaintext
|
903
|
+
oauth_session.refresh_token = secret_dict["plaintext"] if not token_secret._was_encrypted else None
|
904
|
+
else:
|
905
|
+
# No encryption, store plaintext
|
906
|
+
oauth_session.refresh_token = session_update.refresh_token
|
907
|
+
oauth_session.refresh_token_enc = None
|
908
|
+
|
787
909
|
if session_update.token_type is not None:
|
788
910
|
oauth_session.token_type = session_update.token_type
|
789
911
|
if session_update.expires_at is not None:
|
@@ -792,8 +914,22 @@ class MCPManager:
|
|
792
914
|
oauth_session.scope = session_update.scope
|
793
915
|
if session_update.client_id is not None:
|
794
916
|
oauth_session.client_id = session_update.client_id
|
917
|
+
|
918
|
+
# Handle encryption for client_secret
|
795
919
|
if session_update.client_secret is not None:
|
796
|
-
|
920
|
+
from letta.settings import settings
|
921
|
+
|
922
|
+
if settings.encryption_key:
|
923
|
+
secret_secret = Secret.from_plaintext(session_update.client_secret)
|
924
|
+
secret_dict = secret_secret.to_dict()
|
925
|
+
oauth_session.client_secret_enc = secret_dict["encrypted"]
|
926
|
+
# During migration phase, also update plaintext
|
927
|
+
oauth_session.client_secret = secret_dict["plaintext"] if not secret_secret._was_encrypted else None
|
928
|
+
else:
|
929
|
+
# No encryption, store plaintext
|
930
|
+
oauth_session.client_secret = session_update.client_secret
|
931
|
+
oauth_session.client_secret_enc = None
|
932
|
+
|
797
933
|
if session_update.redirect_uri is not None:
|
798
934
|
oauth_session.redirect_uri = session_update.redirect_uri
|
799
935
|
if session_update.status is not None:
|
@@ -804,27 +940,7 @@ class MCPManager:
|
|
804
940
|
|
805
941
|
oauth_session = await oauth_session.update_async(db_session=session, actor=actor)
|
806
942
|
|
807
|
-
return
|
808
|
-
id=oauth_session.id,
|
809
|
-
state=oauth_session.state,
|
810
|
-
server_url=oauth_session.server_url,
|
811
|
-
server_name=oauth_session.server_name,
|
812
|
-
user_id=oauth_session.user_id,
|
813
|
-
organization_id=oauth_session.organization_id,
|
814
|
-
authorization_url=oauth_session.authorization_url,
|
815
|
-
authorization_code=oauth_session.authorization_code,
|
816
|
-
access_token=oauth_session.access_token,
|
817
|
-
refresh_token=oauth_session.refresh_token,
|
818
|
-
token_type=oauth_session.token_type,
|
819
|
-
expires_at=oauth_session.expires_at,
|
820
|
-
scope=oauth_session.scope,
|
821
|
-
client_id=oauth_session.client_id,
|
822
|
-
client_secret=oauth_session.client_secret,
|
823
|
-
redirect_uri=oauth_session.redirect_uri,
|
824
|
-
status=oauth_session.status,
|
825
|
-
created_at=oauth_session.created_at,
|
826
|
-
updated_at=oauth_session.updated_at,
|
827
|
-
)
|
943
|
+
return self._oauth_orm_to_pydantic(oauth_session)
|
828
944
|
|
829
945
|
@enforce_types
|
830
946
|
async def delete_oauth_session(self, session_id: str, actor: PydanticUser) -> None:
|
letta/services/step_manager.py
CHANGED
@@ -9,12 +9,14 @@ from sqlalchemy.orm import Session
|
|
9
9
|
from letta.helpers.singleton import singleton
|
10
10
|
from letta.orm.errors import NoResultFound
|
11
11
|
from letta.orm.job import Job as JobModel
|
12
|
+
from letta.orm.message import Message as MessageModel
|
12
13
|
from letta.orm.sqlalchemy_base import AccessType
|
13
14
|
from letta.orm.step import Step as StepModel
|
14
15
|
from letta.orm.step_metrics import StepMetrics as StepMetricsModel
|
15
16
|
from letta.otel.tracing import get_trace_id, trace_method
|
16
17
|
from letta.schemas.enums import StepStatus
|
17
18
|
from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
|
19
|
+
from letta.schemas.message import Message as PydanticMessage
|
18
20
|
from letta.schemas.openai.chat_completion_response import UsageStatistics
|
19
21
|
from letta.schemas.step import Step as PydanticStep
|
20
22
|
from letta.schemas.step_metrics import StepMetrics as PydanticStepMetrics
|
@@ -237,6 +239,30 @@ class StepManager:
|
|
237
239
|
await session.commit()
|
238
240
|
return step.to_pydantic()
|
239
241
|
|
242
|
+
@enforce_types
|
243
|
+
@trace_method
|
244
|
+
async def list_step_messages_async(
|
245
|
+
self,
|
246
|
+
step_id: str,
|
247
|
+
actor: PydanticUser,
|
248
|
+
before: str | None = None,
|
249
|
+
after: str | None = None,
|
250
|
+
limit: int = 100,
|
251
|
+
ascending: bool = False,
|
252
|
+
) -> List[PydanticMessage]:
|
253
|
+
async with db_registry.async_session() as session:
|
254
|
+
messages = MessageModel.list(
|
255
|
+
db_session=session,
|
256
|
+
before=before,
|
257
|
+
after=after,
|
258
|
+
ascending=ascending,
|
259
|
+
limit=limit,
|
260
|
+
actor=actor,
|
261
|
+
join_model=StepModel,
|
262
|
+
join_conditions=[MessageModel.step.id == step_id],
|
263
|
+
)
|
264
|
+
return [message.to_pydantic() for message in messages]
|
265
|
+
|
240
266
|
@enforce_types
|
241
267
|
@trace_method
|
242
268
|
async def update_step_stop_reason(self, actor: PydanticUser, step_id: str, stop_reason: StopReasonType) -> PydanticStep:
|
@@ -19,7 +19,6 @@ from letta.services.agent_manager import AgentManager
|
|
19
19
|
from letta.services.message_manager import MessageManager
|
20
20
|
from letta.services.summarizer.enums import SummarizationMode
|
21
21
|
from letta.system import package_summarize_message_no_counts
|
22
|
-
from letta.templates.template_helper import render_template
|
23
22
|
from letta.utils import safe_create_task
|
24
23
|
|
25
24
|
logger = get_logger(__name__)
|
@@ -280,8 +279,7 @@ class Summarizer:
|
|
280
279
|
formatted_evicted_messages = [f"{i}. {msg}" for (i, msg) in enumerate(formatted_evicted_messages)]
|
281
280
|
formatted_in_context_messages = [f"{i + offset}. {msg}" for (i, msg) in enumerate(formatted_in_context_messages)]
|
282
281
|
|
283
|
-
summary_request_text =
|
284
|
-
"summary_request_text.j2",
|
282
|
+
summary_request_text = build_summary_request_text(
|
285
283
|
retain_count=retain_count,
|
286
284
|
evicted_messages=formatted_evicted_messages,
|
287
285
|
in_context_messages=formatted_in_context_messages,
|
@@ -304,6 +302,30 @@ def simple_formatter(messages: List[Message], include_system: bool = False) -> s
|
|
304
302
|
return "\n".join(json.dumps(msg) for msg in parsed_messages)
|
305
303
|
|
306
304
|
|
305
|
+
def build_summary_request_text(retain_count: int, evicted_messages: List[str], in_context_messages: List[str]) -> str:
|
306
|
+
parts: List[str] = []
|
307
|
+
if retain_count == 0:
|
308
|
+
parts.append(
|
309
|
+
"You’re a memory-recall helper for an AI that is about to forget all prior messages. Scan the conversation history and write crisp notes that capture any important facts or insights about the conversation history."
|
310
|
+
)
|
311
|
+
else:
|
312
|
+
parts.append(
|
313
|
+
f"You’re a memory-recall helper for an AI that can only keep the last {retain_count} messages. Scan the conversation history, focusing on messages about to drop out of that window, and write crisp notes that capture any important facts or insights about the human so they aren’t lost."
|
314
|
+
)
|
315
|
+
|
316
|
+
if evicted_messages:
|
317
|
+
parts.append("\n(Older) Evicted Messages:")
|
318
|
+
for item in evicted_messages:
|
319
|
+
parts.append(f" {item}")
|
320
|
+
|
321
|
+
if retain_count > 0 and in_context_messages:
|
322
|
+
parts.append("\n(Newer) In-Context Messages:")
|
323
|
+
for item in in_context_messages:
|
324
|
+
parts.append(f" {item}")
|
325
|
+
|
326
|
+
return "\n".join(parts) + "\n"
|
327
|
+
|
328
|
+
|
307
329
|
def simple_message_wrapper(openai_msg: dict) -> Message:
|
308
330
|
"""Extremely simple way to map from role/content to Message object w/ throwaway dummy fields"""
|
309
331
|
|
@@ -26,6 +26,7 @@ class TelemetryManager:
|
|
26
26
|
async def create_provider_trace_async(self, actor: PydanticUser, provider_trace_create: ProviderTraceCreate) -> PydanticProviderTrace:
|
27
27
|
async with db_registry.async_session() as session:
|
28
28
|
provider_trace = ProviderTraceModel(**provider_trace_create.model_dump())
|
29
|
+
provider_trace.organization_id = actor.organization_id
|
29
30
|
if provider_trace_create.request_json:
|
30
31
|
request_json_str = json_dumps(provider_trace_create.request_json)
|
31
32
|
provider_trace.request_json = json_loads(request_json_str)
|
@@ -43,6 +44,7 @@ class TelemetryManager:
|
|
43
44
|
def create_provider_trace(self, actor: PydanticUser, provider_trace_create: ProviderTraceCreate) -> PydanticProviderTrace:
|
44
45
|
with db_registry.session() as session:
|
45
46
|
provider_trace = ProviderTraceModel(**provider_trace_create.model_dump())
|
47
|
+
provider_trace.organization_id = actor.organization_id
|
46
48
|
if provider_trace_create.request_json:
|
47
49
|
request_json_str = json_dumps(provider_trace_create.request_json)
|
48
50
|
provider_trace.request_json = json_loads(request_json_str)
|
@@ -51,7 +51,7 @@ class ExternalComposioToolExecutor(ToolExecutor):
|
|
51
51
|
|
52
52
|
def _get_entity_id(self, agent_state: AgentState) -> Optional[str]:
|
53
53
|
"""Extract the entity ID from environment variables."""
|
54
|
-
for env_var in agent_state.
|
54
|
+
for env_var in agent_state.secrets:
|
55
55
|
if env_var.key == COMPOSIO_ENTITY_ENV_VAR_KEY:
|
56
56
|
return env_var.value
|
57
57
|
return None
|
@@ -36,7 +36,7 @@ class SandboxToolExecutor(ToolExecutor):
|
|
36
36
|
) -> ToolExecutionResult:
|
37
37
|
# Store original memory state
|
38
38
|
if agent_state:
|
39
|
-
orig_memory_str =
|
39
|
+
orig_memory_str = agent_state.memory.compile()
|
40
40
|
else:
|
41
41
|
orig_memory_str = None
|
42
42
|
|
@@ -89,7 +89,7 @@ class SandboxToolExecutor(ToolExecutor):
|
|
89
89
|
|
90
90
|
# Verify memory integrity
|
91
91
|
if agent_state:
|
92
|
-
new_memory_str =
|
92
|
+
new_memory_str = agent_state.memory.compile()
|
93
93
|
assert orig_memory_str == new_memory_str, "Memory should not be modified in a sandbox tool"
|
94
94
|
|
95
95
|
# Update agent memory if needed
|