letta-nightly 0.11.6.dev20250902104140__py3-none-any.whl → 0.11.7.dev20250904045700__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 (138) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +10 -14
  3. letta/agents/base_agent.py +18 -0
  4. letta/agents/helpers.py +32 -7
  5. letta/agents/letta_agent.py +953 -762
  6. letta/agents/voice_agent.py +1 -1
  7. letta/client/streaming.py +0 -1
  8. letta/constants.py +11 -8
  9. letta/errors.py +9 -0
  10. letta/functions/function_sets/base.py +77 -69
  11. letta/functions/function_sets/builtin.py +41 -22
  12. letta/functions/function_sets/multi_agent.py +1 -2
  13. letta/functions/schema_generator.py +0 -1
  14. letta/helpers/converters.py +8 -3
  15. letta/helpers/datetime_helpers.py +5 -4
  16. letta/helpers/message_helper.py +1 -2
  17. letta/helpers/pinecone_utils.py +0 -1
  18. letta/helpers/tool_rule_solver.py +10 -0
  19. letta/helpers/tpuf_client.py +848 -0
  20. letta/interface.py +8 -8
  21. letta/interfaces/anthropic_streaming_interface.py +7 -0
  22. letta/interfaces/openai_streaming_interface.py +29 -6
  23. letta/llm_api/anthropic_client.py +188 -18
  24. letta/llm_api/azure_client.py +0 -1
  25. letta/llm_api/bedrock_client.py +1 -2
  26. letta/llm_api/deepseek_client.py +319 -5
  27. letta/llm_api/google_vertex_client.py +75 -17
  28. letta/llm_api/groq_client.py +0 -1
  29. letta/llm_api/helpers.py +2 -2
  30. letta/llm_api/llm_api_tools.py +1 -50
  31. letta/llm_api/llm_client.py +6 -8
  32. letta/llm_api/mistral.py +1 -1
  33. letta/llm_api/openai.py +16 -13
  34. letta/llm_api/openai_client.py +31 -16
  35. letta/llm_api/together_client.py +0 -1
  36. letta/llm_api/xai_client.py +0 -1
  37. letta/local_llm/chat_completion_proxy.py +7 -6
  38. letta/local_llm/settings/settings.py +1 -1
  39. letta/orm/__init__.py +1 -0
  40. letta/orm/agent.py +8 -6
  41. letta/orm/archive.py +9 -1
  42. letta/orm/block.py +3 -4
  43. letta/orm/block_history.py +3 -1
  44. letta/orm/group.py +2 -3
  45. letta/orm/identity.py +1 -2
  46. letta/orm/job.py +1 -2
  47. letta/orm/llm_batch_items.py +1 -2
  48. letta/orm/message.py +8 -4
  49. letta/orm/mixins.py +18 -0
  50. letta/orm/organization.py +2 -0
  51. letta/orm/passage.py +8 -1
  52. letta/orm/passage_tag.py +55 -0
  53. letta/orm/sandbox_config.py +1 -3
  54. letta/orm/step.py +1 -2
  55. letta/orm/tool.py +1 -0
  56. letta/otel/resource.py +2 -2
  57. letta/plugins/plugins.py +1 -1
  58. letta/prompts/prompt_generator.py +10 -2
  59. letta/schemas/agent.py +11 -0
  60. letta/schemas/archive.py +4 -0
  61. letta/schemas/block.py +13 -0
  62. letta/schemas/embedding_config.py +0 -1
  63. letta/schemas/enums.py +24 -7
  64. letta/schemas/group.py +12 -0
  65. letta/schemas/letta_message.py +55 -1
  66. letta/schemas/letta_message_content.py +28 -0
  67. letta/schemas/letta_request.py +21 -4
  68. letta/schemas/letta_stop_reason.py +9 -1
  69. letta/schemas/llm_config.py +24 -8
  70. letta/schemas/mcp.py +0 -3
  71. letta/schemas/memory.py +14 -0
  72. letta/schemas/message.py +245 -141
  73. letta/schemas/openai/chat_completion_request.py +2 -1
  74. letta/schemas/passage.py +1 -0
  75. letta/schemas/providers/bedrock.py +1 -1
  76. letta/schemas/providers/openai.py +2 -2
  77. letta/schemas/tool.py +11 -5
  78. letta/schemas/tool_execution_result.py +0 -1
  79. letta/schemas/tool_rule.py +71 -0
  80. letta/serialize_schemas/marshmallow_agent.py +1 -2
  81. letta/server/rest_api/app.py +3 -3
  82. letta/server/rest_api/auth/index.py +0 -1
  83. letta/server/rest_api/interface.py +3 -11
  84. letta/server/rest_api/redis_stream_manager.py +3 -4
  85. letta/server/rest_api/routers/v1/agents.py +143 -84
  86. letta/server/rest_api/routers/v1/blocks.py +1 -1
  87. letta/server/rest_api/routers/v1/folders.py +1 -1
  88. letta/server/rest_api/routers/v1/groups.py +23 -22
  89. letta/server/rest_api/routers/v1/internal_templates.py +68 -0
  90. letta/server/rest_api/routers/v1/sandbox_configs.py +11 -5
  91. letta/server/rest_api/routers/v1/sources.py +1 -1
  92. letta/server/rest_api/routers/v1/tools.py +167 -15
  93. letta/server/rest_api/streaming_response.py +4 -3
  94. letta/server/rest_api/utils.py +75 -18
  95. letta/server/server.py +24 -35
  96. letta/services/agent_manager.py +359 -45
  97. letta/services/agent_serialization_manager.py +23 -3
  98. letta/services/archive_manager.py +72 -3
  99. letta/services/block_manager.py +1 -2
  100. letta/services/context_window_calculator/token_counter.py +11 -6
  101. letta/services/file_manager.py +1 -3
  102. letta/services/files_agents_manager.py +2 -4
  103. letta/services/group_manager.py +73 -12
  104. letta/services/helpers/agent_manager_helper.py +5 -5
  105. letta/services/identity_manager.py +8 -3
  106. letta/services/job_manager.py +2 -14
  107. letta/services/llm_batch_manager.py +1 -3
  108. letta/services/mcp/base_client.py +1 -2
  109. letta/services/mcp_manager.py +5 -6
  110. letta/services/message_manager.py +536 -15
  111. letta/services/organization_manager.py +1 -2
  112. letta/services/passage_manager.py +287 -12
  113. letta/services/provider_manager.py +1 -3
  114. letta/services/sandbox_config_manager.py +12 -7
  115. letta/services/source_manager.py +1 -2
  116. letta/services/step_manager.py +0 -1
  117. letta/services/summarizer/summarizer.py +4 -2
  118. letta/services/telemetry_manager.py +1 -3
  119. letta/services/tool_executor/builtin_tool_executor.py +136 -316
  120. letta/services/tool_executor/core_tool_executor.py +231 -74
  121. letta/services/tool_executor/files_tool_executor.py +2 -2
  122. letta/services/tool_executor/mcp_tool_executor.py +0 -1
  123. letta/services/tool_executor/multi_agent_tool_executor.py +2 -2
  124. letta/services/tool_executor/sandbox_tool_executor.py +0 -1
  125. letta/services/tool_executor/tool_execution_sandbox.py +2 -3
  126. letta/services/tool_manager.py +181 -64
  127. letta/services/tool_sandbox/modal_deployment_manager.py +2 -2
  128. letta/services/user_manager.py +1 -2
  129. letta/settings.py +5 -3
  130. letta/streaming_interface.py +3 -3
  131. letta/system.py +1 -1
  132. letta/utils.py +0 -1
  133. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/METADATA +11 -7
  134. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/RECORD +137 -135
  135. letta/llm_api/deepseek.py +0 -303
  136. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/WHEEL +0 -0
  137. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/entry_points.txt +0 -0
  138. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/licenses/LICENSE +0 -0
