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