letta-nightly 0.7.15.dev20250514104255__py3-none-any.whl → 0.7.16.dev20250515205957__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 (36) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +12 -0
  3. letta/agents/helpers.py +48 -5
  4. letta/agents/letta_agent.py +46 -18
  5. letta/agents/letta_agent_batch.py +44 -26
  6. letta/agents/voice_sleeptime_agent.py +6 -4
  7. letta/client/client.py +16 -1
  8. letta/constants.py +3 -0
  9. letta/functions/async_composio_toolset.py +1 -1
  10. letta/groups/sleeptime_multi_agent.py +1 -0
  11. letta/interfaces/anthropic_streaming_interface.py +40 -6
  12. letta/jobs/llm_batch_job_polling.py +6 -2
  13. letta/orm/agent.py +102 -1
  14. letta/orm/block.py +3 -0
  15. letta/orm/sqlalchemy_base.py +365 -133
  16. letta/schemas/agent.py +10 -2
  17. letta/schemas/block.py +3 -0
  18. letta/schemas/memory.py +7 -2
  19. letta/server/rest_api/routers/v1/agents.py +13 -13
  20. letta/server/rest_api/routers/v1/messages.py +6 -6
  21. letta/server/rest_api/routers/v1/tools.py +3 -3
  22. letta/server/server.py +74 -0
  23. letta/services/agent_manager.py +421 -7
  24. letta/services/block_manager.py +12 -8
  25. letta/services/helpers/agent_manager_helper.py +19 -0
  26. letta/services/job_manager.py +99 -0
  27. letta/services/llm_batch_manager.py +28 -27
  28. letta/services/message_manager.py +51 -19
  29. letta/services/tool_executor/tool_executor.py +19 -1
  30. letta/services/tool_manager.py +13 -3
  31. letta/types/__init__.py +0 -0
  32. {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/METADATA +3 -3
  33. {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/RECORD +36 -35
  34. {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/LICENSE +0 -0
  35. {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/WHEEL +0 -0
  36. {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/entry_points.txt +0 -0
@@ -62,6 +62,7 @@ from letta.services.helpers.agent_manager_helper import (
62
62
  _apply_filters,
63
63
  _apply_identity_filters,
64
64
  _apply_pagination,
65
+ _apply_pagination_async,
65
66
  _apply_tag_filter,
66
67
  _process_relationship,
67
68
  check_supports_structured_output,
@@ -122,7 +123,35 @@ class AgentManager:
122
123
  return name_to_id, id_to_name
123
124
 
124
125
  @staticmethod
125
- @trace_method
126
+ async def _resolve_tools_async(session, names: Set[str], ids: Set[str], org_id: str) -> Tuple[Dict[str, str], Dict[str, str]]:
127
+ """
128
+ Bulk‑fetch all ToolModel rows matching either name ∈ names or id ∈ ids
129
+ (and scoped to this organization), and return two maps:
130
+ name_to_id, id_to_name.
131
+ Raises if any requested name or id was not found.
132
+ """
133
+ stmt = select(ToolModel.id, ToolModel.name).where(
134
+ ToolModel.organization_id == org_id,
135
+ or_(
136
+ ToolModel.name.in_(names),
137
+ ToolModel.id.in_(ids),
138
+ ),
139
+ )
140
+ result = await session.execute(stmt)
141
+ rows = result.fetchall() # Use fetchall()
142
+ name_to_id = {row[1]: row[0] for row in rows} # row[1] is name, row[0] is id
143
+ id_to_name = {row[0]: row[1] for row in rows} # row[0] is id, row[1] is name
144
+
145
+ missing_names = names - set(name_to_id.keys())
146
+ missing_ids = ids - set(id_to_name.keys())
147
+ if missing_names:
148
+ raise ValueError(f"Tools not found by name: {missing_names}")
149
+ if missing_ids:
150
+ raise ValueError(f"Tools not found by id: {missing_ids}")
151
+
152
+ return name_to_id, id_to_name
153
+
154
+ @staticmethod
126
155
  def _bulk_insert_pivot(session, table, rows: list[dict]):
127
156
  if not rows:
128
157
  return
@@ -146,7 +175,29 @@ class AgentManager:
146
175
  session.execute(stmt)
147
176
 
148
177
  @staticmethod
149
- @trace_method
178
+ async def _bulk_insert_pivot_async(session, table, rows: list[dict]):
179
+ if not rows:
180
+ return
181
+
182
+ dialect = session.bind.dialect.name
183
+ if dialect == "postgresql":
184
+ stmt = pg_insert(table).values(rows).on_conflict_do_nothing()
185
+ elif dialect == "sqlite":
186
+ stmt = sa.insert(table).values(rows).prefix_with("OR IGNORE")
187
+ else:
188
+ # fallback: filter out exact-duplicate dicts in Python
189
+ seen = set()
190
+ filtered = []
191
+ for row in rows:
192
+ key = tuple(sorted(row.items()))
193
+ if key not in seen:
194
+ seen.add(key)
195
+ filtered.append(row)
196
+ stmt = sa.insert(table).values(filtered)
197
+
198
+ await session.execute(stmt)
199
+
200
+ @staticmethod
150
201
  def _replace_pivot_rows(session, table, agent_id: str, rows: list[dict]):
151
202
  """
152
203
  Replace all pivot rows for an agent with *exactly* the provided list.
@@ -157,6 +208,17 @@ class AgentManager:
157
208
  if rows:
158
209
  AgentManager._bulk_insert_pivot(session, table, rows)
159
210
 
211
+ @staticmethod
212
+ async def _replace_pivot_rows_async(session, table, agent_id: str, rows: list[dict]):
213
+ """
214
+ Replace all pivot rows for an agent with *exactly* the provided list.
215
+ Uses two bulk statements (DELETE + INSERT ... ON CONFLICT DO NOTHING).
216
+ """
217
+ # delete all existing rows for this agent
218
+ await session.execute(delete(table).where(table.c.agent_id == agent_id))
219
+ if rows:
220
+ await AgentManager._bulk_insert_pivot_async(session, table, rows)
221
+
160
222
  # ======================================================================================================================
161
223
  # Basic CRUD operations
162
224
  # ======================================================================================================================
@@ -252,6 +314,7 @@ class AgentManager:
252
314
  session.flush()
253
315
  aid = new_agent.id
254
316
 
317
+ # Note: These methods may need async versions if they perform database operations
255
318
  self._bulk_insert_pivot(
256
319
  session,
257
320
  ToolsAgents.__table__,
@@ -259,10 +322,8 @@ class AgentManager:
259
322
  )
260
323
 
261
324
  if block_ids:
262
- rows = [
263
- {"agent_id": aid, "block_id": bid, "block_label": lbl}
264
- for bid, lbl in session.execute(select(BlockModel.id, BlockModel.label).where(BlockModel.id.in_(block_ids))).all()
265
- ]
325
+ result = session.execute(select(BlockModel.id, BlockModel.label).where(BlockModel.id.in_(block_ids)))
326
+ rows = [{"agent_id": aid, "block_id": bid, "block_label": lbl} for bid, lbl in result.all()]
266
327
  self._bulk_insert_pivot(session, BlocksAgents.__table__, rows)
267
328
 
268
329
  self._bulk_insert_pivot(
@@ -303,9 +364,162 @@ class AgentManager:
303
364
 
304
365
  session.refresh(new_agent)
305
366
 
367
+ # Using the synchronous version since we don't have an async version yet
368
+ # If you implement an async version of create_many_messages, you can switch to that
306
369
  self.message_manager.create_many_messages(pydantic_msgs=init_messages, actor=actor)
307
370
  return new_agent.to_pydantic()
308
371
 
372
+ @trace_method
373
+ async def create_agent_async(
374
+ self, agent_create: CreateAgent, actor: PydanticUser, _test_only_force_id: Optional[str] = None
375
+ ) -> PydanticAgentState:
376
+ # validate required configs
377
+ if not agent_create.llm_config or not agent_create.embedding_config:
378
+ raise ValueError("llm_config and embedding_config are required")
379
+
380
+ # blocks
381
+ block_ids = list(agent_create.block_ids or [])
382
+ if agent_create.memory_blocks:
383
+ pydantic_blocks = [PydanticBlock(**b.model_dump(to_orm=True)) for b in agent_create.memory_blocks]
384
+ created_blocks = self.block_manager.batch_create_blocks(
385
+ pydantic_blocks,
386
+ actor=actor,
387
+ )
388
+ block_ids.extend([blk.id for blk in created_blocks])
389
+
390
+ # tools
391
+ tool_names = set(agent_create.tools or [])
392
+ if agent_create.include_base_tools:
393
+ if agent_create.agent_type == AgentType.voice_sleeptime_agent:
394
+ tool_names |= set(BASE_VOICE_SLEEPTIME_TOOLS)
395
+ elif agent_create.agent_type == AgentType.voice_convo_agent:
396
+ tool_names |= set(BASE_VOICE_SLEEPTIME_CHAT_TOOLS)
397
+ elif agent_create.agent_type == AgentType.sleeptime_agent:
398
+ tool_names |= set(BASE_SLEEPTIME_TOOLS)
399
+ elif agent_create.enable_sleeptime:
400
+ tool_names |= set(BASE_SLEEPTIME_CHAT_TOOLS)
401
+ else:
402
+ tool_names |= set(BASE_TOOLS + BASE_MEMORY_TOOLS)
403
+ if agent_create.include_multi_agent_tools:
404
+ tool_names |= set(MULTI_AGENT_TOOLS)
405
+
406
+ supplied_ids = set(agent_create.tool_ids or [])
407
+
408
+ source_ids = agent_create.source_ids or []
409
+ identity_ids = agent_create.identity_ids or []
410
+ tag_values = agent_create.tags or []
411
+
412
+ async with db_registry.async_session() as session:
413
+ async with session.begin():
414
+ # Note: This will need to be modified if _resolve_tools needs an async version
415
+ name_to_id, id_to_name = await self._resolve_tools_async(
416
+ session,
417
+ tool_names,
418
+ supplied_ids,
419
+ actor.organization_id,
420
+ )
421
+
422
+ tool_ids = set(name_to_id.values()) | set(id_to_name.keys())
423
+ tool_names = set(name_to_id.keys()) # now canonical
424
+
425
+ tool_rules = list(agent_create.tool_rules or [])
426
+ if agent_create.include_base_tool_rules:
427
+ for tn in tool_names:
428
+ if tn in {"send_message", "send_message_to_agent_async", "memory_finish_edits"}:
429
+ tool_rules.append(TerminalToolRule(tool_name=tn))
430
+ elif tn in (BASE_TOOLS + BASE_MEMORY_TOOLS + BASE_SLEEPTIME_TOOLS):
431
+ tool_rules.append(ContinueToolRule(tool_name=tn))
432
+
433
+ if tool_rules:
434
+ check_supports_structured_output(model=agent_create.llm_config.model, tool_rules=tool_rules)
435
+
436
+ new_agent = AgentModel(
437
+ name=agent_create.name,
438
+ system=derive_system_message(
439
+ agent_type=agent_create.agent_type,
440
+ enable_sleeptime=agent_create.enable_sleeptime,
441
+ system=agent_create.system,
442
+ ),
443
+ agent_type=agent_create.agent_type,
444
+ llm_config=agent_create.llm_config,
445
+ embedding_config=agent_create.embedding_config,
446
+ organization_id=actor.organization_id,
447
+ description=agent_create.description,
448
+ metadata_=agent_create.metadata,
449
+ tool_rules=tool_rules,
450
+ project_id=agent_create.project_id,
451
+ template_id=agent_create.template_id,
452
+ base_template_id=agent_create.base_template_id,
453
+ message_buffer_autoclear=agent_create.message_buffer_autoclear,
454
+ enable_sleeptime=agent_create.enable_sleeptime,
455
+ response_format=agent_create.response_format,
456
+ created_by_id=actor.id,
457
+ last_updated_by_id=actor.id,
458
+ )
459
+
460
+ if _test_only_force_id:
461
+ new_agent.id = _test_only_force_id
462
+
463
+ session.add(new_agent)
464
+ await session.flush()
465
+ aid = new_agent.id
466
+
467
+ # Note: These methods may need async versions if they perform database operations
468
+ await self._bulk_insert_pivot_async(
469
+ session,
470
+ ToolsAgents.__table__,
471
+ [{"agent_id": aid, "tool_id": tid} for tid in tool_ids],
472
+ )
473
+
474
+ if block_ids:
475
+ result = await session.execute(select(BlockModel.id, BlockModel.label).where(BlockModel.id.in_(block_ids)))
476
+ rows = [{"agent_id": aid, "block_id": bid, "block_label": lbl} for bid, lbl in result.all()]
477
+ await self._bulk_insert_pivot_async(session, BlocksAgents.__table__, rows)
478
+
479
+ await self._bulk_insert_pivot_async(
480
+ session,
481
+ SourcesAgents.__table__,
482
+ [{"agent_id": aid, "source_id": sid} for sid in source_ids],
483
+ )
484
+ await self._bulk_insert_pivot_async(
485
+ session,
486
+ AgentsTags.__table__,
487
+ [{"agent_id": aid, "tag": tag} for tag in tag_values],
488
+ )
489
+ await self._bulk_insert_pivot_async(
490
+ session,
491
+ IdentitiesAgents.__table__,
492
+ [{"agent_id": aid, "identity_id": iid} for iid in identity_ids],
493
+ )
494
+
495
+ if agent_create.tool_exec_environment_variables:
496
+ env_rows = [
497
+ {
498
+ "agent_id": aid,
499
+ "key": key,
500
+ "value": val,
501
+ "organization_id": actor.organization_id,
502
+ }
503
+ for key, val in agent_create.tool_exec_environment_variables.items()
504
+ ]
505
+ await session.execute(insert(AgentEnvironmentVariable).values(env_rows))
506
+
507
+ # initial message sequence
508
+ agent_state = await new_agent.to_pydantic_async(include_relationships={"memory"})
509
+ init_messages = self._generate_initial_message_sequence(
510
+ actor,
511
+ agent_state=agent_state,
512
+ supplied_initial_message_sequence=agent_create.initial_message_sequence,
513
+ )
514
+ new_agent.message_ids = [msg.id for msg in init_messages]
515
+
516
+ await session.refresh(new_agent)
517
+
518
+ # Using the synchronous version since we don't have an async version yet
519
+ # If you implement an async version of create_many_messages, you can switch to that
520
+ await self.message_manager.create_many_messages_async(pydantic_msgs=init_messages, actor=actor)
521
+ return await new_agent.to_pydantic_async()
522
+
309
523
  @enforce_types
310
524
  def _generate_initial_message_sequence(
311
525
  self, actor: PydanticUser, agent_state: PydanticAgentState, supplied_initial_message_sequence: Optional[List[MessageCreate]] = None
@@ -459,6 +673,123 @@ class AgentManager:
459
673
 
460
674
  return agent.to_pydantic()
461
675
 
676
+ @enforce_types
677
+ async def update_agent_async(
678
+ self,
679
+ agent_id: str,
680
+ agent_update: UpdateAgent,
681
+ actor: PydanticUser,
682
+ ) -> PydanticAgentState:
683
+
684
+ new_tools = set(agent_update.tool_ids or [])
685
+ new_sources = set(agent_update.source_ids or [])
686
+ new_blocks = set(agent_update.block_ids or [])
687
+ new_idents = set(agent_update.identity_ids or [])
688
+ new_tags = set(agent_update.tags or [])
689
+
690
+ async with db_registry.async_session() as session, session.begin():
691
+
692
+ agent: AgentModel = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
693
+ agent.updated_at = datetime.now(timezone.utc)
694
+ agent.last_updated_by_id = actor.id
695
+
696
+ scalar_updates = {
697
+ "name": agent_update.name,
698
+ "system": agent_update.system,
699
+ "llm_config": agent_update.llm_config,
700
+ "embedding_config": agent_update.embedding_config,
701
+ "message_ids": agent_update.message_ids,
702
+ "tool_rules": agent_update.tool_rules,
703
+ "description": agent_update.description,
704
+ "project_id": agent_update.project_id,
705
+ "template_id": agent_update.template_id,
706
+ "base_template_id": agent_update.base_template_id,
707
+ "message_buffer_autoclear": agent_update.message_buffer_autoclear,
708
+ "enable_sleeptime": agent_update.enable_sleeptime,
709
+ "response_format": agent_update.response_format,
710
+ }
711
+ for col, val in scalar_updates.items():
712
+ if val is not None:
713
+ setattr(agent, col, val)
714
+
715
+ if agent_update.metadata is not None:
716
+ agent.metadata_ = agent_update.metadata
717
+
718
+ aid = agent.id
719
+
720
+ if agent_update.tool_ids is not None:
721
+ await self._replace_pivot_rows_async(
722
+ session,
723
+ ToolsAgents.__table__,
724
+ aid,
725
+ [{"agent_id": aid, "tool_id": tid} for tid in new_tools],
726
+ )
727
+ session.expire(agent, ["tools"])
728
+
729
+ if agent_update.source_ids is not None:
730
+ await self._replace_pivot_rows_async(
731
+ session,
732
+ SourcesAgents.__table__,
733
+ aid,
734
+ [{"agent_id": aid, "source_id": sid} for sid in new_sources],
735
+ )
736
+ session.expire(agent, ["sources"])
737
+
738
+ if agent_update.block_ids is not None:
739
+ rows = []
740
+ if new_blocks:
741
+ result = await session.execute(select(BlockModel.id, BlockModel.label).where(BlockModel.id.in_(new_blocks)))
742
+ label_map = {bid: lbl for bid, lbl in result.all()}
743
+ rows = [{"agent_id": aid, "block_id": bid, "block_label": label_map[bid]} for bid in new_blocks]
744
+
745
+ await self._replace_pivot_rows_async(session, BlocksAgents.__table__, aid, rows)
746
+ session.expire(agent, ["core_memory"])
747
+
748
+ if agent_update.identity_ids is not None:
749
+ await self._replace_pivot_rows_async(
750
+ session,
751
+ IdentitiesAgents.__table__,
752
+ aid,
753
+ [{"agent_id": aid, "identity_id": iid} for iid in new_idents],
754
+ )
755
+ session.expire(agent, ["identities"])
756
+
757
+ if agent_update.tags is not None:
758
+ await self._replace_pivot_rows_async(
759
+ session,
760
+ AgentsTags.__table__,
761
+ aid,
762
+ [{"agent_id": aid, "tag": tag} for tag in new_tags],
763
+ )
764
+ session.expire(agent, ["tags"])
765
+
766
+ if agent_update.tool_exec_environment_variables is not None:
767
+ await session.execute(delete(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
768
+ env_rows = [
769
+ {
770
+ "agent_id": aid,
771
+ "key": k,
772
+ "value": v,
773
+ "organization_id": agent.organization_id,
774
+ }
775
+ for k, v in agent_update.tool_exec_environment_variables.items()
776
+ ]
777
+ if env_rows:
778
+ await self._bulk_insert_pivot_async(session, AgentEnvironmentVariable.__table__, env_rows)
779
+ session.expire(agent, ["tool_exec_environment_variables"])
780
+
781
+ if agent_update.enable_sleeptime and agent_update.system is None:
782
+ agent.system = derive_system_message(
783
+ agent_type=agent.agent_type,
784
+ enable_sleeptime=agent_update.enable_sleeptime,
785
+ system=agent.system,
786
+ )
787
+
788
+ await session.flush()
789
+ await session.refresh(agent)
790
+
791
+ return await agent.to_pydantic_async()
792
+
462
793
  # TODO: Make this general and think about how to roll this into sqlalchemybase
463
794
  def list_agents(
464
795
  self,
@@ -514,9 +845,73 @@ class AgentManager:
514
845
  if limit:
515
846
  query = query.limit(limit)
516
847
 
517
- agents = session.execute(query).scalars().all()
848
+ result = session.execute(query)
849
+ agents = result.scalars().all()
518
850
  return [agent.to_pydantic(include_relationships=include_relationships) for agent in agents]
519
851
 
852
+ async def list_agents_async(
853
+ self,
854
+ actor: PydanticUser,
855
+ name: Optional[str] = None,
856
+ tags: Optional[List[str]] = None,
857
+ match_all_tags: bool = False,
858
+ before: Optional[str] = None,
859
+ after: Optional[str] = None,
860
+ limit: Optional[int] = 50,
861
+ query_text: Optional[str] = None,
862
+ project_id: Optional[str] = None,
863
+ template_id: Optional[str] = None,
864
+ base_template_id: Optional[str] = None,
865
+ identity_id: Optional[str] = None,
866
+ identifier_keys: Optional[List[str]] = None,
867
+ include_relationships: Optional[List[str]] = None,
868
+ ascending: bool = True,
869
+ ) -> List[PydanticAgentState]:
870
+ """
871
+ Retrieves agents with optimized filtering and optional field selection.
872
+
873
+ Args:
874
+ actor: The User requesting the list
875
+ name (Optional[str]): Filter by agent name.
876
+ tags (Optional[List[str]]): Filter agents by tags.
877
+ match_all_tags (bool): If True, only return agents that match ALL given tags.
878
+ before (Optional[str]): Cursor for pagination.
879
+ after (Optional[str]): Cursor for pagination.
880
+ limit (Optional[int]): Maximum number of agents to return.
881
+ query_text (Optional[str]): Search agents by name.
882
+ project_id (Optional[str]): Filter by project ID.
883
+ template_id (Optional[str]): Filter by template ID.
884
+ base_template_id (Optional[str]): Filter by base template ID.
885
+ identity_id (Optional[str]): Filter by identifier ID.
886
+ identifier_keys (Optional[List[str]]): Search agents by identifier keys.
887
+ include_relationships (Optional[List[str]]): List of fields to load for performance optimization.
888
+ ascending
889
+
890
+ Returns:
891
+ List[PydanticAgentState]: The filtered list of matching agents.
892
+ """
893
+ async with db_registry.async_session() as session:
894
+ query = select(AgentModel).distinct(AgentModel.created_at, AgentModel.id)
895
+ query = AgentModel.apply_access_predicate(query, actor, ["read"], AccessType.ORGANIZATION)
896
+
897
+ # Apply filters
898
+ query = _apply_filters(query, name, query_text, project_id, template_id, base_template_id)
899
+ query = _apply_identity_filters(query, identity_id, identifier_keys)
900
+ query = _apply_tag_filter(query, tags, match_all_tags)
901
+ query = await _apply_pagination_async(query, before, after, session, ascending=ascending)
902
+
903
+ if limit:
904
+ query = query.limit(limit)
905
+
906
+ result = await session.execute(query)
907
+ agents = result.scalars().all()
908
+ pydantic_agents = []
909
+ for agent in agents:
910
+ pydantic_agent = await agent.to_pydantic_async(include_relationships=include_relationships)
911
+ pydantic_agents.append(pydantic_agent)
912
+
913
+ return pydantic_agents
914
+
520
915
  @enforce_types
521
916
  def list_agents_matching_tags(
522
917
  self,
@@ -577,6 +972,20 @@ class AgentManager:
577
972
  agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
578
973
  return agent.to_pydantic()
579
974
 
975
+ @enforce_types
976
+ async def get_agent_by_id_async(self, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
977
+ """Fetch an agent by its ID."""
978
+ async with db_registry.async_session() as session:
979
+ agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
980
+ return agent.to_pydantic()
981
+
982
+ @enforce_types
983
+ async def get_agents_by_ids_async(self, agent_ids: list[str], actor: PydanticUser) -> list[PydanticAgentState]:
984
+ """Fetch a list of agents by their IDs."""
985
+ async with db_registry.async_session() as session:
986
+ agents = await AgentModel.read_multiple_async(db_session=session, identifiers=agent_ids, actor=actor)
987
+ return [await agent.to_pydantic_async() for agent in agents]
988
+
580
989
  @enforce_types
581
990
  def get_agent_by_name(self, agent_name: str, actor: PydanticUser) -> PydanticAgentState:
582
991
  """Fetch an agent by its ID."""
@@ -784,6 +1193,11 @@ class AgentManager:
784
1193
  message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
785
1194
  return self.message_manager.get_messages_by_ids(message_ids=message_ids, actor=actor)
786
1195
 
1196
+ @enforce_types
1197
+ async def get_in_context_messages_async(self, agent_id: str, actor: PydanticUser) -> List[PydanticMessage]:
1198
+ message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
1199
+ return await self.message_manager.get_messages_by_ids_async(message_ids=message_ids, actor=actor)
1200
+
787
1201
  @enforce_types
788
1202
  def get_system_message(self, agent_id: str, actor: PydanticUser) -> PydanticMessage:
789
1203
  message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
@@ -1,5 +1,6 @@
1
1
  from typing import Dict, List, Optional
2
2
 
3
+ from sqlalchemy import select
3
4
  from sqlalchemy.orm import Session
4
5
 
5
6
  from letta.log import get_logger
@@ -454,7 +455,7 @@ class BlockManager:
454
455
  return block.to_pydantic()
455
456
 
456
457
  @enforce_types
457
- def bulk_update_block_values(
458
+ async def bulk_update_block_values_async(
458
459
  self, updates: Dict[str, str], actor: PydanticUser, return_hydrated: bool = False
459
460
  ) -> Optional[List[PydanticBlock]]:
460
461
  """
@@ -469,12 +470,13 @@ class BlockManager:
469
470
  the updated Block objects as Pydantic schemas
470
471
 
471
472
  Raises:
472
- NoResultFound if any block_id doesnt exist or isnt visible to this actor
473
- ValueError if any new value exceeds its blocks limit
473
+ NoResultFound if any block_id doesn't exist or isn't visible to this actor
474
+ ValueError if any new value exceeds its block's limit
474
475
  """
475
- with db_registry.session() as session:
476
- q = session.query(BlockModel).filter(BlockModel.id.in_(updates.keys()), BlockModel.organization_id == actor.organization_id)
477
- blocks = q.all()
476
+ async with db_registry.async_session() as session:
477
+ query = select(BlockModel).where(BlockModel.id.in_(updates.keys()), BlockModel.organization_id == actor.organization_id)
478
+ result = await session.execute(query)
479
+ blocks = result.scalars().all()
478
480
 
479
481
  found_ids = {b.id for b in blocks}
480
482
  missing = set(updates.keys()) - found_ids
@@ -488,8 +490,10 @@ class BlockManager:
488
490
  new_val = new_val[: block.limit]
489
491
  block.value = new_val
490
492
 
491
- session.commit()
493
+ await session.commit()
492
494
 
493
495
  if return_hydrated:
494
- return [b.to_pydantic() for b in blocks]
496
+ # TODO: implement for async
497
+ pass
498
+
495
499
  return None
@@ -402,6 +402,25 @@ def _apply_pagination(query, before: Optional[str], after: Optional[str], sessio
402
402
  return query
403
403
 
404
404
 
405
+ async def _apply_pagination_async(query, before: Optional[str], after: Optional[str], session, ascending: bool = True) -> any:
406
+ if after:
407
+ result = (await session.execute(select(AgentModel.created_at, AgentModel.id).where(AgentModel.id == after))).first()
408
+ if result:
409
+ after_created_at, after_id = result
410
+ query = query.where(_cursor_filter(AgentModel.created_at, AgentModel.id, after_created_at, after_id, forward=ascending))
411
+
412
+ if before:
413
+ result = (await session.execute(select(AgentModel.created_at, AgentModel.id).where(AgentModel.id == before))).first()
414
+ if result:
415
+ before_created_at, before_id = result
416
+ query = query.where(_cursor_filter(AgentModel.created_at, AgentModel.id, before_created_at, before_id, forward=not ascending))
417
+
418
+ # Apply ordering
419
+ order_fn = asc if ascending else desc
420
+ query = query.order_by(order_fn(AgentModel.created_at), order_fn(AgentModel.id))
421
+ return query
422
+
423
+
405
424
  def _apply_tag_filter(query, tags: Optional[List[str]], match_all_tags: bool):
406
425
  """
407
426
  Apply tag-based filtering to the agent query.