@@ -28,6 +28,7 @@ from letta.schemas.agent_file import (
28
28
  ToolSchema,
29
29
  )
30
30
  from letta.schemas.block import Block
31
+ from letta.schemas.embedding_config import EmbeddingConfig
31
32
  from letta.schemas.enums import FileProcessingStatus
32
33
  from letta.schemas.file import FileMetadata
33
34
  from letta.schemas.group import Group, GroupCreate
@@ -432,6 +433,8 @@ class AgentSerializationManager:
432
433
  override_existing_tools: bool = True,
433
434
  dry_run: bool = False,
434
435
  env_vars: Optional[Dict[str, Any]] = None,
436
+ override_embedding_config: Optional[EmbeddingConfig] = None,
437
+ project_id: Optional[str] = None,
435
438
  ) -> ImportResult:
436
439
  """
437
440
  Import AgentFileSchema into the database.
@@ -530,6 +533,12 @@ class AgentSerializationManager:
530
533
  source_names_to_check = [s.name for s in schema.sources]
531
534
  existing_source_names = await self.source_manager.get_existing_source_names(source_names_to_check, actor)
532
535
 
536
+ # override embedding_config
537
+ if override_embedding_config:
538
+ for source_schema in schema.sources:
539
+ source_schema.embedding_config = override_embedding_config
540
+ source_schema.embedding = override_embedding_config.handle
541
+
533
542
  for source_schema in schema.sources:
534
543
  source_data = source_schema.model_dump(exclude={"id", "embedding", "embedding_chunk_size"})
535
544
 
@@ -577,10 +586,12 @@ class AgentSerializationManager:
577
586
  # Start background tasks for file processing
578
587
  background_tasks = []
579
588
  if schema.files and any(f.content for f in schema.files):
589
+ # Use override embedding config if provided, otherwise use agent's config
590
+ embedder_config = override_embedding_config if override_embedding_config else schema.agents[0].embedding_config
580
591
  if should_use_pinecone():
581
- embedder = PineconeEmbedder(embedding_config=schema.agents[0].embedding_config)
592
+ embedder = PineconeEmbedder(embedding_config=embedder_config)
582
593
  else:
583
- embedder = OpenAIEmbedder(embedding_config=schema.agents[0].embedding_config)
594
+ embedder = OpenAIEmbedder(embedding_config=embedder_config)
584
595
  file_processor = FileProcessor(
585
596
  file_parser=self.file_parser,
586
597
  embedder=embedder,
@@ -613,6 +624,11 @@ class AgentSerializationManager:
613
624
 
614
625
  # 6. Create agents with empty message history
615
626
  for agent_schema in schema.agents:
627
+ # Override embedding_config if provided
628
+ if override_embedding_config:
629
+ agent_schema.embedding_config = override_embedding_config
630
+ agent_schema.embedding = override_embedding_config.handle
631
+
616
632
  # Convert AgentSchema back to CreateAgent, remapping tool/block IDs
617
633
  agent_data = agent_schema.model_dump(exclude={"id", "in_context_message_ids", "messages"})
618
634
  if append_copy_suffix:
@@ -634,6 +650,10 @@ class AgentSerializationManager:
634
650
  for var in agent_data["tool_exec_environment_variables"]:
635
651
  var["value"] = env_vars.get(var["key"], "")
636
652
 
653
+ # Override project_id if provided
654
+ if project_id:
655
+ agent_data["project_id"] = project_id
656
+
637
657
  agent_create = CreateAgent(**agent_data)
638
658
  created_agent = await self.agent_manager.create_agent_async(agent_create, actor, _init_with_no_messages=True)
639
659
  file_to_db_ids[agent_schema.id] = created_agent.id
@@ -648,7 +668,7 @@ class AgentSerializationManager:
648
668
  messages = []
649
669
  for message_schema in agent_schema.messages:
650
670
  # Convert MessageSchema back to Message, setting agent_id to new DB ID
651
- message_data = message_schema.model_dump(exclude={"id"})
671
+ message_data = message_schema.model_dump(exclude={"id", "type"})
652
672
  message_data["agent_id"] = agent_db_id # Remap agent_id to new database ID
653
673
  message_obj = Message(**message_data)
654
674
  messages.append(message_obj)
@@ -2,13 +2,14 @@ from typing import List, Optional
2
2
 
3
3
  from sqlalchemy import select
4
4
 
5
+ from letta.helpers.tpuf_client import should_use_tpuf
5
6
  from letta.log import get_logger
6
- from letta.orm import ArchivalPassage
7
- from letta.orm import Archive as ArchiveModel
8
- from letta.orm import ArchivesAgents
7
+ from letta.orm import ArchivalPassage, Archive as ArchiveModel, ArchivesAgents
9
8
  from letta.schemas.archive import Archive as PydanticArchive
9
+ from letta.schemas.enums import VectorDBProvider
10
10
  from letta.schemas.user import User as PydanticUser
11
11
  from letta.server.db import db_registry
12
+ from letta.settings import settings
12
13
  from letta.utils import enforce_types
13
14
 
14
15
  logger = get_logger(__name__)
@@ -27,10 +28,14 @@ class ArchiveManager:
27
28
  """Create a new archive."""
28
29
  try:
29
30
  with db_registry.session() as session:
31
+ # determine vector db provider based on settings
32
+ vector_db_provider = VectorDBProvider.TPUF if should_use_tpuf() else VectorDBProvider.NATIVE
33
+
30
34
  archive = ArchiveModel(
31
35
  name=name,
32
36
  description=description,
33
37
  organization_id=actor.organization_id,
38
+ vector_db_provider=vector_db_provider,
34
39
  )
35
40
  archive.create(session, actor=actor)
36
41
  return archive.to_pydantic()
@@ -48,10 +53,14 @@ class ArchiveManager:
48
53
  """Create a new archive."""
49
54
  try:
50
55
  async with db_registry.async_session() as session:
56
+ # determine vector db provider based on settings
57
+ vector_db_provider = VectorDBProvider.TPUF if should_use_tpuf() else VectorDBProvider.NATIVE
58
+
51
59
  archive = ArchiveModel(
52
60
  name=name,
53
61
  description=description,
54
62
  organization_id=actor.organization_id,
63
+ vector_db_provider=vector_db_provider,
55
64
  )
56
65
  await archive.create_async(session, actor=actor)
57
66
  return archive.to_pydantic()
@@ -138,6 +147,37 @@ class ArchiveManager:
138
147
  session.add(archives_agents)
139
148
  await session.commit()
140
149
 
150
+ @enforce_types
151
+ async def get_default_archive_for_agent_async(
152
+ self,
153
+ agent_id: str,
154
+ actor: PydanticUser = None,
155
+ ) -> Optional[PydanticArchive]:
156
+ """Get the agent's default archive if it exists, return None otherwise."""
157
+ # First check if agent has any archives
158
+ from letta.services.agent_manager import AgentManager
159
+
160
+ agent_manager = AgentManager()
161
+
162
+ archive_ids = await agent_manager.get_agent_archive_ids_async(
163
+ agent_id=agent_id,
164
+ actor=actor,
165
+ )
166
+
167
+ if archive_ids:
168
+ # TODO: Remove this check once we support multiple archives per agent
169
+ if len(archive_ids) > 1:
170
+ raise ValueError(f"Agent {agent_id} has multiple archives, which is not yet supported")
171
+ # Get the archive
172
+ archive = await self.get_archive_by_id_async(
173
+ archive_id=archive_ids[0],
174
+ actor=actor,
175
+ )
176
+ return archive
177
+
178
+ # No archive found, return None
179
+ return None
180
+
141
181
  @enforce_types
