letta-nightly 0.7.13.dev20250511104036__py3-none-any.whl → 0.7.14.dev20250513020711__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 +14 -17
  3. letta/agents/base_agent.py +112 -1
  4. letta/agents/letta_agent.py +35 -55
  5. letta/agents/letta_agent_batch.py +22 -45
  6. letta/agents/voice_agent.py +10 -42
  7. letta/functions/schema_generator.py +7 -3
  8. letta/llm_api/anthropic.py +4 -2
  9. letta/llm_api/openai.py +4 -2
  10. letta/orm/agents_tags.py +5 -2
  11. letta/orm/blocks_agents.py +3 -1
  12. letta/orm/sqlalchemy_base.py +91 -1
  13. letta/schemas/message.py +1 -1
  14. letta/serialize_schemas/marshmallow_agent.py +4 -4
  15. letta/server/db.py +180 -88
  16. letta/server/rest_api/app.py +6 -3
  17. letta/server/rest_api/chat_completions_interface.py +1 -0
  18. letta/server/rest_api/interface.py +54 -16
  19. letta/server/rest_api/routers/v1/sources.py +1 -0
  20. letta/server/server.py +1 -2
  21. letta/services/agent_manager.py +40 -31
  22. letta/services/block_manager.py +61 -34
  23. letta/services/group_manager.py +11 -15
  24. letta/services/identity_manager.py +9 -13
  25. letta/services/job_manager.py +12 -17
  26. letta/services/llm_batch_manager.py +17 -21
  27. letta/services/message_manager.py +53 -31
  28. letta/services/organization_manager.py +7 -14
  29. letta/services/passage_manager.py +6 -10
  30. letta/services/provider_manager.py +5 -9
  31. letta/services/sandbox_config_manager.py +13 -17
  32. letta/services/source_manager.py +13 -17
  33. letta/services/step_manager.py +5 -9
  34. letta/services/tool_manager.py +9 -14
  35. letta/services/user_manager.py +7 -12
  36. letta/settings.py +2 -0
  37. letta/streaming_interface.py +2 -0
  38. letta/utils.py +1 -1
  39. {letta_nightly-0.7.13.dev20250511104036.dist-info → letta_nightly-0.7.14.dev20250513020711.dist-info}/METADATA +2 -1
  40. {letta_nightly-0.7.13.dev20250511104036.dist-info → letta_nightly-0.7.14.dev20250513020711.dist-info}/RECORD +43 -43
  41. {letta_nightly-0.7.13.dev20250511104036.dist-info → letta_nightly-0.7.14.dev20250513020711.dist-info}/LICENSE +0 -0
  42. {letta_nightly-0.7.13.dev20250511104036.dist-info → letta_nightly-0.7.14.dev20250513020711.dist-info}/WHEEL +0 -0
  43. {letta_nightly-0.7.13.dev20250511104036.dist-info → letta_nightly-0.7.14.dev20250513020711.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,3 @@
1
- import os
2
1
  from typing import Dict, List, Optional
3
2
 
4
3
  from sqlalchemy.orm import Session
@@ -10,9 +9,10 @@ from letta.orm.enums import ActorType
10
9
  from letta.orm.errors import NoResultFound
11
10
  from letta.schemas.agent import AgentState as PydanticAgentState
12
11
  from letta.schemas.block import Block as PydanticBlock
13
- from letta.schemas.block import BlockUpdate, Human, Persona
12
+ from letta.schemas.block import BlockUpdate
14
13
  from letta.schemas.user import User as PydanticUser
15
- from letta.utils import enforce_types, list_human_files, list_persona_files
14
+ from letta.server.db import db_registry
15
+ from letta.utils import enforce_types
16
16
 
17
17
  logger = get_logger(__name__)
18
18
 
@@ -20,12 +20,6 @@ logger = get_logger(__name__)
20
20
  class BlockManager:
21
21
  """Manager class to handle business logic related to Blocks."""
22
22
 
23
- def __init__(self):
24
- # Fetching the db_context similarly as in ToolManager
25
- from letta.server.db import db_context
26
-
27
- self.session_maker = db_context
28
-
29
23
  @enforce_types
30
24
  def create_or_update_block(self, block: PydanticBlock, actor: PydanticUser) -> PydanticBlock:
31
25
  """Create a new block based on the Block schema."""
@@ -34,7 +28,7 @@ class BlockManager:
34
28
  update_data = BlockUpdate(**block.model_dump(to_orm=True, exclude_none=True))
35
29
  self.update_block(block.id, update_data, actor)
36
30
  else:
37
- with self.session_maker() as session:
31
+ with db_registry.session() as session:
38
32
  data = block.model_dump(to_orm=True, exclude_none=True)
39
33
  block = BlockModel(**data, organization_id=actor.organization_id)
40
34
  block.create(session, actor=actor)
@@ -53,7 +47,7 @@ class BlockManager:
53
47
  if not blocks:
54
48
  return []
55
49
 
56
- with self.session_maker() as session:
50
+ with db_registry.session() as session:
57
51
  block_models = [
58
52
  BlockModel(**block.model_dump(to_orm=True, exclude_none=True), organization_id=actor.organization_id) for block in blocks
59
53
  ]
@@ -68,7 +62,7 @@ class BlockManager:
68
62
  """Update a block by its ID with the given BlockUpdate object."""
69
63
  # Safety check for block
70
64
 
71
- with self.session_maker() as session:
65
+ with db_registry.session() as session:
72
66
  block = BlockModel.read(db_session=session, identifier=block_id, actor=actor)
73
67
  update_data = block_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
74
68
 
@@ -81,7 +75,7 @@ class BlockManager:
81
75
  @enforce_types
82
76
  def delete_block(self, block_id: str, actor: PydanticUser) -> PydanticBlock:
83
77
  """Delete a block by its ID."""
84
- with self.session_maker() as session:
78
+ with db_registry.session() as session:
85
79
  block = BlockModel.read(db_session=session, identifier=block_id)
86
80
  block.hard_delete(db_session=session, actor=actor)
87
81
  return block.to_pydantic()
@@ -100,7 +94,7 @@ class BlockManager:
100
94
  limit: Optional[int] = 50,
101
95
  ) -> List[PydanticBlock]:
