letta-nightly 0.7.22.dev20250523081403__py3-none-any.whl → 0.7.23.dev20250523164139__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 CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.7.22"
1
+ __version__ = "0.7.23"
2
2
 
3
3
  # import clients
4
4
  from letta.client.client import RESTClient
@@ -7,7 +7,10 @@ from letta.errors import ErrorCode, LLMAuthenticationError, LLMError
7
7
  from letta.llm_api.google_constants import GOOGLE_MODEL_FOR_API_KEY_CHECK
8
8
  from letta.llm_api.google_vertex_client import GoogleVertexClient
9
9
  from letta.log import get_logger
10
+ from letta.schemas.llm_config import LLMConfig
11
+ from letta.schemas.message import Message as PydanticMessage
10
12
  from letta.settings import model_settings
13
+ from letta.tracing import trace_method
11
14
 
12
15
  logger = get_logger(__name__)
13
16
 
@@ -17,6 +20,18 @@ class GoogleAIClient(GoogleVertexClient):
17
20
  def _get_client(self):
18
21
  return genai.Client(api_key=model_settings.gemini_api_key)
19
22
 
23
+ @trace_method
24
+ def build_request_data(
25
+ self,
26
+ messages: List[PydanticMessage],
27
+ llm_config: LLMConfig,
28
+ tools: List[dict],
29
+ force_tool_call: Optional[str] = None,
30
+ ) -> dict:
31
+ request = super().build_request_data(messages, llm_config, tools, force_tool_call)
32
+ del request["config"]["thinking_config"]
33
+ return request
34
+
20
35
 
