letta-nightly 0.7.8.dev20250502104219__py3-none-any.whl → 0.7.9.dev20250503104103__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 +2 -2
- letta/agents/helpers.py +58 -1
- letta/agents/letta_agent.py +13 -3
- letta/agents/letta_agent_batch.py +33 -17
- letta/agents/voice_agent.py +1 -2
- letta/agents/voice_sleeptime_agent.py +75 -320
- letta/functions/function_sets/multi_agent.py +1 -1
- letta/functions/function_sets/voice.py +20 -32
- letta/functions/helpers.py +7 -7
- letta/helpers/datetime_helpers.py +6 -0
- letta/helpers/message_helper.py +19 -18
- letta/jobs/scheduler.py +233 -49
- letta/llm_api/google_ai_client.py +13 -4
- letta/llm_api/google_vertex_client.py +5 -1
- letta/llm_api/openai.py +10 -2
- letta/llm_api/openai_client.py +14 -2
- letta/orm/message.py +4 -0
- letta/prompts/system/voice_sleeptime.txt +2 -3
- letta/schemas/letta_message.py +1 -0
- letta/schemas/letta_request.py +8 -1
- letta/schemas/letta_response.py +5 -0
- letta/schemas/llm_batch_job.py +6 -4
- letta/schemas/llm_config.py +9 -0
- letta/schemas/message.py +23 -2
- letta/schemas/providers.py +3 -1
- letta/server/rest_api/app.py +15 -7
- letta/server/rest_api/routers/v1/agents.py +3 -0
- letta/server/rest_api/routers/v1/messages.py +46 -1
- letta/server/rest_api/routers/v1/steps.py +1 -1
- letta/server/rest_api/utils.py +25 -6
- letta/server/server.py +11 -3
- letta/services/llm_batch_manager.py +60 -1
- letta/services/message_manager.py +1 -0
- letta/services/summarizer/summarizer.py +42 -36
- letta/settings.py +1 -0
- letta/tracing.py +5 -0
- {letta_nightly-0.7.8.dev20250502104219.dist-info → letta_nightly-0.7.9.dev20250503104103.dist-info}/METADATA +2 -2
- {letta_nightly-0.7.8.dev20250502104219.dist-info → letta_nightly-0.7.9.dev20250503104103.dist-info}/RECORD +41 -41
- {letta_nightly-0.7.8.dev20250502104219.dist-info → letta_nightly-0.7.9.dev20250503104103.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.8.dev20250502104219.dist-info → letta_nightly-0.7.9.dev20250503104103.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.8.dev20250502104219.dist-info → letta_nightly-0.7.9.dev20250503104103.dist-info}/entry_points.txt +0 -0
@@ -1,332 +1,138 @@
|
|
1
|
-
import
|
2
|
-
import xml.etree.ElementTree as ET
|
3
|
-
from typing import AsyncGenerator, Dict, List, Optional, Tuple, Union
|
1
|
+
from typing import AsyncGenerator, List, Tuple, Union
|
4
2
|
|
5
|
-
import
|
6
|
-
|
7
|
-
from letta.
|
3
|
+
from letta.agents.helpers import _create_letta_response, serialize_message_history
|
4
|
+
from letta.agents.letta_agent import LettaAgent
|
5
|
+
from letta.orm.enums import ToolType
|
8
6
|
from letta.schemas.agent import AgentState
|
9
7
|
from letta.schemas.block import BlockUpdate
|
10
8
|
from letta.schemas.enums import MessageStreamStatus
|
11
9
|
from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage
|
12
|
-
from letta.schemas.letta_message_content import TextContent
|
13
10
|
from letta.schemas.letta_response import LettaResponse
|
14
|
-
from letta.schemas.message import
|
15
|
-
from letta.schemas.
|
16
|
-
from letta.schemas.usage import LettaUsageStatistics
|
11
|
+
from letta.schemas.message import MessageCreate
|
12
|
+
from letta.schemas.tool_rule import ChildToolRule, ContinueToolRule, InitToolRule, TerminalToolRule
|
17
13
|
from letta.schemas.user import User
|
18
|
-
from letta.server.rest_api.utils import convert_in_context_letta_messages_to_openai, create_input_messages
|
19
14
|
from letta.services.agent_manager import AgentManager
|
20
15
|
from letta.services.block_manager import BlockManager
|
21
16
|
from letta.services.message_manager import MessageManager
|
22
|
-
from letta.
|
17
|
+
from letta.services.passage_manager import PassageManager
|
18
|
+
from letta.services.summarizer.enums import SummarizationMode
|
19
|
+
from letta.services.summarizer.summarizer import Summarizer
|
20
|
+
from letta.tracing import trace_method
|
23
21
|
|
24
22
|
|
25
|
-
|
26
|
-
class VoiceSleeptimeAgent(BaseAgent):
|
23
|
+
class VoiceSleeptimeAgent(LettaAgent):
|
27
24
|
"""
|
28
|
-
A
|
25
|
+
A special variant of the LettaAgent that helps with offline memory computations specifically for voice.
|
29
26
|
"""
|
30
27
|
|
31
28
|
def __init__(
|
32
29
|
self,
|
33
30
|
agent_id: str,
|
34
31
|
convo_agent_state: AgentState,
|
35
|
-
openai_client: openai.AsyncClient,
|
36
32
|
message_manager: MessageManager,
|
37
33
|
agent_manager: AgentManager,
|
38
34
|
block_manager: BlockManager,
|
35
|
+
passage_manager: PassageManager,
|
39
36
|
target_block_label: str,
|
40
|
-
message_transcripts: List[str],
|
41
37
|
actor: User,
|
42
38
|
):
|
43
39
|
super().__init__(
|
44
40
|
agent_id=agent_id,
|
45
|
-
openai_client=openai_client,
|
46
41
|
message_manager=message_manager,
|
47
42
|
agent_manager=agent_manager,
|
43
|
+
block_manager=block_manager,
|
44
|
+
passage_manager=passage_manager,
|
48
45
|
actor=actor,
|
49
46
|
)
|
50
47
|
|
51
48
|
self.convo_agent_state = convo_agent_state
|
52
|
-
self.block_manager = block_manager
|
53
49
|
self.target_block_label = target_block_label
|
54
|
-
self.message_transcripts =
|
50
|
+
self.message_transcripts = []
|
51
|
+
self.summarizer = Summarizer(
|
52
|
+
mode=SummarizationMode.STATIC_MESSAGE_BUFFER,
|
53
|
+
summarizer_agent=None,
|
54
|
+
message_buffer_limit=20,
|
55
|
+
message_buffer_min=10,
|
56
|
+
)
|
55
57
|
|
56
58
|
def update_message_transcript(self, message_transcripts: List[str]):
|
57
59
|
self.message_transcripts = message_transcripts
|
58
60
|
|
59
|
-
async def step(self, input_messages: List[MessageCreate], max_steps: int =
|
61
|
+
async def step(self, input_messages: List[MessageCreate], max_steps: int = 20) -> LettaResponse:
|
60
62
|
"""
|
61
63
|
Process the user's input message, allowing the model to call memory-related tools
|
62
64
|
until it decides to stop and provide a final response.
|
63
65
|
"""
|
64
|
-
agent_state = self.agent_manager.get_agent_by_id(
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
# Process tool calls
|
75
|
-
tool_call = assistant_message.tool_calls[0]
|
76
|
-
function_name = tool_call.function.name
|
77
|
-
function_args = json.loads(tool_call.function.arguments)
|
78
|
-
|
79
|
-
if function_name == "store_memories":
|
80
|
-
print("Called store_memories")
|
81
|
-
print(function_args)
|
82
|
-
chunks = function_args.get("chunks", [])
|
83
|
-
results = [self.store_memory(agent_state=self.convo_agent_state, **chunk_args) for chunk_args in chunks]
|
84
|
-
|
85
|
-
aggregated_result = next((res for res, _ in results if res is not None), None)
|
86
|
-
aggregated_success = all(success for _, success in results)
|
87
|
-
|
88
|
-
else:
|
89
|
-
raise ValueError("Error: Unknown tool function '{function_name}'")
|
66
|
+
agent_state = self.agent_manager.get_agent_by_id(self.agent_id, actor=self.actor)
|
67
|
+
|
68
|
+
# Add tool rules to the agent_state specifically for this type of agent
|
69
|
+
agent_state.tool_rules = [
|
70
|
+
InitToolRule(tool_name="store_memories"),
|
71
|
+
ChildToolRule(tool_name="store_memories", children=["rethink_user_memory"]),
|
72
|
+
ContinueToolRule(tool_name="rethink_user_memory"),
|
73
|
+
TerminalToolRule(tool_name="finish_rethinking_memory"),
|
74
|
+
]
|
90
75
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
"tool_calls": [
|
95
|
-
{
|
96
|
-
"id": tool_call.id,
|
97
|
-
"type": "function",
|
98
|
-
"function": {"name": function_name, "arguments": tool_call.function.arguments},
|
99
|
-
}
|
100
|
-
],
|
101
|
-
}
|
102
|
-
openai_messages.append(assistant_message)
|
103
|
-
in_context_messages.append(
|
104
|
-
Message.dict_to_message(
|
105
|
-
agent_id=self.agent_id,
|
106
|
-
openai_message_dict=assistant_message,
|
107
|
-
model=agent_state.llm_config.model,
|
108
|
-
name=function_name,
|
109
|
-
)
|
76
|
+
# Summarize
|
77
|
+
current_in_context_messages, new_in_context_messages = await super()._step(
|
78
|
+
agent_state=agent_state, input_messages=input_messages, max_steps=max_steps
|
110
79
|
)
|
111
|
-
|
112
|
-
|
113
|
-
"tool_call_id": tool_call.id,
|
114
|
-
"content": package_function_response(was_success=aggregated_success, response_string=str(aggregated_result)),
|
115
|
-
}
|
116
|
-
openai_messages.append(tool_call_message)
|
117
|
-
in_context_messages.append(
|
118
|
-
Message.dict_to_message(
|
119
|
-
agent_id=self.agent_id,
|
120
|
-
openai_message_dict=tool_call_message,
|
121
|
-
model=agent_state.llm_config.model,
|
122
|
-
name=function_name,
|
123
|
-
tool_returns=[ToolReturn(status="success" if aggregated_success else "error")],
|
124
|
-
)
|
80
|
+
new_in_context_messages, updated = self.summarizer.summarize(
|
81
|
+
in_context_messages=current_in_context_messages, new_letta_messages=new_in_context_messages
|
125
82
|
)
|
126
|
-
|
127
|
-
|
128
|
-
human_block_content = self.agent_manager.get_block_with_label(
|
129
|
-
agent_id=self.agent_id, block_label=self.target_block_label, actor=self.actor
|
83
|
+
self.agent_manager.set_in_context_messages(
|
84
|
+
agent_id=self.agent_id, message_ids=[m.id for m in new_in_context_messages], actor=self.actor
|
130
85
|
)
|
131
|
-
rethink_command = f"""
|
132
|
-
Here is the current memory block created earlier:
|
133
|
-
|
134
|
-
### CURRENT MEMORY
|
135
|
-
{human_block_content}
|
136
|
-
### END CURRENT MEMORY
|
137
|
-
|
138
|
-
Please refine this block:
|
139
86
|
|
140
|
-
|
141
|
-
- Organize related information together (e.g., preferences, background, ongoing goals).
|
142
|
-
- Add any light, supportable inferences that deepen understanding—but do not invent unsupported details.
|
87
|
+
return _create_letta_response(new_in_context_messages=new_in_context_messages, use_assistant_message=self.use_assistant_message)
|
143
88
|
|
144
|
-
|
89
|
+
@trace_method
|
90
|
+
async def _execute_tool(self, tool_name: str, tool_args: dict, agent_state: AgentState) -> Tuple[str, bool]:
|
145
91
|
"""
|
146
|
-
|
147
|
-
openai_messages.append(rethink_command.model_dump())
|
148
|
-
|
149
|
-
for _ in range(max_steps):
|
150
|
-
request = self._build_openai_request(openai_messages, agent_state, tools=self._build_sleeptime_tools())
|
151
|
-
chat_completion = await self.openai_client.chat.completions.create(**request.model_dump(exclude_unset=True))
|
152
|
-
assistant_message = chat_completion.choices[0].message
|
153
|
-
|
154
|
-
# Process tool calls
|
155
|
-
tool_call = assistant_message.tool_calls[0]
|
156
|
-
function_name = tool_call.function.name
|
157
|
-
function_args = json.loads(tool_call.function.arguments)
|
158
|
-
|
159
|
-
if function_name == "rethink_user_memory":
|
160
|
-
print("Called rethink_user_memory")
|
161
|
-
print(function_args)
|
162
|
-
result, success = self.rethink_user_memory(agent_state=agent_state, **function_args)
|
163
|
-
elif function_name == "finish_rethinking_memory":
|
164
|
-
print("Called finish_rethinking_memory")
|
165
|
-
result, success = None, True
|
166
|
-
break
|
167
|
-
else:
|
168
|
-
print(f"Error: Unknown tool function '{function_name}'")
|
169
|
-
raise ValueError(f"Error: Unknown tool function '{function_name}'", False)
|
170
|
-
assistant_message = {
|
171
|
-
"role": "assistant",
|
172
|
-
"content": assistant_message.content,
|
173
|
-
"tool_calls": [
|
174
|
-
{
|
175
|
-
"id": tool_call.id,
|
176
|
-
"type": "function",
|
177
|
-
"function": {"name": function_name, "arguments": tool_call.function.arguments},
|
178
|
-
}
|
179
|
-
],
|
180
|
-
}
|
181
|
-
openai_messages.append(assistant_message)
|
182
|
-
in_context_messages.append(
|
183
|
-
Message.dict_to_message(
|
184
|
-
agent_id=self.agent_id,
|
185
|
-
openai_message_dict=assistant_message,
|
186
|
-
model=agent_state.llm_config.model,
|
187
|
-
name=function_name,
|
188
|
-
)
|
189
|
-
)
|
190
|
-
tool_call_message = {
|
191
|
-
"role": "tool",
|
192
|
-
"tool_call_id": tool_call.id,
|
193
|
-
"content": package_function_response(was_success=success, response_string=str(result)),
|
194
|
-
}
|
195
|
-
openai_messages.append(tool_call_message)
|
196
|
-
in_context_messages.append(
|
197
|
-
Message.dict_to_message(
|
198
|
-
agent_id=self.agent_id,
|
199
|
-
openai_message_dict=tool_call_message,
|
200
|
-
model=agent_state.llm_config.model,
|
201
|
-
name=function_name,
|
202
|
-
tool_returns=[ToolReturn(status="success" if success else "error")],
|
203
|
-
)
|
204
|
-
)
|
205
|
-
|
206
|
-
# Actually save the memory:
|
207
|
-
target_block = agent_state.memory.get_block(self.target_block_label)
|
208
|
-
self.block_manager.update_block(block_id=target_block.id, block_update=BlockUpdate(value=target_block.value), actor=self.actor)
|
209
|
-
|
210
|
-
self.message_manager.create_many_messages(pydantic_msgs=in_context_messages, actor=self.actor)
|
211
|
-
return LettaResponse(messages=[msg for m in in_context_messages for msg in m.to_letta_messages()], usage=LettaUsageStatistics())
|
212
|
-
|
213
|
-
def _format_messages_llm_friendly(self):
|
214
|
-
messages = self.message_manager.list_messages_for_agent(agent_id=self.agent_id, actor=self.actor)
|
215
|
-
|
216
|
-
llm_friendly_messages = [f"{m.role}: {m.content[0].text}" for m in messages if m.content and isinstance(m.content[0], TextContent)]
|
217
|
-
return "\n".join(llm_friendly_messages)
|
218
|
-
|
219
|
-
def _build_openai_request(self, openai_messages: List[Dict], agent_state: AgentState, tools: List[Tool]) -> ChatCompletionRequest:
|
220
|
-
openai_request = ChatCompletionRequest(
|
221
|
-
model=agent_state.llm_config.model, # TODO: Separate config for summarizer?
|
222
|
-
messages=openai_messages,
|
223
|
-
tools=tools,
|
224
|
-
tool_choice="required",
|
225
|
-
user=self.actor.id,
|
226
|
-
max_completion_tokens=agent_state.llm_config.max_tokens,
|
227
|
-
temperature=agent_state.llm_config.temperature,
|
228
|
-
stream=False,
|
229
|
-
)
|
230
|
-
return openai_request
|
231
|
-
|
232
|
-
def _build_store_memory_tool_schemas(self) -> List[Tool]:
|
233
|
-
"""
|
234
|
-
Build the schemas for the three memory-related tools.
|
92
|
+
Executes a tool and returns (result, success_flag).
|
235
93
|
"""
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
"name": "store_memories",
|
241
|
-
"description": "Archive coherent chunks of dialogue that will be evicted, preserving raw lines and a brief contextual description.",
|
242
|
-
"parameters": {
|
243
|
-
"type": "object",
|
244
|
-
"properties": {
|
245
|
-
"chunks": {
|
246
|
-
"type": "array",
|
247
|
-
"items": {
|
248
|
-
"type": "object",
|
249
|
-
"properties": {
|
250
|
-
"start_index": {"type": "integer", "description": "Index of first line in original history."},
|
251
|
-
"end_index": {"type": "integer", "description": "Index of last line in original history."},
|
252
|
-
"context": {
|
253
|
-
"type": "string",
|
254
|
-
"description": "A high-level description providing context for why this chunk matters.",
|
255
|
-
},
|
256
|
-
},
|
257
|
-
"required": ["start_index", "end_index", "context"],
|
258
|
-
},
|
259
|
-
}
|
260
|
-
},
|
261
|
-
"required": ["chunks"],
|
262
|
-
"additionalProperties": False,
|
263
|
-
},
|
264
|
-
},
|
265
|
-
),
|
266
|
-
]
|
267
|
-
|
268
|
-
return tools
|
94
|
+
# Special memory case
|
95
|
+
target_tool = next((x for x in agent_state.tools if x.name == tool_name), None)
|
96
|
+
if not target_tool:
|
97
|
+
return f"Tool not found: {tool_name}", False
|
269
98
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
"The new memory with information integrated from the memory block. "
|
289
|
-
"If there is no new information, then this should be the same as the "
|
290
|
-
"content in the source block."
|
291
|
-
),
|
292
|
-
},
|
293
|
-
},
|
294
|
-
"required": ["new_memory"],
|
295
|
-
"additionalProperties": False,
|
296
|
-
},
|
297
|
-
},
|
298
|
-
),
|
299
|
-
Tool(
|
300
|
-
type="function",
|
301
|
-
function={
|
302
|
-
"name": "finish_rethinking_memory",
|
303
|
-
"description": ("This function is called when the agent is done rethinking the memory."),
|
304
|
-
"parameters": {
|
305
|
-
"type": "object",
|
306
|
-
"properties": {},
|
307
|
-
"required": [],
|
308
|
-
"additionalProperties": False,
|
309
|
-
},
|
310
|
-
},
|
311
|
-
),
|
312
|
-
]
|
313
|
-
|
314
|
-
return tools
|
99
|
+
try:
|
100
|
+
if target_tool.name == "rethink_user_memory" and target_tool.tool_type == ToolType.LETTA_VOICE_SLEEPTIME_CORE:
|
101
|
+
return self.rethink_user_memory(agent_state=agent_state, **tool_args)
|
102
|
+
elif target_tool.name == "finish_rethinking_memory" and target_tool.tool_type == ToolType.LETTA_VOICE_SLEEPTIME_CORE:
|
103
|
+
return "", True
|
104
|
+
elif target_tool.name == "store_memories" and target_tool.tool_type == ToolType.LETTA_VOICE_SLEEPTIME_CORE:
|
105
|
+
chunks = tool_args.get("chunks", [])
|
106
|
+
results = [self.store_memory(agent_state=self.convo_agent_state, **chunk_args) for chunk_args in chunks]
|
107
|
+
|
108
|
+
aggregated_result = next((res for res, _ in results if res is not None), None)
|
109
|
+
aggregated_success = all(success for _, success in results)
|
110
|
+
|
111
|
+
return aggregated_result, aggregated_success # Note that here we store to the convo agent's archival memory
|
112
|
+
else:
|
113
|
+
result = f"Voice sleeptime agent tried invoking invalid tool with type {target_tool.tool_type}: {target_tool}"
|
114
|
+
return result, False
|
115
|
+
except Exception as e:
|
116
|
+
return f"Failed to call tool. Error: {e}", False
|
315
117
|
|
316
|
-
def rethink_user_memory(self, new_memory: str, agent_state: AgentState) -> Tuple[
|
118
|
+
def rethink_user_memory(self, new_memory: str, agent_state: AgentState) -> Tuple[str, bool]:
|
317
119
|
if agent_state.memory.get_block(self.target_block_label) is None:
|
318
120
|
agent_state.memory.create_block(label=self.target_block_label, value=new_memory)
|
319
121
|
|
320
122
|
agent_state.memory.update_block_value(label=self.target_block_label, value=new_memory)
|
321
|
-
return None, True
|
322
123
|
|
323
|
-
|
124
|
+
target_block = agent_state.memory.get_block(self.target_block_label)
|
125
|
+
self.block_manager.update_block(block_id=target_block.id, block_update=BlockUpdate(value=target_block.value), actor=self.actor)
|
126
|
+
|
127
|
+
return "", True
|
128
|
+
|
129
|
+
def store_memory(self, start_index: int, end_index: int, context: str, agent_state: AgentState) -> Tuple[str, bool]:
|
324
130
|
"""
|
325
131
|
Store a memory.
|
326
132
|
"""
|
327
133
|
try:
|
328
134
|
messages = self.message_transcripts[start_index : end_index + 1]
|
329
|
-
memory =
|
135
|
+
memory = serialize_message_history(messages, context)
|
330
136
|
self.agent_manager.passage_manager.insert_passage(
|
331
137
|
agent_state=agent_state,
|
332
138
|
agent_id=agent_state.id,
|
@@ -335,63 +141,12 @@ Use `rethink_user_memory(new_memory)` as many times as you need to iteratively i
|
|
335
141
|
)
|
336
142
|
self.agent_manager.rebuild_system_prompt(agent_id=agent_state.id, actor=self.actor, force=True)
|
337
143
|
|
338
|
-
return
|
144
|
+
return "", True
|
339
145
|
except Exception as e:
|
340
146
|
return f"Failed to store memory given start_index {start_index} and end_index {end_index}: {e}", False
|
341
147
|
|
342
|
-
def serialize(self, messages: List[str], context: str) -> str:
|
343
|
-
"""
|
344
|
-
Produce an XML document like:
|
345
|
-
|
346
|
-
<memory>
|
347
|
-
<messages>
|
348
|
-
<message>…</message>
|
349
|
-
<message>…</message>
|
350
|
-
…
|
351
|
-
</messages>
|
352
|
-
<context>…</context>
|
353
|
-
</memory>
|
354
|
-
"""
|
355
|
-
root = ET.Element("memory")
|
356
|
-
|
357
|
-
msgs_el = ET.SubElement(root, "messages")
|
358
|
-
for msg in messages:
|
359
|
-
m = ET.SubElement(msgs_el, "message")
|
360
|
-
m.text = msg
|
361
|
-
|
362
|
-
sum_el = ET.SubElement(root, "context")
|
363
|
-
sum_el.text = context
|
364
|
-
|
365
|
-
# ET.tostring will escape reserved chars for you
|
366
|
-
return ET.tostring(root, encoding="unicode")
|
367
|
-
|
368
|
-
def deserialize(self, xml_str: str) -> Tuple[List[str], str]:
|
369
|
-
"""
|
370
|
-
Parse the XML back into (messages, context). Raises ValueError if tags are missing.
|
371
|
-
"""
|
372
|
-
try:
|
373
|
-
root = ET.fromstring(xml_str)
|
374
|
-
except ET.ParseError as e:
|
375
|
-
raise ValueError(f"Invalid XML: {e}")
|
376
|
-
|
377
|
-
msgs_el = root.find("messages")
|
378
|
-
if msgs_el is None:
|
379
|
-
raise ValueError("Missing <messages> section")
|
380
|
-
|
381
|
-
messages = []
|
382
|
-
for m in msgs_el.findall("message"):
|
383
|
-
# .text may be None if empty, so coerce to empty string
|
384
|
-
messages.append(m.text or "")
|
385
|
-
|
386
|
-
sum_el = root.find("context")
|
387
|
-
if sum_el is None:
|
388
|
-
raise ValueError("Missing <context> section")
|
389
|
-
context = sum_el.text or ""
|
390
|
-
|
391
|
-
return messages, context
|
392
|
-
|
393
148
|
async def step_stream(
|
394
|
-
self, input_messages: List[MessageCreate], max_steps: int = 10
|
149
|
+
self, input_messages: List[MessageCreate], max_steps: int = 10, use_assistant_message: bool = False
|
395
150
|
) -> AsyncGenerator[Union[LettaMessage, LegacyLettaMessage, MessageStreamStatus], None]:
|
396
151
|
"""
|
397
152
|
This agent is synchronous-only. If called in an async context, raise an error.
|
@@ -68,7 +68,7 @@ def send_message_to_agent_async(self: "Agent", message: str, other_agent_id: str
|
|
68
68
|
messages=messages,
|
69
69
|
other_agent_id=other_agent_id,
|
70
70
|
log_prefix="[send_message_to_agent_async]",
|
71
|
-
use_retries=False, # or True if you want to use
|
71
|
+
use_retries=False, # or True if you want to use _async_send_message_with_retries
|
72
72
|
)
|
73
73
|
|
74
74
|
# Immediately return to caller
|
@@ -6,15 +6,10 @@ from pydantic import BaseModel, Field
|
|
6
6
|
|
7
7
|
def rethink_user_memory(agent_state: "AgentState", new_memory: str) -> None:
|
8
8
|
"""
|
9
|
-
Rewrite memory block for the main agent, new_memory should contain all current
|
10
|
-
information from the block that is not outdated or inconsistent, integrating any
|
11
|
-
new information, resulting in a new memory block that is organized, readable, and
|
12
|
-
comprehensive.
|
9
|
+
Rewrite memory block for the main agent, new_memory should contain all current information from the block that is not outdated or inconsistent, integrating any new information, resulting in a new memory block that is organized, readable, and comprehensive.
|
13
10
|
|
14
11
|
Args:
|
15
|
-
new_memory (str): The new memory with information integrated from the memory block.
|
16
|
-
If there is no new information, then this should be the same as
|
17
|
-
the content in the source block.
|
12
|
+
new_memory (str): The new memory with information integrated from the memory block. If there is no new information, then this should be the same as the content in the source block.
|
18
13
|
|
19
14
|
Returns:
|
20
15
|
None: None is always returned as this function does not produce a response.
|
@@ -34,26 +29,27 @@ def finish_rethinking_memory(agent_state: "AgentState") -> None: # type: ignore
|
|
34
29
|
|
35
30
|
|
36
31
|
class MemoryChunk(BaseModel):
|
37
|
-
start_index: int = Field(
|
38
|
-
|
39
|
-
|
32
|
+
start_index: int = Field(
|
33
|
+
...,
|
34
|
+
description="Zero-based index of the first evicted line in this chunk.",
|
35
|
+
)
|
36
|
+
end_index: int = Field(
|
37
|
+
...,
|
38
|
+
description="Zero-based index of the last evicted line (inclusive).",
|
39
|
+
)
|
40
|
+
context: str = Field(
|
41
|
+
...,
|
42
|
+
description="1-3 sentence paraphrase capturing key facts/details, user preferences, or goals that this chunk reveals—written for future retrieval.",
|
43
|
+
)
|
40
44
|
|
41
45
|
|
42
46
|
def store_memories(agent_state: "AgentState", chunks: List[MemoryChunk]) -> None:
|
43
47
|
"""
|
44
|
-
|
45
|
-
and a brief contextual description.
|
48
|
+
Persist dialogue that is about to fall out of the agent’s context window.
|
46
49
|
|
47
50
|
Args:
|
48
|
-
agent_state (AgentState):
|
49
|
-
The agent’s current memory state, exposing both its in-session history
|
50
|
-
and the archival memory API.
|
51
51
|
chunks (List[MemoryChunk]):
|
52
|
-
|
53
|
-
• start_index (int): Index of the first line in the original history.
|
54
|
-
• end_index (int): Index of the last line in the original history.
|
55
|
-
• context (str): A concise, high-level description of why this chunk
|
56
|
-
matters and what it contains.
|
52
|
+
Each chunk pinpoints a contiguous block of **evicted** lines and provides a short, forward-looking synopsis (`context`) that will be embedded for future semantic lookup.
|
57
53
|
|
58
54
|
Returns:
|
59
55
|
None
|
@@ -69,20 +65,12 @@ def search_memory(
|
|
69
65
|
end_minutes_ago: Optional[int],
|
70
66
|
) -> Optional[str]:
|
71
67
|
"""
|
72
|
-
Look in long-term or earlier-conversation memory only when the user asks about
|
73
|
-
something missing from the visible context. The user’s latest utterance is sent
|
74
|
-
automatically as the main query.
|
68
|
+
Look in long-term or earlier-conversation memory only when the user asks about something missing from the visible context. The user’s latest utterance is sent automatically as the main query.
|
75
69
|
|
76
70
|
Args:
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
(e.g., order ID, place name) to refine the search when the request is vague.
|
81
|
-
Set to None if the user’s utterance is already specific.
|
82
|
-
start_minutes_ago (Optional[int]): Newer bound of the time window for results,
|
83
|
-
specified in minutes ago. Set to None if no lower time bound is needed.
|
84
|
-
end_minutes_ago (Optional[int]): Older bound of the time window for results,
|
85
|
-
specified in minutes ago. Set to None if no upper time bound is needed.
|
71
|
+
convo_keyword_queries (Optional[List[str]]): Extra keywords (e.g., order ID, place name). Use *null* if not appropriate for the latest user message.
|
72
|
+
start_minutes_ago (Optional[int]): Newer bound of the time window for results, specified in minutes ago. Use *null* if no lower time bound is needed.
|
73
|
+
end_minutes_ago (Optional[int]): Older bound of the time window, in minutes ago. Use *null* if no upper bound is needed.
|
86
74
|
|
87
75
|
Returns:
|
88
76
|
Optional[str]: A formatted string of matching memory entries, or None if no
|
letta/functions/helpers.py
CHANGED
@@ -231,7 +231,7 @@ async def async_execute_send_message_to_agent(
|
|
231
231
|
"""
|
232
232
|
Async helper to:
|
233
233
|
1) validate the target agent exists & is in the same org,
|
234
|
-
2) send a message via
|
234
|
+
2) send a message via _async_send_message_with_retries.
|
235
235
|
"""
|
236
236
|
server = get_letta_server()
|
237
237
|
|
@@ -242,7 +242,7 @@ async def async_execute_send_message_to_agent(
|
|
242
242
|
raise ValueError(f"Target agent {other_agent_id} either does not exist or is not in org " f"({sender_agent.user.organization_id}).")
|
243
243
|
|
244
244
|
# 2. Use your async retry logic
|
245
|
-
return await
|
245
|
+
return await _async_send_message_with_retries(
|
246
246
|
server=server,
|
247
247
|
sender_agent=sender_agent,
|
248
248
|
target_agent_id=other_agent_id,
|
@@ -304,7 +304,7 @@ async def _async_send_message_with_retries(
|
|
304
304
|
timeout: int,
|
305
305
|
logging_prefix: Optional[str] = None,
|
306
306
|
) -> str:
|
307
|
-
logging_prefix = logging_prefix or "[
|
307
|
+
logging_prefix = logging_prefix or "[_async_send_message_with_retries]"
|
308
308
|
|
309
309
|
for attempt in range(1, max_retries + 1):
|
310
310
|
try:
|
@@ -363,7 +363,7 @@ def fire_and_forget_send_to_agent(
|
|
363
363
|
messages (List[MessageCreate]): The messages to send.
|
364
364
|
other_agent_id (str): The ID of the target agent.
|
365
365
|
log_prefix (str): Prefix for logging.
|
366
|
-
use_retries (bool): If True, uses
|
366
|
+
use_retries (bool): If True, uses _async_send_message_with_retries;
|
367
367
|
if False, calls server.send_message_to_agent directly.
|
368
368
|
"""
|
369
369
|
server = get_letta_server()
|
@@ -381,7 +381,7 @@ def fire_and_forget_send_to_agent(
|
|
381
381
|
async def background_task():
|
382
382
|
try:
|
383
383
|
if use_retries:
|
384
|
-
result = await
|
384
|
+
result = await _async_send_message_with_retries(
|
385
385
|
server=server,
|
386
386
|
sender_agent=sender_agent,
|
387
387
|
target_agent_id=other_agent_id,
|
@@ -434,7 +434,7 @@ async def _send_message_to_agents_matching_tags_async(
|
|
434
434
|
sender_agent: "Agent", server: "SyncServer", messages: List[MessageCreate], matching_agents: List["AgentState"]
|
435
435
|
) -> List[str]:
|
436
436
|
async def _send_single(agent_state):
|
437
|
-
return await
|
437
|
+
return await _async_send_message_with_retries(
|
438
438
|
server=server,
|
439
439
|
sender_agent=sender_agent,
|
440
440
|
target_agent_id=agent_state.id,
|
@@ -475,7 +475,7 @@ async def _send_message_to_all_agents_in_group_async(sender_agent: "Agent", mess
|
|
475
475
|
|
476
476
|
async def _send_single(agent_state):
|
477
477
|
async with sem:
|
478
|
-
return await
|
478
|
+
return await _async_send_message_with_retries(
|
479
479
|
server=server,
|
480
480
|
sender_agent=sender_agent,
|
481
481
|
target_agent_id=agent_state.id,
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import re
|
2
|
+
import time
|
2
3
|
from datetime import datetime, timedelta, timezone
|
3
4
|
from time import strftime
|
4
5
|
|
@@ -77,6 +78,11 @@ def get_utc_time_int() -> int:
|
|
77
78
|
return int(get_utc_time().timestamp())
|
78
79
|
|
79
80
|
|
81
|
+
def get_utc_timestamp_ns() -> int:
|
82
|
+
"""Get the current UTC time in nanoseconds"""
|
83
|
+
return int(time.time_ns())
|
84
|
+
|
85
|
+
|
80
86
|
def timestamp_to_datetime(timestamp_seconds: int) -> datetime:
|
81
87
|
"""Convert Unix timestamp in seconds to UTC datetime object"""
|
82
88
|
return datetime.fromtimestamp(timestamp_seconds, tz=timezone.utc)
|