102
96
  """Retrieve blocks based on various optional filters."""
103
- with self.session_maker() as session:
97
+ with db_registry.session() as session:
104
98
  # Prepare filters
105
99
  filters = {"organization_id": actor.organization_id}
106
100
  if label:
@@ -126,7 +120,7 @@ class BlockManager:
126
120
  @enforce_types
127
121
  def get_block_by_id(self, block_id: str, actor: Optional[PydanticUser] = None) -> Optional[PydanticBlock]:
128
122
  """Retrieve a block by its name."""
129
- with self.session_maker() as session:
123
+ with db_registry.session() as session:
130
124
  try:
131
125
  block = BlockModel.read(db_session=session, identifier=block_id, actor=actor)
132
126
  return block.to_pydantic()
@@ -136,32 +130,65 @@ class BlockManager:
136
130
  @enforce_types
137
131
  def get_all_blocks_by_ids(self, block_ids: List[str], actor: Optional[PydanticUser] = None) -> List[PydanticBlock]:
138
132
  """Retrieve blocks by their ids."""
139
- with self.session_maker() as session:
133
+ with db_registry.session() as session:
140
134
  blocks = [block.to_pydantic() for block in BlockModel.read_multiple(db_session=session, identifiers=block_ids, actor=actor)]
141
135
  # backwards compatibility. previous implementation added None for every block not found.
142
136
  blocks.extend([None for _ in range(len(block_ids) - len(blocks))])
143
137
  return blocks
144
138
 
145
139
  @enforce_types