142
182
  async def get_or_create_default_archive_for_agent_async(
143
183
  self,
@@ -267,3 +307,32 @@ class ArchiveManager:
267
307
 
268
308
  # For now, return the first agent (backwards compatibility)
269
309
  return agent_ids[0]
310
+
311
+ @enforce_types
312
+ async def get_or_set_vector_db_namespace_async(
313
+ self,
314
+ archive_id: str,
315
+ ) -> str:
316
+ """Get the vector database namespace for an archive, creating it if it doesn't exist."""
317
+ from sqlalchemy import update
318
+
319
+ async with db_registry.async_session() as session:
320
+ # check if namespace already exists
321
+ result = await session.execute(select(ArchiveModel._vector_db_namespace).where(ArchiveModel.id == archive_id))
322
+ row = result.fetchone()
323
+
324
+ if row and row[0]:
325
+ return row[0]
326
+
327
+ # generate namespace name using same logic as tpuf_client
328
+ environment = settings.environment
329
+ if environment:
330
+ namespace_name = f"archive_{archive_id}_{environment.lower()}"
331
+ else:
332
+ namespace_name = f"archive_{archive_id}"
333
+
334
+ # update the archive with the namespace
335
+ await session.execute(update(ArchiveModel).where(ArchiveModel.id == archive_id).values(_vector_db_namespace=namespace_name))
336
+ await session.commit()
337
+
338
+ return namespace_name
@@ -13,8 +13,7 @@ from letta.orm.blocks_agents import BlocksAgents
13
13
  from letta.orm.errors import NoResultFound
14
14
  from letta.otel.tracing import trace_method
15
15
  from letta.schemas.agent import AgentState as PydanticAgentState
16
- from letta.schemas.block import Block as PydanticBlock
17
- from letta.schemas.block import BlockUpdate
16
+ from letta.schemas.block import Block as PydanticBlock, BlockUpdate
18
17
  from letta.schemas.enums import ActorType
19
18
  from letta.schemas.user import User as PydanticUser
20
19
  from letta.server.db import db_registry
@@ -6,6 +6,7 @@ from typing import Any, Dict, List
6
6
  from letta.helpers.decorators import async_redis_cache
7
7
  from letta.llm_api.anthropic_client import AnthropicClient
8
8
  from letta.otel.tracing import trace_method
9
+ from letta.schemas.message import Message
9
10
  from letta.schemas.openai.chat_completion_request import Tool as OpenAITool
10
11
  from letta.utils import count_tokens
11
12
 
@@ -50,7 +51,8 @@ class AnthropicTokenCounter(TokenCounter):
50
51
 
51
52
  @trace_method
52
53
  @async_redis_cache(
53
- key_func=lambda self, messages: f"anthropic_message_tokens:{self.model}:{hashlib.sha256(json.dumps(messages, sort_keys=True).encode()).hexdigest()[:16]}",
54
+ key_func=lambda self,
55
+ messages: f"anthropic_message_tokens:{self.model}:{hashlib.sha256(json.dumps(messages, sort_keys=True).encode()).hexdigest()[:16]}",
54
56
  prefix="token_counter",
55
57
  ttl_s=3600, # cache for 1 hour
56
58
  )
@@ -61,7 +63,8 @@ class AnthropicTokenCounter(TokenCounter):
61
63
 
62
64
  @trace_method
63
65
  @async_redis_cache(
64
- key_func=lambda self, tools: f"anthropic_tool_tokens:{self.model}:{hashlib.sha256(json.dumps([t.model_dump() for t in tools], sort_keys=True).encode()).hexdigest()[:16]}",
66
+ key_func=lambda self,
67
+ tools: f"anthropic_tool_tokens:{self.model}:{hashlib.sha256(json.dumps([t.model_dump() for t in tools], sort_keys=True).encode()).hexdigest()[:16]}",
65
68
  prefix="token_counter",
66
69
  ttl_s=3600, # cache for 1 hour
67
70
  )
@@ -71,7 +74,7 @@ class AnthropicTokenCounter(TokenCounter):
71
74
  return await self.client.count_tokens(model=self.model, tools=tools)
72
75
 
73
76
  def convert_messages(self, messages: List[Any]) -> List[Dict[str, Any]]:
74
- return [m.to_anthropic_dict() for m in messages]
77
+ return Message.to_anthropic_dicts_from_list(messages)
75
78
 
76
79
 
77
80
  class TiktokenCounter(TokenCounter):
@@ -93,7 +96,8 @@ class TiktokenCounter(TokenCounter):
93
96
 
94
97
  @trace_method
95
98
  @async_redis_cache(
96
- key_func=lambda self, messages: f"tiktoken_message_tokens:{self.model}:{hashlib.sha256(json.dumps(messages, sort_keys=True).encode()).hexdigest()[:16]}",
99
+ key_func=lambda self,
100
+ messages: f"tiktoken_message_tokens:{self.model}:{hashlib.sha256(json.dumps(messages, sort_keys=True).encode()).hexdigest()[:16]}",
97
101
  prefix="token_counter",
98
102
  ttl_s=3600, # cache for 1 hour
99
103
  )
@@ -106,7 +110,8 @@ class TiktokenCounter(TokenCounter):
106
110
 
107
111
  @trace_method
108
112
  @async_redis_cache(
109
- key_func=lambda self, tools: f"tiktoken_tool_tokens:{self.model}:{hashlib.sha256(json.dumps([t.model_dump() for t in tools], sort_keys=True).encode()).hexdigest()[:16]}",
113
+ key_func=lambda self,
114
+ tools: f"tiktoken_tool_tokens:{self.model}:{hashlib.sha256(json.dumps([t.model_dump() for t in tools], sort_keys=True).encode()).hexdigest()[:16]}",
110
115
  prefix="token_counter",
111
116
  ttl_s=3600, # cache for 1 hour
112
117
  )
@@ -120,4 +125,4 @@ class TiktokenCounter(TokenCounter):
120
125
  return num_tokens_from_functions(functions=functions, model=self.model)
121
126
 
122
127
  def convert_messages(self, messages: List[Any]) -> List[Dict[str, Any]]:
123
- return [m.to_openai_dict() for m in messages]
128
+ return Message.to_openai_dicts_from_list(messages)
@@ -12,8 +12,7 @@ from letta.constants import MAX_FILENAME_LENGTH
12
12
  from letta.helpers.pinecone_utils import list_pinecone_index_for_files, should_use_pinecone
13
13
  from letta.log import get_logger
14
14
  from letta.orm.errors import NoResultFound
15
- from letta.orm.file import FileContent as FileContentModel
16
- from letta.orm.file import FileMetadata as FileMetadataModel
15
+ from letta.orm.file import FileContent as FileContentModel, FileMetadata as FileMetadataModel
17
16
  from letta.orm.sqlalchemy_base import AccessType
18
17
  from letta.otel.tracing import trace_method
19
18
  from letta.schemas.enums import FileProcessingStatus
@@ -60,7 +59,6 @@ class FileManager:
60
59
  *,
61
60
  text: Optional[str] = None,
62
61
  ) -> PydanticFileMetadata:
