letta-nightly 0.7.15.dev20250514104255__py3-none-any.whl → 0.7.16.dev20250515205957__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- letta/__init__.py +1 -1
- letta/agent.py +12 -0
- letta/agents/helpers.py +48 -5
- letta/agents/letta_agent.py +46 -18
- letta/agents/letta_agent_batch.py +44 -26
- letta/agents/voice_sleeptime_agent.py +6 -4
- letta/client/client.py +16 -1
- letta/constants.py +3 -0
- letta/functions/async_composio_toolset.py +1 -1
- letta/groups/sleeptime_multi_agent.py +1 -0
- letta/interfaces/anthropic_streaming_interface.py +40 -6
- letta/jobs/llm_batch_job_polling.py +6 -2
- letta/orm/agent.py +102 -1
- letta/orm/block.py +3 -0
- letta/orm/sqlalchemy_base.py +365 -133
- letta/schemas/agent.py +10 -2
- letta/schemas/block.py +3 -0
- letta/schemas/memory.py +7 -2
- letta/server/rest_api/routers/v1/agents.py +13 -13
- letta/server/rest_api/routers/v1/messages.py +6 -6
- letta/server/rest_api/routers/v1/tools.py +3 -3
- letta/server/server.py +74 -0
- letta/services/agent_manager.py +421 -7
- letta/services/block_manager.py +12 -8
- letta/services/helpers/agent_manager_helper.py +19 -0
- letta/services/job_manager.py +99 -0
- letta/services/llm_batch_manager.py +28 -27
- letta/services/message_manager.py +51 -19
- letta/services/tool_executor/tool_executor.py +19 -1
- letta/services/tool_manager.py +13 -3
- letta/types/__init__.py +0 -0
- {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/METADATA +3 -3
- {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/RECORD +36 -35
- {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/entry_points.txt +0 -0
letta/__init__.py
CHANGED
letta/agent.py
CHANGED
@@ -179,6 +179,15 @@ class Agent(BaseAgent):
|
|
179
179
|
raise ValueError(f"Invalid JSON format in message: {text_content}")
|
180
180
|
return None
|
181
181
|
|
182
|
+
def ensure_read_only_block_not_modified(self, new_memory: Memory) -> None:
|
183
|
+
"""
|
184
|
+
Throw an error if a read-only block has been modified
|
185
|
+
"""
|
186
|
+
for label in self.agent_state.memory.list_block_labels():
|
187
|
+
if self.agent_state.memory.get_block(label).read_only:
|
188
|
+
if new_memory.get_block(label).value != self.agent_state.memory.get_block(label).value:
|
189
|
+
raise ValueError(READ_ONLY_BLOCK_EDIT_ERROR)
|
190
|
+
|
182
191
|
def update_memory_if_changed(self, new_memory: Memory) -> bool:
|
183
192
|
"""
|
184
193
|
Update internal memory object and system prompt if there have been modifications.
|
@@ -1277,6 +1286,9 @@ class Agent(BaseAgent):
|
|
1277
1286
|
agent_state_copy = self.agent_state.__deepcopy__()
|
1278
1287
|
function_args["agent_state"] = agent_state_copy # need to attach self to arg since it's dynamically linked
|
1279
1288
|
function_response = callable_func(**function_args)
|
1289
|
+
self.ensure_read_only_block_not_modified(
|
1290
|
+
new_memory=agent_state_copy.memory
|
1291
|
+
) # memory editing tools cannot edit read-only blocks
|
1280
1292
|
self.update_memory_if_changed(agent_state_copy.memory)
|
1281
1293
|
elif target_letta_tool.tool_type == ToolType.EXTERNAL_COMPOSIO:
|
1282
1294
|
action_name = generate_composio_action_from_func_name(target_letta_tool.name)
|
letta/agents/helpers.py
CHANGED
@@ -10,14 +10,18 @@ from letta.server.rest_api.utils import create_input_messages
|
|
10
10
|
from letta.services.message_manager import MessageManager
|
11
11
|
|
12
12
|
|
13
|
-
def _create_letta_response(
|
13
|
+
def _create_letta_response(
|
14
|
+
new_in_context_messages: list[Message], use_assistant_message: bool, usage: LettaUsageStatistics
|
15
|
+
) -> LettaResponse:
|
14
16
|
"""
|
15
17
|
Converts the newly created/persisted messages into a LettaResponse.
|
16
18
|
"""
|
17
|
-
|
18
|
-
for
|
19
|
-
|
20
|
-
|
19
|
+
# NOTE: hacky solution to avoid returning heartbeat messages and the original user message
|
20
|
+
filter_user_messages = [m for m in new_in_context_messages if m.role != "user"]
|
21
|
+
response_messages = Message.to_letta_messages_from_list(
|
22
|
+
messages=filter_user_messages, use_assistant_message=use_assistant_message, reverse=False
|
23
|
+
)
|
24
|
+
return LettaResponse(messages=response_messages, usage=usage)
|
21
25
|
|
22
26
|
|
23
27
|
def _prepare_in_context_messages(
|
@@ -56,6 +60,45 @@ def _prepare_in_context_messages(
|
|
56
60
|
return current_in_context_messages, new_in_context_messages
|
57
61
|
|
58
62
|
|
63
|
+
async def _prepare_in_context_messages_async(
|
64
|
+
input_messages: List[MessageCreate],
|
65
|
+
agent_state: AgentState,
|
66
|
+
message_manager: MessageManager,
|
67
|
+
actor: User,
|
68
|
+
) -> Tuple[List[Message], List[Message]]:
|
69
|
+
"""
|
70
|
+
Prepares in-context messages for an agent, based on the current state and a new user input.
|
71
|
+
Async version of _prepare_in_context_messages.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
input_messages (List[MessageCreate]): The new user input messages to process.
|
75
|
+
agent_state (AgentState): The current state of the agent, including message buffer config.
|
76
|
+
message_manager (MessageManager): The manager used to retrieve and create messages.
|
77
|
+
actor (User): The user performing the action, used for access control and attribution.
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
Tuple[List[Message], List[Message]]: A tuple containing:
|
81
|
+
- The current in-context messages (existing context for the agent).
|
82
|
+
- The new in-context messages (messages created from the new input).
|
83
|
+
"""
|
84
|
+
|
85
|
+
if agent_state.message_buffer_autoclear:
|
86
|
+
# If autoclear is enabled, only include the most recent system message (usually at index 0)
|
87
|
+
current_in_context_messages = [
|
88
|
+
(await message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=actor))[0]
|
89
|
+
]
|
90
|
+
else:
|
91
|
+
# Otherwise, include the full list of messages by ID for context
|
92
|
+
current_in_context_messages = await message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=actor)
|
93
|
+
|
94
|
+
# Create a new user message from the input and store it
|
95
|
+
new_in_context_messages = await message_manager.create_many_messages_async(
|
96
|
+
create_input_messages(input_messages=input_messages, agent_id=agent_state.id, actor=actor), actor=actor
|
97
|
+
)
|
98
|
+
|
99
|
+
return current_in_context_messages, new_in_context_messages
|
100
|
+
|
101
|
+
|
59
102
|
def serialize_message_history(messages: List[str], context: str) -> str:
|
60
103
|
"""
|
61
104
|
Produce an XML document like:
|
letta/agents/letta_agent.py
CHANGED
@@ -4,6 +4,7 @@ import uuid
|
|
4
4
|
from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union
|
5
5
|
|
6
6
|
from openai import AsyncStream
|
7
|
+
from openai.types import CompletionUsage
|
7
8
|
from openai.types.chat import ChatCompletion, ChatCompletionChunk
|
8
9
|
|
9
10
|
from letta.agents.base_agent import BaseAgent
|
@@ -23,6 +24,7 @@ from letta.schemas.letta_message_content import OmittedReasoningContent, Reasoni
|
|
23
24
|
from letta.schemas.letta_response import LettaResponse
|
24
25
|
from letta.schemas.message import Message, MessageCreate
|
25
26
|
from letta.schemas.openai.chat_completion_response import ToolCall
|
27
|
+
from letta.schemas.usage import LettaUsageStatistics
|
26
28
|
from letta.schemas.user import User
|
27
29
|
from letta.server.rest_api.utils import create_letta_messages_from_llm_response
|
28
30
|
from letta.services.agent_manager import AgentManager
|
@@ -47,7 +49,6 @@ class LettaAgent(BaseAgent):
|
|
47
49
|
block_manager: BlockManager,
|
48
50
|
passage_manager: PassageManager,
|
49
51
|
actor: User,
|
50
|
-
use_assistant_message: bool = True,
|
51
52
|
):
|
52
53
|
super().__init__(agent_id=agent_id, openai_client=None, message_manager=message_manager, agent_manager=agent_manager, actor=actor)
|
53
54
|
|
@@ -55,26 +56,31 @@ class LettaAgent(BaseAgent):
|
|
55
56
|
# Summarizer settings
|
56
57
|
self.block_manager = block_manager
|
57
58
|
self.passage_manager = passage_manager
|
58
|
-
self.use_assistant_message = use_assistant_message
|
59
59
|
self.response_messages: List[Message] = []
|
60
60
|
|
61
|
-
self.last_function_response =
|
61
|
+
self.last_function_response = None
|
62
|
+
|
63
|
+
# Cached archival memory/message size
|
64
|
+
self.num_messages = self.message_manager.size(actor=self.actor, agent_id=agent_id)
|
65
|
+
self.num_archival_memories = self.passage_manager.size(actor=self.actor, agent_id=agent_id)
|
62
66
|
|
63
67
|
# Cached archival memory/message size
|
64
68
|
self.num_messages = self.message_manager.size(actor=self.actor, agent_id=agent_id)
|
65
69
|
self.num_archival_memories = self.passage_manager.size(actor=self.actor, agent_id=agent_id)
|
66
70
|
|
67
71
|
@trace_method
|
68
|
-
async def step(self, input_messages: List[MessageCreate], max_steps: int = 10) -> LettaResponse:
|
69
|
-
agent_state = self.agent_manager.
|
70
|
-
current_in_context_messages, new_in_context_messages = await self._step(
|
72
|
+
async def step(self, input_messages: List[MessageCreate], max_steps: int = 10, use_assistant_message: bool = True) -> LettaResponse:
|
73
|
+
agent_state = await self.agent_manager.get_agent_by_id_async(self.agent_id, actor=self.actor)
|
74
|
+
current_in_context_messages, new_in_context_messages, usage = await self._step(
|
71
75
|
agent_state=agent_state, input_messages=input_messages, max_steps=max_steps
|
72
76
|
)
|
73
|
-
return _create_letta_response(
|
77
|
+
return _create_letta_response(
|
78
|
+
new_in_context_messages=new_in_context_messages, use_assistant_message=use_assistant_message, usage=usage
|
79
|
+
)
|
74
80
|
|
75
81
|
async def _step(
|
76
82
|
self, agent_state: AgentState, input_messages: List[MessageCreate], max_steps: int = 10
|
77
|
-
) -> Tuple[List[Message], List[Message]]:
|
83
|
+
) -> Tuple[List[Message], List[Message], CompletionUsage]:
|
78
84
|
current_in_context_messages, new_in_context_messages = _prepare_in_context_messages(
|
79
85
|
input_messages, agent_state, self.message_manager, self.actor
|
80
86
|
)
|
@@ -84,6 +90,7 @@ class LettaAgent(BaseAgent):
|
|
84
90
|
put_inner_thoughts_first=True,
|
85
91
|
actor=self.actor,
|
86
92
|
)
|
93
|
+
usage = LettaUsageStatistics()
|
87
94
|
for _ in range(max_steps):
|
88
95
|
response = await self._get_ai_reply(
|
89
96
|
llm_client=llm_client,
|
@@ -95,11 +102,21 @@ class LettaAgent(BaseAgent):
|
|
95
102
|
)
|
96
103
|
|
97
104
|
tool_call = response.choices[0].message.tool_calls[0]
|
105
|
+
reasoning = [TextContent(text=response.choices[0].message.content)] # reasoning placed into content for legacy reasons
|
98
106
|
|
99
|
-
persisted_messages, should_continue = await self._handle_ai_response(
|
107
|
+
persisted_messages, should_continue = await self._handle_ai_response(
|
108
|
+
tool_call, agent_state, tool_rules_solver, reasoning_content=reasoning
|
109
|
+
)
|
100
110
|
self.response_messages.extend(persisted_messages)
|
101
111
|
new_in_context_messages.extend(persisted_messages)
|
102
112
|
|
113
|
+
# update usage
|
114
|
+
# TODO: add run_id
|
115
|
+
usage.step_count += 1
|
116
|
+
usage.completion_tokens += response.usage.completion_tokens
|
117
|
+
usage.prompt_tokens += response.usage.prompt_tokens
|
118
|
+
usage.total_tokens += response.usage.total_tokens
|
119
|
+
|
103
120
|
if not should_continue:
|
104
121
|
break
|
105
122
|
|
@@ -108,17 +125,17 @@ class LettaAgent(BaseAgent):
|
|
108
125
|
message_ids = [m.id for m in (current_in_context_messages + new_in_context_messages)]
|
109
126
|
self.agent_manager.set_in_context_messages(agent_id=self.agent_id, message_ids=message_ids, actor=self.actor)
|
110
127
|
|
111
|
-
return current_in_context_messages, new_in_context_messages
|
128
|
+
return current_in_context_messages, new_in_context_messages, usage
|
112
129
|
|
113
130
|
@trace_method
|
114
131
|
async def step_stream(
|
115
|
-
self, input_messages: List[MessageCreate], max_steps: int = 10, use_assistant_message: bool =
|
132
|
+
self, input_messages: List[MessageCreate], max_steps: int = 10, use_assistant_message: bool = True
|
116
133
|
) -> AsyncGenerator[str, None]:
|
117
134
|
"""
|
118
135
|
Main streaming loop that yields partial tokens.
|
119
136
|
Whenever we detect a tool call, we yield from _handle_ai_response as well.
|
120
137
|
"""
|
121
|
-
agent_state = self.agent_manager.
|
138
|
+
agent_state = await self.agent_manager.get_agent_by_id_async(self.agent_id, actor=self.actor)
|
122
139
|
current_in_context_messages, new_in_context_messages = _prepare_in_context_messages(
|
123
140
|
input_messages, agent_state, self.message_manager, self.actor
|
124
141
|
)
|
@@ -128,6 +145,7 @@ class LettaAgent(BaseAgent):
|
|
128
145
|
put_inner_thoughts_first=True,
|
129
146
|
actor=self.actor,
|
130
147
|
)
|
148
|
+
usage = LettaUsageStatistics()
|
131
149
|
|
132
150
|
for _ in range(max_steps):
|
133
151
|
stream = await self._get_ai_reply(
|
@@ -137,7 +155,6 @@ class LettaAgent(BaseAgent):
|
|
137
155
|
tool_rules_solver=tool_rules_solver,
|
138
156
|
stream=True,
|
139
157
|
)
|
140
|
-
|
141
158
|
# TODO: THIS IS INCREDIBLY UGLY
|
142
159
|
# TODO: THERE ARE MULTIPLE COPIES OF THE LLM_CONFIG EVERYWHERE THAT ARE GETTING MANIPULATED
|
143
160
|
interface = AnthropicStreamingInterface(
|
@@ -146,6 +163,12 @@ class LettaAgent(BaseAgent):
|
|
146
163
|
async for chunk in interface.process(stream):
|
147
164
|
yield f"data: {chunk.model_dump_json()}\n\n"
|
148
165
|
|
166
|
+
# update usage
|
167
|
+
usage.step_count += 1
|
168
|
+
usage.completion_tokens += interface.output_tokens
|
169
|
+
usage.prompt_tokens += interface.input_tokens
|
170
|
+
usage.total_tokens += interface.input_tokens + interface.output_tokens
|
171
|
+
|
149
172
|
# Process resulting stream content
|
150
173
|
tool_call = interface.get_tool_call_object()
|
151
174
|
reasoning_content = interface.get_reasoning_content()
|
@@ -160,6 +183,10 @@ class LettaAgent(BaseAgent):
|
|
160
183
|
self.response_messages.extend(persisted_messages)
|
161
184
|
new_in_context_messages.extend(persisted_messages)
|
162
185
|
|
186
|
+
if not use_assistant_message or should_continue:
|
187
|
+
tool_return = [msg for msg in persisted_messages if msg.role == "tool"][-1].to_letta_messages()[0]
|
188
|
+
yield f"data: {tool_return.model_dump_json()}\n\n"
|
189
|
+
|
163
190
|
if not should_continue:
|
164
191
|
break
|
165
192
|
|
@@ -174,7 +201,7 @@ class LettaAgent(BaseAgent):
|
|
174
201
|
self.num_archival_memories = self.passage_manager.size(actor=self.actor, agent_id=agent_state.id)
|
175
202
|
|
176
203
|
# TODO: Also yield out a letta usage stats SSE
|
177
|
-
|
204
|
+
yield f"data: {usage.model_dump_json()}\n\n"
|
178
205
|
yield f"data: {MessageStreamStatus.done.model_dump_json()}\n\n"
|
179
206
|
|
180
207
|
@trace_method
|
@@ -214,6 +241,8 @@ class LettaAgent(BaseAgent):
|
|
214
241
|
]
|
215
242
|
|
216
243
|
# Mirror the sync agent loop: get allowed tools or allow all if none are allowed
|
244
|
+
if self.last_function_response is None:
|
245
|
+
self.last_function_response = await self._load_last_function_response_async()
|
217
246
|
valid_tool_names = tool_rules_solver.get_allowed_tool_names(
|
218
247
|
available_tools=set([t.name for t in tools]),
|
219
248
|
last_function_response=self.last_function_response,
|
@@ -307,7 +336,7 @@ class LettaAgent(BaseAgent):
|
|
307
336
|
pre_computed_assistant_message_id=pre_computed_assistant_message_id,
|
308
337
|
pre_computed_tool_message_id=pre_computed_tool_message_id,
|
309
338
|
)
|
310
|
-
persisted_messages = self.message_manager.
|
339
|
+
persisted_messages = await self.message_manager.create_many_messages_async(tool_call_messages, actor=self.actor)
|
311
340
|
self.last_function_response = function_response
|
312
341
|
|
313
342
|
return persisted_messages, continue_stepping
|
@@ -359,7 +388,6 @@ class LettaAgent(BaseAgent):
|
|
359
388
|
block_manager=self.block_manager,
|
360
389
|
passage_manager=self.passage_manager,
|
361
390
|
actor=self.actor,
|
362
|
-
use_assistant_message=True,
|
363
391
|
)
|
364
392
|
|
365
393
|
augmented_message = (
|
@@ -394,9 +422,9 @@ class LettaAgent(BaseAgent):
|
|
394
422
|
results = await asyncio.gather(*tasks)
|
395
423
|
return results
|
396
424
|
|
397
|
-
def
|
425
|
+
async def _load_last_function_response_async(self):
|
398
426
|
"""Load the last function response from message history"""
|
399
|
-
in_context_messages = self.agent_manager.
|
427
|
+
in_context_messages = await self.agent_manager.get_in_context_messages_async(agent_id=self.agent_id, actor=self.actor)
|
400
428
|
for msg in reversed(in_context_messages):
|
401
429
|
if msg.role == MessageRole.tool and msg.content and len(msg.content) == 1 and isinstance(msg.content[0], TextContent):
|
402
430
|
text_content = msg.content[0].text
|
@@ -7,7 +7,7 @@ from aiomultiprocess import Pool
|
|
7
7
|
from anthropic.types.beta.messages import BetaMessageBatchCanceledResult, BetaMessageBatchErroredResult, BetaMessageBatchSucceededResult
|
8
8
|
|
9
9
|
from letta.agents.base_agent import BaseAgent
|
10
|
-
from letta.agents.helpers import
|
10
|
+
from letta.agents.helpers import _prepare_in_context_messages_async
|
11
11
|
from letta.helpers import ToolRulesSolver
|
12
12
|
from letta.helpers.datetime_helpers import get_utc_time
|
13
13
|
from letta.helpers.tool_execution_helper import enable_strict_mode
|
@@ -107,7 +107,6 @@ class LettaAgentBatch(BaseAgent):
|
|
107
107
|
sandbox_config_manager: SandboxConfigManager,
|
108
108
|
job_manager: JobManager,
|
109
109
|
actor: User,
|
110
|
-
use_assistant_message: bool = True,
|
111
110
|
max_steps: int = 10,
|
112
111
|
):
|
113
112
|
self.message_manager = message_manager
|
@@ -117,7 +116,6 @@ class LettaAgentBatch(BaseAgent):
|
|
117
116
|
self.batch_manager = batch_manager
|
118
117
|
self.sandbox_config_manager = sandbox_config_manager
|
119
118
|
self.job_manager = job_manager
|
120
|
-
self.use_assistant_message = use_assistant_message
|
121
119
|
self.actor = actor
|
122
120
|
self.max_steps = max_steps
|
123
121
|
|
@@ -128,6 +126,7 @@ class LettaAgentBatch(BaseAgent):
|
|
128
126
|
letta_batch_job_id: str,
|
129
127
|
agent_step_state_mapping: Optional[Dict[str, AgentStepState]] = None,
|
130
128
|
) -> LettaBatchResponse:
|
129
|
+
"""Carry out agent steps until the LLM request is sent."""
|
131
130
|
log_event(name="validate_inputs")
|
132
131
|
if not batch_requests:
|
133
132
|
raise ValueError("Empty list of batch_requests passed in!")
|
@@ -135,15 +134,26 @@ class LettaAgentBatch(BaseAgent):
|
|
135
134
|
agent_step_state_mapping = {}
|
136
135
|
|
137
136
|
log_event(name="load_and_prepare_agents")
|
138
|
-
|
139
|
-
|
137
|
+
# prepares (1) agent states, (2) step states, (3) LLMBatchItems (4) message batch_item_ids (5) messages per agent (6) tools per agent
|
138
|
+
|
139
|
+
agent_messages_mapping: dict[str, list[Message]] = {}
|
140
|
+
agent_tools_mapping: dict[str, list[dict]] = {}
|
140
141
|
# TODO: This isn't optimal, moving fast - prone to bugs because we pass around this half formed pydantic object
|
141
|
-
agent_batch_item_mapping:
|
142
|
+
agent_batch_item_mapping: dict[str, LLMBatchItem] = {}
|
143
|
+
|
144
|
+
# fetch agent states in batch
|
145
|
+
agent_mapping = {
|
146
|
+
agent_state.id: agent_state
|
147
|
+
for agent_state in await self.agent_manager.get_agents_by_ids_async(
|
148
|
+
agent_ids=[request.agent_id for request in batch_requests], actor=self.actor
|
149
|
+
)
|
150
|
+
}
|
151
|
+
|
142
152
|
agent_states = []
|
143
153
|
for batch_request in batch_requests:
|
144
154
|
agent_id = batch_request.agent_id
|
145
|
-
agent_state =
|
146
|
-
agent_states.append(agent_state)
|
155
|
+
agent_state = agent_mapping[agent_id]
|
156
|
+
agent_states.append(agent_state) # keeping this to maintain ordering, but may not be necessary
|
147
157
|
|
148
158
|
if agent_id not in agent_step_state_mapping:
|
149
159
|
agent_step_state_mapping[agent_id] = AgentStepState(
|
@@ -164,7 +174,7 @@ class LettaAgentBatch(BaseAgent):
|
|
164
174
|
for msg in batch_request.messages:
|
165
175
|
msg.batch_item_id = llm_batch_item.id
|
166
176
|
|
167
|
-
agent_messages_mapping[agent_id] = self.
|
177
|
+
agent_messages_mapping[agent_id] = await self._prepare_in_context_messages_per_agent_async(
|
168
178
|
agent_state=agent_state, input_messages=batch_request.messages
|
169
179
|
)
|
170
180
|
|
@@ -186,7 +196,7 @@ class LettaAgentBatch(BaseAgent):
|
|
186
196
|
)
|
187
197
|
|
188
198
|
log_event(name="persist_llm_batch_job")
|
189
|
-
llm_batch_job = self.batch_manager.
|
199
|
+
llm_batch_job = await self.batch_manager.create_llm_batch_job_async(
|
190
200
|
llm_provider=ProviderType.anthropic, # TODO: Expand to more providers
|
191
201
|
create_batch_response=batch_response,
|
192
202
|
actor=self.actor,
|
@@ -204,7 +214,7 @@ class LettaAgentBatch(BaseAgent):
|
|
204
214
|
|
205
215
|
if batch_items:
|
206
216
|
log_event(name="bulk_create_batch_items")
|
207
|
-
batch_items_persisted = self.batch_manager.
|
217
|
+
batch_items_persisted = await self.batch_manager.create_llm_batch_items_bulk_async(batch_items, actor=self.actor)
|
208
218
|
|
209
219
|
log_event(name="return_batch_response")
|
210
220
|
return LettaBatchResponse(
|
@@ -219,7 +229,7 @@ class LettaAgentBatch(BaseAgent):
|
|
219
229
|
@trace_method
|
220
230
|
async def resume_step_after_request(self, letta_batch_id: str, llm_batch_id: str) -> LettaBatchResponse:
|
221
231
|
log_event(name="load_context")
|
222
|
-
llm_batch_job = self.batch_manager.
|
232
|
+
llm_batch_job = await self.batch_manager.get_llm_batch_job_by_id_async(llm_batch_id=llm_batch_id, actor=self.actor)
|
223
233
|
ctx = await self._collect_resume_context(llm_batch_id)
|
224
234
|
|
225
235
|
log_event(name="update_statuses")
|
@@ -229,7 +239,7 @@ class LettaAgentBatch(BaseAgent):
|
|
229
239
|
exec_results = await self._execute_tools(ctx)
|
230
240
|
|
231
241
|
log_event(name="persist_messages")
|
232
|
-
msg_map = self._persist_tool_messages(exec_results, ctx)
|
242
|
+
msg_map = await self._persist_tool_messages(exec_results, ctx)
|
233
243
|
|
234
244
|
log_event(name="mark_steps_done")
|
235
245
|
self._mark_steps_complete(llm_batch_id, ctx.agent_ids)
|
@@ -237,7 +247,9 @@ class LettaAgentBatch(BaseAgent):
|
|
237
247
|
log_event(name="prepare_next")
|
238
248
|
next_reqs, next_step_state = self._prepare_next_iteration(exec_results, ctx, msg_map)
|
239
249
|
if len(next_reqs) == 0:
|
240
|
-
self.job_manager.
|
250
|
+
await self.job_manager.update_job_by_id_async(
|
251
|
+
job_id=letta_batch_id, job_update=JobUpdate(status=JobStatus.completed), actor=self.actor
|
252
|
+
)
|
241
253
|
return LettaBatchResponse(
|
242
254
|
letta_batch_id=llm_batch_job.letta_batch_job_id,
|
243
255
|
last_llm_batch_id=llm_batch_job.id,
|
@@ -256,18 +268,22 @@ class LettaAgentBatch(BaseAgent):
|
|
256
268
|
@trace_method
|
257
269
|
async def _collect_resume_context(self, llm_batch_id: str) -> _ResumeContext:
|
258
270
|
# NOTE: We only continue for items with successful results
|
259
|
-
batch_items = self.batch_manager.
|
271
|
+
batch_items = await self.batch_manager.list_llm_batch_items_async(llm_batch_id=llm_batch_id, request_status=JobStatus.completed)
|
260
272
|
|
261
|
-
agent_ids
|
262
|
-
provider_results
|
273
|
+
agent_ids = []
|
274
|
+
provider_results = {}
|
263
275
|
request_status_updates: List[RequestStatusUpdateInfo] = []
|
264
276
|
|
265
277
|
for item in batch_items:
|
266
278
|
aid = item.agent_id
|
267
279
|
agent_ids.append(aid)
|
268
|
-
agent_state_map[aid] = self.agent_manager.get_agent_by_id(aid, actor=self.actor)
|
269
280
|
provider_results[aid] = item.batch_request_result.result
|
270
281
|
|
282
|
+
agent_states = await self.agent_manager.get_agents_by_ids_async(agent_ids, actor=self.actor)
|
283
|
+
agent_state_map = {agent.id: agent for agent in agent_states}
|
284
|
+
|
285
|
+
name_map, args_map, cont_map = {}, {}, {}
|
286
|
+
for aid in agent_ids:
|
271
287
|
# status bookkeeping
|
272
288
|
pr = provider_results[aid]
|
273
289
|
status = (
|
@@ -344,14 +360,14 @@ class LettaAgentBatch(BaseAgent):
|
|
344
360
|
tool_params.append(param)
|
345
361
|
|
346
362
|
if rethink_memory_params:
|
347
|
-
return self.
|
363
|
+
return await self._bulk_rethink_memory_async(rethink_memory_params)
|
348
364
|
|
349
365
|
if tool_params:
|
350
366
|
async with Pool() as pool:
|
351
367
|
return await pool.map(execute_tool_wrapper, tool_params)
|
352
368
|
|
353
369
|
@trace_method
|
354
|
-
def
|
370
|
+
async def _bulk_rethink_memory_async(self, params: List[ToolExecutionParams]) -> Sequence[Tuple[str, Tuple[str, bool]]]:
|
355
371
|
updates = {}
|
356
372
|
result = []
|
357
373
|
for param in params:
|
@@ -372,11 +388,11 @@ class LettaAgentBatch(BaseAgent):
|
|
372
388
|
# TODO: This is quite ugly and confusing - this is mostly to align with the returns of other tools
|
373
389
|
result.append((param.agent_id, ("", True)))
|
374
390
|
|
375
|
-
self.block_manager.
|
391
|
+
await self.block_manager.bulk_update_block_values_async(updates=updates, actor=self.actor)
|
376
392
|
|
377
393
|
return result
|
378
394
|
|
379
|
-
def _persist_tool_messages(
|
395
|
+
async def _persist_tool_messages(
|
380
396
|
self,
|
381
397
|
exec_results: Sequence[Tuple[str, Tuple[str, bool]]],
|
382
398
|
ctx: _ResumeContext,
|
@@ -398,7 +414,7 @@ class LettaAgentBatch(BaseAgent):
|
|
398
414
|
)
|
399
415
|
msg_map[aid] = msgs
|
400
416
|
# flatten & persist
|
401
|
-
self.message_manager.
|
417
|
+
await self.message_manager.create_many_messages_async([m for msgs in msg_map.values() for m in msgs], actor=self.actor)
|
402
418
|
return msg_map
|
403
419
|
|
404
420
|
def _mark_steps_complete(self, llm_batch_id: str, agent_ids: List[str]) -> None:
|
@@ -530,12 +546,14 @@ class LettaAgentBatch(BaseAgent):
|
|
530
546
|
valid_tool_names = tool_rules_solver.get_allowed_tool_names(available_tools=set([t.name for t in tools]))
|
531
547
|
return [enable_strict_mode(t.json_schema) for t in tools if t.name in set(valid_tool_names)]
|
532
548
|
|
533
|
-
def
|
534
|
-
|
549
|
+
async def _prepare_in_context_messages_per_agent_async(
|
550
|
+
self, agent_state: AgentState, input_messages: List[MessageCreate]
|
551
|
+
) -> List[Message]:
|
552
|
+
current_in_context_messages, new_in_context_messages = await _prepare_in_context_messages_async(
|
535
553
|
input_messages, agent_state, self.message_manager, self.actor
|
536
554
|
)
|
537
555
|
|
538
|
-
in_context_messages = self.
|
556
|
+
in_context_messages = await self._rebuild_memory_async(current_in_context_messages + new_in_context_messages, agent_state)
|
539
557
|
return in_context_messages
|
540
558
|
|
541
559
|
# TODO: Make this a bullk function
|
@@ -58,7 +58,7 @@ class VoiceSleeptimeAgent(LettaAgent):
|
|
58
58
|
def update_message_transcript(self, message_transcripts: List[str]):
|
59
59
|
self.message_transcripts = message_transcripts
|
60
60
|
|
61
|
-
async def step(self, input_messages: List[MessageCreate], max_steps: int = 20) -> LettaResponse:
|
61
|
+
async def step(self, input_messages: List[MessageCreate], max_steps: int = 20, use_assistant_message: bool = True) -> LettaResponse:
|
62
62
|
"""
|
63
63
|
Process the user's input message, allowing the model to call memory-related tools
|
64
64
|
until it decides to stop and provide a final response.
|
@@ -74,7 +74,7 @@ class VoiceSleeptimeAgent(LettaAgent):
|
|
74
74
|
]
|
75
75
|
|
76
76
|
# Summarize
|
77
|
-
current_in_context_messages, new_in_context_messages = await super()._step(
|
77
|
+
current_in_context_messages, new_in_context_messages, usage = await super()._step(
|
78
78
|
agent_state=agent_state, input_messages=input_messages, max_steps=max_steps
|
79
79
|
)
|
80
80
|
new_in_context_messages, updated = self.summarizer.summarize(
|
@@ -84,7 +84,9 @@ class VoiceSleeptimeAgent(LettaAgent):
|
|
84
84
|
agent_id=self.agent_id, message_ids=[m.id for m in new_in_context_messages], actor=self.actor
|
85
85
|
)
|
86
86
|
|
87
|
-
return _create_letta_response(
|
87
|
+
return _create_letta_response(
|
88
|
+
new_in_context_messages=new_in_context_messages, use_assistant_message=use_assistant_message, usage=usage
|
89
|
+
)
|
88
90
|
|
89
91
|
@trace_method
|
90
92
|
async def _execute_tool(self, tool_name: str, tool_args: dict, agent_state: AgentState) -> Tuple[str, bool]:
|
@@ -146,7 +148,7 @@ class VoiceSleeptimeAgent(LettaAgent):
|
|
146
148
|
return f"Failed to store memory given start_index {start_index} and end_index {end_index}: {e}", False
|
147
149
|
|
148
150
|
async def step_stream(
|
149
|
-
self, input_messages: List[MessageCreate], max_steps: int = 10, use_assistant_message: bool =
|
151
|
+
self, input_messages: List[MessageCreate], max_steps: int = 10, use_assistant_message: bool = True
|
150
152
|
) -> AsyncGenerator[Union[LettaMessage, LegacyLettaMessage, MessageStreamStatus], None]:
|
151
153
|
"""
|
152
154
|
This agent is synchronous-only. If called in an async context, raise an error.
|
letta/client/client.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import asyncio
|
1
2
|
import logging
|
2
3
|
import sys
|
3
4
|
import time
|
@@ -3055,7 +3056,21 @@ class LocalClient(AbstractClient):
|
|
3055
3056
|
Returns:
|
3056
3057
|
tools (List[Tool]): List of tools
|
3057
3058
|
"""
|
3058
|
-
|
3059
|
+
# Get the current event loop or create a new one if there isn't one
|
3060
|
+
try:
|
3061
|
+
loop = asyncio.get_event_loop()
|
3062
|
+
if loop.is_running():
|
3063
|
+
# We're in an async context but can't await - use a new loop via run_coroutine_threadsafe
|
3064
|
+
concurrent_future = asyncio.run_coroutine_threadsafe(
|
3065
|
+
self.server.tool_manager.list_tools_async(actor=self.user, after=after, limit=limit), loop
|
3066
|
+
)
|
3067
|
+
return concurrent_future.result()
|
3068
|
+
else:
|
3069
|
+
# We have a loop but it's not running - we can just run the coroutine
|
3070
|
+
return loop.run_until_complete(self.server.tool_manager.list_tools_async(actor=self.user, after=after, limit=limit))
|
3071
|
+
except RuntimeError:
|
3072
|
+
# No running event loop - create a new one with asyncio.run
|
3073
|
+
return asyncio.run(self.server.tool_manager.list_tools_async(actor=self.user, after=after, limit=limit))
|
3059
3074
|
|
3060
3075
|
def get_tool(self, id: str) -> Optional[Tool]:
|
3061
3076
|
"""
|
letta/constants.py
CHANGED
@@ -195,6 +195,9 @@ DATA_SOURCE_ATTACH_ALERT = (
|
|
195
195
|
"[ALERT] New data was just uploaded to archival memory. You can view this data by calling the archival_memory_search tool."
|
196
196
|
)
|
197
197
|
|
198
|
+
# Throw an error message when a read-only block is edited
|
199
|
+
READ_ONLY_BLOCK_EDIT_ERROR = f"{ERROR_MESSAGE_PREFIX} This block is read-only and cannot be edited."
|
200
|
+
|
198
201
|
# The ackknowledgement message used in the summarize sequence
|
199
202
|
MESSAGE_SUMMARY_REQUEST_ACK = "Understood, I will respond with a summary of the message (and only the summary, nothing else) once I receive the conversation history. I'm ready."
|
200
203
|
|
@@ -12,7 +12,7 @@ from composio.exceptions import (
|
|
12
12
|
)
|
13
13
|
|
14
14
|
|
15
|
-
class AsyncComposioToolSet(BaseComposioToolSet, runtime="letta"):
|
15
|
+
class AsyncComposioToolSet(BaseComposioToolSet, runtime="letta", description_char_limit=1024):
|
16
16
|
"""
|
17
17
|
Async version of ComposioToolSet client for interacting with Composio API
|
18
18
|
Used to asynchronously hit the execute action endpoint
|
@@ -42,6 +42,7 @@ class SleeptimeMultiAgent(Agent):
|
|
42
42
|
self.group_manager = GroupManager()
|
43
43
|
self.message_manager = MessageManager()
|
44
44
|
self.job_manager = JobManager()
|
45
|
+
self.mcp_clients = mcp_clients
|
45
46
|
|
46
47
|
def _run_async_in_new_thread(self, coro):
|
47
48
|
"""Run an async coroutine in a new thread with its own event loop"""
|