146
- def add_default_blocks(self, actor: PydanticUser):
147
- for persona_file in list_persona_files():
148
- with open(persona_file, "r", encoding="utf-8") as f:
149
- text = f.read()
150
- name = os.path.basename(persona_file).replace(".txt", "")
151
- self.create_or_update_block(Persona(template_name=name, value=text, is_template=True), actor=actor)
152
-
153
- for human_file in list_human_files():
154
- with open(human_file, "r", encoding="utf-8") as f:
155
- text = f.read()
156
- name = os.path.basename(human_file).replace(".txt", "")
157
- self.create_or_update_block(Human(template_name=name, value=text, is_template=True), actor=actor)
140
+ async def get_all_blocks_by_ids_async(self, block_ids: List[str], actor: Optional[PydanticUser] = None) -> List[PydanticBlock]:
141
+ """Retrieve blocks by their ids without loading unnecessary relationships. Async implementation."""
142
+ from sqlalchemy import select
143
+ from sqlalchemy.orm import noload
144
+
145
+ from letta.orm.sqlalchemy_base import AccessType
146
+
147
+ if not block_ids:
148
+ return []
149
+
150
+ async with db_registry.async_session() as session:
151
+ # Start with a basic query
152
+ query = select(BlockModel)
153
+
154
+ # Add ID filter
155
+ query = query.where(BlockModel.id.in_(block_ids))
156
+
157
+ # Explicitly avoid loading relationships
158
+ query = query.options(noload(BlockModel.agents), noload(BlockModel.identities), noload(BlockModel.groups))
159
+
160
+ # Apply access control if actor is provided
161
+ if actor:
162
+ query = BlockModel.apply_access_predicate(query, actor, ["read"], AccessType.ORGANIZATION)
163
+
164
+ # Add soft delete filter if applicable
165
+ if hasattr(BlockModel, "is_deleted"):
166
+ query = query.where(BlockModel.is_deleted == False)
167
+
168
+ # Execute the query
169
+ result = await session.execute(query)
170
+ blocks = result.scalars().all()
171
+
172
+ # Convert to Pydantic models
173
+ pydantic_blocks = [block.to_pydantic() for block in blocks]
174
+
175
+ # For backward compatibility, add None for missing blocks
176
+ if len(pydantic_blocks) < len(block_ids):
177
+ {block.id for block in pydantic_blocks}
178
+ result_blocks = []
179
+ for block_id in block_ids:
180
+ block = next((b for b in pydantic_blocks if b.id == block_id), None)
181
+ result_blocks.append(block)
182
+ return result_blocks
183
+
184
+ return pydantic_blocks
158
185
 
159
186
  @enforce_types
160
187
  def get_agents_for_block(self, block_id: str, actor: PydanticUser) -> List[PydanticAgentState]:
161
188
  """
162
189
  Retrieve all agents associated with a given block.
163
190
  """
164
- with self.session_maker() as session:
191
+ with db_registry.session() as session:
165
192
  block = BlockModel.read(db_session=session, identifier=block_id, actor=actor)
166
193
  agents_orm = block.agents
167
194
  agents_pydantic = [agent.to_pydantic() for agent in agents_orm]
@@ -176,7 +203,7 @@ class BlockManager:
176
203
  """
177
204
  Get the total count of blocks for the given user.
178
205
  """
179
- with self.session_maker() as session:
206
+ with db_registry.session() as session:
180
207
  return BlockModel.size(db_session=session, actor=actor)
181
208
 
182
209
  # Block History Functions
@@ -199,7 +226,7 @@ class BlockManager:
199
226
  strictly linear history.
200
227
  - A single commit at the end ensures atomicity.
201
228
  """
202
- with self.session_maker() as session:
229
+ with db_registry.session() as session:
203
230
  # 1) Load the Block
204
231
  if use_preloaded_block is not None:
205
232
  block = session.merge(use_preloaded_block)
@@ -291,7 +318,7 @@ class BlockManager:
291
318
  If older sequences have been pruned, we jump to the largest sequence
292
319
  number that is still < current_seq.
293
320
  """
294
- with self.session_maker() as session:
321
+ with db_registry.session() as session:
295
322
  # 1) Load the current block