63
-
64
62
  # short-circuit if it already exists
65
63
  existing = await self.get_file_by_id(file_metadata.id, actor=actor)
66
64
  if existing:
@@ -7,10 +7,8 @@ from letta.log import get_logger
7
7
  from letta.orm.errors import NoResultFound
8
8
  from letta.orm.files_agents import FileAgent as FileAgentModel
9
9
  from letta.otel.tracing import trace_method
10
- from letta.schemas.block import Block as PydanticBlock
11
- from letta.schemas.block import FileBlock as PydanticFileBlock
12
- from letta.schemas.file import FileAgent as PydanticFileAgent
13
- from letta.schemas.file import FileMetadata
10
+ from letta.schemas.block import Block as PydanticBlock, FileBlock as PydanticFileBlock
11
+ from letta.schemas.file import FileAgent as PydanticFileAgent, FileMetadata
14
12
  from letta.schemas.user import User as PydanticUser
15
13
  from letta.server.db import db_registry
16
14
  from letta.utils import enforce_types
@@ -1,6 +1,6 @@
1
- from typing import List, Optional
1
+ from typing import List, Optional, Union
2
2
 
3
- from sqlalchemy import select
3
+ from sqlalchemy import delete, select
4
4
  from sqlalchemy.orm import Session
5
5
 
