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.
- letta/__init__.py +1 -1
- letta/agent.py +8 -12
- letta/agents/exceptions.py +6 -0
- letta/agents/helpers.py +1 -1
- letta/agents/letta_agent.py +48 -35
- letta/agents/letta_agent_batch.py +6 -2
- letta/agents/voice_agent.py +41 -59
- letta/agents/{ephemeral_memory_agent.py → voice_sleeptime_agent.py} +106 -129
- letta/client/client.py +3 -3
- letta/constants.py +18 -2
- letta/functions/composio_helpers.py +100 -0
- letta/functions/function_sets/base.py +0 -10
- letta/functions/function_sets/voice.py +92 -0
- letta/functions/functions.py +4 -2
- letta/functions/helpers.py +19 -101
- letta/groups/helpers.py +1 -0
- letta/groups/sleeptime_multi_agent.py +5 -1
- letta/helpers/message_helper.py +21 -4
- letta/helpers/tool_execution_helper.py +1 -1
- letta/interfaces/anthropic_streaming_interface.py +165 -158
- letta/interfaces/openai_chat_completions_streaming_interface.py +1 -1
- letta/llm_api/anthropic.py +15 -10
- letta/llm_api/anthropic_client.py +5 -1
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/llm_api_tools.py +7 -0
- letta/llm_api/llm_client.py +12 -2
- letta/llm_api/llm_client_base.py +4 -0
- letta/llm_api/openai.py +9 -3
- letta/llm_api/openai_client.py +18 -4
- letta/memory.py +3 -1
- letta/orm/enums.py +1 -0
- letta/orm/group.py +2 -0
- letta/orm/provider.py +10 -0
- letta/personas/examples/voice_memory_persona.txt +5 -0
- letta/prompts/system/voice_chat.txt +29 -0
- letta/prompts/system/voice_sleeptime.txt +74 -0
- letta/schemas/agent.py +14 -2
- letta/schemas/enums.py +11 -0
- letta/schemas/group.py +37 -2
- letta/schemas/llm_config.py +1 -0
- letta/schemas/llm_config_overrides.py +2 -2
- letta/schemas/message.py +4 -3
- letta/schemas/providers.py +75 -213
- letta/schemas/tool.py +8 -12
- letta/server/rest_api/app.py +12 -0
- letta/server/rest_api/chat_completions_interface.py +1 -1
- letta/server/rest_api/interface.py +8 -10
- letta/server/rest_api/{optimistic_json_parser.py → json_parser.py} +62 -26
- letta/server/rest_api/routers/v1/agents.py +1 -1
- letta/server/rest_api/routers/v1/embeddings.py +4 -3
- letta/server/rest_api/routers/v1/llms.py +4 -3
- letta/server/rest_api/routers/v1/providers.py +4 -1
- letta/server/rest_api/routers/v1/voice.py +0 -2
- letta/server/rest_api/utils.py +22 -33
- letta/server/server.py +91 -37
- letta/services/agent_manager.py +14 -7
- letta/services/group_manager.py +61 -0
- letta/services/helpers/agent_manager_helper.py +69 -12
- letta/services/message_manager.py +2 -2
- letta/services/passage_manager.py +13 -4
- letta/services/provider_manager.py +25 -14
- letta/services/summarizer/summarizer.py +20 -15
- letta/services/tool_executor/tool_execution_manager.py +1 -1
- letta/services/tool_executor/tool_executor.py +3 -3
- letta/services/tool_manager.py +32 -7
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/METADATA +4 -5
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/RECORD +70 -64
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/WHEEL +0 -0
- {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,
|
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
|
-
|
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 == "
|
78
|
-
print("Called
|
79
|
+
if function_name == "store_memories":
|
80
|
+
print("Called store_memories")
|
79
81
|
print(function_args)
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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 `
|
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 == "
|
136
|
-
print("Called
|
159
|
+
if function_name == "rethink_user_memory":
|
160
|
+
print("Called rethink_user_memory")
|
137
161
|
print(function_args)
|
138
|
-
result = self.
|
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
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
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=
|
177
|
-
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": "
|
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": "
|
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
|
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
|
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
|
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("
|
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.
|
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.
|
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.
|
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.
|
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(
|
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
|
letta/functions/functions.py
CHANGED
@@ -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
|
-
|
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
|
|