296
323
  block = (
297
324
  session.merge(use_preloaded_block)
@@ -333,7 +360,7 @@ class BlockManager:
333
360
  If some middle checkpoints have been pruned, we jump to the smallest
334
361
  sequence > current_seq that remains.
335
362
  """
336
- with self.session_maker() as session:
363
+ with db_registry.session() as session:
337
364
  block = (
338
365
  session.merge(use_preloaded_block)
339
366
  if use_preloaded_block
@@ -383,7 +410,7 @@ class BlockManager:
383
410
  NoResultFound if any block_id doesn’t exist or isn’t visible to this actor
384
411
  ValueError if any new value exceeds its block’s limit
385
412
  """
386
- with self.session_maker() as session:
413
+ with db_registry.session() as session:
387
414
  q = session.query(BlockModel).filter(BlockModel.id.in_(updates.keys()), BlockModel.organization_id == actor.organization_id)
388
415
  blocks = q.all()
389
416
 
@@ -11,16 +11,12 @@ from letta.schemas.group import GroupCreate, GroupUpdate, ManagerType
11
11
  from letta.schemas.letta_message import LettaMessage
12
12
  from letta.schemas.message import Message as PydanticMessage
13
13
  from letta.schemas.user import User as PydanticUser
14
+ from letta.server.db import db_registry
14
15
  from letta.utils import enforce_types
15
16
 
16
17
 
17
18
  class GroupManager:
18
19
 
19
- def __init__(self):
20
- from letta.server.db import db_context
21
-
22
- self.session_maker = db_context
23
-
24
20
  @enforce_types
25
21
  def list_groups(
26
22
  self,
@@ -31,7 +27,7 @@ class GroupManager:
31
27
  after: Optional[str] = None,
32
28
  limit: Optional[int] = 50,
33
29
  ) -> list[PydanticGroup]:
34
- with self.session_maker() as session:
30
+ with db_registry.session() as session:
35
31
  filters = {"organization_id": actor.organization_id}
36
32
  if project_id:
37
33
  filters["project_id"] = project_id
@@ -48,13 +44,13 @@ class GroupManager:
48
44
 
49
45
  @enforce_types
50
46
  def retrieve_group(self, group_id: str, actor: PydanticUser) -> PydanticGroup:
51
- with self.session_maker() as session:
47
+ with db_registry.session() as session:
52
48
  group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
53
49
  return group.to_pydantic()
54
50
 
55
51
  @enforce_types
56
52
  def create_group(self, group: GroupCreate, actor: PydanticUser) -> PydanticGroup:
57
- with self.session_maker() as session:
53
+ with db_registry.session() as session:
58
54
  new_group = GroupModel()
59
55
  new_group.organization_id = actor.organization_id
60
56
  new_group.description = group.description
@@ -99,7 +95,7 @@ class GroupManager:
99
95
 
100
96
  @enforce_types
101
97
  def modify_group(self, group_id: str, group_update: GroupUpdate, actor: PydanticUser) -> PydanticGroup:
102
- with self.session_maker() as session:
98
+ with db_registry.session() as session:
103
99
  group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
104
100
 
105
101
  sleeptime_agent_frequency = None
@@ -161,7 +157,7 @@ class GroupManager:
161
157
 
162
158
  @enforce_types
163
159
  def delete_group(self, group_id: str, actor: PydanticUser) -> None:
164
- with self.session_maker() as session:
160
+ with db_registry.session() as session:
165
161
  # Retrieve the agent
166
162
  group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
167
163
  group.hard_delete(session)
@@ -178,7 +174,7 @@ class GroupManager:
178
174
  assistant_message_tool_name: str = "send_message",
179
175
  assistant_message_tool_kwarg: str = "message",
180
176
  ) -> list[LettaMessage]:
181
- with self.session_maker() as session:
177
+ with db_registry.session() as session:
182
178
  filters = {
183
179
  "organization_id": actor.organization_id,
184
180
  "group_id": group_id,
@@ -204,7 +200,7 @@ class GroupManager:
204
200
 
205
201
  @enforce_types
206
202
  def reset_messages(self, group_id: str, actor: PydanticUser) -> None:
207
- with self.session_maker() as session:
203
+ with db_registry.session() as session:
208
204
  # Ensure group is loadable by user
209
205
  group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
210
206
 
@@ -217,7 +213,7 @@ class GroupManager:
217
213
 
218
214
  @enforce_types
219
215
  def bump_turns_counter(self, group_id: str, actor: PydanticUser) -> int:
220
- with self.session_maker() as session:
216
+ with db_registry.session() as session:
221
217
  # Ensure group is loadable by user
222
218
  group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
223
219
 
@@ -228,7 +224,7 @@ class GroupManager:
228
224
 
229
225
  @enforce_types
230
226
  def get_last_processed_message_id_and_update(self, group_id: str, last_processed_message_id: str, actor: PydanticUser) -> str:
231
- with self.session_maker() as session:
227
+ with db_registry.session() as session:
232
228
  # Ensure group is loadable by user
233
229
  group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
234
230
 
@@ -247,7 +243,7 @@ class GroupManager:
247
243
  """
248
244
  Get the total count of groups for the given user.
249
245
  """
250
- with self.session_maker() as session:
246
+ with db_registry.session() as session:
251
247
  return GroupModel.size(db_session=session, actor=actor)
252
248
 
253
249
  def _process_agent_relationship(self, session: Session, group: GroupModel, agent_ids: List[str], allow_partial=False, replace=True):
@@ -10,16 +10,12 @@ from letta.orm.identity import Identity as IdentityModel
10
10
  from letta.schemas.identity import Identity as PydanticIdentity
11
11
  from letta.schemas.identity import IdentityCreate, IdentityProperty, IdentityType, IdentityUpdate, IdentityUpsert
12
12
  from letta.schemas.user import User as PydanticUser
13
+ from letta.server.db import db_registry
13
14
  from letta.utils import enforce_types
14
15
 
15
16
 
16
17
  class IdentityManager:
17
18
 
18
- def __init__(self):
19
- from letta.server.db import db_context
20
-
21
- self.session_maker = db_context
22
-
23
19
  @enforce_types
24
20
  def list_identities(
25
21
  self,
@@ -32,7 +28,7 @@ class IdentityManager:
32
28
  limit: Optional[int] = 50,
33
29
  actor: PydanticUser = None,
34
30
  ) -> list[PydanticIdentity]:
35
- with self.session_maker() as session:
31
+ with db_registry.session() as session:
36
32
  filters = {"organization_id": actor.organization_id}
37
33
  if project_id:
38
34
  filters["project_id"] = project_id
@@ -52,13 +48,13 @@ class IdentityManager:
52
48
 
53
49
  @enforce_types
54
50
  def get_identity(self, identity_id: str, actor: PydanticUser) -> PydanticIdentity:
55
- with self.session_maker() as session:
51
+ with db_registry.session() as session:
56
52
  identity = IdentityModel.read(db_session=session, identifier=identity_id, actor=actor)
57
53
  return identity.to_pydantic()
58
54
 
59
55
  @enforce_types
60
56
  def create_identity(self, identity: IdentityCreate, actor: PydanticUser) -> PydanticIdentity:
61
- with self.session_maker() as session:
57
+ with db_registry.session() as session:
62
58
  new_identity = IdentityModel(**identity.model_dump(exclude={"agent_ids", "block_ids"}, exclude_unset=True))
63
59
  new_identity.organization_id = actor.organization_id
64
60
  self._process_relationship(
@@ -82,7 +78,7 @@ class IdentityManager:
82
78
 
83
79
  @enforce_types
84
80
  def upsert_identity(self, identity: IdentityUpsert, actor: PydanticUser) -> PydanticIdentity:
85
- with self.session_maker() as session:
81
+ with db_registry.session() as session:
86
82
  existing_identity = IdentityModel.read(
87
83
  db_session=session,
88
84
  identifier_key=identity.identifier_key,
@@ -107,7 +103,7 @@ class IdentityManager:
107
103
 
108
104
  @enforce_types
109
105
  def update_identity(self, identity_id: str, identity: IdentityUpdate, actor: PydanticUser, replace: bool = False) -> PydanticIdentity:
110
- with self.session_maker() as session:
106
+ with db_registry.session() as session:
111
107
  try:
112
108
  existing_identity = IdentityModel.read(db_session=session, identifier=identity_id, actor=actor)
113
109
  except NoResultFound:
@@ -167,7 +163,7 @@ class IdentityManager:
167
163
 
168
164
  @enforce_types
169
165
  def upsert_identity_properties(self, identity_id: str, properties: List[IdentityProperty], actor: PydanticUser) -> PydanticIdentity:
170
- with self.session_maker() as session:
166
+ with db_registry.session() as session:
171
167
  existing_identity = IdentityModel.read(db_session=session, identifier=identity_id, actor=actor)
172
168
  if existing_identity is None:
173
169
  raise HTTPException(status_code=404, detail="Identity not found")
@@ -181,7 +177,7 @@ class IdentityManager:
181
177
 
182
178
  @enforce_types
183
179
  def delete_identity(self, identity_id: str, actor: PydanticUser) -> None:
184
- with self.session_maker() as session:
180
+ with db_registry.session() as session:
185
181
  identity = IdentityModel.read(db_session=session, identifier=identity_id)
186
182
  if identity is None:
187
183
  raise HTTPException(status_code=404, detail="Identity not found")
@@ -198,7 +194,7 @@ class IdentityManager:
198
194
  """
199
195
  Get the total count of identities for the given user.
200
196
  """
201
- with self.session_maker() as session:
197
+ with db_registry.session() as session:
202
198
  return IdentityModel.size(db_session=session, actor=actor)
203
199
 
204
200
  def _process_relationship(
@@ -24,24 +24,19 @@ from letta.schemas.run import Run as PydanticRun
24
24
  from letta.schemas.step import Step as PydanticStep
25
25
  from letta.schemas.usage import LettaUsageStatistics
26
26
  from letta.schemas.user import User as PydanticUser
27
+ from letta.server.db import db_registry
27
28
  from letta.utils import enforce_types
28
29
 
29
30
 
30
31
  class JobManager:
31
32
  """Manager class to handle business logic related to Jobs."""
32
33
 
33
- def __init__(self):
34
- # Fetching the db_context similarly as in OrganizationManager
35
- from letta.server.db import db_context
36
-
37
- self.session_maker = db_context
38
-
39
34
  @enforce_types
40
35
  def create_job(
41
36
  self, pydantic_job: Union[PydanticJob, PydanticRun, PydanticBatchJob], actor: PydanticUser
42
37
  ) -> Union[PydanticJob, PydanticRun, PydanticBatchJob]:
43
38
  """Create a new job based on the JobCreate schema."""
44
- with self.session_maker() as session:
39
+ with db_registry.session() as session:
45
40
  # Associate the job with the user
46
41
  pydantic_job.user_id = actor.id
47
42
  job_data = pydantic_job.model_dump(to_orm=True)
@@ -52,7 +47,7 @@ class JobManager:
52
47
  @enforce_types
53
48
  def update_job_by_id(self, job_id: str, job_update: JobUpdate, actor: PydanticUser) -> PydanticJob:
54
49
  """Update a job by its ID with the given JobUpdate object."""
55
- with self.session_maker() as session:
50
+ with db_registry.session() as session:
56
51
  # Fetch the job by ID
57
52
  job = self._verify_job_access(session=session, job_id=job_id, actor=actor, access=["write"])
58
53
 
@@ -76,7 +71,7 @@ class JobManager:
76
71
  @enforce_types
77
72
  def get_job_by_id(self, job_id: str, actor: PydanticUser) -> PydanticJob:
78
73
  """Fetch a job by its ID."""
79
- with self.session_maker() as session:
74
+ with db_registry.session() as session:
80
75
  # Retrieve job by ID using the Job model's read method
81
76
  job = JobModel.read(db_session=session, identifier=job_id, actor=actor, access_type=AccessType.USER)
82
77
  return job.to_pydantic()
@@ -93,7 +88,7 @@ class JobManager:
93
88
  ascending: bool = True,
94
89
  ) -> List[PydanticJob]:
95
90
  """List all jobs with optional pagination and status filter."""
96
- with self.session_maker() as session:
91
+ with db_registry.session() as session:
97
92
  filter_kwargs = {"user_id": actor.id, "job_type": job_type}
98
93
 
99
94
  # Add status filter if provided
@@ -113,7 +108,7 @@ class JobManager:
113
108
  @enforce_types
114
109
  def delete_job_by_id(self, job_id: str, actor: PydanticUser) -> PydanticJob:
115
110
  """Delete a job by its ID."""
116
- with self.session_maker() as session:
111
+ with db_registry.session() as session:
117
112
  job = self._verify_job_access(session=session, job_id=job_id, actor=actor)
118
113
  job.hard_delete(db_session=session, actor=actor)
119
114
  return job.to_pydantic()
@@ -147,7 +142,7 @@ class JobManager:
147
142
  Raises:
148
143
  NoResultFound: If the job does not exist or user does not have access
149
144
  """
150
- with self.session_maker() as session:
145
+ with db_registry.session() as session:
151
146
  # Build filters
152
147
  filters = {}
153
148
  if role is not None:
@@ -195,7 +190,7 @@ class JobManager:
195
190
  Raises:
196
191
  NoResultFound: If the job does not exist or user does not have access
197
192
  """
198
- with self.session_maker() as session:
193
+ with db_registry.session() as session:
199
194
  # Build filters
200
195
  filters = {}
201
196
  filters["job_id"] = job_id
@@ -227,7 +222,7 @@ class JobManager:
227
222
  Raises:
228
223
  NoResultFound: If the job does not exist or user does not have access
229
224
  """
230
- with self.session_maker() as session:
225
+ with db_registry.session() as session:
231
226
  # First verify job exists and user has access
232
227
  self._verify_job_access(session, job_id, actor, access=["write"])
233
228
 
@@ -251,7 +246,7 @@ class JobManager:
251
246
  Raises:
252
247
  NoResultFound: If the job does not exist or user does not have access
253
248
  """
254
- with self.session_maker() as session:
249
+ with db_registry.session() as session:
255
250
  # First verify job exists and user has access
256
251
  self._verify_job_access(session, job_id, actor)
257
252
 
@@ -293,7 +288,7 @@ class JobManager:
293
288
  Raises:
294
289
  NoResultFound: If the job does not exist or user does not have access
295
290
  """
296
- with self.session_maker() as session:
291
+ with db_registry.session() as session:
297
292
  # First verify job exists and user has access
298
293
  self._verify_job_access(session, job_id, actor, access=["write"])
299
294
 
@@ -453,7 +448,7 @@ class JobManager:
453
448
  Returns:
454
449
  The request config for the job
455
450
  """
456
- with self.session_maker() as session:
451
+ with db_registry.session() as session:
457
452
  job = session.query(JobModel).filter(JobModel.id == run_id).first()
458
453
  request_config = job.request_config or LettaRequestConfig()
459
454
  return request_config
@@ -16,6 +16,7 @@ from letta.schemas.llm_batch_job import LLMBatchJob as PydanticLLMBatchJob
16
16
  from letta.schemas.llm_config import LLMConfig
17
17
  from letta.schemas.message import Message as PydanticMessage
18
18
  from letta.schemas.user import User as PydanticUser
19
+ from letta.server.db import db_registry
19
20
  from letta.utils import enforce_types
20
21
 
21
22
  logger = get_logger(__name__)
@@ -24,11 +25,6 @@ logger = get_logger(__name__)
24
25
  class LLMBatchManager:
25
26
  """Manager for handling both LLMBatchJob and LLMBatchItem operations."""
26
27
 
27
- def __init__(self):
28
- from letta.server.db import db_context
29
-
30
- self.session_maker = db_context
31
-
32
28
  @enforce_types
33
29
  def create_llm_batch_job(
34
30
  self,
@@ -39,7 +35,7 @@ class LLMBatchManager:
39
35
  status: JobStatus = JobStatus.created,
40
36
  ) -> PydanticLLMBatchJob:
41
37
  """Create a new LLM batch job."""
42
- with self.session_maker() as session:
38
+ with db_registry.session() as session:
43
39
  batch = LLMBatchJob(
44
40
  status=status,
45
41
  llm_provider=llm_provider,
@@ -53,7 +49,7 @@ class LLMBatchManager:
53
49
  @enforce_types
54
50
  def get_llm_batch_job_by_id(self, llm_batch_id: str, actor: Optional[PydanticUser] = None) -> PydanticLLMBatchJob:
55
51
  """Retrieve a single batch job by ID."""
56
- with self.session_maker() as session:
52
+ with db_registry.session() as session:
57
53
  batch = LLMBatchJob.read(db_session=session, identifier=llm_batch_id, actor=actor)
58
54
  return batch.to_pydantic()
59
55
 
@@ -66,7 +62,7 @@ class LLMBatchManager:
66
62
  latest_polling_response: Optional[BetaMessageBatch] = None,
67
63
  ) -> PydanticLLMBatchJob:
68
64
  """Update a batch job’s status and optionally its polling response."""
69
- with self.session_maker() as session:
65
+ with db_registry.session() as session:
70
66
  batch = LLMBatchJob.read(db_session=session, identifier=llm_batch_id, actor=actor)
71
67
  batch.status = status
72
68
  batch.latest_polling_response = latest_polling_response
@@ -85,7 +81,7 @@ class LLMBatchManager:
85
81
  """
86
82
  now = datetime.datetime.now(datetime.timezone.utc)
87
83
 
88
- with self.session_maker() as session:
84
+ with db_registry.session() as session:
89
85
  mappings = []
90
86
  for llm_batch_id, status, response in updates:
91
87
  mappings.append(
@@ -119,7 +115,7 @@ class LLMBatchManager:
119
115
 
120
116
  The results are ordered by their id in ascending order.
121
117
  """
122
- with self.session_maker() as session:
118
+ with db_registry.session() as session:
123
119
  query = session.query(LLMBatchJob).filter(LLMBatchJob.letta_batch_job_id == letta_batch_id)
124
120
 
125
121
  if actor is not None:
@@ -140,7 +136,7 @@ class LLMBatchManager:
140
136
  @enforce_types
141
137
  def delete_llm_batch_request(self, llm_batch_id: str, actor: PydanticUser) -> None:
142
138
  """Hard delete a batch job by ID."""
143
- with self.session_maker() as session:
139
+ with db_registry.session() as session:
144
140
  batch = LLMBatchJob.read(db_session=session, identifier=llm_batch_id, actor=actor)
145
141
  batch.hard_delete(db_session=session, actor=actor)
146
142
 
@@ -158,7 +154,7 @@ class LLMBatchManager:
158
154
  Retrieve messages across all LLM batch jobs associated with a Letta batch job.
159
155
  Optimized for PostgreSQL performance using ID-based keyset pagination.
160
156
  """
161
- with self.session_maker() as session:
157
+ with db_registry.session() as session:
162
158
  # If cursor is provided, get sequence_id for that message
163
159
  cursor_sequence_id = None
164
160
  if cursor:
@@ -203,7 +199,7 @@ class LLMBatchManager:
203
199
  @enforce_types
204
200
  def list_running_llm_batches(self, actor: Optional[PydanticUser] = None) -> List[PydanticLLMBatchJob]:
205
201
  """Return all running LLM batch jobs, optionally filtered by actor's organization."""
206
- with self.session_maker() as session:
202
+ with db_registry.session() as session:
207
203
  query = session.query(LLMBatchJob).filter(LLMBatchJob.status == JobStatus.running)
208
204
 
209
205
  if actor is not None:
@@ -224,7 +220,7 @@ class LLMBatchManager:
224
220
  step_state: Optional[AgentStepState] = None,
225
221
  ) -> PydanticLLMBatchItem:
226
222
  """Create a new batch item."""
227
- with self.session_maker() as session:
223
+ with db_registry.session() as session:
228
224
  item = LLMBatchItem(
229
225
  llm_batch_id=llm_batch_id,
230
226
  agent_id=agent_id,
@@ -249,7 +245,7 @@ class LLMBatchManager:
249
245
  Returns:
250
246
  List of created batch items as Pydantic models
251
247
  """
252
- with self.session_maker() as session:
248
+ with db_registry.session() as session:
253
249
  # Convert Pydantic models to ORM objects
254
250
  orm_items = []
255
251
  for item in llm_batch_items:
@@ -274,7 +270,7 @@ class LLMBatchManager:
274
270
  @enforce_types
275
271
  def get_llm_batch_item_by_id(self, item_id: str, actor: PydanticUser) -> PydanticLLMBatchItem:
276
272
  """Retrieve a single batch item by ID."""
277
- with self.session_maker() as session:
273
+ with db_registry.session() as session:
278
274
  item = LLMBatchItem.read(db_session=session, identifier=item_id, actor=actor)
279
275
  return item.to_pydantic()
280
276
 
@@ -289,7 +285,7 @@ class LLMBatchManager:
289
285
  step_state: Optional[AgentStepState] = None,
290
286
  ) -> PydanticLLMBatchItem:
291
287
  """Update fields on a batch item."""
292
- with self.session_maker() as session:
288
+ with db_registry.session() as session:
293
289
  item = LLMBatchItem.read(db_session=session, identifier=item_id, actor=actor)
294
290
 
295
291
  if request_status:
@@ -325,7 +321,7 @@ class LLMBatchManager:
325
321
 
326
322
  The results are ordered by their id in ascending order.
327
323
  """
328
- with self.session_maker() as session:
324
+ with db_registry.session() as session:
329
325
  query = session.query(LLMBatchItem).filter(LLMBatchItem.llm_batch_id == llm_batch_id)
330
326
 
331
327
  if actor is not None:
@@ -367,7 +363,7 @@ class LLMBatchManager:
367
363
  if len(llm_batch_id_agent_id_pairs) != len(field_updates):
368
364
  raise ValueError("llm_batch_id_agent_id_pairs and field_updates must have the same length")
369
365
 
370
- with self.session_maker() as session:
366
+ with db_registry.session() as session:
371
367
  # Lookup primary keys for all requested (batch_id, agent_id) pairs
372
368
  items = (
373
369
  session.query(LLMBatchItem.id, LLMBatchItem.llm_batch_id, LLMBatchItem.agent_id)
@@ -434,7 +430,7 @@ class LLMBatchManager:
434
430
  @enforce_types
435
431
  def delete_llm_batch_item(self, item_id: str, actor: PydanticUser) -> None:
436
432
  """Hard delete a batch item by ID."""
437
- with self.session_maker() as session:
433
+ with db_registry.session() as session:
438
434
  item = LLMBatchItem.read(db_session=session, identifier=item_id, actor=actor)
439
435
  item.hard_delete(db_session=session, actor=actor)
440
436
 
@@ -449,6 +445,6 @@ class LLMBatchManager:
449
445
  Returns:
450
446
  int: The total number of batch items associated with the given llm_batch_id.
451
447
  """
452
- with self.session_maker() as session:
448
+ with db_registry.session() as session:
453
449
  count = session.query(func.count(LLMBatchItem.id)).filter(LLMBatchItem.llm_batch_id == llm_batch_id).scalar()
454
450
  return count or 0