6
6
  from letta.orm.agent import Agent as AgentModel
@@ -8,8 +8,7 @@ from letta.orm.errors import NoResultFound
8
8
  from letta.orm.group import Group as GroupModel
9
9
  from letta.orm.message import Message as MessageModel
10
10
  from letta.otel.tracing import trace_method
11
- from letta.schemas.group import Group as PydanticGroup
12
- from letta.schemas.group import GroupCreate, GroupUpdate, ManagerType
11
+ from letta.schemas.group import Group as PydanticGroup, GroupCreate, GroupUpdate, InternalTemplateGroupCreate, ManagerType
13
12
  from letta.schemas.letta_message import LettaMessage
14
13
  from letta.schemas.message import Message as PydanticMessage
15
14
  from letta.schemas.user import User as PydanticUser
@@ -20,7 +19,7 @@ from letta.utils import enforce_types
20
19
  class GroupManager:
21
20
  @enforce_types
22
21
  @trace_method
23
- def list_groups(
22
+ async def list_groups_async(
24
23
  self,
25
24
  actor: PydanticUser,
26
25
  project_id: Optional[str] = None,
@@ -29,13 +28,13 @@ class GroupManager:
29
28
  after: Optional[str] = None,
30
29
  limit: Optional[int] = 50,
31
30
  ) -> list[PydanticGroup]:
32
- with db_registry.session() as session:
31
+ async with db_registry.async_session() as session:
33
32
  filters = {"organization_id": actor.organization_id}
34
33
  if project_id:
35
34
  filters["project_id"] = project_id
36
35
  if manager_type:
37
36
  filters["manager_type"] = manager_type
38
- groups = GroupModel.list(
37
+ groups = await GroupModel.list_async(
39
38
  db_session=session,
40
39
  before=before,
41
40
  after=after,
@@ -60,7 +59,7 @@ class GroupManager:
60
59
 
61
60
  @enforce_types
62
61
  @trace_method
63
- def create_group(self, group: GroupCreate, actor: PydanticUser) -> PydanticGroup:
62
+ def create_group(self, group: Union[GroupCreate, InternalTemplateGroupCreate], actor: PydanticUser) -> PydanticGroup:
64
63
  with db_registry.session() as session:
65
64
  new_group = GroupModel()
66
65
  new_group.organization_id = actor.organization_id
@@ -96,6 +95,11 @@ class GroupManager:
96
95
  case _:
97
96
  raise ValueError(f"Unsupported manager type: {group.manager_config.manager_type}")
98
97
 
98
+ if isinstance(group, InternalTemplateGroupCreate):
99
+ new_group.base_template_id = group.base_template_id
100
+ new_group.template_id = group.template_id
101
+ new_group.deployment_id = group.deployment_id
102
+
99
103
  self._process_agent_relationship(session=session, group=new_group, agent_ids=group.agent_ids, allow_partial=False)
100
104
 
101
105
  if group.shared_block_ids:
@@ -105,7 +109,7 @@ class GroupManager:
105
109
  return new_group.to_pydantic()
106
110
 
107
111
  @enforce_types
108
- async def create_group_async(self, group: GroupCreate, actor: PydanticUser) -> PydanticGroup:
112
+ async def create_group_async(self, group: Union[GroupCreate, InternalTemplateGroupCreate], actor: PydanticUser) -> PydanticGroup:
109
113
  async with db_registry.async_session() as session:
110
114
  new_group = GroupModel()
111
115
  new_group.organization_id = actor.organization_id
@@ -141,6 +145,11 @@ class GroupManager:
141
145
  case _:
142
146
  raise ValueError(f"Unsupported manager type: {group.manager_config.manager_type}")
143
147
 
148
+ if isinstance(group, InternalTemplateGroupCreate):
149
+ new_group.base_template_id = group.base_template_id
150
+ new_group.template_id = group.template_id
151
+ new_group.deployment_id = group.deployment_id
152
+
144
153
  await self._process_agent_relationship_async(session=session, group=new_group, agent_ids=group.agent_ids, allow_partial=False)
145
154
 
146
155
  if group.shared_block_ids:
@@ -264,6 +273,43 @@ class GroupManager:
264
273
 
265
274
  return messages
266
275
 
276
+ @enforce_types
277
+ @trace_method
278
+ async def list_group_messages_async(
279
+ self,
280
+ actor: PydanticUser,
281
+ group_id: Optional[str] = None,
282
+ before: Optional[str] = None,
283
+ after: Optional[str] = None,
284
+ limit: Optional[int] = 50,
285
+ use_assistant_message: bool = True,
286
+ assistant_message_tool_name: str = "send_message",
287
+ assistant_message_tool_kwarg: str = "message",
288
+ ) -> list[LettaMessage]:
289
+ async with db_registry.async_session() as session:
290
+ filters = {
291
+ "organization_id": actor.organization_id,
292
+ "group_id": group_id,
293
+ }
294
+ messages = await MessageModel.list_async(
295
+ db_session=session,
296
+ before=before,
297
+ after=after,
298
+ limit=limit,
299
+ **filters,
300
+ )
301
+
302
+ messages = PydanticMessage.to_letta_messages_from_list(
303
+ messages=[msg.to_pydantic() for msg in messages],
304
+ use_assistant_message=use_assistant_message,
305
+ assistant_message_tool_name=assistant_message_tool_name,
306
+ assistant_message_tool_kwarg=assistant_message_tool_kwarg,
307
+ )
308
+
309
+ # TODO: filter messages to return a clean conversation history
310
+
311
+ return messages
312
+
267
313
  @enforce_types
268
314
  @trace_method
269
315
  def reset_messages(self, group_id: str, actor: PydanticUser) -> None:
@@ -278,6 +324,21 @@ class GroupManager:
278
324
 
279
325
  session.commit()
280
326
 
327
+ @enforce_types
328
+ @trace_method
329
+ async def reset_messages_async(self, group_id: str, actor: PydanticUser) -> None:
330
+ async with db_registry.async_session() as session:
331
+ # Ensure group is loadable by user
332
+ group = await GroupModel.read_async(db_session=session, identifier=group_id, actor=actor)
333
+
334
+ # Delete all messages in the group
335
+ delete_stmt = delete(MessageModel).where(
336
+ MessageModel.organization_id == actor.organization_id, MessageModel.group_id == group_id
337
+ )
338
+ await session.execute(delete_stmt)
339
+
340
+ await session.commit()
341
+
281
342
  @enforce_types
282
343
  @trace_method
283
344
  def bump_turns_counter(self, group_id: str, actor: PydanticUser) -> int:
@@ -332,15 +393,15 @@ class GroupManager:
332
393
  return prev_last_processed_message_id
333
394
 
334
395
  @enforce_types
335
- def size(
396
+ async def size(
336
397
  self,
337
398
  actor: PydanticUser,
338
399
  ) -> int:
339
400
  """
340
401
  Get the total count of groups for the given user.
341
402
  """
342
- with db_registry.session() as session:
343
- return GroupModel.size(db_session=session, actor=actor)
403
+ async with db_registry.async_session() as session:
404
+ return await GroupModel.size_async(db_session=session, actor=actor)
344
405
 
345
406
  def _process_agent_relationship(self, session: Session, group: GroupModel, agent_ids: List[str], allow_partial=False, replace=True):
346
407
  if not agent_ids:
@@ -1,4 +1,3 @@
1
- import os
2
1
  import uuid
3
2
  from datetime import datetime
4
3
  from typing import List, Literal, Optional, Set
@@ -465,7 +464,6 @@ def package_initial_message_sequence(
465
464
  # create the agent object
466
465
  init_messages = []
467
466
  for message_create in initial_message_sequence:
468
-
469
467
  if message_create.role == MessageRole.user:
470
468
  packed_message = system.package_user_message(
471
469
  user_message=message_create.content,
@@ -499,8 +497,10 @@ def package_initial_message_sequence(
499
497
  import json
500
498
  import uuid
501
499
 
502
- from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall
503
- from openai.types.chat.chat_completion_message_tool_call import Function as OpenAIFunction
500
+ from openai.types.chat.chat_completion_message_tool_call import (
501
+ ChatCompletionMessageToolCall as OpenAIToolCall,
502
+ Function as OpenAIFunction,
503
+ )
504
504
 
505
505
  from letta.constants import DEFAULT_MESSAGE_TOOL
506
506
 
@@ -1208,7 +1208,7 @@ def calculate_base_tools(is_v2: bool) -> Set[str]:
1208
1208
 
1209
1209
  def calculate_multi_agent_tools() -> Set[str]:
1210
1210
  """Calculate multi-agent tools, excluding local-only tools in production environment."""
1211
- if os.getenv("LETTA_ENVIRONMENT") == "PRODUCTION":
1211
+ if settings.environment == "PRODUCTION":
1212
1212
  return set(MULTI_AGENT_TOOLS) - set(LOCAL_ONLY_MULTI_AGENT_TOOLS)
1213
1213
  else:
1214
1214
  return set(MULTI_AGENT_TOOLS)
@@ -9,8 +9,14 @@ from letta.orm.block import Block as BlockModel
9
9
  from letta.orm.errors import UniqueConstraintViolationError
10
10
  from letta.orm.identity import Identity as IdentityModel
11
11
  from letta.otel.tracing import trace_method
12
- from letta.schemas.identity import Identity as PydanticIdentity
13
- from letta.schemas.identity import IdentityCreate, IdentityProperty, IdentityType, IdentityUpdate, IdentityUpsert
12
+ from letta.schemas.identity import (
13
+ Identity as PydanticIdentity,
14
+ IdentityCreate,
15
+ IdentityProperty,
16
+ IdentityType,
17
+ IdentityUpdate,
18
+ IdentityUpsert,
19
+ )
14
20
  from letta.schemas.user import User as PydanticUser
15
21
  from letta.server.db import db_registry
16
22
  from letta.settings import DatabaseChoice, settings
@@ -18,7 +24,6 @@ from letta.utils import enforce_types
18
24
 
19
25
 
20
26
  class IdentityManager:
21
-
22
27
  @enforce_types
23
28
  @trace_method
24
29
  async def list_identities_async(
@@ -1,4 +1,3 @@
1
- from datetime import datetime
2
1
  from functools import partial, reduce
3
2
  from operator import add
4
3
  from typing import List, Literal, Optional, Union
@@ -14,13 +13,10 @@ from letta.orm.job import Job as JobModel
14
13
  from letta.orm.job_messages import JobMessage
15
14
  from letta.orm.message import Message as MessageModel
16
15
  from letta.orm.sqlalchemy_base import AccessType
17
- from letta.orm.step import Step
18
- from letta.orm.step import Step as StepModel
16
+ from letta.orm.step import Step, Step as StepModel
19
17
  from letta.otel.tracing import log_event, trace_method
20
18
  from letta.schemas.enums import JobStatus, JobType, MessageRole
21
- from letta.schemas.job import BatchJob as PydanticBatchJob
22
- from letta.schemas.job import Job as PydanticJob
23
- from letta.schemas.job import JobUpdate, LettaRequestConfig
19
+ from letta.schemas.job import BatchJob as PydanticBatchJob, Job as PydanticJob, JobUpdate, LettaRequestConfig
24
20
  from letta.schemas.letta_message import LettaMessage
25
21
  from letta.schemas.message import Message as PydanticMessage
26
22
  from letta.schemas.run import Run as PydanticRun
@@ -28,7 +24,6 @@ from letta.schemas.step import Step as PydanticStep
28
24
  from letta.schemas.usage import LettaUsageStatistics
29
25
  from letta.schemas.user import User as PydanticUser
30
26
  from letta.server.db import db_registry
31
- from letta.settings import DatabaseChoice, settings
32
27
  from letta.utils import enforce_types
33
28
 
34
29
  logger = get_logger(__name__)
@@ -337,11 +332,7 @@ class JobManager:
337
332
  conditions = []
338
333
  if before_obj:
339
334
  # records before this cursor (older)
340
-
341
335
  before_timestamp = before_obj.created_at
342
- # SQLite does not support as granular timestamping, so we need to round the timestamp
343
- if settings.database_engine is DatabaseChoice.SQLITE and isinstance(before_timestamp, datetime):
344
- before_timestamp = before_timestamp.strftime("%Y-%m-%d %H:%M:%S")
345
336
 
346
337
  conditions.append(
347
338
  or_(
@@ -353,9 +344,6 @@ class JobManager:
353
344
  if after_obj:
354
345
  # records after this cursor (newer)
355
346
  after_timestamp = after_obj.created_at
356
- # SQLite does not support as granular timestamping, so we need to round the timestamp
357
- if settings.database_engine is DatabaseChoice.SQLITE and isinstance(after_timestamp, datetime):
358
- after_timestamp = after_timestamp.strftime("%Y-%m-%d %H:%M:%S")
359
347
 
360
348
  conditions.append(
361
349
  or_(JobModel.created_at > after_timestamp, and_(JobModel.created_at == after_timestamp, JobModel.id > after_obj.id))
@@ -11,9 +11,7 @@ from letta.orm.llm_batch_items import LLMBatchItem
11
11
  from letta.orm.llm_batch_job import LLMBatchJob
12
12
  from letta.otel.tracing import trace_method
13
13
  from letta.schemas.enums import AgentStepStatus, JobStatus, ProviderType
14
- from letta.schemas.llm_batch_job import AgentStepState
15
- from letta.schemas.llm_batch_job import LLMBatchItem as PydanticLLMBatchItem
16
- from letta.schemas.llm_batch_job import LLMBatchJob as PydanticLLMBatchJob
14
+ from letta.schemas.llm_batch_job import AgentStepState, LLMBatchItem as PydanticLLMBatchItem, LLMBatchJob as PydanticLLMBatchJob
17
15
  from letta.schemas.llm_config import LLMConfig
18
16
  from letta.schemas.message import Message as PydanticMessage
19
17
  from letta.schemas.user import User as PydanticUser
@@ -1,8 +1,7 @@
1
1
  from contextlib import AsyncExitStack
2
2
  from typing import Optional, Tuple
3
3
 
4
- from mcp import ClientSession
5
- from mcp import Tool as MCPTool
4
+ from mcp import ClientSession, Tool as MCPTool
6
5
  from mcp.client.auth import OAuthClientProvider
7
6
  from mcp.types import TextContent
8
7
 
@@ -33,8 +33,7 @@ from letta.schemas.mcp import (
33
33
  UpdateStdioMCPServer,
34
34
  UpdateStreamableHTTPMCPServer,
35
35
  )
36
- from letta.schemas.tool import Tool as PydanticTool
37
- from letta.schemas.tool import ToolCreate
36
+ from letta.schemas.tool import Tool as PydanticTool, ToolCreate
38
37
  from letta.schemas.user import User as PydanticUser
39
38
  from letta.server.db import db_registry
40
39
  from letta.services.mcp.sse_client import MCP_CONFIG_TOPLEVEL_KEY, AsyncSSEMCPClient
@@ -137,8 +136,7 @@ class MCPManager:
137
136
  if mcp_tool.health:
138
137
  if mcp_tool.health.status == "INVALID":
139
138
  raise ValueError(
140
- f"Tool {mcp_tool_name} cannot be attached, JSON schema is invalid."
141
- f"Reasons: {', '.join(mcp_tool.health.reasons)}"
139
+ f"Tool {mcp_tool_name} cannot be attached, JSON schema is invalid.Reasons: {', '.join(mcp_tool.health.reasons)}"
142
140
  )
143
141
 
144
142
  tool_create = ToolCreate.from_mcp(mcp_server_name=mcp_server_name, mcp_tool=mcp_tool)
@@ -305,7 +303,9 @@ class MCPManager:
305
303
 
306
304
  async with db_registry.async_session() as session:
307
305
  mcp_servers = await MCPServerModel.list_async(
308
- db_session=session, organization_id=actor.organization_id, id=mcp_server_ids # This will use the IN operator
306
+ db_session=session,
307
+ organization_id=actor.organization_id,
308
+ id=mcp_server_ids, # This will use the IN operator
309
309
  )
310
310
  return [mcp_server.to_pydantic() for mcp_server in mcp_servers]
311
311
 
@@ -407,7 +407,6 @@ class MCPManager:
407
407
  # with the value being the schema from StdioServerParameters
408
408
  if MCP_CONFIG_TOPLEVEL_KEY in mcp_config:
409
409
  for server_name, server_params_raw in mcp_config[MCP_CONFIG_TOPLEVEL_KEY].items():
410
-
411
410
  # No support for duplicate server names
412
411
  if server_name in mcp_server_list:
413
412
  # Duplicate server names are configuration issues, not system errors