letta-nightly 0.7.8.dev20250502104219__py3-none-any.whl → 0.7.9.dev20250502222710__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. letta/__init__.py +2 -2
  2. letta/agents/helpers.py +58 -1
  3. letta/agents/letta_agent.py +13 -3
  4. letta/agents/letta_agent_batch.py +33 -17
  5. letta/agents/voice_agent.py +1 -2
  6. letta/agents/voice_sleeptime_agent.py +75 -320
  7. letta/functions/function_sets/multi_agent.py +1 -1
  8. letta/functions/function_sets/voice.py +20 -32
  9. letta/functions/helpers.py +7 -7
  10. letta/helpers/datetime_helpers.py +6 -0
  11. letta/helpers/message_helper.py +19 -18
  12. letta/jobs/scheduler.py +233 -49
  13. letta/llm_api/google_ai_client.py +13 -4
  14. letta/llm_api/google_vertex_client.py +5 -1
  15. letta/llm_api/openai.py +10 -2
  16. letta/llm_api/openai_client.py +14 -2
  17. letta/orm/message.py +4 -0
  18. letta/prompts/system/voice_sleeptime.txt +2 -3
  19. letta/schemas/letta_message.py +1 -0
  20. letta/schemas/letta_request.py +8 -1
  21. letta/schemas/letta_response.py +5 -0
  22. letta/schemas/llm_batch_job.py +6 -4
  23. letta/schemas/llm_config.py +9 -0
  24. letta/schemas/message.py +23 -2
  25. letta/schemas/providers.py +3 -1
  26. letta/server/rest_api/app.py +15 -7
  27. letta/server/rest_api/routers/v1/agents.py +3 -0
  28. letta/server/rest_api/routers/v1/messages.py +46 -1
  29. letta/server/rest_api/routers/v1/steps.py +1 -1
  30. letta/server/rest_api/utils.py +25 -6
  31. letta/server/server.py +11 -3
  32. letta/services/llm_batch_manager.py +60 -1
  33. letta/services/message_manager.py +1 -0
  34. letta/services/summarizer/summarizer.py +42 -36
  35. letta/settings.py +1 -0
  36. letta/tracing.py +5 -0
  37. {letta_nightly-0.7.8.dev20250502104219.dist-info → letta_nightly-0.7.9.dev20250502222710.dist-info}/METADATA +2 -2
  38. {letta_nightly-0.7.8.dev20250502104219.dist-info → letta_nightly-0.7.9.dev20250502222710.dist-info}/RECORD +41 -41
  39. {letta_nightly-0.7.8.dev20250502104219.dist-info → letta_nightly-0.7.9.dev20250502222710.dist-info}/LICENSE +0 -0
  40. {letta_nightly-0.7.8.dev20250502104219.dist-info → letta_nightly-0.7.9.dev20250502222710.dist-info}/WHEEL +0 -0
  41. {letta_nightly-0.7.8.dev20250502104219.dist-info → letta_nightly-0.7.9.dev20250502222710.dist-info}/entry_points.txt +0 -0
@@ -1,332 +1,138 @@
1
- import json
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 openai
6
-
7
- from letta.agents.base_agent import BaseAgent
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 Message, MessageCreate, ToolReturn
15
- from letta.schemas.openai.chat_completion_request import ChatCompletionRequest, Tool, UserMessage
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.system import package_function_response
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
- # TODO: Move this to the new Letta Agent loop
26
- class VoiceSleeptimeAgent(BaseAgent):
23
+ class VoiceSleeptimeAgent(LettaAgent):
27
24
  """
28
- A stateless agent that helps with offline memory computations.
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 = 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 = 10) -> LettaResponse:
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(agent_id=self.agent_id, actor=self.actor)
65
- in_context_messages = create_input_messages(input_messages=input_messages, agent_id=self.agent_id, actor=self.actor)
66
- openai_messages = convert_in_context_letta_messages_to_openai(in_context_messages, exclude_system_messages=True)
67
-
68
- # 1. Store memories
69
- request = self._build_openai_request(openai_messages, agent_state, tools=self._build_store_memory_tool_schemas())
70
-
71
- chat_completion = await self.openai_client.chat.completions.create(**request.model_dump(exclude_unset=True))
72
- assistant_message = chat_completion.choices[0].message
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
- assistant_message = {
92
- "role": "assistant",
93
- "content": assistant_message.content,
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
- tool_call_message = {
112
- "role": "tool",
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
- # 2. Execute rethink block memory loop
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
- - Merge in any new facts and remove outdated or contradictory details.
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
- Use `rethink_user_memory(new_memory)` as many times as you need to iteratively improve the text. When it’s fully polished and complete, call `finish_rethinking_memory()`.
89
+ @trace_method
90
+ async def _execute_tool(self, tool_name: str, tool_args: dict, agent_state: AgentState) -> Tuple[str, bool]:
145
91
  """
146
- rethink_command = UserMessage(content=rethink_command)
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
- tools = [
237
- Tool(
238
- type="function",
239
- function={
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
- def _build_sleeptime_tools(self) -> List[Tool]:
271
- tools = [
272
- Tool(
273
- type="function",
274
- function={
275
- "name": "rethink_user_memory",
276
- "description": (
277
- "Rewrite memory block for the main agent, new_memory should contain all current "
278
- "information from the block that is not outdated or inconsistent, integrating any "
279
- "new information, resulting in a new memory block that is organized, readable, and "
280
- "comprehensive."
281
- ),
282
- "parameters": {
283
- "type": "object",
284
- "properties": {
285
- "new_memory": {
286
- "type": "string",
287
- "description": (
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[Optional[str], bool]:
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
- def store_memory(self, start_index: int, end_index: int, context: str, agent_state: AgentState) -> Tuple[Optional[str], bool]:
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 = self.serialize(messages, context)
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 None, True
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 async_send_message_with_retries
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(..., description="Index of the first line in the original conversation history.")
38
- end_index: int = Field(..., description="Index of the last line in the original conversation history.")
39
- context: str = Field(..., description="A concise, high-level note explaining why this chunk matters.")
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
- Archive coherent chunks of dialogue that will be evicted, preserving raw lines
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
- A list of MemoryChunk models, each representing a segment to archive:
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
- agent_state (AgentState): The current state of the agent, including its
78
- memory stores and context.
79
- convo_keyword_queries (Optional[List[str]]): Extra keywords or identifiers
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
@@ -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 async_send_message_with_retries.
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 async_send_message_with_retries(
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 "[async_send_message_with_retries]"
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 async_send_message_with_retries;
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 async_send_message_with_retries(
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 async_send_message_with_retries(
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 async_send_message_with_retries(
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)