letta-nightly 0.6.48.dev20250407104216__py3-none-any.whl → 0.6.49.dev20250408104230__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.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +1 -1
- letta/agent.py +47 -12
- letta/agents/base_agent.py +7 -4
- letta/agents/helpers.py +52 -0
- letta/agents/letta_agent.py +105 -42
- letta/agents/voice_agent.py +2 -2
- letta/constants.py +13 -1
- letta/errors.py +10 -3
- letta/functions/function_sets/base.py +65 -0
- letta/functions/interface.py +2 -2
- letta/functions/mcp_client/base_client.py +18 -1
- letta/{dynamic_multi_agent.py → groups/dynamic_multi_agent.py} +3 -0
- letta/groups/helpers.py +113 -0
- letta/{round_robin_multi_agent.py → groups/round_robin_multi_agent.py} +2 -0
- letta/groups/sleeptime_multi_agent.py +259 -0
- letta/{supervisor_multi_agent.py → groups/supervisor_multi_agent.py} +1 -0
- letta/helpers/converters.py +109 -7
- letta/helpers/message_helper.py +1 -0
- letta/helpers/tool_rule_solver.py +40 -23
- letta/interface.py +12 -5
- letta/interfaces/anthropic_streaming_interface.py +329 -0
- letta/llm_api/anthropic.py +12 -1
- letta/llm_api/anthropic_client.py +65 -14
- letta/llm_api/azure_openai.py +2 -2
- letta/llm_api/google_ai_client.py +13 -2
- letta/llm_api/google_constants.py +3 -0
- letta/llm_api/google_vertex_client.py +2 -2
- letta/llm_api/llm_api_tools.py +1 -1
- letta/llm_api/llm_client.py +7 -0
- letta/llm_api/llm_client_base.py +2 -7
- letta/llm_api/openai.py +7 -1
- letta/llm_api/openai_client.py +250 -0
- letta/orm/__init__.py +4 -0
- letta/orm/agent.py +6 -0
- letta/orm/block.py +32 -2
- letta/orm/block_history.py +46 -0
- letta/orm/custom_columns.py +60 -0
- letta/orm/enums.py +7 -0
- letta/orm/group.py +6 -0
- letta/orm/groups_blocks.py +13 -0
- letta/orm/llm_batch_items.py +55 -0
- letta/orm/llm_batch_job.py +48 -0
- letta/orm/message.py +7 -1
- letta/orm/organization.py +2 -0
- letta/orm/sqlalchemy_base.py +18 -15
- letta/prompts/system/memgpt_sleeptime_chat.txt +52 -0
- letta/prompts/system/sleeptime.txt +26 -0
- letta/schemas/agent.py +13 -1
- letta/schemas/enums.py +17 -2
- letta/schemas/group.py +14 -1
- letta/schemas/letta_message.py +5 -3
- letta/schemas/llm_batch_job.py +53 -0
- letta/schemas/llm_config.py +14 -4
- letta/schemas/message.py +44 -0
- letta/schemas/tool.py +3 -0
- letta/schemas/usage.py +1 -0
- letta/server/db.py +2 -0
- letta/server/rest_api/app.py +1 -1
- letta/server/rest_api/chat_completions_interface.py +8 -3
- letta/server/rest_api/interface.py +36 -7
- letta/server/rest_api/routers/v1/agents.py +53 -39
- letta/server/rest_api/routers/v1/runs.py +14 -2
- letta/server/rest_api/utils.py +15 -4
- letta/server/server.py +120 -71
- letta/services/agent_manager.py +70 -6
- letta/services/block_manager.py +190 -2
- letta/services/group_manager.py +68 -0
- letta/services/helpers/agent_manager_helper.py +6 -4
- letta/services/llm_batch_manager.py +139 -0
- letta/services/message_manager.py +17 -31
- letta/services/tool_executor/tool_execution_sandbox.py +1 -3
- letta/services/tool_executor/tool_executor.py +9 -20
- letta/services/tool_manager.py +14 -3
- letta/services/tool_sandbox/__init__.py +0 -0
- letta/services/tool_sandbox/base.py +188 -0
- letta/services/tool_sandbox/e2b_sandbox.py +116 -0
- letta/services/tool_sandbox/local_sandbox.py +221 -0
- letta/sleeptime_agent.py +61 -0
- letta/streaming_interface.py +20 -10
- letta/utils.py +4 -0
- {letta_nightly-0.6.48.dev20250407104216.dist-info → letta_nightly-0.6.49.dev20250408104230.dist-info}/METADATA +2 -2
- {letta_nightly-0.6.48.dev20250407104216.dist-info → letta_nightly-0.6.49.dev20250408104230.dist-info}/RECORD +85 -69
- letta/offline_memory_agent.py +0 -173
- letta/services/tool_executor/async_tool_execution_sandbox.py +0 -397
- {letta_nightly-0.6.48.dev20250407104216.dist-info → letta_nightly-0.6.49.dev20250408104230.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.48.dev20250407104216.dist-info → letta_nightly-0.6.49.dev20250408104230.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.48.dev20250407104216.dist-info → letta_nightly-0.6.49.dev20250408104230.dist-info}/entry_points.txt +0 -0
letta/services/block_manager.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from typing import List, Optional
|
|
3
3
|
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
|
|
4
6
|
from letta.orm.block import Block as BlockModel
|
|
7
|
+
from letta.orm.block_history import BlockHistory
|
|
8
|
+
from letta.orm.enums import ActorType
|
|
5
9
|
from letta.orm.errors import NoResultFound
|
|
6
10
|
from letta.schemas.agent import AgentState as PydanticAgentState
|
|
7
|
-
from letta.schemas.block import Block
|
|
8
11
|
from letta.schemas.block import Block as PydanticBlock
|
|
9
12
|
from letta.schemas.block import BlockUpdate, Human, Persona
|
|
10
13
|
from letta.schemas.user import User as PydanticUser
|
|
@@ -21,7 +24,7 @@ class BlockManager:
|
|
|
21
24
|
self.session_maker = db_context
|
|
22
25
|
|
|
23
26
|
@enforce_types
|
|
24
|
-
def create_or_update_block(self, block:
|
|
27
|
+
def create_or_update_block(self, block: PydanticBlock, actor: PydanticUser) -> PydanticBlock:
|
|
25
28
|
"""Create a new block based on the Block schema."""
|
|
26
29
|
db_block = self.get_block_by_id(block.id, actor)
|
|
27
30
|
if db_block:
|
|
@@ -140,3 +143,188 @@ class BlockManager:
|
|
|
140
143
|
agents_pydantic = [agent.to_pydantic() for agent in agents_orm]
|
|
141
144
|
|
|
142
145
|
return agents_pydantic
|
|
146
|
+
|
|
147
|
+
# Block History Functions
|
|
148
|
+
|
|
149
|
+
@enforce_types
|
|
150
|
+
def checkpoint_block(
|
|
151
|
+
self,
|
|
152
|
+
block_id: str,
|
|
153
|
+
actor: PydanticUser,
|
|
154
|
+
agent_id: Optional[str] = None,
|
|
155
|
+
use_preloaded_block: Optional[BlockModel] = None, # For concurrency tests
|
|
156
|
+
) -> PydanticBlock:
|
|
157
|
+
"""
|
|
158
|
+
Create a new checkpoint for the given Block by copying its
|
|
159
|
+
current state into BlockHistory, using SQLAlchemy's built-in
|
|
160
|
+
version_id_col for concurrency checks.
|
|
161
|
+
|
|
162
|
+
- If the block was undone to an earlier checkpoint, we remove
|
|
163
|
+
any "future" checkpoints beyond the current state to keep a
|
|
164
|
+
strictly linear history.
|
|
165
|
+
- A single commit at the end ensures atomicity.
|
|
166
|
+
"""
|
|
167
|
+
with self.session_maker() as session:
|
|
168
|
+
# 1) Load the Block
|
|
169
|
+
if use_preloaded_block is not None:
|
|
170
|
+
block = session.merge(use_preloaded_block)
|
|
171
|
+
else:
|
|
172
|
+
block = BlockModel.read(db_session=session, identifier=block_id, actor=actor)
|
|
173
|
+
|
|
174
|
+
# 2) Identify the block's current checkpoint (if any)
|
|
175
|
+
current_entry = None
|
|
176
|
+
if block.current_history_entry_id:
|
|
177
|
+
current_entry = session.get(BlockHistory, block.current_history_entry_id)
|
|
178
|
+
|
|
179
|
+
# The current sequence, or 0 if no checkpoints exist
|
|
180
|
+
current_seq = current_entry.sequence_number if current_entry else 0
|
|
181
|
+
|
|
182
|
+
# 3) Truncate any future checkpoints
|
|
183
|
+
# If we are at seq=2, but there's a seq=3 or higher from a prior "redo chain",
|
|
184
|
+
# remove those, so we maintain a strictly linear undo/redo stack.
|
|
185
|
+
session.query(BlockHistory).filter(BlockHistory.block_id == block.id, BlockHistory.sequence_number > current_seq).delete()
|
|
186
|
+
|
|
187
|
+
# 4) Determine the next sequence number
|
|
188
|
+
next_seq = current_seq + 1
|
|
189
|
+
|
|
190
|
+
# 5) Create a new BlockHistory row reflecting the block's current state
|
|
191
|
+
history_entry = BlockHistory(
|
|
192
|
+
organization_id=actor.organization_id,
|
|
193
|
+
block_id=block.id,
|
|
194
|
+
sequence_number=next_seq,
|
|
195
|
+
description=block.description,
|
|
196
|
+
label=block.label,
|
|
197
|
+
value=block.value,
|
|
198
|
+
limit=block.limit,
|
|
199
|
+
metadata_=block.metadata_,
|
|
200
|
+
actor_type=ActorType.LETTA_AGENT if agent_id else ActorType.LETTA_USER,
|
|
201
|
+
actor_id=agent_id if agent_id else actor.id,
|
|
202
|
+
)
|
|
203
|
+
history_entry.create(session, actor=actor, no_commit=True)
|
|
204
|
+
|
|
205
|
+
# 6) Update the block’s pointer to the new checkpoint
|
|
206
|
+
block.current_history_entry_id = history_entry.id
|
|
207
|
+
|
|
208
|
+
# 7) Flush changes, then commit once
|
|
209
|
+
block = block.update(db_session=session, actor=actor, no_commit=True)
|
|
210
|
+
session.commit()
|
|
211
|
+
|
|
212
|
+
return block.to_pydantic()
|
|
213
|
+
|
|
214
|
+
@enforce_types
|
|
215
|
+
def _move_block_to_sequence(self, session: Session, block: BlockModel, target_seq: int, actor: PydanticUser) -> BlockModel:
|
|
216
|
+
"""
|
|
217
|
+
Internal helper that moves the 'block' to the specified 'target_seq' within BlockHistory.
|
|
218
|
+
1) Find the BlockHistory row at sequence_number=target_seq
|
|
219
|
+
2) Copy fields into the block
|
|
220
|
+
3) Update and flush (no_commit=True) - the caller is responsible for final commit
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
NoResultFound: if no BlockHistory row for (block_id, target_seq)
|
|
224
|
+
"""
|
|
225
|
+
if not block.id:
|
|
226
|
+
raise ValueError("Block is missing an ID. Cannot move sequence.")
|
|
227
|
+
|
|
228
|
+
target_entry = (
|
|
229
|
+
session.query(BlockHistory)
|
|
230
|
+
.filter(
|
|
231
|
+
BlockHistory.block_id == block.id,
|
|
232
|
+
BlockHistory.sequence_number == target_seq,
|
|
233
|
+
)
|
|
234
|
+
.one_or_none()
|
|
235
|
+
)
|
|
236
|
+
if not target_entry:
|
|
237
|
+
raise NoResultFound(f"No BlockHistory row found for block_id={block.id} at sequence={target_seq}")
|
|
238
|
+
|
|
239
|
+
# Copy fields from target_entry to block
|
|
240
|
+
block.description = target_entry.description # type: ignore
|
|
241
|
+
block.label = target_entry.label # type: ignore
|
|
242
|
+
block.value = target_entry.value # type: ignore
|
|
243
|
+
block.limit = target_entry.limit # type: ignore
|
|
244
|
+
block.metadata_ = target_entry.metadata_ # type: ignore
|
|
245
|
+
block.current_history_entry_id = target_entry.id # type: ignore
|
|
246
|
+
|
|
247
|
+
# Update in DB (optimistic locking).
|
|
248
|
+
# We'll do a flush now; the caller does final commit.
|
|
249
|
+
updated_block = block.update(db_session=session, actor=actor, no_commit=True)
|
|
250
|
+
return updated_block
|
|
251
|
+
|
|
252
|
+
@enforce_types
|
|
253
|
+
def undo_checkpoint_block(self, block_id: str, actor: PydanticUser, use_preloaded_block: Optional[BlockModel] = None) -> PydanticBlock:
|
|
254
|
+
"""
|
|
255
|
+
Move the block to the immediately previous checkpoint in BlockHistory.
|
|
256
|
+
If older sequences have been pruned, we jump to the largest sequence
|
|
257
|
+
number that is still < current_seq.
|
|
258
|
+
"""
|
|
259
|
+
with self.session_maker() as session:
|
|
260
|
+
# 1) Load the current block
|
|
261
|
+
block = (
|
|
262
|
+
session.merge(use_preloaded_block)
|
|
263
|
+
if use_preloaded_block
|
|
264
|
+
else BlockModel.read(db_session=session, identifier=block_id, actor=actor)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if not block.current_history_entry_id:
|
|
268
|
+
raise ValueError(f"Block {block_id} has no history entry - cannot undo.")
|
|
269
|
+
|
|
270
|
+
current_entry = session.get(BlockHistory, block.current_history_entry_id)
|
|
271
|
+
if not current_entry:
|
|
272
|
+
raise NoResultFound(f"BlockHistory row not found for id={block.current_history_entry_id}")
|
|
273
|
+
|
|
274
|
+
current_seq = current_entry.sequence_number
|
|
275
|
+
|
|
276
|
+
# 2) Find the largest sequence < current_seq
|
|
277
|
+
previous_entry = (
|
|
278
|
+
session.query(BlockHistory)
|
|
279
|
+
.filter(BlockHistory.block_id == block.id, BlockHistory.sequence_number < current_seq)
|
|
280
|
+
.order_by(BlockHistory.sequence_number.desc())
|
|
281
|
+
.first()
|
|
282
|
+
)
|
|
283
|
+
if not previous_entry:
|
|
284
|
+
# No earlier checkpoint available
|
|
285
|
+
raise ValueError(f"Block {block_id} is already at the earliest checkpoint (seq={current_seq}). Cannot undo further.")
|
|
286
|
+
|
|
287
|
+
# 3) Move to that sequence
|
|
288
|
+
block = self._move_block_to_sequence(session, block, previous_entry.sequence_number, actor)
|
|
289
|
+
|
|
290
|
+
# 4) Commit
|
|
291
|
+
session.commit()
|
|
292
|
+
return block.to_pydantic()
|
|
293
|
+
|
|
294
|
+
@enforce_types
|
|
295
|
+
def redo_checkpoint_block(self, block_id: str, actor: PydanticUser, use_preloaded_block: Optional[BlockModel] = None) -> PydanticBlock:
|
|
296
|
+
"""
|
|
297
|
+
Move the block to the next checkpoint if it exists.
|
|
298
|
+
If some middle checkpoints have been pruned, we jump to the smallest
|
|
299
|
+
sequence > current_seq that remains.
|
|
300
|
+
"""
|
|
301
|
+
with self.session_maker() as session:
|
|
302
|
+
block = (
|
|
303
|
+
session.merge(use_preloaded_block)
|
|
304
|
+
if use_preloaded_block
|
|
305
|
+
else BlockModel.read(db_session=session, identifier=block_id, actor=actor)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if not block.current_history_entry_id:
|
|
309
|
+
raise ValueError(f"Block {block_id} has no history entry - cannot redo.")
|
|
310
|
+
|
|
311
|
+
current_entry = session.get(BlockHistory, block.current_history_entry_id)
|
|
312
|
+
if not current_entry:
|
|
313
|
+
raise NoResultFound(f"BlockHistory row not found for id={block.current_history_entry_id}")
|
|
314
|
+
|
|
315
|
+
current_seq = current_entry.sequence_number
|
|
316
|
+
|
|
317
|
+
# Find the smallest sequence that is > current_seq
|
|
318
|
+
next_entry = (
|
|
319
|
+
session.query(BlockHistory)
|
|
320
|
+
.filter(BlockHistory.block_id == block.id, BlockHistory.sequence_number > current_seq)
|
|
321
|
+
.order_by(BlockHistory.sequence_number.asc())
|
|
322
|
+
.first()
|
|
323
|
+
)
|
|
324
|
+
if not next_entry:
|
|
325
|
+
raise ValueError(f"Block {block_id} is at the highest checkpoint (seq={current_seq}). Cannot redo further.")
|
|
326
|
+
|
|
327
|
+
block = self._move_block_to_sequence(session, block, next_entry.sequence_number, actor)
|
|
328
|
+
|
|
329
|
+
session.commit()
|
|
330
|
+
return block.to_pydantic()
|
letta/services/group_manager.py
CHANGED
|
@@ -71,11 +71,20 @@ class GroupManager:
|
|
|
71
71
|
case ManagerType.supervisor:
|
|
72
72
|
new_group.manager_type = ManagerType.supervisor
|
|
73
73
|
new_group.manager_agent_id = group.manager_config.manager_agent_id
|
|
74
|
+
case ManagerType.sleeptime:
|
|
75
|
+
new_group.manager_type = ManagerType.sleeptime
|
|
76
|
+
new_group.manager_agent_id = group.manager_config.manager_agent_id
|
|
77
|
+
new_group.sleeptime_agent_frequency = group.manager_config.sleeptime_agent_frequency
|
|
78
|
+
if new_group.sleeptime_agent_frequency:
|
|
79
|
+
new_group.turns_counter = 0
|
|
74
80
|
case _:
|
|
75
81
|
raise ValueError(f"Unsupported manager type: {group.manager_config.manager_type}")
|
|
76
82
|
|
|
77
83
|
self._process_agent_relationship(session=session, group=new_group, agent_ids=group.agent_ids, allow_partial=False)
|
|
78
84
|
|
|
85
|
+
if group.shared_block_ids:
|
|
86
|
+
self._process_shared_block_relationship(session=session, group=new_group, block_ids=group.shared_block_ids)
|
|
87
|
+
|
|
79
88
|
new_group.create(session, actor=actor)
|
|
80
89
|
return new_group.to_pydantic()
|
|
81
90
|
|
|
@@ -84,6 +93,7 @@ class GroupManager:
|
|
|
84
93
|
with self.session_maker() as session:
|
|
85
94
|
group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
|
|
86
95
|
|
|
96
|
+
sleeptime_agent_frequency = None
|
|
87
97
|
max_turns = None
|
|
88
98
|
termination_token = None
|
|
89
99
|
manager_agent_id = None
|
|
@@ -99,9 +109,16 @@ class GroupManager:
|
|
|
99
109
|
termination_token = group_update.manager_config.termination_token
|
|
100
110
|
case ManagerType.supervisor:
|
|
101
111
|
manager_agent_id = group_update.manager_config.manager_agent_id
|
|
112
|
+
case ManagerType.sleeptime:
|
|
113
|
+
manager_agent_id = group_update.manager_config.manager_agent_id
|
|
114
|
+
sleeptime_agent_frequency = group_update.manager_config.sleeptime_agent_frequency
|
|
115
|
+
if sleeptime_agent_frequency and group.turns_counter is None:
|
|
116
|
+
group.turns_counter = 0
|
|
102
117
|
case _:
|
|
103
118
|
raise ValueError(f"Unsupported manager type: {group_update.manager_config.manager_type}")
|
|
104
119
|
|
|
120
|
+
if sleeptime_agent_frequency:
|
|
121
|
+
group.sleeptime_agent_frequency = sleeptime_agent_frequency
|
|
105
122
|
if max_turns:
|
|
106
123
|
group.max_turns = max_turns
|
|
107
124
|
if termination_token:
|
|
@@ -174,6 +191,30 @@ class GroupManager:
|
|
|
174
191
|
|
|
175
192
|
session.commit()
|
|
176
193
|
|
|
194
|
+
@enforce_types
|
|
195
|
+
def bump_turns_counter(self, group_id: str, actor: PydanticUser) -> int:
|
|
196
|
+
with self.session_maker() as session:
|
|
197
|
+
# Ensure group is loadable by user
|
|
198
|
+
group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
|
|
199
|
+
|
|
200
|
+
# Update turns counter
|
|
201
|
+
group.turns_counter = (group.turns_counter + 1) % group.sleeptime_agent_frequency
|
|
202
|
+
group.update(session, actor=actor)
|
|
203
|
+
return group.turns_counter
|
|
204
|
+
|
|
205
|
+
@enforce_types
|
|
206
|
+
def get_last_processed_message_id_and_update(self, group_id: str, last_processed_message_id: str, actor: PydanticUser) -> str:
|
|
207
|
+
with self.session_maker() as session:
|
|
208
|
+
# Ensure group is loadable by user
|
|
209
|
+
group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
|
|
210
|
+
|
|
211
|
+
# Update last processed message id
|
|
212
|
+
prev_last_processed_message_id = group.last_processed_message_id
|
|
213
|
+
group.last_processed_message_id = last_processed_message_id
|
|
214
|
+
group.update(session, actor=actor)
|
|
215
|
+
|
|
216
|
+
return prev_last_processed_message_id
|
|
217
|
+
|
|
177
218
|
def _process_agent_relationship(self, session: Session, group: GroupModel, agent_ids: List[str], allow_partial=False, replace=True):
|
|
178
219
|
if not agent_ids:
|
|
179
220
|
if replace:
|
|
@@ -203,3 +244,30 @@ class GroupManager:
|
|
|
203
244
|
setattr(group, "agent_ids", agent_ids)
|
|
204
245
|
else:
|
|
205
246
|
raise ValueError("Extend relationship is not supported for groups.")
|
|
247
|
+
|
|
248
|
+
def _process_shared_block_relationship(
|
|
249
|
+
self,
|
|
250
|
+
session: Session,
|
|
251
|
+
group: GroupModel,
|
|
252
|
+
block_ids: List[str],
|
|
253
|
+
):
|
|
254
|
+
"""Process shared block relationships for a group and its agents."""
|
|
255
|
+
from letta.orm import Agent, Block, BlocksAgents
|
|
256
|
+
|
|
257
|
+
# Add blocks to group
|
|
258
|
+
blocks = session.query(Block).filter(Block.id.in_(block_ids)).all()
|
|
259
|
+
group.shared_blocks = blocks
|
|
260
|
+
|
|
261
|
+
# Add blocks to all agents
|
|
262
|
+
if group.agent_ids:
|
|
263
|
+
agents = session.query(Agent).filter(Agent.id.in_(group.agent_ids)).all()
|
|
264
|
+
for agent in agents:
|
|
265
|
+
for block in blocks:
|
|
266
|
+
session.add(BlocksAgents(agent_id=agent.id, block_id=block.id, block_label=block.label))
|
|
267
|
+
|
|
268
|
+
# Add blocks to manager agent if exists
|
|
269
|
+
if group.manager_agent_id:
|
|
270
|
+
manager_agent = session.query(Agent).filter(Agent.id == group.manager_agent_id).first()
|
|
271
|
+
if manager_agent:
|
|
272
|
+
for block in blocks:
|
|
273
|
+
session.add(BlocksAgents(agent_id=manager_agent.id, block_id=block.id, block_label=block.label))
|
|
@@ -89,13 +89,15 @@ def _process_tags(agent: AgentModel, tags: List[str], replace=True):
|
|
|
89
89
|
agent.tags.extend([tag for tag in new_tags if tag.tag not in existing_tags])
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def derive_system_message(agent_type: AgentType, system: Optional[str] = None):
|
|
92
|
+
def derive_system_message(agent_type: AgentType, enable_sleeptime: Optional[bool] = None, system: Optional[str] = None):
|
|
93
93
|
if system is None:
|
|
94
94
|
# TODO: don't hardcode
|
|
95
|
-
if agent_type == AgentType.memgpt_agent:
|
|
95
|
+
if agent_type == AgentType.memgpt_agent and not enable_sleeptime:
|
|
96
96
|
system = gpt_system.get_system_text("memgpt_chat")
|
|
97
|
-
elif agent_type == AgentType.
|
|
98
|
-
system = gpt_system.get_system_text("
|
|
97
|
+
elif agent_type == AgentType.memgpt_agent and enable_sleeptime:
|
|
98
|
+
system = gpt_system.get_system_text("memgpt_sleeptime_chat")
|
|
99
|
+
elif agent_type == AgentType.sleeptime_agent:
|
|
100
|
+
system = gpt_system.get_system_text("sleeptime")
|
|
99
101
|
else:
|
|
100
102
|
raise ValueError(f"Invalid agent type: {agent_type}")
|
|
101
103
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from anthropic.types.beta.messages import BetaMessageBatch, BetaMessageBatchIndividualResponse
|
|
5
|
+
|
|
6
|
+
from letta.log import get_logger
|
|
7
|
+
from letta.orm.llm_batch_items import LLMBatchItem
|
|
8
|
+
from letta.orm.llm_batch_job import LLMBatchJob
|
|
9
|
+
from letta.schemas.agent import AgentStepState
|
|
10
|
+
from letta.schemas.enums import AgentStepStatus, JobStatus
|
|
11
|
+
from letta.schemas.llm_batch_job import LLMBatchItem as PydanticLLMBatchItem
|
|
12
|
+
from letta.schemas.llm_batch_job import LLMBatchJob as PydanticLLMBatchJob
|
|
13
|
+
from letta.schemas.llm_config import LLMConfig
|
|
14
|
+
from letta.schemas.user import User as PydanticUser
|
|
15
|
+
from letta.utils import enforce_types
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LLMBatchManager:
|
|
21
|
+
"""Manager for handling both LLMBatchJob and LLMBatchItem operations."""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
from letta.server.db import db_context
|
|
25
|
+
|
|
26
|
+
self.session_maker = db_context
|
|
27
|
+
|
|
28
|
+
@enforce_types
|
|
29
|
+
def create_batch_request(
|
|
30
|
+
self,
|
|
31
|
+
llm_provider: str,
|
|
32
|
+
create_batch_response: BetaMessageBatch,
|
|
33
|
+
actor: PydanticUser,
|
|
34
|
+
status: JobStatus = JobStatus.created,
|
|
35
|
+
) -> PydanticLLMBatchJob:
|
|
36
|
+
"""Create a new LLM batch job."""
|
|
37
|
+
with self.session_maker() as session:
|
|
38
|
+
batch = LLMBatchJob(
|
|
39
|
+
status=status,
|
|
40
|
+
llm_provider=llm_provider,
|
|
41
|
+
create_batch_response=create_batch_response,
|
|
42
|
+
organization_id=actor.organization_id,
|
|
43
|
+
)
|
|
44
|
+
batch.create(session, actor=actor)
|
|
45
|
+
return batch.to_pydantic()
|
|
46
|
+
|
|
47
|
+
@enforce_types
|
|
48
|
+
def get_batch_request_by_id(self, batch_id: str, actor: PydanticUser) -> PydanticLLMBatchJob:
|
|
49
|
+
"""Retrieve a single batch job by ID."""
|
|
50
|
+
with self.session_maker() as session:
|
|
51
|
+
batch = LLMBatchJob.read(db_session=session, identifier=batch_id, actor=actor)
|
|
52
|
+
return batch.to_pydantic()
|
|
53
|
+
|
|
54
|
+
@enforce_types
|
|
55
|
+
def update_batch_status(
|
|
56
|
+
self,
|
|
57
|
+
batch_id: str,
|
|
58
|
+
status: JobStatus,
|
|
59
|
+
actor: PydanticUser,
|
|
60
|
+
latest_polling_response: Optional[BetaMessageBatch] = None,
|
|
61
|
+
) -> PydanticLLMBatchJob:
|
|
62
|
+
"""Update a batch job’s status and optionally its polling response."""
|
|
63
|
+
with self.session_maker() as session:
|
|
64
|
+
batch = LLMBatchJob.read(db_session=session, identifier=batch_id, actor=actor)
|
|
65
|
+
batch.status = status
|
|
66
|
+
batch.latest_polling_response = latest_polling_response
|
|
67
|
+
batch.last_polled_at = datetime.datetime.now(datetime.timezone.utc)
|
|
68
|
+
return batch.update(db_session=session, actor=actor).to_pydantic()
|
|
69
|
+
|
|
70
|
+
@enforce_types
|
|
71
|
+
def delete_batch_request(self, batch_id: str, actor: PydanticUser) -> None:
|
|
72
|
+
"""Hard delete a batch job by ID."""
|
|
73
|
+
with self.session_maker() as session:
|
|
74
|
+
batch = LLMBatchJob.read(db_session=session, identifier=batch_id, actor=actor)
|
|
75
|
+
batch.hard_delete(db_session=session, actor=actor)
|
|
76
|
+
|
|
77
|
+
@enforce_types
|
|
78
|
+
def create_batch_item(
|
|
79
|
+
self,
|
|
80
|
+
batch_id: str,
|
|
81
|
+
agent_id: str,
|
|
82
|
+
llm_config: LLMConfig,
|
|
83
|
+
actor: PydanticUser,
|
|
84
|
+
request_status: JobStatus = JobStatus.created,
|
|
85
|
+
step_status: AgentStepStatus = AgentStepStatus.paused,
|
|
86
|
+
step_state: Optional[AgentStepState] = None,
|
|
87
|
+
) -> PydanticLLMBatchItem:
|
|
88
|
+
"""Create a new batch item."""
|
|
89
|
+
with self.session_maker() as session:
|
|
90
|
+
item = LLMBatchItem(
|
|
91
|
+
batch_id=batch_id,
|
|
92
|
+
agent_id=agent_id,
|
|
93
|
+
llm_config=llm_config,
|
|
94
|
+
request_status=request_status,
|
|
95
|
+
step_status=step_status,
|
|
96
|
+
step_state=step_state,
|
|
97
|
+
organization_id=actor.organization_id,
|
|
98
|
+
)
|
|
99
|
+
item.create(session, actor=actor)
|
|
100
|
+
return item.to_pydantic()
|
|
101
|
+
|
|
102
|
+
@enforce_types
|
|
103
|
+
def get_batch_item_by_id(self, item_id: str, actor: PydanticUser) -> PydanticLLMBatchItem:
|
|
104
|
+
"""Retrieve a single batch item by ID."""
|
|
105
|
+
with self.session_maker() as session:
|
|
106
|
+
item = LLMBatchItem.read(db_session=session, identifier=item_id, actor=actor)
|
|
107
|
+
return item.to_pydantic()
|
|
108
|
+
|
|
109
|
+
@enforce_types
|
|
110
|
+
def update_batch_item(
|
|
111
|
+
self,
|
|
112
|
+
item_id: str,
|
|
113
|
+
actor: PydanticUser,
|
|
114
|
+
request_status: Optional[JobStatus] = None,
|
|
115
|
+
step_status: Optional[AgentStepStatus] = None,
|
|
116
|
+
llm_request_response: Optional[BetaMessageBatchIndividualResponse] = None,
|
|
117
|
+
step_state: Optional[AgentStepState] = None,
|
|
118
|
+
) -> PydanticLLMBatchItem:
|
|
119
|
+
"""Update fields on a batch item."""
|
|
120
|
+
with self.session_maker() as session:
|
|
121
|
+
item = LLMBatchItem.read(db_session=session, identifier=item_id, actor=actor)
|
|
122
|
+
|
|
123
|
+
if request_status:
|
|
124
|
+
item.request_status = request_status
|
|
125
|
+
if step_status:
|
|
126
|
+
item.step_status = step_status
|
|
127
|
+
if llm_request_response:
|
|
128
|
+
item.batch_request_result = llm_request_response
|
|
129
|
+
if step_state:
|
|
130
|
+
item.step_state = step_state
|
|
131
|
+
|
|
132
|
+
return item.update(db_session=session, actor=actor).to_pydantic()
|
|
133
|
+
|
|
134
|
+
@enforce_types
|
|
135
|
+
def delete_batch_item(self, item_id: str, actor: PydanticUser) -> None:
|
|
136
|
+
"""Hard delete a batch item by ID."""
|
|
137
|
+
with self.session_maker() as session:
|
|
138
|
+
item = LLMBatchItem.read(db_session=session, identifier=item_id, actor=actor)
|
|
139
|
+
item.hard_delete(db_session=session, actor=actor)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from typing import List, Optional, Sequence
|
|
3
3
|
|
|
4
|
-
from sqlalchemy import
|
|
4
|
+
from sqlalchemy import exists, func, select, text
|
|
5
5
|
|
|
6
6
|
from letta.log import get_logger
|
|
7
7
|
from letta.orm.agent import Agent as AgentModel
|
|
@@ -270,19 +270,20 @@ class MessageManager:
|
|
|
270
270
|
Most performant query to list messages for an agent by directly querying the Message table.
|
|
271
271
|
|
|
272
272
|
This function filters by the agent_id (leveraging the index on messages.agent_id)
|
|
273
|
-
and applies
|
|
273
|
+
and applies pagination using sequence_id as the cursor.
|
|
274
274
|
If query_text is provided, it will filter messages whose text content partially matches the query.
|
|
275
275
|
If role is provided, it will filter messages by the specified role.
|
|
276
276
|
|
|
277
277
|
Args:
|
|
278
278
|
agent_id: The ID of the agent whose messages are queried.
|
|
279
279
|
actor: The user performing the action (used for permission checks).
|
|
280
|
-
after: A message ID; if provided, only messages *after* this message (
|
|
281
|
-
before: A message ID; if provided, only messages *before* this message are returned.
|
|
280
|
+
after: A message ID; if provided, only messages *after* this message (by sequence_id) are returned.
|
|
281
|
+
before: A message ID; if provided, only messages *before* this message (by sequence_id) are returned.
|
|
282
282
|
query_text: Optional string to partially match the message text content.
|
|
283
283
|
roles: Optional MessageRole to filter messages by role.
|
|
284
284
|
limit: Maximum number of messages to return.
|
|
285
|
-
ascending: If True, sort by
|
|
285
|
+
ascending: If True, sort by sequence_id ascending; if False, sort descending.
|
|
286
|
+
group_id: Optional group ID to filter messages by group_id.
|
|
286
287
|
|
|
287
288
|
Returns:
|
|
288
289
|
List[PydanticMessage]: A list of messages (converted via .to_pydantic()).
|
|
@@ -290,6 +291,7 @@ class MessageManager:
|
|
|
290
291
|
Raises:
|
|
291
292
|
NoResultFound: If the provided after/before message IDs do not exist.
|
|
292
293
|
"""
|
|
294
|
+
|
|
293
295
|
with self.session_maker() as session:
|
|
294
296
|
# Permission check: raise if the agent doesn't exist or actor is not allowed.
|
|
295
297
|
AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
|
@@ -301,7 +303,7 @@ class MessageManager:
|
|
|
301
303
|
if group_id:
|
|
302
304
|
query = query.filter(MessageModel.group_id == group_id)
|
|
303
305
|
|
|
304
|
-
# If query_text is provided, filter messages using subquery.
|
|
306
|
+
# If query_text is provided, filter messages using subquery + json_array_elements.
|
|
305
307
|
if query_text:
|
|
306
308
|
content_element = func.json_array_elements(MessageModel.content).alias("content_element")
|
|
307
309
|
query = query.filter(
|
|
@@ -313,48 +315,32 @@ class MessageManager:
|
|
|
313
315
|
)
|
|
314
316
|
)
|
|
315
317
|
|
|
316
|
-
# If role
|
|
318
|
+
# If role(s) are provided, filter messages by those roles.
|
|
317
319
|
if roles:
|
|
318
320
|
role_values = [r.value for r in roles]
|
|
319
321
|
query = query.filter(MessageModel.role.in_(role_values))
|
|
320
322
|
|
|
321
323
|
# Apply 'after' pagination if specified.
|
|
322
324
|
if after:
|
|
323
|
-
after_ref = session.query(MessageModel.
|
|
325
|
+
after_ref = session.query(MessageModel.sequence_id).filter(MessageModel.id == after).one_or_none()
|
|
324
326
|
if not after_ref:
|
|
325
327
|
raise NoResultFound(f"No message found with id '{after}' for agent '{agent_id}'.")
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
MessageModel.created_at > after_ref.created_at,
|
|
329
|
-
and_(
|
|
330
|
-
MessageModel.created_at == after_ref.created_at,
|
|
331
|
-
MessageModel.id > after_ref.id,
|
|
332
|
-
),
|
|
333
|
-
)
|
|
334
|
-
)
|
|
328
|
+
# Filter out any messages with a sequence_id <= after_ref.sequence_id
|
|
329
|
+
query = query.filter(MessageModel.sequence_id > after_ref.sequence_id)
|
|
335
330
|
|
|
336
331
|
# Apply 'before' pagination if specified.
|
|
337
332
|
if before:
|
|
338
|
-
before_ref = (
|
|
339
|
-
session.query(MessageModel.created_at, MessageModel.id).filter(MessageModel.id == before).limit(1).one_or_none()
|
|
340
|
-
)
|
|
333
|
+
before_ref = session.query(MessageModel.sequence_id).filter(MessageModel.id == before).one_or_none()
|
|
341
334
|
if not before_ref:
|
|
342
335
|
raise NoResultFound(f"No message found with id '{before}' for agent '{agent_id}'.")
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
MessageModel.created_at < before_ref.created_at,
|
|
346
|
-
and_(
|
|
347
|
-
MessageModel.created_at == before_ref.created_at,
|
|
348
|
-
MessageModel.id < before_ref.id,
|
|
349
|
-
),
|
|
350
|
-
)
|
|
351
|
-
)
|
|
336
|
+
# Filter out any messages with a sequence_id >= before_ref.sequence_id
|
|
337
|
+
query = query.filter(MessageModel.sequence_id < before_ref.sequence_id)
|
|
352
338
|
|
|
353
339
|
# Apply ordering based on the ascending flag.
|
|
354
340
|
if ascending:
|
|
355
|
-
query = query.order_by(MessageModel.
|
|
341
|
+
query = query.order_by(MessageModel.sequence_id.asc())
|
|
356
342
|
else:
|
|
357
|
-
query = query.order_by(MessageModel.
|
|
343
|
+
query = query.order_by(MessageModel.sequence_id.desc())
|
|
358
344
|
|
|
359
345
|
# Limit the number of results.
|
|
360
346
|
query = query.limit(limit)
|
|
@@ -387,9 +387,7 @@ class ToolExecutionSandbox:
|
|
|
387
387
|
sbx = Sandbox(sandbox_config.get_e2b_config().template, metadata={self.METADATA_CONFIG_STATE_KEY: state_hash})
|
|
388
388
|
else:
|
|
389
389
|
# no template
|
|
390
|
-
sbx = Sandbox(
|
|
391
|
-
metadata={self.METADATA_CONFIG_STATE_KEY: state_hash}, **e2b_config.model_dump(to_orm=True, exclude={"pip_requirements"})
|
|
392
|
-
)
|
|
390
|
+
sbx = Sandbox(metadata={self.METADATA_CONFIG_STATE_KEY: state_hash}, **e2b_config.model_dump(exclude={"pip_requirements"}))
|
|
393
391
|
|
|
394
392
|
# install pip requirements
|
|
395
393
|
if e2b_config.pip_requirements:
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import ast
|
|
2
1
|
import math
|
|
3
2
|
from abc import ABC, abstractmethod
|
|
4
3
|
from typing import Any, Optional, Tuple
|
|
@@ -15,7 +14,9 @@ from letta.schemas.user import User
|
|
|
15
14
|
from letta.services.agent_manager import AgentManager
|
|
16
15
|
from letta.services.message_manager import MessageManager
|
|
17
16
|
from letta.services.passage_manager import PassageManager
|
|
18
|
-
from letta.services.
|
|
17
|
+
from letta.services.tool_sandbox.e2b_sandbox import AsyncToolSandboxE2B
|
|
18
|
+
from letta.services.tool_sandbox.local_sandbox import AsyncToolSandboxLocal
|
|
19
|
+
from letta.settings import tool_settings
|
|
19
20
|
from letta.utils import get_friendly_error_msg
|
|
20
21
|
|
|
21
22
|
|
|
@@ -40,6 +41,7 @@ class LettaCoreToolExecutor(ToolExecutor):
|
|
|
40
41
|
"send_message": self.send_message,
|
|
41
42
|
"conversation_search": self.conversation_search,
|
|
42
43
|
"archival_memory_search": self.archival_memory_search,
|
|
44
|
+
"archival_memory_insert": self.archival_memory_insert,
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
if function_name not in function_map:
|
|
@@ -334,16 +336,13 @@ class SandboxToolExecutor(ToolExecutor):
|
|
|
334
336
|
|
|
335
337
|
agent_state_copy = self._create_agent_state_copy(agent_state)
|
|
336
338
|
|
|
337
|
-
#
|
|
338
|
-
if
|
|
339
|
-
|
|
339
|
+
# Execute in sandbox depending on API key
|
|
340
|
+
if tool_settings.e2b_api_key:
|
|
341
|
+
sandbox = AsyncToolSandboxE2B(function_name, function_args, actor, tool_object=tool)
|
|
340
342
|
else:
|
|
341
|
-
|
|
343
|
+
sandbox = AsyncToolSandboxLocal(function_name, function_args, actor, tool_object=tool)
|
|
342
344
|
|
|
343
|
-
|
|
344
|
-
sandbox_run_result = await AsyncToolExecutionSandbox(function_name, function_args, actor, tool_object=tool).run(
|
|
345
|
-
agent_state=agent_state_copy, inject_agent_state=inject_agent_state
|
|
346
|
-
)
|
|
345
|
+
sandbox_run_result = await sandbox.run(agent_state=agent_state_copy)
|
|
347
346
|
|
|
348
347
|
function_response, updated_agent_state = sandbox_run_result.func_return, sandbox_run_result.agent_state
|
|
349
348
|
|
|
@@ -371,16 +370,6 @@ class SandboxToolExecutor(ToolExecutor):
|
|
|
371
370
|
# This is defensive programming - we try to coerce but fall back if it fails
|
|
372
371
|
return function_args
|
|
373
372
|
|
|
374
|
-
def parse_function_arguments(self, source_code: str, tool_name: str):
|
|
375
|
-
"""Get arguments of a function from its source code"""
|
|
376
|
-
tree = ast.parse(source_code)
|
|
377
|
-
args = []
|
|
378
|
-
for node in ast.walk(tree):
|
|
379
|
-
if isinstance(node, ast.FunctionDef) and node.name == tool_name:
|
|
380
|
-
for arg in node.args.args:
|
|
381
|
-
args.append(arg.arg)
|
|
382
|
-
return args
|
|
383
|
-
|
|
384
373
|
def _create_agent_state_copy(self, agent_state: AgentState):
|
|
385
374
|
"""Create a copy of agent state for sandbox execution."""
|
|
386
375
|
agent_state_copy = agent_state.__deepcopy__()
|