letta-nightly 0.7.13.dev20250512104305__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.
- letta/__init__.py +1 -1
- letta/agent.py +14 -17
- letta/agents/base_agent.py +112 -1
- letta/agents/letta_agent.py +35 -55
- letta/agents/letta_agent_batch.py +22 -45
- letta/agents/voice_agent.py +10 -42
- letta/functions/schema_generator.py +7 -3
- letta/llm_api/anthropic.py +4 -2
- letta/llm_api/openai.py +4 -2
- letta/orm/agents_tags.py +5 -2
- letta/orm/blocks_agents.py +3 -1
- letta/orm/sqlalchemy_base.py +91 -1
- letta/schemas/message.py +1 -1
- letta/serialize_schemas/marshmallow_agent.py +4 -4
- letta/server/db.py +180 -88
- letta/server/rest_api/app.py +6 -3
- letta/server/rest_api/chat_completions_interface.py +1 -0
- letta/server/rest_api/interface.py +54 -16
- letta/server/rest_api/routers/v1/sources.py +1 -0
- letta/server/server.py +1 -2
- letta/services/agent_manager.py +40 -31
- letta/services/block_manager.py +61 -34
- letta/services/group_manager.py +11 -15
- letta/services/identity_manager.py +9 -13
- letta/services/job_manager.py +12 -17
- letta/services/llm_batch_manager.py +17 -21
- letta/services/message_manager.py +53 -31
- letta/services/organization_manager.py +7 -14
- letta/services/passage_manager.py +6 -10
- letta/services/provider_manager.py +5 -9
- letta/services/sandbox_config_manager.py +13 -17
- letta/services/source_manager.py +13 -17
- letta/services/step_manager.py +5 -9
- letta/services/tool_manager.py +9 -14
- letta/services/user_manager.py +7 -12
- letta/settings.py +2 -0
- letta/streaming_interface.py +2 -0
- letta/utils.py +1 -1
- {letta_nightly-0.7.13.dev20250512104305.dist-info → letta_nightly-0.7.14.dev20250513020711.dist-info}/METADATA +2 -1
- {letta_nightly-0.7.13.dev20250512104305.dist-info → letta_nightly-0.7.14.dev20250513020711.dist-info}/RECORD +43 -43
- {letta_nightly-0.7.13.dev20250512104305.dist-info → letta_nightly-0.7.14.dev20250513020711.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.13.dev20250512104305.dist-info → letta_nightly-0.7.14.dev20250513020711.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.13.dev20250512104305.dist-info → letta_nightly-0.7.14.dev20250513020711.dist-info}/entry_points.txt +0 -0
letta/services/block_manager.py
CHANGED
@@ -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
|
12
|
+
from letta.schemas.block import BlockUpdate
|
14
13
|
from letta.schemas.user import User as PydanticUser
|
15
|
-
from letta.
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
|
letta/services/group_manager.py
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
197
|
+
with db_registry.session() as session:
|
202
198
|
return IdentityModel.size(db_session=session, actor=actor)
|
203
199
|
|
204
200
|
def _process_relationship(
|
letta/services/job_manager.py
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|