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.
Files changed (99) hide show
  1. letta/__init__.py +10 -2
  2. letta/adapters/letta_llm_request_adapter.py +0 -1
  3. letta/adapters/letta_llm_stream_adapter.py +0 -1
  4. letta/agent.py +1 -1
  5. letta/agents/letta_agent.py +1 -4
  6. letta/agents/letta_agent_v2.py +2 -1
  7. letta/agents/voice_agent.py +1 -1
  8. letta/functions/function_sets/multi_agent.py +1 -1
  9. letta/functions/helpers.py +1 -1
  10. letta/helpers/converters.py +8 -2
  11. letta/helpers/crypto_utils.py +144 -0
  12. letta/llm_api/llm_api_tools.py +0 -1
  13. letta/llm_api/llm_client_base.py +0 -2
  14. letta/orm/__init__.py +1 -0
  15. letta/orm/agent.py +5 -1
  16. letta/orm/job.py +3 -1
  17. letta/orm/mcp_oauth.py +6 -0
  18. letta/orm/mcp_server.py +7 -1
  19. letta/orm/sqlalchemy_base.py +2 -1
  20. letta/prompts/gpt_system.py +13 -15
  21. letta/prompts/system_prompts/__init__.py +27 -0
  22. letta/prompts/{system/memgpt_chat.txt → system_prompts/memgpt_chat.py} +2 -0
  23. letta/prompts/{system/memgpt_generate_tool.txt → system_prompts/memgpt_generate_tool.py} +4 -2
  24. letta/prompts/{system/memgpt_v2_chat.txt → system_prompts/memgpt_v2_chat.py} +2 -0
  25. letta/prompts/{system/react.txt → system_prompts/react.py} +2 -0
  26. letta/prompts/{system/sleeptime_doc_ingest.txt → system_prompts/sleeptime_doc_ingest.py} +2 -0
  27. letta/prompts/{system/sleeptime_v2.txt → system_prompts/sleeptime_v2.py} +2 -0
  28. letta/prompts/{system/summary_system_prompt.txt → system_prompts/summary_system_prompt.py} +2 -0
  29. letta/prompts/{system/voice_chat.txt → system_prompts/voice_chat.py} +2 -0
  30. letta/prompts/{system/voice_sleeptime.txt → system_prompts/voice_sleeptime.py} +2 -0
  31. letta/prompts/{system/workflow.txt → system_prompts/workflow.py} +2 -0
  32. letta/schemas/agent.py +10 -7
  33. letta/schemas/job.py +10 -0
  34. letta/schemas/mcp.py +146 -6
  35. letta/schemas/provider_trace.py +0 -2
  36. letta/schemas/run.py +2 -0
  37. letta/schemas/secret.py +378 -0
  38. letta/serialize_schemas/marshmallow_agent.py +4 -0
  39. letta/server/rest_api/dependencies.py +37 -0
  40. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +4 -3
  41. letta/server/rest_api/routers/v1/__init__.py +2 -0
  42. letta/server/rest_api/routers/v1/agents.py +115 -107
  43. letta/server/rest_api/routers/v1/archives.py +113 -0
  44. letta/server/rest_api/routers/v1/blocks.py +44 -20
  45. letta/server/rest_api/routers/v1/embeddings.py +3 -3
  46. letta/server/rest_api/routers/v1/folders.py +107 -47
  47. letta/server/rest_api/routers/v1/groups.py +52 -32
  48. letta/server/rest_api/routers/v1/identities.py +110 -21
  49. letta/server/rest_api/routers/v1/internal_templates.py +28 -13
  50. letta/server/rest_api/routers/v1/jobs.py +19 -14
  51. letta/server/rest_api/routers/v1/llms.py +6 -8
  52. letta/server/rest_api/routers/v1/messages.py +14 -14
  53. letta/server/rest_api/routers/v1/organizations.py +1 -1
  54. letta/server/rest_api/routers/v1/providers.py +40 -16
  55. letta/server/rest_api/routers/v1/runs.py +28 -20
  56. letta/server/rest_api/routers/v1/sandbox_configs.py +25 -25
  57. letta/server/rest_api/routers/v1/sources.py +44 -45
  58. letta/server/rest_api/routers/v1/steps.py +27 -25
  59. letta/server/rest_api/routers/v1/tags.py +11 -7
  60. letta/server/rest_api/routers/v1/telemetry.py +11 -6
  61. letta/server/rest_api/routers/v1/tools.py +78 -80
  62. letta/server/rest_api/routers/v1/users.py +1 -1
  63. letta/server/rest_api/routers/v1/voice.py +6 -5
  64. letta/server/rest_api/utils.py +1 -18
  65. letta/services/agent_manager.py +17 -9
  66. letta/services/agent_serialization_manager.py +11 -3
  67. letta/services/archive_manager.py +73 -0
  68. letta/services/file_manager.py +6 -0
  69. letta/services/group_manager.py +2 -1
  70. letta/services/helpers/agent_manager_helper.py +6 -1
  71. letta/services/identity_manager.py +67 -0
  72. letta/services/job_manager.py +18 -2
  73. letta/services/mcp_manager.py +198 -82
  74. letta/services/provider_manager.py +14 -1
  75. letta/services/source_manager.py +11 -1
  76. letta/services/telemetry_manager.py +2 -0
  77. letta/services/tool_executor/composio_tool_executor.py +1 -1
  78. letta/services/tool_manager.py +46 -9
  79. letta/services/tool_sandbox/base.py +2 -3
  80. letta/utils.py +4 -2
  81. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/METADATA +5 -2
  82. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/RECORD +85 -94
  83. letta/prompts/system/memgpt_base.txt +0 -54
  84. letta/prompts/system/memgpt_chat_compressed.txt +0 -13
  85. letta/prompts/system/memgpt_chat_fstring.txt +0 -51
  86. letta/prompts/system/memgpt_convo_only.txt +0 -12
  87. letta/prompts/system/memgpt_doc.txt +0 -50
  88. letta/prompts/system/memgpt_gpt35_extralong.txt +0 -53
  89. letta/prompts/system/memgpt_intuitive_knowledge.txt +0 -31
  90. letta/prompts/system/memgpt_memory_only.txt +0 -29
  91. letta/prompts/system/memgpt_modified_chat.txt +0 -23
  92. letta/prompts/system/memgpt_modified_o1.txt +0 -31
  93. letta/prompts/system/memgpt_offline_memory.txt +0 -23
  94. letta/prompts/system/memgpt_offline_memory_chat.txt +0 -35
  95. letta/prompts/system/memgpt_sleeptime_chat.txt +0 -52
  96. letta/prompts/system/sleeptime.txt +0 -37
  97. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/WHEEL +0 -0
  98. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/entry_points.txt +0 -0
  99. {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/licenses/LICENSE +0 -0
@@ -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
- # Ensure custom_headers None is stored as SQL NULL, not JSON null
424
- if update_data.get("custom_headers") is None:
425
- update_data.pop("custom_headers", None)
426
- setattr(mcp_server, "custom_headers", null())
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 MCPOAuthSession(
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 MCPOAuthSession(
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 MCPOAuthSession(
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
- oauth_session.access_token = session_update.access_token
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
- oauth_session.refresh_token = session_update.refresh_token
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
- oauth_session.client_secret = session_update.client_secret
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 MCPOAuthSession(
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
- """List all providers with optional pagination."""
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]:
@@ -230,15 +230,25 @@ class SourceManager:
230
230
  @enforce_types
231
231
  @trace_method
232
232
  async def list_sources(
233
- self, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50, **kwargs
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.tool_exec_environment_variables:
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
@@ -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 optional pagination."""
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
- # Apply pagination if specified
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
- query = query.where(
424
- or_(
425
- ToolModel.created_at < after_tool.created_at,
426
- and_(ToolModel.created_at == after_tool.created_at, ToolModel.id < after_tool.id),
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
- # Order by created_at and id for consistent pagination
435
- query = query.order_by(ToolModel.created_at.desc(), ToolModel.id.desc())
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 await render_template_in_thread(
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
- human_text = open(file_path, encoding="utf-8").read().strip()
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
- persona_text = open(file_path, encoding="utf-8").read().strip()
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.dev20250915104130
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
- To get started, install the Letta SDK (available for both Python and TypeScript):
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