21
36
  def get_gemini_endpoint_and_headers(
22
37
  base_url: str, model: Optional[str], api_key: str, key_in_header: bool = True, generate_content: bool = False
@@ -244,11 +244,10 @@ class GoogleVertexClient(LLMClientBase):
244
244
  # Add thinking_config
245
245
  # If enable_reasoner is False, set thinking_budget to 0
246
246
  # Otherwise, use the value from max_reasoning_tokens
247
- if llm_config.enable_reasoner:
248
- thinking_config = ThinkingConfig(
249
- thinking_budget=llm_config.max_reasoning_tokens,
250
- )
251
- request_data["config"]["thinking_config"] = thinking_config.model_dump()
247
+ thinking_config = ThinkingConfig(
248
+ thinking_budget=llm_config.max_reasoning_tokens if llm_config.enable_reasoner else 0,
249
+ )
250
+ request_data["config"]["thinking_config"] = thinking_config.model_dump()
252
251
 
253
252
  return request_data
254
253
 
@@ -54,7 +54,7 @@ class Provider(ProviderBase):
54
54
  return []
55
55
 
56
56
  async def list_embedding_models_async(self) -> List[EmbeddingConfig]:
57
- return []
57
+ return self.list_embedding_models()
58
58
 
59
59
  def get_model_context_window(self, model_name: str) -> Optional[int]:
60
60
  raise NotImplementedError
letta/server/db.py CHANGED
@@ -17,8 +17,6 @@ from letta.tracing import trace_method
17
17
 
18
18
  logger = get_logger(__name__)
19
19
 
20
- logger = get_logger(__name__)
21
-
22
20
 
23
21
  def print_sqlite_schema_error():
24
22
  """Print a formatted error message for SQLite schema issues"""
@@ -50,47 +50,46 @@ def count_blocks(
50
50
 
51
51
 
52
52
  @router.post("/", response_model=Block, operation_id="create_block")
53
- def create_block(
53
+ async def create_block(
54
54
  create_block: CreateBlock = Body(...),
55
55
  server: SyncServer = Depends(get_letta_server),
56
56
  actor_id: Optional[str] = Header(None, alias="user_id"),
57
57
  ):
58
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
58
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
59
59
  block = Block(**create_block.model_dump())
60
- return server.block_manager.create_or_update_block(actor=actor, block=block)
60
+ return await server.block_manager.create_or_update_block_async(actor=actor, block=block)
61
61
 
62
62
 
63
63
  @router.patch("/{block_id}", response_model=Block, operation_id="modify_block")
64
- def modify_block(
64
+ async def modify_block(
65
65
  block_id: str,
66
66
  block_update: BlockUpdate = Body(...),
67
67
  server: SyncServer = Depends(get_letta_server),
68
68
  actor_id: Optional[str] = Header(None, alias="user_id"),
69
69
  ):
70
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
71
- return server.block_manager.update_block(block_id=block_id, block_update=block_update, actor=actor)
70
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
71
+ return await server.block_manager.update_block_async(block_id=block_id, block_update=block_update, actor=actor)
72
72
 
73
73
 
74
74
  @router.delete("/{block_id}", response_model=Block, operation_id="delete_block")
75
- def delete_block(
75
+ async def delete_block(
76
76
  block_id: str,
77
77
  server: SyncServer = Depends(get_letta_server),
78
78
  actor_id: Optional[str] = Header(None, alias="user_id"),
79
79
  ):
80
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
81
- return server.block_manager.delete_block(block_id=block_id, actor=actor)
80
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
81
+ return await server.block_manager.delete_block_async(block_id=block_id, actor=actor)
82
82
 
83
83
 
84
84
  @router.get("/{block_id}", response_model=Block, operation_id="retrieve_block")
85
- def retrieve_block(
85
+ async def retrieve_block(
86
86
  block_id: str,
87
87
  server: SyncServer = Depends(get_letta_server),
88
88
  actor_id: Optional[str] = Header(None, alias="user_id"),
89
89
  ):
90
- print("call get block", block_id)
91
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
90
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
92
91
  try:
93
- block = server.block_manager.get_block_by_id(block_id=block_id, actor=actor)
92
+ block = await server.block_manager.get_block_by_id_async(block_id=block_id, actor=actor)
94
93
  if block is None:
95
94
  raise HTTPException(status_code=404, detail="Block not found")
96
95
  return block
@@ -78,17 +78,6 @@ async def list_sources(
78
78
  return await server.source_manager.list_sources(actor=actor)
79
79
 
80
80
 
81
- @router.get("/count", response_model=int, operation_id="count_sources")
82
- def count_sources(
83
- server: "SyncServer" = Depends(get_letta_server),
84
- actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
85
- ):
86
- """
87
- Count all data sources created by a user.
88
- """
89
- return server.source_manager.size(actor=server.user_manager.get_user_or_default(user_id=actor_id))
90
-
91
-
92
81
  @router.post("/", response_model=Source, operation_id="create_source")
93
82
  async def create_source(
94
83
  source_create: SourceCreate,
letta/server/server.py CHANGED
@@ -741,6 +741,13 @@ class SyncServer(Server):
741
741
  self._llm_config_cache[key] = self.get_llm_config_from_handle(actor=actor, **kwargs)
742
742
  return self._llm_config_cache[key]
743
743
 
744
+ @trace_method
745
+ async def get_cached_llm_config_async(self, actor: User, **kwargs):
746
+ key = make_key(**kwargs)
747
+ if key not in self._llm_config_cache:
748
+ self._llm_config_cache[key] = await self.get_llm_config_from_handle_async(actor=actor, **kwargs)
749
+ return self._llm_config_cache[key]
750
+
744
751
  @trace_method
745
752
  def get_cached_embedding_config(self, actor: User, **kwargs):
746
753
  key = make_key(**kwargs)
@@ -748,6 +755,13 @@ class SyncServer(Server):
748
755
  self._embedding_config_cache[key] = self.get_embedding_config_from_handle(actor=actor, **kwargs)
749
756
  return self._embedding_config_cache[key]
750
757
 
758
+ @trace_method
759
+ async def get_cached_embedding_config_async(self, actor: User, **kwargs):
760
+ key = make_key(**kwargs)
761
+ if key not in self._embedding_config_cache:
762
+ self._embedding_config_cache[key] = await self.get_embedding_config_from_handle_async(actor=actor, **kwargs)
763
+ return self._embedding_config_cache[key]
764
+
751
765
  @trace_method
752
766
  def create_agent(
753
767
  self,
@@ -815,7 +829,7 @@ class SyncServer(Server):
815
829
  "enable_reasoner": request.enable_reasoner,
816
830
  }
817
831
  log_event(name="start get_cached_llm_config", attributes=config_params)
818
- request.llm_config = self.get_cached_llm_config(actor=actor, **config_params)
832
+ request.llm_config = await self.get_cached_llm_config_async(actor=actor, **config_params)
819
833
  log_event(name="end get_cached_llm_config", attributes=config_params)
820
834
 
821
835
  if request.embedding_config is None:
@@ -826,7 +840,7 @@ class SyncServer(Server):
826
840
  "embedding_chunk_size": request.embedding_chunk_size or constants.DEFAULT_EMBEDDING_CHUNK_SIZE,
827
841
  }
828
842
  log_event(name="start get_cached_embedding_config", attributes=embedding_config_params)
829
- request.embedding_config = self.get_cached_embedding_config(actor=actor, **embedding_config_params)
843
+ request.embedding_config = await self.get_cached_embedding_config_async(actor=actor, **embedding_config_params)
830
844
  log_event(name="end get_cached_embedding_config", attributes=embedding_config_params)
831
845
 
832
846
  log_event(name="start create_agent db")
@@ -877,10 +891,10 @@ class SyncServer(Server):
877
891
  actor: User,
878
892
  ) -> AgentState:
879
893
  if request.model is not None:
880
- request.llm_config = self.get_llm_config_from_handle(handle=request.model, actor=actor)
894
+ request.llm_config = await self.get_llm_config_from_handle_async(handle=request.model, actor=actor)
881
895
 
882
896
  if request.embedding is not None:
883
- request.embedding_config = self.get_embedding_config_from_handle(handle=request.embedding, actor=actor)
897
+ request.embedding_config = await self.get_embedding_config_from_handle_async(handle=request.embedding, actor=actor)
884
898
 
885
899
  if request.enable_sleeptime:
886
900
  agent = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor)
@@ -1568,6 +1582,61 @@ class SyncServer(Server):
1568
1582
 
1569
1583
  return llm_config
1570
1584
 
1585
+ @trace_method
1586
+ async def get_llm_config_from_handle_async(
1587
+ self,
1588
+ actor: User,
1589
+ handle: str,
1590
+ context_window_limit: Optional[int] = None,
1591
+ max_tokens: Optional[int] = None,
1592
+ max_reasoning_tokens: Optional[int] = None,
1593
+ enable_reasoner: Optional[bool] = None,
1594
+ ) -> LLMConfig:
1595
+ try:
1596
+ provider_name, model_name = handle.split("/", 1)
1597
+ provider = await self.get_provider_from_name_async(provider_name, actor)
1598
+
1599
+ all_llm_configs = await provider.list_llm_models_async()
1600
+ llm_configs = [config for config in all_llm_configs if config.handle == handle]
1601
+ if not llm_configs:
1602
+ llm_configs = [config for config in all_llm_configs if config.model == model_name]
1603
+ if not llm_configs:
1604
+ available_handles = [config.handle for config in all_llm_configs]
1605
+ raise HandleNotFoundError(handle, available_handles)
1606
+ except ValueError as e:
1607
+ llm_configs = [config for config in self.get_local_llm_configs() if config.handle == handle]
1608
+ if not llm_configs:
1609
+ llm_configs = [config for config in self.get_local_llm_configs() if config.model == model_name]
1610
+ if not llm_configs:
1611
+ raise e
1612
+
1613
+ if len(llm_configs) == 1:
1614
+ llm_config = llm_configs[0]
1615
+ elif len(llm_configs) > 1:
1616
+ raise ValueError(f"Multiple LLM models with name {model_name} supported by {provider_name}")
1617
+ else:
1618
+ llm_config = llm_configs[0]
1619
+
1620
+ if context_window_limit is not None:
1621
+ if context_window_limit > llm_config.context_window:
1622
+ raise ValueError(f"Context window limit ({context_window_limit}) is greater than maximum of ({llm_config.context_window})")
1623
+ llm_config.context_window = context_window_limit
1624
+ else:
1625
+ llm_config.context_window = min(llm_config.context_window, model_settings.global_max_context_window_limit)
1626
+
1627
+ if max_tokens is not None:
1628
+ llm_config.max_tokens = max_tokens
1629
+ if max_reasoning_tokens is not None:
1630
+ if not max_tokens or max_reasoning_tokens > max_tokens:
1631
+ raise ValueError(f"Max reasoning tokens ({max_reasoning_tokens}) must be less than max tokens ({max_tokens})")
1632
+ llm_config.max_reasoning_tokens = max_reasoning_tokens
1633
+ if enable_reasoner is not None:
1634
+ llm_config.enable_reasoner = enable_reasoner
1635
+ if enable_reasoner and llm_config.model_endpoint_type == "anthropic":
1636
+ llm_config.put_inner_thoughts_in_kwargs = False
1637
+
1638
+ return llm_config
1639
+
1571
1640
  @trace_method
1572
1641
  def get_embedding_config_from_handle(
1573
1642
  self, actor: User, handle: str, embedding_chunk_size: int = constants.DEFAULT_EMBEDDING_CHUNK_SIZE
@@ -1597,6 +1666,36 @@ class SyncServer(Server):
1597
1666
 
1598
1667
  return embedding_config
1599
1668
 
1669
+ @trace_method
1670
+ async def get_embedding_config_from_handle_async(
1671
+ self, actor: User, handle: str, embedding_chunk_size: int = constants.DEFAULT_EMBEDDING_CHUNK_SIZE
1672
+ ) -> EmbeddingConfig:
1673
+ try:
1674
+ provider_name, model_name = handle.split("/", 1)
1675
+ provider = await self.get_provider_from_name_async(provider_name, actor)
1676
+
1677
+ all_embedding_configs = await provider.list_embedding_models_async()
1678
+ embedding_configs = [config for config in all_embedding_configs if config.handle == handle]
1679
+ if not embedding_configs:
1680
+ raise ValueError(f"Embedding model {model_name} is not supported by {provider_name}")
1681
+ except ValueError as e:
1682
+ # search local configs
1683
+ embedding_configs = [config for config in self.get_local_embedding_configs() if config.handle == handle]
1684
+ if not embedding_configs:
1685
+ raise e
1686
+
1687
+ if len(embedding_configs) == 1:
1688
+ embedding_config = embedding_configs[0]
1689
+ elif len(embedding_configs) > 1:
1690
+ raise ValueError(f"Multiple embedding models with name {model_name} supported by {provider_name}")
1691
+ else:
1692
+ embedding_config = embedding_configs[0]
1693
+
1694
+ if embedding_chunk_size:
1695
+ embedding_config.embedding_chunk_size = embedding_chunk_size
1696
+
1697
+ return embedding_config
1698
+
1600
1699
  def get_provider_from_name(self, provider_name: str, actor: User) -> Provider:
1601
1700
  providers = [provider for provider in self.get_enabled_providers(actor) if provider.name == provider_name]
1602
1701
  if not providers:
@@ -1608,6 +1707,18 @@ class SyncServer(Server):
1608
1707
 
1609
1708
  return provider
1610
1709
 
1710
+ async def get_provider_from_name_async(self, provider_name: str, actor: User) -> Provider:
1711
+ all_providers = await self.get_enabled_providers_async(actor)
1712
+ providers = [provider for provider in all_providers if provider.name == provider_name]
1713
+ if not providers:
1714
+ raise ValueError(f"Provider {provider_name} is not supported")
1715
+ elif len(providers) > 1:
1716
+ raise ValueError(f"Multiple providers with name {provider_name} supported")
1717
+ else:
1718
+ provider = providers[0]
1719
+
1720
+ return provider
1721
+
1611
1722
  def get_local_llm_configs(self):
1612
1723
  llm_models = []
1613
1724
  try:
@@ -1023,34 +1023,6 @@ class AgentManager:
1023
1023
  return await asyncio.gather(*[agent.to_pydantic_async(include_relationships=include_relationships) for agent in agents])
1024
1024
 
1025
1025
  @trace_method
1026
- @enforce_types
1027
- async def get_agent_by_id_async(
1028
- self,
1029
- agent_id: str,
1030
- actor: PydanticUser,
1031
- include_relationships: Optional[List[str]] = None,
1032
- ) -> PydanticAgentState:
1033
- """Fetch an agent by its ID."""
1034
- async with db_registry.async_session() as session:
1035
- agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
1036
- return await agent.to_pydantic_async(include_relationships=include_relationships)
1037
-
1038
- @enforce_types
1039
- async def get_agents_by_ids_async(
1040
- self,
1041
- agent_ids: list[str],
1042
- actor: PydanticUser,
1043
- include_relationships: Optional[List[str]] = None,
1044
- ) -> list[PydanticAgentState]:
1045
- """Fetch a list of agents by their IDs."""
1046
- async with db_registry.async_session() as session:
1047
- agents = await AgentModel.read_multiple_async(
1048
- db_session=session,
1049
- identifiers=agent_ids,
1050
- actor=actor,
1051
- )
1052
- return await asyncio.gather(*[agent.to_pydantic_async(include_relationships=include_relationships) for agent in agents])
1053
-
1054
1026
  @enforce_types
1055
1027
  def get_agent_by_name(self, agent_name: str, actor: PydanticUser) -> PydanticAgentState:
1056
1028
  """Fetch an agent by its ID."""
@@ -1321,11 +1293,6 @@ class AgentManager:
1321
1293
  return await self.message_manager.get_messages_by_ids_async(message_ids=agent.message_ids, actor=actor)
1322
1294
 
1323
1295
  @trace_method
1324
- @enforce_types
1325
- async def get_in_context_messages_async(self, agent_id: str, actor: PydanticUser) -> List[PydanticMessage]:
1326
- agent = await self.get_agent_by_id_async(agent_id=agent_id, include_relationships=[], actor=actor)
1327
- return await self.message_manager.get_messages_by_ids_async(message_ids=agent.message_ids, actor=actor)
1328
-
1329
1296
  @enforce_types
1330
1297
  def get_system_message(self, agent_id: str, actor: PydanticUser) -> PydanticMessage:
1331
1298
  message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
@@ -1475,73 +1442,6 @@ class AgentManager:
1475
1442
  return agent_state
1476
1443
 
1477
1444
  @trace_method
1478
- @enforce_types
1479
- async def rebuild_system_prompt_async(
1480
- self, agent_id: str, actor: PydanticUser, force=False, update_timestamp=True
1481
- ) -> PydanticAgentState:
1482
- """Rebuilds the system message with the latest memory object and any shared memory block updates
1483
-
1484
- Updates to core memory blocks should trigger a "rebuild", which itself will create a new message object
1485
-
1486
- Updates to the memory header should *not* trigger a rebuild, since that will simply flood recall storage with excess messages
1487
- """
1488
- agent_state = await self.get_agent_by_id_async(agent_id=agent_id, include_relationships=["memory"], actor=actor)
1489
-
1490
- curr_system_message = await self.get_system_message_async(
1491
- agent_id=agent_id, actor=actor
1492
- ) # this is the system + memory bank, not just the system prompt
1493
- curr_system_message_openai = curr_system_message.to_openai_dict()
1494
-
1495
- # note: we only update the system prompt if the core memory is changed
1496
- # this means that the archival/recall memory statistics may be someout out of date
1497
- curr_memory_str = agent_state.memory.compile()
1498
- if curr_memory_str in curr_system_message_openai["content"] and not force:
1499
- # NOTE: could this cause issues if a block is removed? (substring match would still work)
1500
- logger.debug(
1501
- f"Memory hasn't changed for agent id={agent_id} and actor=({actor.id}, {actor.name}), skipping system prompt rebuild"
1502
- )
1503
- return agent_state
1504
-
1505
- # If the memory didn't update, we probably don't want to update the timestamp inside
1506
- # For example, if we're doing a system prompt swap, this should probably be False
1507
- if update_timestamp:
1508
- memory_edit_timestamp = get_utc_time()
1509
- else:
1510
- # NOTE: a bit of a hack - we pull the timestamp from the message created_by
1511
- memory_edit_timestamp = curr_system_message.created_at
1512
-
1513
- num_messages = await self.message_manager.size_async(actor=actor, agent_id=agent_id)
1514
- num_archival_memories = await self.passage_manager.size_async(actor=actor, agent_id=agent_id)
1515
-
1516
- # update memory (TODO: potentially update recall/archival stats separately)
1517
- new_system_message_str = compile_system_message(
1518
- system_prompt=agent_state.system,
1519
- in_context_memory=agent_state.memory,
1520
- in_context_memory_last_edit=memory_edit_timestamp,
1521
- recent_passages=self.list_passages(actor=actor, agent_id=agent_id, ascending=False, limit=10),
1522
- previous_message_count=num_messages,
1523
- archival_memory_size=num_archival_memories,
1524
- )
1525
-
1526
- diff = united_diff(curr_system_message_openai["content"], new_system_message_str)
1527
- if len(diff) > 0: # there was a diff
1528
- logger.debug(f"Rebuilding system with new memory...\nDiff:\n{diff}")
1529
-
1530
- # Swap the system message out (only if there is a diff)
1531
- message = PydanticMessage.dict_to_message(
1532
- agent_id=agent_id,
1533
- model=agent_state.llm_config.model,
1534
- openai_message_dict={"role": "system", "content": new_system_message_str},
1535
- )
1536
- message = await self.message_manager.update_message_by_id_async(
1537
- message_id=curr_system_message.id,
1538
- message_update=MessageUpdate(**message.model_dump()),
1539
- actor=actor,
1540
- )
1541
- return await self.set_in_context_messages_async(agent_id=agent_id, message_ids=agent_state.message_ids, actor=actor)
1542
- else:
1543
- return agent_state
1544
-
1545
1445
  @enforce_types
1546
1446
  def set_in_context_messages(self, agent_id: str, message_ids: List[str], actor: PydanticUser) -> PydanticAgentState:
1547
1447
  return self.update_agent(agent_id=agent_id, agent_update=UpdateAgent(message_ids=message_ids), actor=actor)
@@ -1552,10 +1452,6 @@ class AgentManager:
1552
1452
  return await self.update_agent_async(agent_id=agent_id, agent_update=UpdateAgent(message_ids=message_ids), actor=actor)
1553
1453
 
1554
1454
  @trace_method
1555
- @enforce_types
1556
- async def set_in_context_messages_async(self, agent_id: str, message_ids: List[str], actor: PydanticUser) -> PydanticAgentState:
1557
- return await self.update_agent_async(agent_id=agent_id, agent_update=UpdateAgent(message_ids=message_ids), actor=actor)
1558
-
1559
1455
  @enforce_types
1560
1456
  def trim_older_in_context_messages(self, num: int, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
1561
1457
  message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
@@ -1786,25 +1682,6 @@ class AgentManager:
1786
1682
  return [source.to_pydantic() for source in agent.sources]
1787
1683
 
1788
1684
  @trace_method
1789
- @enforce_types
1790
- async def list_attached_sources_async(self, agent_id: str, actor: PydanticUser) -> List[PydanticSource]:
1791
- """
1792
- Lists all sources attached to an agent.
1793
-
1794
- Args:
1795
- agent_id: ID of the agent to list sources for
1796
- actor: User performing the action
1797
-
1798
- Returns:
1799
- List[str]: List of source IDs attached to the agent
1800
- """
1801
- async with db_registry.async_session() as session:
1802
- # Verify agent exists and user has permission to access it
1803
- agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
1804
-
1805
- # Use the lazy-loaded relationship to get sources
1806
- return [source.to_pydantic() for source in agent.sources]
1807
-
1808
1685
  @enforce_types
1809
1686
  def detach_source(self, agent_id: str, source_id: str, actor: PydanticUser) -> PydanticAgentState:
1810
1687
  """
@@ -1896,33 +1773,6 @@ class AgentManager:
1896
1773
  return block.to_pydantic()
1897
1774
 
1898
1775
  @trace_method
1899
- @enforce_types
1900
- async def modify_block_by_label_async(
1901
- self,
1902
- agent_id: str,
1903
- block_label: str,
1904
- block_update: BlockUpdate,
1905
- actor: PydanticUser,
1906
- ) -> PydanticBlock:
1907
- """Gets a block attached to an agent by its label."""
1908
- async with db_registry.async_session() as session:
1909
- block = None
1910
- agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
1911
- for block in agent.core_memory:
1912
- if block.label == block_label:
1913
- block = block
1914
- break
1915
- if not block:
1916
- raise NoResultFound(f"No block with label '{block_label}' found for agent '{agent_id}'")
1917
-
1918
- update_data = block_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
1919
-
1920
- for key, value in update_data.items():
1921
- setattr(block, key, value)
1922
-
1923
- await block.update_async(session, actor=actor)
1924
- return block.to_pydantic()
1925
-
1926
1776
  @enforce_types
1927
1777
  def update_block_with_label(
1928
1778
  self,
@@ -2337,65 +2187,6 @@ class AgentManager:
2337
2187
  return [p.to_pydantic() for p in passages]
2338
2188
 
2339
2189
  @trace_method
2340
- @enforce_types
2341
- async def list_passages_async(
2342
- self,
2343
- actor: PydanticUser,
2344
- agent_id: Optional[str] = None,
2345
- file_id: Optional[str] = None,
2346
- limit: Optional[int] = 50,
2347
- query_text: Optional[str] = None,
2348
- start_date: Optional[datetime] = None,
2349
- end_date: Optional[datetime] = None,
2350
- before: Optional[str] = None,
2351
- after: Optional[str] = None,
2352
- source_id: Optional[str] = None,
2353
- embed_query: bool = False,
2354
- ascending: bool = True,
2355
- embedding_config: Optional[EmbeddingConfig] = None,
2356
- agent_only: bool = False,
2357
- ) -> List[PydanticPassage]:
2358
- """Lists all passages attached to an agent."""
2359
- async with db_registry.async_session() as session:
2360
- main_query = self._build_passage_query(
2361
- actor=actor,
2362
- agent_id=agent_id,
2363
- file_id=file_id,
2364
- query_text=query_text,
2365
- start_date=start_date,
2366
- end_date=end_date,
2367
- before=before,
2368
- after=after,
2369
- source_id=source_id,
2370
- embed_query=embed_query,
2371
- ascending=ascending,
2372
- embedding_config=embedding_config,
2373
- agent_only=agent_only,
2374
- )
2375
-
2376
- # Add limit
2377
- if limit:
2378
- main_query = main_query.limit(limit)
2379
-
2380
- # Execute query
2381
- result = await session.execute(main_query)
2382
-
2383
- passages = []
2384
- for row in result:
2385
- data = dict(row._mapping)
2386
- if data["agent_id"] is not None:
2387
- # This is an AgentPassage - remove source fields
2388
- data.pop("source_id", None)
2389
- data.pop("file_id", None)
2390
- passage = AgentPassage(**data)
2391
- else:
2392
- # This is a SourcePassage - remove agent field
2393
- data.pop("agent_id", None)
2394
- passage = SourcePassage(**data)
2395
- passages.append(passage)
2396
-
2397
- return [p.to_pydantic() for p in passages]
2398
-
2399
2190
  @enforce_types
2400
2191
  def passage_size(
2401
2192
  self,
@@ -38,6 +38,21 @@ class BlockManager:
38
38
  block.create(session, actor=actor)
39
39
  return block.to_pydantic()
40
40
 
41
+ @trace_method
42
+ @enforce_types
43
+ async def create_or_update_block_async(self, block: PydanticBlock, actor: PydanticUser) -> PydanticBlock:
44
+ """Create a new block based on the Block schema."""
45
+ db_block = await self.get_block_by_id_async(block.id, actor)
46
+ if db_block:
47
+ update_data = BlockUpdate(**block.model_dump(to_orm=True, exclude_none=True))
48
+ return await self.update_block_async(block.id, update_data, actor)
49
+ else:
50
+ async with db_registry.async_session() as session:
51
+ data = block.model_dump(to_orm=True, exclude_none=True)
52
+ block = BlockModel(**data, organization_id=actor.organization_id)
53
+ await block.create_async(session, actor=actor)
54
+ return block.to_pydantic()
55
+
41
56
  @trace_method
42
57
  @enforce_types
43
58
  def batch_create_blocks(self, blocks: List[PydanticBlock], actor: PydanticUser) -> List[PydanticBlock]:
@@ -78,6 +93,22 @@ class BlockManager:
78
93
  block.update(db_session=session, actor=actor)
79
94
  return block.to_pydantic()
80
95
 
96
+ @trace_method
97
+ @enforce_types
98
+ async def update_block_async(self, block_id: str, block_update: BlockUpdate, actor: PydanticUser) -> PydanticBlock:
99
+ """Update a block by its ID with the given BlockUpdate object."""
100
+ # Safety check for block
101
+
102
+ async with db_registry.async_session() as session:
103
+ block = await BlockModel.read_async(db_session=session, identifier=block_id, actor=actor)
104
+ update_data = block_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
105
+
106
+ for key, value in update_data.items():
107
+ setattr(block, key, value)
108
+
109
+ await block.update_async(db_session=session, actor=actor)
110
+ return block.to_pydantic()
111
+
81
112
  @trace_method
82
113
  @enforce_types
83
114
  def delete_block(self, block_id: str, actor: PydanticUser) -> PydanticBlock:
@@ -87,6 +118,15 @@ class BlockManager:
87
118
  block.hard_delete(db_session=session, actor=actor)
88
119
  return block.to_pydantic()
89
120
 
121
+ @trace_method
122
+ @enforce_types
123
+ async def delete_block_async(self, block_id: str, actor: PydanticUser) -> PydanticBlock:
124
+ """Delete a block by its ID."""
125
+ async with db_registry.async_session() as session:
126
+ block = await BlockModel.read_async(db_session=session, identifier=block_id, actor=actor)
127
+ await block.hard_delete_async(db_session=session, actor=actor)
128
+ return block.to_pydantic()
129
+
90
130
  @trace_method
91
131
  @enforce_types
92
132
  async def get_blocks_async(
@@ -161,6 +201,17 @@ class BlockManager:
161
201
  except NoResultFound:
162
202
  return None
163
203
 
204
+ @trace_method
205
+ @enforce_types
206
+ async def get_block_by_id_async(self, block_id: str, actor: Optional[PydanticUser] = None) -> Optional[PydanticBlock]:
207
+ """Retrieve a block by its name."""
208
+ async with db_registry.async_session() as session:
209
+ try:
210
+ block = await BlockModel.read_async(db_session=session, identifier=block_id, actor=actor)
211
+ return block.to_pydantic()
212
+ except NoResultFound:
213
+ return None
214
+
164
215
  @trace_method
165
216
  @enforce_types
166
217
  async def get_all_blocks_by_ids_async(self, block_ids: List[str], actor: Optional[PydanticUser] = None) -> List[PydanticBlock]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: letta-nightly
3
- Version: 0.7.22.dev20250523081403
3
+ Version: 0.7.23.dev20250523164139
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  License: Apache License
6
6
  Author: Letta Team
@@ -1,4 +1,4 @@
1
- letta/__init__.py,sha256=AkQ-S44jf9AYGDZrBONaQfZis2Gxwo-wIMuyk9rGkyA,888
1
+ letta/__init__.py,sha256=vzj6HtecPrKm7mPJweZmynwWQsIiHIWSopQMTD3u96E,888
2
2
  letta/agent.py,sha256=2r6xovRHeUnmWZ6WJoIP217ryse5Q3Bkco1JXiV599w,87459
3
3
  letta/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  letta/agents/base_agent.py,sha256=mdFEpYBVyFjmt6BzO9YrpJnH99RkBWZ9gnP3Q_bnbBI,5505
@@ -76,9 +76,9 @@ letta/llm_api/azure_openai.py,sha256=YAkXwKyfnJFNhB45pkJVFsoxUNB_M74rQYchtw_CN6I
76
76
  letta/llm_api/azure_openai_constants.py,sha256=ZaR2IasJThijG0uhLKJThrixdAxLPD2IojfeaJ-KQMQ,294
77
77
  letta/llm_api/cohere.py,sha256=IZ6LXyOFMYjWHTeNG9lvFxCdV_NIl0hY2q9SPFYXNkQ,14849
78
78
  letta/llm_api/deepseek.py,sha256=b1mSW8gnBrpAI8d2GcBpDyLYDnuC-P1UP6xJPalfQS4,12456
79
- letta/llm_api/google_ai_client.py,sha256=yIP4lJ1sJoryS_76FjSyzOTCOEnXZoyoo-MarR9YzU8,9401
79
+ letta/llm_api/google_ai_client.py,sha256=WSLTayAd7FQPilIZ7MxJnSCgw4JCl2vY_KE2aorvr5Q,9933
80
80
  letta/llm_api/google_constants.py,sha256=4PKWUNNbBHgHi4K5u9YaHr_8UC3fokfI6Qb6Dfpt4mU,693
81
- letta/llm_api/google_vertex_client.py,sha256=OCjEp0tTsTKkDVgY-3EwHSJRfyXvAv5ikYzNfIHACaU,22814
81
+ letta/llm_api/google_vertex_client.py,sha256=XoGDmX_kbaRayHZMjiQuo96VpvisDikg8vYfez_rsNc,22796
82
82
  letta/llm_api/helpers.py,sha256=rpZInutKVKgoywreclisNSi2zVxwFinAzJIuxF6ll4I,17041
83
83
  letta/llm_api/llm_api_tools.py,sha256=gMYoEvs5vSyvjos2eYJN6_BpQ2aNpt3zvyF7D2phbqY,30044
84
84
  letta/llm_api/llm_client.py,sha256=sO9MwiSOJ_ycOFnYrQP0_g6cFkMSnrZqFDz1sUeBHD8,2098
@@ -229,7 +229,7 @@ letta/schemas/openai/openai.py,sha256=Hilo5BiLAGabzxCwnwfzK5QrWqwYD8epaEKFa4Pwnd
229
229
  letta/schemas/organization.py,sha256=TXrHN4IBQnX-mWvRuCOH57XZSLYCVOY0wWm2_UzDQIA,1279
230
230
  letta/schemas/passage.py,sha256=RG0vkaewEu4a_NAZM-FVyMammHjqpPP0RDYAdu27g6A,3723
231
231
  letta/schemas/provider_trace.py,sha256=gsgo1CdfTUFSnm1ep1tSZ0fZfGSx45EdPaVyVJREt_U,1958
232
- letta/schemas/providers.py,sha256=EfCT6NN41EP1IJwkqauHy2rD2_RyxWeE0HJMmWdPkkE,63037
232
+ letta/schemas/providers.py,sha256=p9k2lTNT0qdFGgYAiNLYJpyfdrZrDBeWeU7v112WDl4,63063
233
233
  letta/schemas/response_format.py,sha256=pXNsjbtpA3Tf8HsDyIa40CSmoUbVR_7n2WOfQaX4aFs,2204
234
234
  letta/schemas/run.py,sha256=SRqPRziINIiPunjOhE_NlbnQYgxTvqmbauni_yfBQRA,2085
235
235
  letta/schemas/sandbox_config.py,sha256=Qfkzw422HCQUsE3GKry94oecQGziAzGXIyd6ke8W06M,5985
@@ -252,7 +252,7 @@ letta/serialize_schemas/marshmallow_tool.py,sha256=jwU69BDCakPlYPSk-ta21kuvsURKO
252
252
  letta/serialize_schemas/pydantic_agent_schema.py,sha256=NKq70niUVMI3_lxMKc3u3rOBUhm77bIFaPRj9aidMUQ,3006
253
253
  letta/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
254
254
  letta/server/constants.py,sha256=yAdGbLkzlOU_dLTx0lKDmAnj0ZgRXCEaIcPJWO69eaE,92
255
- letta/server/db.py,sha256=Ob8_W7HXUduJS94a3d0jZ50EtSkYuv8kgOk6AjusyJ8,9985
255
+ letta/server/db.py,sha256=Hj4DDGXu7J1t6Q4oJwvhpIB3IXgNCvU1cQbW6f1osWM,9954
256
256
  letta/server/generate_openapi_schema.sh,sha256=0OtBhkC1g6CobVmNEd_m2B6sTdppjbJLXaM95icejvE,371
257
257
  letta/server/rest_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
258
258
  letta/server/rest_api/app.py,sha256=8Y5R_t_s4IAG7wRfdbmCeF5YT9pfGG4h6_kpUAaUwoQ,14771
@@ -267,7 +267,7 @@ letta/server/rest_api/routers/openai/chat_completions/__init__.py,sha256=47DEQpj
267
267
  letta/server/rest_api/routers/openai/chat_completions/chat_completions.py,sha256=QBWab1fn2LXVDMtc6li3gOzmrNzDiUw5WUJsMeeMZII,5076
268
268
  letta/server/rest_api/routers/v1/__init__.py,sha256=JfSSttkEWu0W18NVVDxl8AGnd8Qhj0BXJNxntOB7070,1768
269
269
  letta/server/rest_api/routers/v1/agents.py,sha256=evL3IHe5PR2Jmo3edT6hGv2BkHd7hB1EfC4Y4z3qOVc,38631
270
- letta/server/rest_api/routers/v1/blocks.py,sha256=am8VeLcpWtZ1V6c9hl0q141uogijGWPbI3rlEoVzBPA,4677
270
+ letta/server/rest_api/routers/v1/blocks.py,sha256=2uCUK91hFFmiAJt0Mrj0wn0Q6UC6J7xcV1dbDLU44r0,4767
271
271
  letta/server/rest_api/routers/v1/embeddings.py,sha256=P-Dvt_HNKoTyjRwkScAMg1hlB3cNxMeAQwV7bSatsKI,957
272
272
  letta/server/rest_api/routers/v1/groups.py,sha256=DT2tc4wwiq_gzmxefltEIrFSoqOntzhvmgqQy23varA,10738
273
273
  letta/server/rest_api/routers/v1/health.py,sha256=MoOjkydhGcJXTiuJrKIB0etVXiRMdTa51S8RQ8-50DQ,399
@@ -279,7 +279,7 @@ letta/server/rest_api/routers/v1/organizations.py,sha256=r7rj-cA3shgAgM0b2JCMqjY
279
279
  letta/server/rest_api/routers/v1/providers.py,sha256=qp6XT20tcZac64XDGF2QUyLhselnShrRcTDQBHExEbQ,4322
280
280
  letta/server/rest_api/routers/v1/runs.py,sha256=rq-k5kYN0On7VBNSzoPJxZcBf13hZFaDx0IUJJ04_K8,8875
281
281
  letta/server/rest_api/routers/v1/sandbox_configs.py,sha256=g6JE1Xcl4vQPRduPdKZL04FexHttK0ZPJuTGds_Heig,8782
282
- letta/server/rest_api/routers/v1/sources.py,sha256=6qL59NYCTv8uJ7SBOPk09c8q90KghnehPB6NTYLVJ7Y,11896
282
+ letta/server/rest_api/routers/v1/sources.py,sha256=ccdCdgCvdd1g81IPrnpoiQwqhh3eP5b7HuLfhrVYZhw,11457
283
283
  letta/server/rest_api/routers/v1/steps.py,sha256=ra7ttm7HDs3N52M6s80XdpwiSMTLyf776_SmEILWDvo,3276
284
284
  letta/server/rest_api/routers/v1/tags.py,sha256=ef94QitUSJ3NQVffWF1ZqANUZ2b2jRyGHp_I3UUjhno,912
285
285
  letta/server/rest_api/routers/v1/telemetry.py,sha256=z53BW3Pefi3eWy47FPJyGhFWbZicX9jPJUi5LC5c3sk,790
@@ -289,7 +289,7 @@ letta/server/rest_api/routers/v1/voice.py,sha256=NZa7ksEqXTWSqh7CqmbVMClO7wOmrql
289
289
  letta/server/rest_api/static_files.py,sha256=NG8sN4Z5EJ8JVQdj19tkFa9iQ1kBPTab9f_CUxd_u4Q,3143
290
290
  letta/server/rest_api/streaming_response.py,sha256=yYTuZHfuZ-DYYbA1Ta6axkBn5MvC6OHuVRHSiBqRNUk,3939
291
291
  letta/server/rest_api/utils.py,sha256=UVWlJI026oBB3uWxjhnA9DRlUiYtCoqBzmekI2JhvOQ,16612
292
- letta/server/server.py,sha256=g2C3Nv1_fer9vv_DpUXMVWva1F8KLWe5guj1vtIv52I,96158
292
+ letta/server/server.py,sha256=iIt6AhxsSHoUEZylUci0-D5MUikIC0mA6NSTu2ky1dw,101509
293
293
  letta/server/startup.sh,sha256=MRXh1RKbS5lyA7XAsk7O6Q4LEKOqnv5B-dwe0SnTHeQ,2514
294
294
  letta/server/static_files/assets/index-048c9598.js,sha256=mR16XppvselwKCcNgONs4L7kZEVa4OEERm4lNZYtLSk,146819
295
295
  letta/server/static_files/assets/index-0e31b727.css,sha256=SBbja96uiQVLDhDOroHgM6NSl7tS4lpJRCREgSS_hA8,7672
@@ -303,8 +303,8 @@ letta/server/ws_api/interface.py,sha256=TWl9vkcMCnLsUtgsuENZ-ku2oMDA-OUTzLh_yNRo
303
303
  letta/server/ws_api/protocol.py,sha256=5mDgpfNZn_kNwHnpt5Dsuw8gdNH298sgxTGed3etzYg,1836
304
304
  letta/server/ws_api/server.py,sha256=cBSzf-V4zT1bL_0i54OTI3cMXhTIIxqjSRF8pYjk7fg,5835
305
305
  letta/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
306
- letta/services/agent_manager.py,sha256=QQ-5Xfnjvbfm4MaP26yEhjWZ6WJtnmnOACAx92NTSaQ,131609
307
- letta/services/block_manager.py,sha256=lZewHLhG5QKAceE3LIh2ty9OmAzvNu4uDGUSln3JULo,19240
306
+ letta/services/agent_manager.py,sha256=N2Wsi0zrwuhRjQyofSvC1aOPwiP3MUSamYSdJFyaslc,122236
307
+ letta/services/block_manager.py,sha256=e4_VAHA5XzOLOdIE3uLqtnxykjEvfTgSJSRqNKDUsWM,21716
308
308
  letta/services/group_manager.py,sha256=zzxrPlk3FTUfiOMPwdjuVr9loxInjtXlJsO_TKReJqk,17267
309
309
  letta/services/helpers/agent_manager_helper.py,sha256=q7GfVgKI-e8k0BZS-V_PuUCjK-PYciZDoig_sYHi_Go,21334
310
310
  letta/services/helpers/noop_helper.py,sha256=OZ6wZLsdNEAg9Q2t5oFTOMK6jp-YUMBPdoyiR8M3T1c,272
@@ -346,8 +346,8 @@ letta/system.py,sha256=mKxmvvekuP8mdgsebRINGBoFbUdJhxLJ260crPBNVyk,8386
346
346
  letta/tracing.py,sha256=YMb9KgoBVz7nwCPwnErk2EJEKMiQ_ohctW1nOwhHd1Y,8458
347
347
  letta/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
348
348
  letta/utils.py,sha256=W8J1FfhRADFqoyx3J8-Z1_aWyG433PBoEh_b5wdOZIg,32262
349
- letta_nightly-0.7.22.dev20250523081403.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
350
- letta_nightly-0.7.22.dev20250523081403.dist-info/METADATA,sha256=qgI9EsbdbgD3p9Glqav-C1wM30Bv1RvjHKkN2At1cIE,22321
351
- letta_nightly-0.7.22.dev20250523081403.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
352
- letta_nightly-0.7.22.dev20250523081403.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
353
- letta_nightly-0.7.22.dev20250523081403.dist-info/RECORD,,
349
+ letta_nightly-0.7.23.dev20250523164139.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
350
+ letta_nightly-0.7.23.dev20250523164139.dist-info/METADATA,sha256=Ojwp8JmVzsZWA7ibbGOtjEWmOgEP6YMBToiSCgQcTww,22321
351
+ letta_nightly-0.7.23.dev20250523164139.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
352
+ letta_nightly-0.7.23.dev20250523164139.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
353
+ letta_nightly-0.7.23.dev20250523164139.dist-info/RECORD,,