letta-nightly 0.11.7.dev20250915104130__py3-none-any.whl → 0.11.7.dev20250917104122__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/functions/function_sets/multi_agent.py +1 -1
- letta/functions/helpers.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/prompts/gpt_system.py +13 -15
- letta/prompts/system_prompts/__init__.py +27 -0
- letta/prompts/{system/memgpt_chat.txt → system_prompts/memgpt_chat.py} +2 -0
- letta/prompts/{system/memgpt_generate_tool.txt → system_prompts/memgpt_generate_tool.py} +4 -2
- letta/prompts/{system/memgpt_v2_chat.txt → system_prompts/memgpt_v2_chat.py} +2 -0
- letta/prompts/{system/react.txt → system_prompts/react.py} +2 -0
- letta/prompts/{system/sleeptime_doc_ingest.txt → system_prompts/sleeptime_doc_ingest.py} +2 -0
- letta/prompts/{system/sleeptime_v2.txt → system_prompts/sleeptime_v2.py} +2 -0
- letta/prompts/{system/summary_system_prompt.txt → system_prompts/summary_system_prompt.py} +2 -0
- letta/prompts/{system/voice_chat.txt → system_prompts/voice_chat.py} +2 -0
- letta/prompts/{system/voice_sleeptime.txt → system_prompts/voice_sleeptime.py} +2 -0
- letta/prompts/{system/workflow.txt → system_prompts/workflow.py} +2 -0
- 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/dependencies.py +37 -0
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +4 -3
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +115 -107
- letta/server/rest_api/routers/v1/archives.py +113 -0
- letta/server/rest_api/routers/v1/blocks.py +44 -20
- letta/server/rest_api/routers/v1/embeddings.py +3 -3
- letta/server/rest_api/routers/v1/folders.py +107 -47
- letta/server/rest_api/routers/v1/groups.py +52 -32
- letta/server/rest_api/routers/v1/identities.py +110 -21
- letta/server/rest_api/routers/v1/internal_templates.py +28 -13
- letta/server/rest_api/routers/v1/jobs.py +19 -14
- letta/server/rest_api/routers/v1/llms.py +6 -8
- letta/server/rest_api/routers/v1/messages.py +14 -14
- letta/server/rest_api/routers/v1/organizations.py +1 -1
- letta/server/rest_api/routers/v1/providers.py +40 -16
- letta/server/rest_api/routers/v1/runs.py +28 -20
- letta/server/rest_api/routers/v1/sandbox_configs.py +25 -25
- letta/server/rest_api/routers/v1/sources.py +44 -45
- letta/server/rest_api/routers/v1/steps.py +27 -25
- letta/server/rest_api/routers/v1/tags.py +11 -7
- letta/server/rest_api/routers/v1/telemetry.py +11 -6
- letta/server/rest_api/routers/v1/tools.py +78 -80
- letta/server/rest_api/routers/v1/users.py +1 -1
- letta/server/rest_api/routers/v1/voice.py +6 -5
- letta/server/rest_api/utils.py +1 -18
- letta/services/agent_manager.py +17 -9
- letta/services/agent_serialization_manager.py +11 -3
- letta/services/archive_manager.py +73 -0
- letta/services/file_manager.py +6 -0
- letta/services/group_manager.py +2 -1
- letta/services/helpers/agent_manager_helper.py +6 -1
- letta/services/identity_manager.py +67 -0
- letta/services/job_manager.py +18 -2
- letta/services/mcp_manager.py +198 -82
- letta/services/provider_manager.py +14 -1
- letta/services/source_manager.py +11 -1
- letta/services/telemetry_manager.py +2 -0
- letta/services/tool_executor/composio_tool_executor.py +1 -1
- letta/services/tool_manager.py +46 -9
- letta/services/tool_sandbox/base.py +2 -3
- letta/utils.py +4 -2
- {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/METADATA +5 -2
- {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/RECORD +85 -94
- letta/prompts/system/memgpt_base.txt +0 -54
- letta/prompts/system/memgpt_chat_compressed.txt +0 -13
- letta/prompts/system/memgpt_chat_fstring.txt +0 -51
- letta/prompts/system/memgpt_convo_only.txt +0 -12
- letta/prompts/system/memgpt_doc.txt +0 -50
- letta/prompts/system/memgpt_gpt35_extralong.txt +0 -53
- letta/prompts/system/memgpt_intuitive_knowledge.txt +0 -31
- letta/prompts/system/memgpt_memory_only.txt +0 -29
- letta/prompts/system/memgpt_modified_chat.txt +0 -23
- letta/prompts/system/memgpt_modified_o1.txt +0 -31
- letta/prompts/system/memgpt_offline_memory.txt +0 -23
- letta/prompts/system/memgpt_offline_memory_chat.txt +0 -35
- letta/prompts/system/memgpt_sleeptime_chat.txt +0 -52
- letta/prompts/system/sleeptime.txt +0 -37
- {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/licenses/LICENSE +0 -0
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:
|
@@ -154,10 +154,14 @@ class ProviderManager:
|
|
154
154
|
actor: PydanticUser,
|
155
155
|
name: Optional[str] = None,
|
156
156
|
provider_type: Optional[ProviderType] = None,
|
157
|
+
before: Optional[str] = None,
|
157
158
|
after: Optional[str] = None,
|
158
159
|
limit: Optional[int] = 50,
|
160
|
+
ascending: bool = False,
|
159
161
|
) -> List[PydanticProvider]:
|
160
|
-
"""
|
162
|
+
"""
|
163
|
+
List all providers with pagination support.
|
164
|
+
"""
|
161
165
|
filter_kwargs = {}
|
162
166
|
if name:
|
163
167
|
filter_kwargs["name"] = name
|
@@ -166,14 +170,23 @@ class ProviderManager:
|
|
166
170
|
async with db_registry.async_session() as session:
|
167
171
|
providers = await ProviderModel.list_async(
|
168
172
|
db_session=session,
|
173
|
+
before=before,
|
169
174
|
after=after,
|
170
175
|
limit=limit,
|
171
176
|
actor=actor,
|
177
|
+
ascending=ascending,
|
172
178
|
check_is_deleted=True,
|
173
179
|
**filter_kwargs,
|
174
180
|
)
|
175
181
|
return [provider.to_pydantic() for provider in providers]
|
176
182
|
|
183
|
+
@enforce_types
|
184
|
+
@trace_method
|
185
|
+
async def get_provider_async(self, provider_id: str, actor: PydanticUser) -> PydanticProvider:
|
186
|
+
async with db_registry.async_session() as session:
|
187
|
+
provider_model = await ProviderModel.read_async(db_session=session, identifier=provider_id, actor=actor)
|
188
|
+
return provider_model.to_pydantic()
|
189
|
+
|
177
190
|
@enforce_types
|
178
191
|
@trace_method
|
179
192
|
def get_provider_id_from_name(self, provider_name: Union[str, None], actor: PydanticUser) -> Optional[str]:
|
letta/services/source_manager.py
CHANGED
@@ -230,15 +230,25 @@ class SourceManager:
|
|
230
230
|
@enforce_types
|
231
231
|
@trace_method
|
232
232
|
async def list_sources(
|
233
|
-
self,
|
233
|
+
self,
|
234
|
+
actor: PydanticUser,
|
235
|
+
before: Optional[str] = None,
|
236
|
+
after: Optional[str] = None,
|
237
|
+
limit: Optional[int] = 50,
|
238
|
+
ascending: bool = True,
|
239
|
+
name: Optional[str] = None,
|
240
|
+
**kwargs,
|
234
241
|
) -> List[PydanticSource]:
|
235
242
|
"""List all sources with optional pagination."""
|
236
243
|
async with db_registry.async_session() as session:
|
237
244
|
sources = await SourceModel.list_async(
|
238
245
|
db_session=session,
|
246
|
+
before=before,
|
239
247
|
after=after,
|
240
248
|
limit=limit,
|
249
|
+
ascending=ascending,
|
241
250
|
organization_id=actor.organization_id,
|
251
|
+
query_text=name,
|
242
252
|
**kwargs,
|
243
253
|
)
|
244
254
|
return [source.to_pydantic() for source in sources]
|
@@ -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
|
letta/services/tool_manager.py
CHANGED
@@ -321,8 +321,10 @@ class ToolManager:
|
|
321
321
|
async def list_tools_async(
|
322
322
|
self,
|
323
323
|
actor: PydanticUser,
|
324
|
+
before: Optional[str] = None,
|
324
325
|
after: Optional[str] = None,
|
325
326
|
limit: Optional[int] = 50,
|
327
|
+
ascending: bool = False,
|
326
328
|
upsert_base_tools: bool = True,
|
327
329
|
tool_types: Optional[List[str]] = None,
|
328
330
|
exclude_tool_types: Optional[List[str]] = None,
|
@@ -331,11 +333,13 @@ class ToolManager:
|
|
331
333
|
search: Optional[str] = None,
|
332
334
|
return_only_letta_tools: bool = False,
|
333
335
|
) -> List[PydanticTool]:
|
334
|
-
"""List all tools with
|
336
|
+
"""List all tools with pagination support."""
|
335
337
|
tools = await self._list_tools_async(
|
336
338
|
actor=actor,
|
339
|
+
before=before,
|
337
340
|
after=after,
|
338
341
|
limit=limit,
|
342
|
+
ascending=ascending,
|
339
343
|
tool_types=tool_types,
|
340
344
|
exclude_tool_types=exclude_tool_types,
|
341
345
|
names=names,
|
@@ -359,8 +363,10 @@ class ToolManager:
|
|
359
363
|
# Re-fetch the tools list after upserting base tools
|
360
364
|
tools = await self._list_tools_async(
|
361
365
|
actor=actor,
|
366
|
+
before=before,
|
362
367
|
after=after,
|
363
368
|
limit=limit,
|
369
|
+
ascending=ascending,
|
364
370
|
tool_types=tool_types,
|
365
371
|
exclude_tool_types=exclude_tool_types,
|
366
372
|
names=names,
|
@@ -376,8 +382,10 @@ class ToolManager:
|
|
376
382
|
async def _list_tools_async(
|
377
383
|
self,
|
378
384
|
actor: PydanticUser,
|
385
|
+
before: Optional[str] = None,
|
379
386
|
after: Optional[str] = None,
|
380
387
|
limit: Optional[int] = 50,
|
388
|
+
ascending: bool = False,
|
381
389
|
tool_types: Optional[List[str]] = None,
|
382
390
|
exclude_tool_types: Optional[List[str]] = None,
|
383
391
|
names: Optional[List[str]] = None,
|
@@ -416,23 +424,52 @@ class ToolManager:
|
|
416
424
|
if return_only_letta_tools:
|
417
425
|
query = query.where(ToolModel.tool_type.like("letta_%"))
|
418
426
|
|
419
|
-
#
|
427
|
+
# Handle pagination cursors
|
420
428
|
if after is not None:
|
421
429
|
after_tool = await session.get(ToolModel, after)
|
422
430
|
if after_tool:
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
431
|
+
if ascending:
|
432
|
+
query = query.where(
|
433
|
+
or_(
|
434
|
+
ToolModel.created_at > after_tool.created_at,
|
435
|
+
and_(ToolModel.created_at == after_tool.created_at, ToolModel.id > after_tool.id),
|
436
|
+
)
|
437
|
+
)
|
438
|
+
else:
|
439
|
+
query = query.where(
|
440
|
+
or_(
|
441
|
+
ToolModel.created_at < after_tool.created_at,
|
442
|
+
and_(ToolModel.created_at == after_tool.created_at, ToolModel.id < after_tool.id),
|
443
|
+
)
|
444
|
+
)
|
445
|
+
|
446
|
+
if before is not None:
|
447
|
+
before_tool = await session.get(ToolModel, before)
|
448
|
+
if before_tool:
|
449
|
+
if ascending:
|
450
|
+
query = query.where(
|
451
|
+
or_(
|
452
|
+
ToolModel.created_at < before_tool.created_at,
|
453
|
+
and_(ToolModel.created_at == before_tool.created_at, ToolModel.id < before_tool.id),
|
454
|
+
)
|
455
|
+
)
|
456
|
+
else:
|
457
|
+
query = query.where(
|
458
|
+
or_(
|
459
|
+
ToolModel.created_at > before_tool.created_at,
|
460
|
+
and_(ToolModel.created_at == before_tool.created_at, ToolModel.id > before_tool.id),
|
461
|
+
)
|
427
462
|
)
|
428
|
-
)
|
429
463
|
|
430
464
|
# Apply limit
|
431
465
|
if limit is not None:
|
432
466
|
query = query.limit(limit)
|
433
467
|
|
434
|
-
#
|
435
|
-
|
468
|
+
# Apply ordering based on ascending parameter
|
469
|
+
if ascending:
|
470
|
+
query = query.order_by(ToolModel.created_at.asc(), ToolModel.id.asc())
|
471
|
+
else:
|
472
|
+
query = query.order_by(ToolModel.created_at.desc(), ToolModel.id.desc())
|
436
473
|
|
437
474
|
# Execute query
|
438
475
|
result = await session.execute(query)
|
@@ -13,6 +13,7 @@ from letta.services.helpers.tool_execution_helper import add_imports_and_pydanti
|
|
13
13
|
from letta.services.helpers.tool_parser_helper import convert_param_to_str_value, parse_function_arguments
|
14
14
|
from letta.services.sandbox_config_manager import SandboxConfigManager
|
15
15
|
from letta.services.tool_manager import ToolManager
|
16
|
+
from letta.templates.template_helper import render_template
|
16
17
|
from letta.types import JsonDict, JsonValue
|
17
18
|
|
18
19
|
|
@@ -80,8 +81,6 @@ class AsyncToolSandboxBase(ABC):
|
|
80
81
|
Generate code to run inside of execution sandbox. Serialize the agent state and arguments, call the tool,
|
81
82
|
then base64-encode/pickle the result. Runs a jinja2 template constructing the python file.
|
82
83
|
"""
|
83
|
-
from letta.templates.template_helper import render_template_in_thread
|
84
|
-
|
85
84
|
# Select the appropriate template based on whether the function is async
|
86
85
|
TEMPLATE_NAME = "sandbox_code_file_async.py.j2" if self.is_async_function else "sandbox_code_file.py.j2"
|
87
86
|
|
@@ -107,7 +106,7 @@ class AsyncToolSandboxBase(ABC):
|
|
107
106
|
|
108
107
|
agent_state_pickle = pickle.dumps(agent_state) if self.inject_agent_state else None
|
109
108
|
|
110
|
-
return
|
109
|
+
return render_template(
|
111
110
|
TEMPLATE_NAME,
|
112
111
|
future_import=future_import,
|
113
112
|
inject_agent_state=self.inject_agent_state,
|
letta/utils.py
CHANGED
@@ -941,7 +941,8 @@ def get_human_text(name: str, enforce_limit=True):
|
|
941
941
|
for file_path in list_human_files():
|
942
942
|
file = os.path.basename(file_path)
|
943
943
|
if f"{name}.txt" == file or name == file:
|
944
|
-
|
944
|
+
with open(file_path, encoding="utf-8") as f:
|
945
|
+
human_text = f.read().strip()
|
945
946
|
if enforce_limit and len(human_text) > CORE_MEMORY_HUMAN_CHAR_LIMIT:
|
946
947
|
raise ValueError(f"Contents of {name}.txt is over the character limit ({len(human_text)} > {CORE_MEMORY_HUMAN_CHAR_LIMIT})")
|
947
948
|
return human_text
|
@@ -953,7 +954,8 @@ def get_persona_text(name: str, enforce_limit=True):
|
|
953
954
|
for file_path in list_persona_files():
|
954
955
|
file = os.path.basename(file_path)
|
955
956
|
if f"{name}.txt" == file or name == file:
|
956
|
-
|
957
|
+
with open(file_path, encoding="utf-8") as f:
|
958
|
+
persona_text = f.read().strip()
|
957
959
|
if enforce_limit and len(persona_text) > CORE_MEMORY_PERSONA_CHAR_LIMIT:
|
958
960
|
raise ValueError(
|
959
961
|
f"Contents of {name}.txt is over the character limit ({len(persona_text)} > {CORE_MEMORY_PERSONA_CHAR_LIMIT})"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: letta-nightly
|
3
|
-
Version: 0.11.7.
|
3
|
+
Version: 0.11.7.dev20250917104122
|
4
4
|
Summary: Create LLM agents with long-term memory and custom tools
|
5
5
|
Author-email: Letta Team <contact@letta.com>
|
6
6
|
License: Apache License
|
@@ -150,9 +150,12 @@ Letta is the platform for building stateful agents: open AI with advanced memory
|
|
150
150
|
* [**Letta Desktop**](https://docs.letta.com/guides/ade/desktop): A fully-local version of the ADE, available on MacOS and Windows
|
151
151
|
* [**Letta Cloud**](https://app.letta.com/): The fastest way to try Letta, with agents running in the cloud
|
152
152
|
|
153
|
+
|
153
154
|
## Get started
|
154
155
|
|
155
|
-
|
156
|
+
### [One-Shot ✨ Vibecoding ⚡️ Prompts](https://github.com/letta-ai/letta/blob/main/fern/pages/getting-started/prompts.mdx)
|
157
|
+
|
158
|
+
Or install the Letta SDK (available for both Python and TypeScript):
|
156
159
|
|
157
160
|
### [Python SDK](https://github.com/letta-ai/letta-python)
|
158
161
|
```sh
|