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.
Files changed (44) 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/helpers/converters.py +8 -2
  9. letta/helpers/crypto_utils.py +144 -0
  10. letta/llm_api/llm_api_tools.py +0 -1
  11. letta/llm_api/llm_client_base.py +0 -2
  12. letta/orm/__init__.py +1 -0
  13. letta/orm/agent.py +5 -1
  14. letta/orm/job.py +3 -1
  15. letta/orm/mcp_oauth.py +6 -0
  16. letta/orm/mcp_server.py +7 -1
  17. letta/orm/sqlalchemy_base.py +2 -1
  18. letta/schemas/agent.py +10 -7
  19. letta/schemas/job.py +10 -0
  20. letta/schemas/mcp.py +146 -6
  21. letta/schemas/provider_trace.py +0 -2
  22. letta/schemas/run.py +2 -0
  23. letta/schemas/secret.py +378 -0
  24. letta/serialize_schemas/marshmallow_agent.py +4 -0
  25. letta/server/rest_api/routers/v1/__init__.py +2 -0
  26. letta/server/rest_api/routers/v1/agents.py +9 -4
  27. letta/server/rest_api/routers/v1/archives.py +113 -0
  28. letta/server/rest_api/routers/v1/jobs.py +7 -2
  29. letta/server/rest_api/routers/v1/runs.py +9 -1
  30. letta/server/rest_api/routers/v1/tools.py +7 -26
  31. letta/services/agent_manager.py +17 -9
  32. letta/services/agent_serialization_manager.py +11 -3
  33. letta/services/archive_manager.py +73 -0
  34. letta/services/helpers/agent_manager_helper.py +6 -1
  35. letta/services/job_manager.py +18 -2
  36. letta/services/mcp_manager.py +198 -82
  37. letta/services/telemetry_manager.py +2 -0
  38. letta/services/tool_executor/composio_tool_executor.py +1 -1
  39. letta/services/tool_sandbox/base.py +2 -3
  40. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/METADATA +5 -2
  41. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/RECORD +44 -41
  42. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/WHEEL +0 -0
  43. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/entry_points.txt +0 -0
  44. {letta_nightly-0.11.7.dev20250916104104.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:
@@ -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
@@ -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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: letta-nightly
3
- Version: 0.11.7.dev20250916104104
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