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/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:
|
@@ -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
|
@@ -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,
|
@@ -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
|