letta-nightly 0.7.6.dev20250430104233__py3-none-any.whl → 0.7.8.dev20250501064110__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 (70) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +8 -12
  3. letta/agents/exceptions.py +6 -0
  4. letta/agents/helpers.py +1 -1
  5. letta/agents/letta_agent.py +48 -35
  6. letta/agents/letta_agent_batch.py +6 -2
  7. letta/agents/voice_agent.py +41 -59
  8. letta/agents/{ephemeral_memory_agent.py → voice_sleeptime_agent.py} +106 -129
  9. letta/client/client.py +3 -3
  10. letta/constants.py +18 -2
  11. letta/functions/composio_helpers.py +100 -0
  12. letta/functions/function_sets/base.py +0 -10
  13. letta/functions/function_sets/voice.py +92 -0
  14. letta/functions/functions.py +4 -2
  15. letta/functions/helpers.py +19 -101
  16. letta/groups/helpers.py +1 -0
  17. letta/groups/sleeptime_multi_agent.py +5 -1
  18. letta/helpers/message_helper.py +21 -4
  19. letta/helpers/tool_execution_helper.py +1 -1
  20. letta/interfaces/anthropic_streaming_interface.py +165 -158
  21. letta/interfaces/openai_chat_completions_streaming_interface.py +1 -1
  22. letta/llm_api/anthropic.py +15 -10
  23. letta/llm_api/anthropic_client.py +5 -1
  24. letta/llm_api/google_vertex_client.py +1 -1
  25. letta/llm_api/llm_api_tools.py +7 -0
  26. letta/llm_api/llm_client.py +12 -2
  27. letta/llm_api/llm_client_base.py +4 -0
  28. letta/llm_api/openai.py +9 -3
  29. letta/llm_api/openai_client.py +18 -4
  30. letta/memory.py +3 -1
  31. letta/orm/enums.py +1 -0
  32. letta/orm/group.py +2 -0
  33. letta/orm/provider.py +10 -0
  34. letta/personas/examples/voice_memory_persona.txt +5 -0
  35. letta/prompts/system/voice_chat.txt +29 -0
  36. letta/prompts/system/voice_sleeptime.txt +74 -0
  37. letta/schemas/agent.py +14 -2
  38. letta/schemas/enums.py +11 -0
  39. letta/schemas/group.py +37 -2
  40. letta/schemas/llm_config.py +1 -0
  41. letta/schemas/llm_config_overrides.py +2 -2
  42. letta/schemas/message.py +4 -3
  43. letta/schemas/providers.py +75 -213
  44. letta/schemas/tool.py +8 -12
  45. letta/server/rest_api/app.py +12 -0
  46. letta/server/rest_api/chat_completions_interface.py +1 -1
  47. letta/server/rest_api/interface.py +8 -10
  48. letta/server/rest_api/{optimistic_json_parser.py → json_parser.py} +62 -26
  49. letta/server/rest_api/routers/v1/agents.py +1 -1
  50. letta/server/rest_api/routers/v1/embeddings.py +4 -3
  51. letta/server/rest_api/routers/v1/llms.py +4 -3
  52. letta/server/rest_api/routers/v1/providers.py +4 -1
  53. letta/server/rest_api/routers/v1/voice.py +0 -2
  54. letta/server/rest_api/utils.py +22 -33
  55. letta/server/server.py +91 -37
  56. letta/services/agent_manager.py +14 -7
  57. letta/services/group_manager.py +61 -0
  58. letta/services/helpers/agent_manager_helper.py +69 -12
  59. letta/services/message_manager.py +2 -2
  60. letta/services/passage_manager.py +13 -4
  61. letta/services/provider_manager.py +25 -14
  62. letta/services/summarizer/summarizer.py +20 -15
  63. letta/services/tool_executor/tool_execution_manager.py +1 -1
  64. letta/services/tool_executor/tool_executor.py +3 -3
  65. letta/services/tool_manager.py +32 -7
  66. {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/METADATA +4 -5
  67. {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/RECORD +70 -64
  68. {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/LICENSE +0 -0
  69. {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/WHEEL +0 -0
  70. {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  import xml.etree.ElementTree as ET
3
- from typing import AsyncGenerator, Dict, List, Tuple, Union
3
+ from typing import AsyncGenerator, Dict, List, Optional, Tuple, Union
4
4
 
5
5
  import openai
6
6
 
@@ -11,17 +11,19 @@ from letta.schemas.enums import MessageStreamStatus
11
11
  from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage
12
12
  from letta.schemas.letta_message_content import TextContent
13
13
  from letta.schemas.letta_response import LettaResponse
14
- from letta.schemas.message import MessageCreate
15
- from letta.schemas.openai.chat_completion_request import ChatCompletionRequest, SystemMessage, Tool, UserMessage
14
+ from letta.schemas.message import Message, MessageCreate, ToolReturn
15
+ from letta.schemas.openai.chat_completion_request import ChatCompletionRequest, Tool, UserMessage
16
16
  from letta.schemas.usage import LettaUsageStatistics
17
17
  from letta.schemas.user import User
18
18
  from letta.server.rest_api.utils import convert_in_context_letta_messages_to_openai, create_input_messages
19
19
  from letta.services.agent_manager import AgentManager
20
20
  from letta.services.block_manager import BlockManager
21
21
  from letta.services.message_manager import MessageManager
22
+ from letta.system import package_function_response
22
23
 
23
24
 
24
- class EphemeralMemoryAgent(BaseAgent):
25
+ # TODO: Move this to the new Letta Agent loop
26
+ class VoiceSleeptimeAgent(BaseAgent):
25
27
  """
26
28
  A stateless agent that helps with offline memory computations.
27
29
  """
@@ -29,6 +31,7 @@ class EphemeralMemoryAgent(BaseAgent):
29
31
  def __init__(
30
32
  self,
31
33
  agent_id: str,
34
+ convo_agent_state: AgentState,
32
35
  openai_client: openai.AsyncClient,
33
36
  message_manager: MessageManager,
34
37
  agent_manager: AgentManager,
@@ -45,6 +48,7 @@ class EphemeralMemoryAgent(BaseAgent):
45
48
  actor=actor,
46
49
  )
47
50
 
51
+ self.convo_agent_state = convo_agent_state
48
52
  self.block_manager = block_manager
49
53
  self.target_block_label = target_block_label
50
54
  self.message_transcripts = message_transcripts
@@ -62,9 +66,7 @@ class EphemeralMemoryAgent(BaseAgent):
62
66
  openai_messages = convert_in_context_letta_messages_to_openai(in_context_messages, exclude_system_messages=True)
63
67
 
64
68
  # 1. Store memories
65
- request = self._build_openai_request(
66
- openai_messages, agent_state, tools=self._build_store_memory_tool_schemas(), system=self._get_memory_store_system_prompt()
67
- )
69
+ request = self._build_openai_request(openai_messages, agent_state, tools=self._build_store_memory_tool_schemas())
68
70
 
69
71
  chat_completion = await self.openai_client.chat.completions.create(**request.model_dump(exclude_unset=True))
70
72
  assistant_message = chat_completion.choices[0].message
@@ -74,29 +76,53 @@ class EphemeralMemoryAgent(BaseAgent):
74
76
  function_name = tool_call.function.name
75
77
  function_args = json.loads(tool_call.function.arguments)
76
78
 
77
- if function_name == "store_memory":
78
- print("Called store_memory")
79
+ if function_name == "store_memories":
80
+ print("Called store_memories")
79
81
  print(function_args)
80
- for chunk_args in function_args.get("chunks"):
81
- self.store_memory(agent_state=agent_state, **chunk_args)
82
- result = "Successfully stored memories"
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
+
83
88
  else:
84
89
  raise ValueError("Error: Unknown tool function '{function_name}'")
85
90
 
86
- openai_messages.append(
87
- {
88
- "role": "assistant",
89
- "content": assistant_message.content,
90
- "tool_calls": [
91
- {
92
- "id": tool_call.id,
93
- "type": "function",
94
- "function": {"name": function_name, "arguments": tool_call.function.arguments},
95
- }
96
- ],
97
- }
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
+ )
110
+ )
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
+ )
98
125
  )
99
- openai_messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": str(result)})
100
126
 
101
127
  # 2. Execute rethink block memory loop
102
128
  human_block_content = self.agent_manager.get_block_with_label(
@@ -115,15 +141,13 @@ Please refine this block:
115
141
  - Organize related information together (e.g., preferences, background, ongoing goals).
116
142
  - Add any light, supportable inferences that deepen understanding—but do not invent unsupported details.
117
143
 
118
- Use `rethink_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()`.
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()`.
119
145
  """
120
146
  rethink_command = UserMessage(content=rethink_command)
121
147
  openai_messages.append(rethink_command.model_dump())
122
148
 
123
149
  for _ in range(max_steps):
124
- request = self._build_openai_request(
125
- openai_messages, agent_state, tools=self._build_sleeptime_tools(), system=self._get_rethink_memory_system_prompt()
126
- )
150
+ request = self._build_openai_request(openai_messages, agent_state, tools=self._build_sleeptime_tools())
127
151
  chat_completion = await self.openai_client.chat.completions.create(**request.model_dump(exclude_unset=True))
128
152
  assistant_message = chat_completion.choices[0].message
129
153
 
@@ -132,35 +156,59 @@ Use `rethink_memory(new_memory)` as many times as you need to iteratively improv
132
156
  function_name = tool_call.function.name
133
157
  function_args = json.loads(tool_call.function.arguments)
134
158
 
135
- if function_name == "rethink_memory":
136
- print("Called rethink_memory")
159
+ if function_name == "rethink_user_memory":
160
+ print("Called rethink_user_memory")
137
161
  print(function_args)
138
- result = self.rethink_memory(agent_state=agent_state, **function_args)
162
+ result, success = self.rethink_user_memory(agent_state=agent_state, **function_args)
139
163
  elif function_name == "finish_rethinking_memory":
140
164
  print("Called finish_rethinking_memory")
165
+ result, success = None, True
141
166
  break
142
167
  else:
143
- result = f"Error: Unknown tool function '{function_name}'"
144
- openai_messages.append(
145
- {
146
- "role": "assistant",
147
- "content": assistant_message.content,
148
- "tool_calls": [
149
- {
150
- "id": tool_call.id,
151
- "type": "function",
152
- "function": {"name": function_name, "arguments": tool_call.function.arguments},
153
- }
154
- ],
155
- }
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
+ )
156
204
  )
157
- openai_messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": str(result)})
158
205
 
159
206
  # Actually save the memory:
160
207
  target_block = agent_state.memory.get_block(self.target_block_label)
161
208
  self.block_manager.update_block(block_id=target_block.id, block_update=BlockUpdate(value=target_block.value), actor=self.actor)
162
209
 
163
- return LettaResponse(messages=[], usage=LettaUsageStatistics())
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())
164
212
 
165
213
  def _format_messages_llm_friendly(self):
166
214
  messages = self.message_manager.list_messages_for_agent(agent_id=self.agent_id, actor=self.actor)
@@ -168,13 +216,10 @@ Use `rethink_memory(new_memory)` as many times as you need to iteratively improv
168
216
  llm_friendly_messages = [f"{m.role}: {m.content[0].text}" for m in messages if m.content and isinstance(m.content[0], TextContent)]
169
217
  return "\n".join(llm_friendly_messages)
170
218
 
171
- def _build_openai_request(
172
- self, openai_messages: List[Dict], agent_state: AgentState, tools: List[Tool], system: str
173
- ) -> ChatCompletionRequest:
174
- system_message = SystemMessage(role="system", content=system)
219
+ def _build_openai_request(self, openai_messages: List[Dict], agent_state: AgentState, tools: List[Tool]) -> ChatCompletionRequest:
175
220
  openai_request = ChatCompletionRequest(
176
- model="gpt-4o", # agent_state.llm_config.model, # TODO: Separate config for summarizer?
177
- messages=[system_message] + openai_messages,
221
+ model=agent_state.llm_config.model, # TODO: Separate config for summarizer?
222
+ messages=openai_messages,
178
223
  tools=tools,
179
224
  tool_choice="required",
180
225
  user=self.actor.id,
@@ -192,7 +237,7 @@ Use `rethink_memory(new_memory)` as many times as you need to iteratively improv
192
237
  Tool(
193
238
  type="function",
194
239
  function={
195
- "name": "store_memory",
240
+ "name": "store_memories",
196
241
  "description": "Archive coherent chunks of dialogue that will be evicted, preserving raw lines and a brief contextual description.",
197
242
  "parameters": {
198
243
  "type": "object",
@@ -227,7 +272,7 @@ Use `rethink_memory(new_memory)` as many times as you need to iteratively improv
227
272
  Tool(
228
273
  type="function",
229
274
  function={
230
- "name": "rethink_memory",
275
+ "name": "rethink_user_memory",
231
276
  "description": (
232
277
  "Rewrite memory block for the main agent, new_memory should contain all current "
233
278
  "information from the block that is not outdated or inconsistent, integrating any "
@@ -268,14 +313,14 @@ Use `rethink_memory(new_memory)` as many times as you need to iteratively improv
268
313
 
269
314
  return tools
270
315
 
271
- def rethink_memory(self, new_memory: str, agent_state: AgentState) -> str:
316
+ def rethink_user_memory(self, new_memory: str, agent_state: AgentState) -> Tuple[Optional[str], bool]:
272
317
  if agent_state.memory.get_block(self.target_block_label) is None:
273
318
  agent_state.memory.create_block(label=self.target_block_label, value=new_memory)
274
319
 
275
320
  agent_state.memory.update_block_value(label=self.target_block_label, value=new_memory)
276
- return "Successfully updated memory"
321
+ return None, True
277
322
 
278
- def store_memory(self, start_index: int, end_index: int, context: str, agent_state: AgentState) -> str:
323
+ def store_memory(self, start_index: int, end_index: int, context: str, agent_state: AgentState) -> Tuple[Optional[str], bool]:
279
324
  """
280
325
  Store a memory.
281
326
  """
@@ -290,9 +335,9 @@ Use `rethink_memory(new_memory)` as many times as you need to iteratively improv
290
335
  )
291
336
  self.agent_manager.rebuild_system_prompt(agent_id=agent_state.id, actor=self.actor, force=True)
292
337
 
293
- return "Sucessfully stored memory"
338
+ return None, True
294
339
  except Exception as e:
295
- return f"Failed to store memory given start_index {start_index} and end_index {end_index}: {e}"
340
+ return f"Failed to store memory given start_index {start_index} and end_index {end_index}: {e}", False
296
341
 
297
342
  def serialize(self, messages: List[str], context: str) -> str:
298
343
  """
@@ -351,72 +396,4 @@ Use `rethink_memory(new_memory)` as many times as you need to iteratively improv
351
396
  """
352
397
  This agent is synchronous-only. If called in an async context, raise an error.
353
398
  """
354
- raise NotImplementedError("EphemeralMemoryAgent does not support async step.")
355
-
356
- # TODO: Move these to independent text files
357
- def _get_memory_store_system_prompt(self) -> str:
358
- return """
359
- You are a memory-recall assistant working asynchronously alongside a main chat agent that retains only a portion of the message history in its context window.
360
-
361
- When given a full transcript with lines marked (Older) or (Newer), you should:
362
- 1. Segment the (Older) portion into coherent chunks by topic, instruction, or preference.
363
- 2. For each chunk, produce only:
364
- - start_index: the first line’s index
365
- - end_index: the last line’s index
366
- - context: a blurb explaining why this chunk matters
367
-
368
- Return exactly one JSON tool call to `store_memory`, consider this miniature example:
369
-
370
- ---
371
-
372
- (Older)
373
- 0. user: Okay. Got it. Keep your answers shorter, please.
374
- 1. assistant: Sure thing! I’ll keep it brief. What would you like to know?
375
- 2. user: I like basketball.
376
- 3. assistant: That's great! Do you have a favorite team or player?
377
-
378
- (Newer)
379
- 4. user: Yeah. I like basketball.
380
- 5. assistant: Awesome! What do you enjoy most about basketball?
381
-
382
- ---
383
-
384
- Example output:
385
-
386
- ```json
387
- {
388
- "name": "store_memory",
389
- "arguments": {
390
- "chunks": [
391
- {
392
- "start_index": 0,
393
- "end_index": 1,
394
- "context": "User explicitly asked the assistant to keep responses concise."
395
- },
396
- {
397
- "start_index": 2,
398
- "end_index": 3,
399
- "context": "User enjoys basketball and prompted follow-up about their favorite team or player."
400
- }
401
- ]
402
- }
403
- }
404
- ```
405
- """
406
-
407
- def _get_rethink_memory_system_prompt(self) -> str:
408
- return """
409
- SYSTEM
410
- You are a Memory-Updater agent. Your job is to iteratively refine the given memory block until it’s concise, organized, and complete.
411
-
412
- Instructions:
413
- - Call `rethink_memory(new_memory: string)` as many times as you like. Each call should submit a fully revised version of the block so far.
414
- - When you’re fully satisfied, call `finish_rethinking_memory()`.
415
- - Don’t output anything else—only the JSON for these tool calls.
416
-
417
- Goals:
418
- - Merge in new facts and remove contradictions.
419
- - Group related details (preferences, biography, goals).
420
- - Draw light, supportable inferences without inventing facts.
421
- - Preserve every critical piece of information.
422
- """
399
+ raise NotImplementedError("VoiceSleeptimeAgent does not support async step.")
letta/client/client.py CHANGED
@@ -1031,7 +1031,7 @@ class RESTClient(AbstractClient):
1031
1031
  # messages = []
1032
1032
  # for m in response.messages:
1033
1033
  # assert isinstance(m, Message)
1034
- # messages += m.to_letta_message()
1034
+ # messages += m.to_letta_messages()
1035
1035
  # response.messages = messages
1036
1036
 
1037
1037
  return response
@@ -2725,14 +2725,14 @@ class LocalClient(AbstractClient):
2725
2725
  # assert isinstance(m, Message), f"Expected Message object, got {type(m)}"
2726
2726
  # letta_messages = []
2727
2727
  # for m in messages:
2728
- # letta_messages += m.to_letta_message()
2728
+ # letta_messages += m.to_letta_messages()
2729
2729
  # return LettaResponse(messages=letta_messages, usage=usage)
2730
2730
 
2731
2731
  # format messages
2732
2732
  messages = self.interface.to_list()
2733
2733
  letta_messages = []
2734
2734
  for m in messages:
2735
- letta_messages += m.to_letta_message()
2735
+ letta_messages += m.to_letta_messages()
2736
2736
 
2737
2737
  return LettaResponse(messages=letta_messages, usage=usage)
2738
2738
 
letta/constants.py CHANGED
@@ -4,7 +4,7 @@ from logging import CRITICAL, DEBUG, ERROR, INFO, NOTSET, WARN, WARNING
4
4
  LETTA_DIR = os.path.join(os.path.expanduser("~"), ".letta")
5
5
  LETTA_TOOL_EXECUTION_DIR = os.path.join(LETTA_DIR, "tool_execution_dir")
6
6
 
7
- LETTA_MODEL_ENDPOINT = "https://inference.memgpt.ai"
7
+ LETTA_MODEL_ENDPOINT = "https://inference.letta.com"
8
8
 
9
9
  ADMIN_PREFIX = "/v1/admin"
10
10
  API_PREFIX = "/v1"
@@ -18,6 +18,8 @@ MCP_TOOL_TAG_NAME_PREFIX = "mcp" # full format, mcp:server_name
18
18
 
19
19
  LETTA_CORE_TOOL_MODULE_NAME = "letta.functions.function_sets.base"
20
20
  LETTA_MULTI_AGENT_TOOL_MODULE_NAME = "letta.functions.function_sets.multi_agent"
21
+ LETTA_VOICE_TOOL_MODULE_NAME = "letta.functions.function_sets.voice"
22
+
21
23
 
22
24
  # String in the error message for when the context window is too large
23
25
  # Example full message:
@@ -33,6 +35,10 @@ TOOL_CALL_ID_MAX_LEN = 29
33
35
  # minimum context window size
34
36
  MIN_CONTEXT_WINDOW = 4096
35
37
 
38
+ # Voice Sleeptime message buffer lengths
39
+ DEFAULT_MAX_MESSAGE_BUFFER_LENGTH = 30
40
+ DEFAULT_MIN_MESSAGE_BUFFER_LENGTH = 15
41
+
36
42
  # embeddings
37
43
  MAX_EMBEDDING_DIM = 4096 # maximum supported embeding size - do NOT change or else DBs will need to be reset
38
44
  DEFAULT_EMBEDDING_CHUNK_SIZE = 300
@@ -67,10 +73,20 @@ BASE_SLEEPTIME_TOOLS = [
67
73
  # "archival_memory_search",
68
74
  # "conversation_search",
69
75
  ]
76
+ # Base tools for the voice agent
77
+ BASE_VOICE_SLEEPTIME_CHAT_TOOLS = [SEND_MESSAGE_TOOL_NAME, "search_memory"]
78
+ # Base memory tools for sleeptime agent
79
+ BASE_VOICE_SLEEPTIME_TOOLS = [
80
+ "store_memories",
81
+ "rethink_user_memory",
82
+ "finish_rethinking_memory",
83
+ ]
70
84
  # Multi agent tools
71
85
  MULTI_AGENT_TOOLS = ["send_message_to_agent_and_wait_for_reply", "send_message_to_agents_matching_tags", "send_message_to_agent_async"]
72
86
  # Set of all built-in Letta tools
73
- LETTA_TOOL_SET = set(BASE_TOOLS + BASE_MEMORY_TOOLS + MULTI_AGENT_TOOLS + BASE_SLEEPTIME_TOOLS)
87
+ LETTA_TOOL_SET = set(
88
+ BASE_TOOLS + BASE_MEMORY_TOOLS + MULTI_AGENT_TOOLS + BASE_SLEEPTIME_TOOLS + BASE_VOICE_SLEEPTIME_TOOLS + BASE_VOICE_SLEEPTIME_CHAT_TOOLS
89
+ )
74
90
 
75
91
  # The name of the tool used to send message to the user
76
92
  # May not be relevant in cases where the agent has multiple ways to message to user (send_imessage, send_discord_mesasge, ...)
@@ -0,0 +1,100 @@
1
+ import asyncio
2
+ import os
3
+ from typing import Any, Optional
4
+
5
+ from composio import ComposioToolSet
6
+ from composio.constants import DEFAULT_ENTITY_ID
7
+ from composio.exceptions import (
8
+ ApiKeyNotProvidedError,
9
+ ComposioSDKError,
10
+ ConnectedAccountNotFoundError,
11
+ EnumMetadataNotFound,
12
+ EnumStringNotFound,
13
+ )
14
+
15
+ from letta.constants import COMPOSIO_ENTITY_ENV_VAR_KEY
16
+
17
+
18
+ # TODO: This is kind of hacky, as this is used to search up the action later on composio's side
19
+ # TODO: So be very careful changing/removing these pair of functions
20
+ def _generate_func_name_from_composio_action(action_name: str) -> str:
21
+ """
22
+ Generates the composio function name from the composio action.
23
+
24
+ Args:
25
+ action_name: The composio action name
26
+
27
+ Returns:
28
+ function name
29
+ """
30
+ return action_name.lower()
31
+
32
+
33
+ def generate_composio_action_from_func_name(func_name: str) -> str:
34
+ """
35
+ Generates the composio action from the composio function name.
36
+
37
+ Args:
38
+ func_name: The composio function name
39
+
40
+ Returns:
41
+ composio action name
42
+ """
43
+ return func_name.upper()
44
+
45
+
46
+ def generate_composio_tool_wrapper(action_name: str) -> tuple[str, str]:
47
+ # Generate func name
48
+ func_name = _generate_func_name_from_composio_action(action_name)
49
+
50
+ wrapper_function_str = f"""\
51
+ def {func_name}(**kwargs):
52
+ raise RuntimeError("Something went wrong - we should never be using the persisted source code for Composio. Please reach out to Letta team")
53
+ """
54
+
55
+ # Compile safety check
56
+ _assert_code_gen_compilable(wrapper_function_str.strip())
57
+
58
+ return func_name, wrapper_function_str.strip()
59
+
60
+
61
+ async def execute_composio_action_async(
62
+ action_name: str, args: dict, api_key: Optional[str] = None, entity_id: Optional[str] = None
63
+ ) -> tuple[str, str]:
64
+ try:
65
+ loop = asyncio.get_running_loop()
66
+ return await loop.run_in_executor(None, execute_composio_action, action_name, args, api_key, entity_id)
67
+ except Exception as e:
68
+ raise RuntimeError(f"Error in execute_composio_action_async: {e}") from e
69
+
70
+
71
+ def execute_composio_action(action_name: str, args: dict, api_key: Optional[str] = None, entity_id: Optional[str] = None) -> Any:
72
+ entity_id = entity_id or os.getenv(COMPOSIO_ENTITY_ENV_VAR_KEY, DEFAULT_ENTITY_ID)
73
+ try:
74
+ composio_toolset = ComposioToolSet(api_key=api_key, entity_id=entity_id, lock=False)
75
+ response = composio_toolset.execute_action(action=action_name, params=args)
76
+ except ApiKeyNotProvidedError:
77
+ raise RuntimeError(
78
+ f"Composio API key is missing for action '{action_name}'. "
79
+ "Please set the sandbox environment variables either through the ADE or the API."
80
+ )
81
+ except ConnectedAccountNotFoundError:
82
+ raise RuntimeError(f"No connected account was found for action '{action_name}'. " "Please link an account and try again.")
83
+ except EnumStringNotFound as e:
84
+ raise RuntimeError(f"Invalid value provided for action '{action_name}': " + str(e) + ". Please check the action parameters.")
85
+ except EnumMetadataNotFound as e:
86
+ raise RuntimeError(f"Invalid value provided for action '{action_name}': " + str(e) + ". Please check the action parameters.")
87
+ except ComposioSDKError as e:
88
+ raise RuntimeError(f"An unexpected error occurred in Composio SDK while executing action '{action_name}': " + str(e))
89
+
90
+ if "error" in response and response["error"]:
91
+ raise RuntimeError(f"Error while executing action '{action_name}': " + str(response["error"]))
92
+
93
+ return response.get("data")
94
+
95
+
96
+ def _assert_code_gen_compilable(code_str):
97
+ try:
98
+ compile(code_str, "<string>", "exec")
99
+ except SyntaxError as e:
100
+ print(f"Syntax error in code: {e}")
@@ -186,16 +186,6 @@ def rethink_memory(agent_state: "AgentState", new_memory: str, target_block_labe
186
186
  return None
187
187
 
188
188
 
189
- def finish_rethinking_memory(agent_state: "AgentState") -> None: # type: ignore
190
- """
191
- This function is called when the agent is done rethinking the memory.
192
-
193
- Returns:
194
- Optional[str]: None is always returned as this function does not produce a response.
195
- """
196
- return None
197
-
198
-
199
189
  ## Attempted v2 of sleep-time function set, meant to work better across all types
200
190
 
201
191
  SNIPPET_LINES: int = 4
@@ -0,0 +1,92 @@
1
+ ## Voice chat + sleeptime tools
2
+ from typing import List, Optional
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ def rethink_user_memory(agent_state: "AgentState", new_memory: str) -> None:
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.
13
+
14
+ 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.
18
+
19
+ Returns:
20
+ None: None is always returned as this function does not produce a response.
21
+ """
22
+ # This is implemented directly in the agent loop
23
+ return None
24
+
25
+
26
+ def finish_rethinking_memory(agent_state: "AgentState") -> None: # type: ignore
27
+ """
28
+ This function is called when the agent is done rethinking the memory.
29
+
30
+ Returns:
31
+ Optional[str]: None is always returned as this function does not produce a response.
32
+ """
33
+ return None
34
+
35
+
36
+ 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.")
40
+
41
+
42
+ def store_memories(agent_state: "AgentState", chunks: List[MemoryChunk]) -> None:
43
+ """
44
+ Archive coherent chunks of dialogue that will be evicted, preserving raw lines
45
+ and a brief contextual description.
46
+
47
+ 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
+ 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.
57
+
58
+ Returns:
59
+ None
60
+ """
61
+ # This is implemented directly in the agent loop
62
+ return None
63
+
64
+
65
+ def search_memory(
66
+ agent_state: "AgentState",
67
+ convo_keyword_queries: Optional[List[str]],
68
+ start_minutes_ago: Optional[int],
69
+ end_minutes_ago: Optional[int],
70
+ ) -> Optional[str]:
71
+ """
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.
75
+
76
+ 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.
86
+
87
+ Returns:
88
+ Optional[str]: A formatted string of matching memory entries, or None if no
89
+ relevant memories are found.
90
+ """
91
+ # This is implemented directly in the agent loop
92
+ return None
@@ -1,8 +1,9 @@
1
1
  import importlib
2
2
  import inspect
3
+ from collections.abc import Callable
3
4
  from textwrap import dedent # remove indentation
4
5
  from types import ModuleType
5
- from typing import Dict, List, Literal, Optional
6
+ from typing import Any, Dict, List, Literal, Optional
6
7
 
7
8
  from letta.errors import LettaToolCreateError
8
9
  from letta.functions.schema_generator import generate_schema
@@ -66,7 +67,8 @@ def parse_source_code(func) -> str:
66
67
  return source_code
67
68
 
68
69
 
69
- def get_function_from_module(module_name: str, function_name: str):
70
+ # TODO (cliandy) refactor below two funcs
71
+ def get_function_from_module(module_name: str, function_name: str) -> Callable[..., Any]:
70
72
  """
71
73
  Dynamically imports a function from a specified module.
72
74