letta-nightly 0.8.0.dev20250606195656__py3-none-any.whl → 0.8.2.dev20250606215616__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 +1 -1
- letta/agents/letta_agent.py +49 -29
- letta/agents/letta_agent_batch.py +1 -2
- letta/agents/voice_agent.py +19 -13
- letta/agents/voice_sleeptime_agent.py +11 -3
- letta/constants.py +18 -0
- letta/data_sources/__init__.py +0 -0
- letta/data_sources/redis_client.py +282 -0
- letta/errors.py +0 -4
- letta/functions/function_sets/files.py +58 -0
- letta/functions/schema_generator.py +18 -1
- letta/groups/sleeptime_multi_agent_v2.py +1 -1
- letta/helpers/datetime_helpers.py +47 -3
- letta/helpers/decorators.py +69 -0
- letta/{services/helpers/noop_helper.py → helpers/singleton.py} +5 -0
- letta/interfaces/anthropic_streaming_interface.py +43 -24
- letta/interfaces/openai_streaming_interface.py +21 -19
- letta/llm_api/anthropic.py +1 -1
- letta/llm_api/anthropic_client.py +22 -14
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/helpers.py +36 -30
- letta/llm_api/llm_api_tools.py +1 -1
- letta/llm_api/llm_client_base.py +29 -1
- letta/llm_api/openai.py +1 -1
- letta/llm_api/openai_client.py +6 -8
- letta/local_llm/chat_completion_proxy.py +1 -1
- letta/memory.py +1 -1
- letta/orm/enums.py +1 -0
- letta/orm/file.py +80 -3
- letta/orm/files_agents.py +13 -0
- letta/orm/sqlalchemy_base.py +34 -11
- letta/otel/__init__.py +0 -0
- letta/otel/context.py +25 -0
- letta/otel/events.py +0 -0
- letta/otel/metric_registry.py +122 -0
- letta/otel/metrics.py +66 -0
- letta/otel/resource.py +26 -0
- letta/{tracing.py → otel/tracing.py} +55 -78
- letta/plugins/README.md +22 -0
- letta/plugins/__init__.py +0 -0
- letta/plugins/defaults.py +11 -0
- letta/plugins/plugins.py +72 -0
- letta/schemas/enums.py +8 -0
- letta/schemas/file.py +12 -0
- letta/schemas/tool.py +4 -0
- letta/server/db.py +7 -7
- letta/server/rest_api/app.py +8 -6
- letta/server/rest_api/routers/v1/agents.py +37 -36
- letta/server/rest_api/routers/v1/groups.py +3 -3
- letta/server/rest_api/routers/v1/sources.py +26 -3
- letta/server/rest_api/utils.py +9 -6
- letta/server/server.py +18 -12
- letta/services/agent_manager.py +185 -193
- letta/services/block_manager.py +1 -1
- letta/services/context_window_calculator/token_counter.py +3 -2
- letta/services/file_processor/chunker/line_chunker.py +34 -0
- letta/services/file_processor/file_processor.py +40 -11
- letta/services/file_processor/parser/mistral_parser.py +11 -1
- letta/services/files_agents_manager.py +96 -7
- letta/services/group_manager.py +6 -6
- letta/services/helpers/agent_manager_helper.py +373 -3
- letta/services/identity_manager.py +1 -1
- letta/services/job_manager.py +1 -1
- letta/services/llm_batch_manager.py +1 -1
- letta/services/message_manager.py +1 -1
- letta/services/organization_manager.py +1 -1
- letta/services/passage_manager.py +1 -1
- letta/services/per_agent_lock_manager.py +1 -1
- letta/services/provider_manager.py +1 -1
- letta/services/sandbox_config_manager.py +1 -1
- letta/services/source_manager.py +178 -19
- letta/services/step_manager.py +2 -2
- letta/services/summarizer/summarizer.py +1 -1
- letta/services/telemetry_manager.py +1 -1
- letta/services/tool_executor/builtin_tool_executor.py +117 -0
- letta/services/tool_executor/composio_tool_executor.py +53 -0
- letta/services/tool_executor/core_tool_executor.py +474 -0
- letta/services/tool_executor/files_tool_executor.py +131 -0
- letta/services/tool_executor/mcp_tool_executor.py +45 -0
- letta/services/tool_executor/multi_agent_tool_executor.py +123 -0
- letta/services/tool_executor/tool_execution_manager.py +34 -14
- letta/services/tool_executor/tool_execution_sandbox.py +1 -1
- letta/services/tool_executor/tool_executor.py +3 -802
- letta/services/tool_executor/tool_executor_base.py +43 -0
- letta/services/tool_manager.py +55 -59
- letta/services/tool_sandbox/e2b_sandbox.py +1 -1
- letta/services/tool_sandbox/local_sandbox.py +6 -3
- letta/services/user_manager.py +6 -3
- letta/settings.py +21 -1
- letta/utils.py +7 -2
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/METADATA +4 -2
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/RECORD +96 -74
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/entry_points.txt +0 -0
@@ -1,720 +1,25 @@
|
|
1
|
-
import asyncio
|
2
|
-
import json
|
3
|
-
import math
|
4
1
|
import traceback
|
5
|
-
from
|
6
|
-
from textwrap import shorten
|
7
|
-
from typing import Any, Dict, List, Literal, Optional
|
2
|
+
from typing import Any, Dict, Optional
|
8
3
|
|
9
|
-
from letta.constants import (
|
10
|
-
COMPOSIO_ENTITY_ENV_VAR_KEY,
|
11
|
-
CORE_MEMORY_LINE_NUMBER_WARNING,
|
12
|
-
MCP_TOOL_TAG_NAME_PREFIX,
|
13
|
-
MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX,
|
14
|
-
READ_ONLY_BLOCK_EDIT_ERROR,
|
15
|
-
RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE,
|
16
|
-
WEB_SEARCH_CLIP_CONTENT,
|
17
|
-
WEB_SEARCH_INCLUDE_SCORE,
|
18
|
-
WEB_SEARCH_SEPARATOR,
|
19
|
-
)
|
20
4
|
from letta.functions.ast_parsers import coerce_dict_args_by_annotations, get_function_annotations_from_source
|
21
|
-
from letta.functions.composio_helpers import execute_composio_action_async, generate_composio_action_from_func_name
|
22
|
-
from letta.helpers.composio_helpers import get_composio_api_key_async
|
23
|
-
from letta.helpers.json_helpers import json_dumps
|
24
5
|
from letta.log import get_logger
|
6
|
+
from letta.otel.tracing import trace_method
|
25
7
|
from letta.schemas.agent import AgentState
|
26
|
-
from letta.schemas.enums import MessageRole
|
27
|
-
from letta.schemas.letta_message import AssistantMessage
|
28
|
-
from letta.schemas.letta_message_content import TextContent
|
29
|
-
from letta.schemas.message import MessageCreate
|
30
8
|
from letta.schemas.sandbox_config import SandboxConfig
|
31
9
|
from letta.schemas.tool import Tool
|
32
10
|
from letta.schemas.tool_execution_result import ToolExecutionResult
|
33
11
|
from letta.schemas.user import User
|
34
12
|
from letta.services.agent_manager import AgentManager
|
35
|
-
from letta.services.
|
36
|
-
from letta.services.mcp_manager import MCPManager
|
37
|
-
from letta.services.message_manager import MessageManager
|
38
|
-
from letta.services.passage_manager import PassageManager
|
13
|
+
from letta.services.tool_executor.tool_executor_base import ToolExecutor
|
39
14
|
from letta.services.tool_sandbox.e2b_sandbox import AsyncToolSandboxE2B
|
40
15
|
from letta.services.tool_sandbox.local_sandbox import AsyncToolSandboxLocal
|
41
16
|
from letta.settings import tool_settings
|
42
|
-
from letta.tracing import trace_method
|
43
17
|
from letta.types import JsonDict
|
44
18
|
from letta.utils import get_friendly_error_msg
|
45
19
|
|
46
20
|
logger = get_logger(__name__)
|
47
21
|
|
48
22
|
|
49
|
-
class ToolExecutor(ABC):
|
50
|
-
"""Abstract base class for tool executors."""
|
51
|
-
|
52
|
-
def __init__(
|
53
|
-
self,
|
54
|
-
message_manager: MessageManager,
|
55
|
-
agent_manager: AgentManager,
|
56
|
-
block_manager: BlockManager,
|
57
|
-
passage_manager: PassageManager,
|
58
|
-
actor: User,
|
59
|
-
):
|
60
|
-
self.message_manager = message_manager
|
61
|
-
self.agent_manager = agent_manager
|
62
|
-
self.block_manager = block_manager
|
63
|
-
self.passage_manager = passage_manager
|
64
|
-
self.actor = actor
|
65
|
-
|
66
|
-
@abstractmethod
|
67
|
-
async def execute(
|
68
|
-
self,
|
69
|
-
function_name: str,
|
70
|
-
function_args: dict,
|
71
|
-
tool: Tool,
|
72
|
-
actor: User,
|
73
|
-
agent_state: Optional[AgentState] = None,
|
74
|
-
sandbox_config: Optional[SandboxConfig] = None,
|
75
|
-
sandbox_env_vars: Optional[Dict[str, Any]] = None,
|
76
|
-
) -> ToolExecutionResult:
|
77
|
-
"""Execute the tool and return the result."""
|
78
|
-
|
79
|
-
|
80
|
-
class LettaCoreToolExecutor(ToolExecutor):
|
81
|
-
"""Executor for LETTA core tools with direct implementation of functions."""
|
82
|
-
|
83
|
-
async def execute(
|
84
|
-
self,
|
85
|
-
function_name: str,
|
86
|
-
function_args: dict,
|
87
|
-
tool: Tool,
|
88
|
-
actor: User,
|
89
|
-
agent_state: Optional[AgentState] = None,
|
90
|
-
sandbox_config: Optional[SandboxConfig] = None,
|
91
|
-
sandbox_env_vars: Optional[Dict[str, Any]] = None,
|
92
|
-
) -> ToolExecutionResult:
|
93
|
-
# Map function names to method calls
|
94
|
-
assert agent_state is not None, "Agent state is required for core tools"
|
95
|
-
function_map = {
|
96
|
-
"send_message": self.send_message,
|
97
|
-
"conversation_search": self.conversation_search,
|
98
|
-
"archival_memory_search": self.archival_memory_search,
|
99
|
-
"archival_memory_insert": self.archival_memory_insert,
|
100
|
-
"core_memory_append": self.core_memory_append,
|
101
|
-
"core_memory_replace": self.core_memory_replace,
|
102
|
-
"memory_replace": self.memory_replace,
|
103
|
-
"memory_insert": self.memory_insert,
|
104
|
-
"memory_rethink": self.memory_rethink,
|
105
|
-
"memory_finish_edits": self.memory_finish_edits,
|
106
|
-
}
|
107
|
-
|
108
|
-
if function_name not in function_map:
|
109
|
-
raise ValueError(f"Unknown function: {function_name}")
|
110
|
-
|
111
|
-
# Execute the appropriate function
|
112
|
-
function_args_copy = function_args.copy() # Make a copy to avoid modifying the original
|
113
|
-
try:
|
114
|
-
function_response = await function_map[function_name](agent_state, actor, **function_args_copy)
|
115
|
-
return ToolExecutionResult(
|
116
|
-
status="success",
|
117
|
-
func_return=function_response,
|
118
|
-
agent_state=agent_state,
|
119
|
-
)
|
120
|
-
except Exception as e:
|
121
|
-
return ToolExecutionResult(
|
122
|
-
status="error",
|
123
|
-
func_return=e,
|
124
|
-
agent_state=agent_state,
|
125
|
-
stderr=[get_friendly_error_msg(function_name=function_name, exception_name=type(e).__name__, exception_message=str(e))],
|
126
|
-
)
|
127
|
-
|
128
|
-
async def send_message(self, agent_state: AgentState, actor: User, message: str) -> Optional[str]:
|
129
|
-
"""
|
130
|
-
Sends a message to the human user.
|
131
|
-
|
132
|
-
Args:
|
133
|
-
message (str): Message contents. All unicode (including emojis) are supported.
|
134
|
-
|
135
|
-
Returns:
|
136
|
-
Optional[str]: None is always returned as this function does not produce a response.
|
137
|
-
"""
|
138
|
-
return "Sent message successfully."
|
139
|
-
|
140
|
-
async def conversation_search(self, agent_state: AgentState, actor: User, query: str, page: Optional[int] = 0) -> Optional[str]:
|
141
|
-
"""
|
142
|
-
Search prior conversation history using case-insensitive string matching.
|
143
|
-
|
144
|
-
Args:
|
145
|
-
query (str): String to search for.
|
146
|
-
page (int): Allows you to page through results. Only use on a follow-up query. Defaults to 0 (first page).
|
147
|
-
|
148
|
-
Returns:
|
149
|
-
str: Query result string
|
150
|
-
"""
|
151
|
-
if page is None or (isinstance(page, str) and page.lower().strip() == "none"):
|
152
|
-
page = 0
|
153
|
-
try:
|
154
|
-
page = int(page)
|
155
|
-
except:
|
156
|
-
raise ValueError(f"'page' argument must be an integer")
|
157
|
-
|
158
|
-
count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
159
|
-
messages = await MessageManager().list_user_messages_for_agent_async(
|
160
|
-
agent_id=agent_state.id,
|
161
|
-
actor=actor,
|
162
|
-
query_text=query,
|
163
|
-
limit=count,
|
164
|
-
)
|
165
|
-
|
166
|
-
total = len(messages)
|
167
|
-
num_pages = math.ceil(total / count) - 1 # 0 index
|
168
|
-
|
169
|
-
if len(messages) == 0:
|
170
|
-
results_str = f"No results found."
|
171
|
-
else:
|
172
|
-
results_pref = f"Showing {len(messages)} of {total} results (page {page}/{num_pages}):"
|
173
|
-
results_formatted = [message.content[0].text for message in messages]
|
174
|
-
results_str = f"{results_pref} {json_dumps(results_formatted)}"
|
175
|
-
|
176
|
-
return results_str
|
177
|
-
|
178
|
-
async def archival_memory_search(
|
179
|
-
self, agent_state: AgentState, actor: User, query: str, page: Optional[int] = 0, start: Optional[int] = 0
|
180
|
-
) -> Optional[str]:
|
181
|
-
"""
|
182
|
-
Search archival memory using semantic (embedding-based) search.
|
183
|
-
|
184
|
-
Args:
|
185
|
-
query (str): String to search for.
|
186
|
-
page (Optional[int]): Allows you to page through results. Only use on a follow-up query. Defaults to 0 (first page).
|
187
|
-
start (Optional[int]): Starting index for the search results. Defaults to 0.
|
188
|
-
|
189
|
-
Returns:
|
190
|
-
str: Query result string
|
191
|
-
"""
|
192
|
-
if page is None or (isinstance(page, str) and page.lower().strip() == "none"):
|
193
|
-
page = 0
|
194
|
-
try:
|
195
|
-
page = int(page)
|
196
|
-
except:
|
197
|
-
raise ValueError(f"'page' argument must be an integer")
|
198
|
-
|
199
|
-
count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
200
|
-
|
201
|
-
try:
|
202
|
-
# Get results using passage manager
|
203
|
-
all_results = await AgentManager().list_passages_async(
|
204
|
-
actor=actor,
|
205
|
-
agent_id=agent_state.id,
|
206
|
-
query_text=query,
|
207
|
-
limit=count + start, # Request enough results to handle offset
|
208
|
-
embedding_config=agent_state.embedding_config,
|
209
|
-
embed_query=True,
|
210
|
-
)
|
211
|
-
|
212
|
-
# Apply pagination
|
213
|
-
end = min(count + start, len(all_results))
|
214
|
-
paged_results = all_results[start:end]
|
215
|
-
|
216
|
-
# Format results to match previous implementation
|
217
|
-
formatted_results = [{"timestamp": str(result.created_at), "content": result.text} for result in paged_results]
|
218
|
-
|
219
|
-
return formatted_results, len(formatted_results)
|
220
|
-
|
221
|
-
except Exception as e:
|
222
|
-
raise e
|
223
|
-
|
224
|
-
async def archival_memory_insert(self, agent_state: AgentState, actor: User, content: str) -> Optional[str]:
|
225
|
-
"""
|
226
|
-
Add to archival memory. Make sure to phrase the memory contents such that it can be easily queried later.
|
227
|
-
|
228
|
-
Args:
|
229
|
-
content (str): Content to write to the memory. All unicode (including emojis) are supported.
|
230
|
-
|
231
|
-
Returns:
|
232
|
-
Optional[str]: None is always returned as this function does not produce a response.
|
233
|
-
"""
|
234
|
-
await PassageManager().insert_passage_async(
|
235
|
-
agent_state=agent_state,
|
236
|
-
agent_id=agent_state.id,
|
237
|
-
text=content,
|
238
|
-
actor=actor,
|
239
|
-
)
|
240
|
-
await AgentManager().rebuild_system_prompt_async(agent_id=agent_state.id, actor=actor, force=True)
|
241
|
-
return None
|
242
|
-
|
243
|
-
async def core_memory_append(self, agent_state: AgentState, actor: User, label: str, content: str) -> Optional[str]:
|
244
|
-
"""
|
245
|
-
Append to the contents of core memory.
|
246
|
-
|
247
|
-
Args:
|
248
|
-
label (str): Section of the memory to be edited (persona or human).
|
249
|
-
content (str): Content to write to the memory. All unicode (including emojis) are supported.
|
250
|
-
|
251
|
-
Returns:
|
252
|
-
Optional[str]: None is always returned as this function does not produce a response.
|
253
|
-
"""
|
254
|
-
if agent_state.memory.get_block(label).read_only:
|
255
|
-
raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
|
256
|
-
current_value = str(agent_state.memory.get_block(label).value)
|
257
|
-
new_value = current_value + "\n" + str(content)
|
258
|
-
agent_state.memory.update_block_value(label=label, value=new_value)
|
259
|
-
await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
|
260
|
-
return None
|
261
|
-
|
262
|
-
async def core_memory_replace(
|
263
|
-
self,
|
264
|
-
agent_state: AgentState,
|
265
|
-
actor: User,
|
266
|
-
label: str,
|
267
|
-
old_content: str,
|
268
|
-
new_content: str,
|
269
|
-
) -> Optional[str]:
|
270
|
-
"""
|
271
|
-
Replace the contents of core memory. To delete memories, use an empty string for new_content.
|
272
|
-
|
273
|
-
Args:
|
274
|
-
label (str): Section of the memory to be edited (persona or human).
|
275
|
-
old_content (str): String to replace. Must be an exact match.
|
276
|
-
new_content (str): Content to write to the memory. All unicode (including emojis) are supported.
|
277
|
-
|
278
|
-
Returns:
|
279
|
-
Optional[str]: None is always returned as this function does not produce a response.
|
280
|
-
"""
|
281
|
-
if agent_state.memory.get_block(label).read_only:
|
282
|
-
raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
|
283
|
-
current_value = str(agent_state.memory.get_block(label).value)
|
284
|
-
if old_content not in current_value:
|
285
|
-
raise ValueError(f"Old content '{old_content}' not found in memory block '{label}'")
|
286
|
-
new_value = current_value.replace(str(old_content), str(new_content))
|
287
|
-
agent_state.memory.update_block_value(label=label, value=new_value)
|
288
|
-
await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
|
289
|
-
return None
|
290
|
-
|
291
|
-
async def memory_replace(
|
292
|
-
self,
|
293
|
-
agent_state: AgentState,
|
294
|
-
actor: User,
|
295
|
-
label: str,
|
296
|
-
old_str: str,
|
297
|
-
new_str: Optional[str] = None,
|
298
|
-
) -> str:
|
299
|
-
"""
|
300
|
-
The memory_replace command allows you to replace a specific string in a memory
|
301
|
-
block with a new string. This is used for making precise edits.
|
302
|
-
|
303
|
-
Args:
|
304
|
-
label (str): Section of the memory to be edited, identified by its label.
|
305
|
-
old_str (str): The text to replace (must match exactly, including whitespace
|
306
|
-
and indentation). Do not include line number prefixes.
|
307
|
-
new_str (Optional[str]): The new text to insert in place of the old text.
|
308
|
-
Omit this argument to delete the old_str. Do not include line number prefixes.
|
309
|
-
|
310
|
-
Returns:
|
311
|
-
str: The success message
|
312
|
-
"""
|
313
|
-
|
314
|
-
if agent_state.memory.get_block(label).read_only:
|
315
|
-
raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
|
316
|
-
|
317
|
-
if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(old_str)):
|
318
|
-
raise ValueError(
|
319
|
-
"old_str contains a line number prefix, which is not allowed. "
|
320
|
-
"Do not include line numbers when calling memory tools (line "
|
321
|
-
"numbers are for display purposes only)."
|
322
|
-
)
|
323
|
-
if CORE_MEMORY_LINE_NUMBER_WARNING in old_str:
|
324
|
-
raise ValueError(
|
325
|
-
"old_str contains a line number warning, which is not allowed. "
|
326
|
-
"Do not include line number information when calling memory tools "
|
327
|
-
"(line numbers are for display purposes only)."
|
328
|
-
)
|
329
|
-
if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(new_str)):
|
330
|
-
raise ValueError(
|
331
|
-
"new_str contains a line number prefix, which is not allowed. "
|
332
|
-
"Do not include line numbers when calling memory tools (line "
|
333
|
-
"numbers are for display purposes only)."
|
334
|
-
)
|
335
|
-
|
336
|
-
old_str = str(old_str).expandtabs()
|
337
|
-
new_str = str(new_str).expandtabs()
|
338
|
-
current_value = str(agent_state.memory.get_block(label).value).expandtabs()
|
339
|
-
|
340
|
-
# Check if old_str is unique in the block
|
341
|
-
occurences = current_value.count(old_str)
|
342
|
-
if occurences == 0:
|
343
|
-
raise ValueError(
|
344
|
-
f"No replacement was performed, old_str `{old_str}` did not appear " f"verbatim in memory block with label `{label}`."
|
345
|
-
)
|
346
|
-
elif occurences > 1:
|
347
|
-
content_value_lines = current_value.split("\n")
|
348
|
-
lines = [idx + 1 for idx, line in enumerate(content_value_lines) if old_str in line]
|
349
|
-
raise ValueError(
|
350
|
-
f"No replacement was performed. Multiple occurrences of "
|
351
|
-
f"old_str `{old_str}` in lines {lines}. Please ensure it is unique."
|
352
|
-
)
|
353
|
-
|
354
|
-
# Replace old_str with new_str
|
355
|
-
new_value = current_value.replace(str(old_str), str(new_str))
|
356
|
-
|
357
|
-
# Write the new content to the block
|
358
|
-
agent_state.memory.update_block_value(label=label, value=new_value)
|
359
|
-
|
360
|
-
await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
|
361
|
-
|
362
|
-
# Create a snippet of the edited section
|
363
|
-
SNIPPET_LINES = 3
|
364
|
-
replacement_line = current_value.split(old_str)[0].count("\n")
|
365
|
-
start_line = max(0, replacement_line - SNIPPET_LINES)
|
366
|
-
end_line = replacement_line + SNIPPET_LINES + new_str.count("\n")
|
367
|
-
snippet = "\n".join(new_value.split("\n")[start_line : end_line + 1])
|
368
|
-
|
369
|
-
# Prepare the success message
|
370
|
-
success_msg = f"The core memory block with label `{label}` has been edited. "
|
371
|
-
# success_msg += self._make_output(
|
372
|
-
# snippet, f"a snippet of {path}", start_line + 1
|
373
|
-
# )
|
374
|
-
# success_msg += f"A snippet of core memory block `{label}`:\n{snippet}\n"
|
375
|
-
success_msg += (
|
376
|
-
"Review the changes and make sure they are as expected (correct indentation, "
|
377
|
-
"no duplicate lines, etc). Edit the memory block again if necessary."
|
378
|
-
)
|
379
|
-
|
380
|
-
# return None
|
381
|
-
return success_msg
|
382
|
-
|
383
|
-
async def memory_insert(
|
384
|
-
self,
|
385
|
-
agent_state: AgentState,
|
386
|
-
actor: User,
|
387
|
-
label: str,
|
388
|
-
new_str: str,
|
389
|
-
insert_line: int = -1,
|
390
|
-
) -> str:
|
391
|
-
"""
|
392
|
-
The memory_insert command allows you to insert text at a specific location
|
393
|
-
in a memory block.
|
394
|
-
|
395
|
-
Args:
|
396
|
-
label (str): Section of the memory to be edited, identified by its label.
|
397
|
-
new_str (str): The text to insert. Do not include line number prefixes.
|
398
|
-
insert_line (int): The line number after which to insert the text (0 for
|
399
|
-
beginning of file). Defaults to -1 (end of the file).
|
400
|
-
|
401
|
-
Returns:
|
402
|
-
str: The success message
|
403
|
-
"""
|
404
|
-
|
405
|
-
if agent_state.memory.get_block(label).read_only:
|
406
|
-
raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
|
407
|
-
|
408
|
-
if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(new_str)):
|
409
|
-
raise ValueError(
|
410
|
-
"new_str contains a line number prefix, which is not allowed. Do not "
|
411
|
-
"include line numbers when calling memory tools (line numbers are for "
|
412
|
-
"display purposes only)."
|
413
|
-
)
|
414
|
-
if CORE_MEMORY_LINE_NUMBER_WARNING in new_str:
|
415
|
-
raise ValueError(
|
416
|
-
"new_str contains a line number warning, which is not allowed. Do not "
|
417
|
-
"include line number information when calling memory tools (line numbers "
|
418
|
-
"are for display purposes only)."
|
419
|
-
)
|
420
|
-
|
421
|
-
current_value = str(agent_state.memory.get_block(label).value).expandtabs()
|
422
|
-
new_str = str(new_str).expandtabs()
|
423
|
-
current_value_lines = current_value.split("\n")
|
424
|
-
n_lines = len(current_value_lines)
|
425
|
-
|
426
|
-
# Check if we're in range, from 0 (pre-line), to 1 (first line), to n_lines (last line)
|
427
|
-
if insert_line == -1:
|
428
|
-
insert_line = n_lines
|
429
|
-
elif insert_line < 0 or insert_line > n_lines:
|
430
|
-
raise ValueError(
|
431
|
-
f"Invalid `insert_line` parameter: {insert_line}. It should be within "
|
432
|
-
f"the range of lines of the memory block: {[0, n_lines]}, or -1 to "
|
433
|
-
f"append to the end of the memory block."
|
434
|
-
)
|
435
|
-
|
436
|
-
# Insert the new string as a line
|
437
|
-
SNIPPET_LINES = 3
|
438
|
-
new_str_lines = new_str.split("\n")
|
439
|
-
new_value_lines = current_value_lines[:insert_line] + new_str_lines + current_value_lines[insert_line:]
|
440
|
-
snippet_lines = (
|
441
|
-
current_value_lines[max(0, insert_line - SNIPPET_LINES) : insert_line]
|
442
|
-
+ new_str_lines
|
443
|
-
+ current_value_lines[insert_line : insert_line + SNIPPET_LINES]
|
444
|
-
)
|
445
|
-
|
446
|
-
# Collate into the new value to update
|
447
|
-
new_value = "\n".join(new_value_lines)
|
448
|
-
snippet = "\n".join(snippet_lines)
|
449
|
-
|
450
|
-
# Write into the block
|
451
|
-
agent_state.memory.update_block_value(label=label, value=new_value)
|
452
|
-
|
453
|
-
await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
|
454
|
-
|
455
|
-
# Prepare the success message
|
456
|
-
success_msg = f"The core memory block with label `{label}` has been edited. "
|
457
|
-
# success_msg += self._make_output(
|
458
|
-
# snippet,
|
459
|
-
# "a snippet of the edited file",
|
460
|
-
# max(1, insert_line - SNIPPET_LINES + 1),
|
461
|
-
# )
|
462
|
-
# success_msg += f"A snippet of core memory block `{label}`:\n{snippet}\n"
|
463
|
-
success_msg += (
|
464
|
-
"Review the changes and make sure they are as expected (correct indentation, "
|
465
|
-
"no duplicate lines, etc). Edit the memory block again if necessary."
|
466
|
-
)
|
467
|
-
|
468
|
-
return success_msg
|
469
|
-
|
470
|
-
async def memory_rethink(self, agent_state: AgentState, actor: User, label: str, new_memory: str) -> str:
|
471
|
-
"""
|
472
|
-
The memory_rethink command allows you to completely rewrite the contents of a
|
473
|
-
memory block. Use this tool to make large sweeping changes (e.g. when you want
|
474
|
-
to condense or reorganize the memory blocks), do NOT use this tool to make small
|
475
|
-
precise edits (e.g. add or remove a line, replace a specific string, etc).
|
476
|
-
|
477
|
-
Args:
|
478
|
-
label (str): The memory block to be rewritten, identified by its label.
|
479
|
-
new_memory (str): The new memory contents with information integrated from
|
480
|
-
existing memory blocks and the conversation context. Do not include line number prefixes.
|
481
|
-
|
482
|
-
Returns:
|
483
|
-
str: The success message
|
484
|
-
"""
|
485
|
-
if agent_state.memory.get_block(label).read_only:
|
486
|
-
raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
|
487
|
-
|
488
|
-
if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(new_memory)):
|
489
|
-
raise ValueError(
|
490
|
-
"new_memory contains a line number prefix, which is not allowed. Do not "
|
491
|
-
"include line numbers when calling memory tools (line numbers are for "
|
492
|
-
"display purposes only)."
|
493
|
-
)
|
494
|
-
if CORE_MEMORY_LINE_NUMBER_WARNING in new_memory:
|
495
|
-
raise ValueError(
|
496
|
-
"new_memory contains a line number warning, which is not allowed. Do not "
|
497
|
-
"include line number information when calling memory tools (line numbers "
|
498
|
-
"are for display purposes only)."
|
499
|
-
)
|
500
|
-
|
501
|
-
if agent_state.memory.get_block(label) is None:
|
502
|
-
agent_state.memory.create_block(label=label, value=new_memory)
|
503
|
-
|
504
|
-
agent_state.memory.update_block_value(label=label, value=new_memory)
|
505
|
-
|
506
|
-
await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
|
507
|
-
|
508
|
-
# Prepare the success message
|
509
|
-
success_msg = f"The core memory block with label `{label}` has been edited. "
|
510
|
-
# success_msg += self._make_output(
|
511
|
-
# snippet, f"a snippet of {path}", start_line + 1
|
512
|
-
# )
|
513
|
-
# success_msg += f"A snippet of core memory block `{label}`:\n{snippet}\n"
|
514
|
-
success_msg += (
|
515
|
-
"Review the changes and make sure they are as expected (correct indentation, "
|
516
|
-
"no duplicate lines, etc). Edit the memory block again if necessary."
|
517
|
-
)
|
518
|
-
|
519
|
-
# return None
|
520
|
-
return success_msg
|
521
|
-
|
522
|
-
async def memory_finish_edits(self, agent_state: AgentState, actor: User) -> None:
|
523
|
-
"""
|
524
|
-
Call the memory_finish_edits command when you are finished making edits
|
525
|
-
(integrating all new information) into the memory blocks. This function
|
526
|
-
is called when the agent is done rethinking the memory.
|
527
|
-
|
528
|
-
Returns:
|
529
|
-
Optional[str]: None is always returned as this function does not produce a response.
|
530
|
-
"""
|
531
|
-
return None
|
532
|
-
|
533
|
-
|
534
|
-
class LettaMultiAgentToolExecutor(ToolExecutor):
|
535
|
-
"""Executor for LETTA multi-agent core tools."""
|
536
|
-
|
537
|
-
async def execute(
|
538
|
-
self,
|
539
|
-
function_name: str,
|
540
|
-
function_args: dict,
|
541
|
-
tool: Tool,
|
542
|
-
actor: User,
|
543
|
-
agent_state: Optional[AgentState] = None,
|
544
|
-
sandbox_config: Optional[SandboxConfig] = None,
|
545
|
-
sandbox_env_vars: Optional[Dict[str, Any]] = None,
|
546
|
-
) -> ToolExecutionResult:
|
547
|
-
assert agent_state is not None, "Agent state is required for multi-agent tools"
|
548
|
-
function_map = {
|
549
|
-
"send_message_to_agent_and_wait_for_reply": self.send_message_to_agent_and_wait_for_reply,
|
550
|
-
"send_message_to_agent_async": self.send_message_to_agent_async,
|
551
|
-
"send_message_to_agents_matching_tags": self.send_message_to_agents_matching_tags_async,
|
552
|
-
}
|
553
|
-
|
554
|
-
if function_name not in function_map:
|
555
|
-
raise ValueError(f"Unknown function: {function_name}")
|
556
|
-
|
557
|
-
# Execute the appropriate function
|
558
|
-
function_args_copy = function_args.copy() # Make a copy to avoid modifying the original
|
559
|
-
function_response = await function_map[function_name](agent_state, **function_args_copy)
|
560
|
-
return ToolExecutionResult(
|
561
|
-
status="success",
|
562
|
-
func_return=function_response,
|
563
|
-
)
|
564
|
-
|
565
|
-
async def send_message_to_agent_and_wait_for_reply(self, agent_state: AgentState, message: str, other_agent_id: str) -> str:
|
566
|
-
augmented_message = (
|
567
|
-
f"[Incoming message from agent with ID '{agent_state.id}' - to reply to this message, "
|
568
|
-
f"make sure to use the 'send_message' at the end, and the system will notify the sender of your response] "
|
569
|
-
f"{message}"
|
570
|
-
)
|
571
|
-
|
572
|
-
return str(await self._process_agent(agent_id=other_agent_id, message=augmented_message))
|
573
|
-
|
574
|
-
async def send_message_to_agent_async(self, agent_state: AgentState, message: str, other_agent_id: str) -> str:
|
575
|
-
# 1) Build the prefixed system‐message
|
576
|
-
prefixed = (
|
577
|
-
f"[Incoming message from agent with ID '{agent_state.id}' - "
|
578
|
-
f"to reply to this message, make sure to use the "
|
579
|
-
f"'send_message_to_agent_async' tool, or the agent will not receive your message] "
|
580
|
-
f"{message}"
|
581
|
-
)
|
582
|
-
|
583
|
-
task = asyncio.create_task(self._process_agent(agent_id=other_agent_id, message=prefixed))
|
584
|
-
|
585
|
-
task.add_done_callback(lambda t: (logger.error(f"Async send_message task failed: {t.exception()}") if t.exception() else None))
|
586
|
-
|
587
|
-
return "Successfully sent message"
|
588
|
-
|
589
|
-
async def send_message_to_agents_matching_tags_async(
|
590
|
-
self, agent_state: AgentState, message: str, match_all: List[str], match_some: List[str]
|
591
|
-
) -> str:
|
592
|
-
# Find matching agents
|
593
|
-
matching_agents = await self.agent_manager.list_agents_matching_tags_async(
|
594
|
-
actor=self.actor, match_all=match_all, match_some=match_some
|
595
|
-
)
|
596
|
-
if not matching_agents:
|
597
|
-
return str([])
|
598
|
-
|
599
|
-
augmented_message = (
|
600
|
-
"[Incoming message from external Letta agent - to reply to this message, "
|
601
|
-
"make sure to use the 'send_message' at the end, and the system will notify "
|
602
|
-
"the sender of your response] "
|
603
|
-
f"{message}"
|
604
|
-
)
|
605
|
-
|
606
|
-
tasks = [
|
607
|
-
asyncio.create_task(self._process_agent(agent_id=agent_state.id, message=augmented_message)) for agent_state in matching_agents
|
608
|
-
]
|
609
|
-
results = await asyncio.gather(*tasks)
|
610
|
-
return str(results)
|
611
|
-
|
612
|
-
async def _process_agent(self, agent_id: str, message: str) -> Dict[str, Any]:
|
613
|
-
from letta.agents.letta_agent import LettaAgent
|
614
|
-
|
615
|
-
try:
|
616
|
-
letta_agent = LettaAgent(
|
617
|
-
agent_id=agent_id,
|
618
|
-
message_manager=self.message_manager,
|
619
|
-
agent_manager=self.agent_manager,
|
620
|
-
block_manager=self.block_manager,
|
621
|
-
passage_manager=self.passage_manager,
|
622
|
-
actor=self.actor,
|
623
|
-
)
|
624
|
-
|
625
|
-
letta_response = await letta_agent.step([MessageCreate(role=MessageRole.system, content=[TextContent(text=message)])])
|
626
|
-
messages = letta_response.messages
|
627
|
-
|
628
|
-
send_message_content = [message.content for message in messages if isinstance(message, AssistantMessage)]
|
629
|
-
|
630
|
-
return {
|
631
|
-
"agent_id": agent_id,
|
632
|
-
"response": send_message_content if send_message_content else ["<no response>"],
|
633
|
-
}
|
634
|
-
|
635
|
-
except Exception as e:
|
636
|
-
return {
|
637
|
-
"agent_id": agent_id,
|
638
|
-
"error": str(e),
|
639
|
-
"type": type(e).__name__,
|
640
|
-
}
|
641
|
-
|
642
|
-
|
643
|
-
class ExternalComposioToolExecutor(ToolExecutor):
|
644
|
-
"""Executor for external Composio tools."""
|
645
|
-
|
646
|
-
@trace_method
|
647
|
-
async def execute(
|
648
|
-
self,
|
649
|
-
function_name: str,
|
650
|
-
function_args: dict,
|
651
|
-
tool: Tool,
|
652
|
-
actor: User,
|
653
|
-
agent_state: Optional[AgentState] = None,
|
654
|
-
sandbox_config: Optional[SandboxConfig] = None,
|
655
|
-
sandbox_env_vars: Optional[Dict[str, Any]] = None,
|
656
|
-
) -> ToolExecutionResult:
|
657
|
-
assert agent_state is not None, "Agent state is required for external Composio tools"
|
658
|
-
action_name = generate_composio_action_from_func_name(tool.name)
|
659
|
-
|
660
|
-
# Get entity ID from the agent_state
|
661
|
-
entity_id = self._get_entity_id(agent_state)
|
662
|
-
|
663
|
-
# Get composio_api_key
|
664
|
-
composio_api_key = await get_composio_api_key_async(actor=actor)
|
665
|
-
|
666
|
-
# TODO (matt): Roll in execute_composio_action into this class
|
667
|
-
function_response = await execute_composio_action_async(
|
668
|
-
action_name=action_name, args=function_args, api_key=composio_api_key, entity_id=entity_id
|
669
|
-
)
|
670
|
-
|
671
|
-
return ToolExecutionResult(
|
672
|
-
status="success",
|
673
|
-
func_return=function_response,
|
674
|
-
)
|
675
|
-
|
676
|
-
def _get_entity_id(self, agent_state: AgentState) -> Optional[str]:
|
677
|
-
"""Extract the entity ID from environment variables."""
|
678
|
-
for env_var in agent_state.tool_exec_environment_variables:
|
679
|
-
if env_var.key == COMPOSIO_ENTITY_ENV_VAR_KEY:
|
680
|
-
return env_var.value
|
681
|
-
return None
|
682
|
-
|
683
|
-
|
684
|
-
class ExternalMCPToolExecutor(ToolExecutor):
|
685
|
-
"""Executor for external MCP tools."""
|
686
|
-
|
687
|
-
@trace_method
|
688
|
-
async def execute(
|
689
|
-
self,
|
690
|
-
function_name: str,
|
691
|
-
function_args: dict,
|
692
|
-
tool: Tool,
|
693
|
-
actor: User,
|
694
|
-
agent_state: Optional[AgentState] = None,
|
695
|
-
sandbox_config: Optional[SandboxConfig] = None,
|
696
|
-
sandbox_env_vars: Optional[Dict[str, Any]] = None,
|
697
|
-
) -> ToolExecutionResult:
|
698
|
-
|
699
|
-
pass
|
700
|
-
|
701
|
-
mcp_server_tag = [tag for tag in tool.tags if tag.startswith(f"{MCP_TOOL_TAG_NAME_PREFIX}:")]
|
702
|
-
if not mcp_server_tag:
|
703
|
-
raise ValueError(f"Tool {tool.name} does not have a valid MCP server tag")
|
704
|
-
mcp_server_name = mcp_server_tag[0].split(":")[1]
|
705
|
-
|
706
|
-
mcp_manager = MCPManager()
|
707
|
-
# TODO: may need to have better client connection management
|
708
|
-
function_response, success = await mcp_manager.execute_mcp_server_tool(
|
709
|
-
mcp_server_name=mcp_server_name, tool_name=function_name, tool_args=function_args, actor=actor
|
710
|
-
)
|
711
|
-
|
712
|
-
return ToolExecutionResult(
|
713
|
-
status="success" if success else "error",
|
714
|
-
func_return=function_response,
|
715
|
-
)
|
716
|
-
|
717
|
-
|
718
23
|
class SandboxToolExecutor(ToolExecutor):
|
719
24
|
"""Executor for sandboxed tools."""
|
720
25
|
|
@@ -801,107 +106,3 @@ class SandboxToolExecutor(ToolExecutor):
|
|
801
106
|
func_return=error_message,
|
802
107
|
stderr=[stderr],
|
803
108
|
)
|
804
|
-
|
805
|
-
|
806
|
-
class LettaBuiltinToolExecutor(ToolExecutor):
|
807
|
-
"""Executor for built in Letta tools."""
|
808
|
-
|
809
|
-
@trace_method
|
810
|
-
async def execute(
|
811
|
-
self,
|
812
|
-
function_name: str,
|
813
|
-
function_args: dict,
|
814
|
-
tool: Tool,
|
815
|
-
actor: User,
|
816
|
-
agent_state: Optional[AgentState] = None,
|
817
|
-
sandbox_config: Optional[SandboxConfig] = None,
|
818
|
-
sandbox_env_vars: Optional[Dict[str, Any]] = None,
|
819
|
-
) -> ToolExecutionResult:
|
820
|
-
function_map = {"run_code": self.run_code, "web_search": self.web_search}
|
821
|
-
|
822
|
-
if function_name not in function_map:
|
823
|
-
raise ValueError(f"Unknown function: {function_name}")
|
824
|
-
|
825
|
-
# Execute the appropriate function
|
826
|
-
function_args_copy = function_args.copy() # Make a copy to avoid modifying the original
|
827
|
-
function_response = await function_map[function_name](**function_args_copy)
|
828
|
-
|
829
|
-
return ToolExecutionResult(
|
830
|
-
status="success",
|
831
|
-
func_return=function_response,
|
832
|
-
agent_state=agent_state,
|
833
|
-
)
|
834
|
-
|
835
|
-
async def run_code(self, code: str, language: Literal["python", "js", "ts", "r", "java"]) -> str:
|
836
|
-
from e2b_code_interpreter import AsyncSandbox
|
837
|
-
|
838
|
-
if tool_settings.e2b_api_key is None:
|
839
|
-
raise ValueError("E2B_API_KEY is not set")
|
840
|
-
|
841
|
-
sbx = await AsyncSandbox.create(api_key=tool_settings.e2b_api_key)
|
842
|
-
params = {"code": code}
|
843
|
-
if language != "python":
|
844
|
-
# Leave empty for python
|
845
|
-
params["language"] = language
|
846
|
-
|
847
|
-
res = self._llm_friendly_result(await sbx.run_code(**params))
|
848
|
-
return json.dumps(res, ensure_ascii=False)
|
849
|
-
|
850
|
-
def _llm_friendly_result(self, res):
|
851
|
-
out = {
|
852
|
-
"results": [r.text if hasattr(r, "text") else str(r) for r in res.results],
|
853
|
-
"logs": {
|
854
|
-
"stdout": getattr(res.logs, "stdout", []),
|
855
|
-
"stderr": getattr(res.logs, "stderr", []),
|
856
|
-
},
|
857
|
-
}
|
858
|
-
err = getattr(res, "error", None)
|
859
|
-
if err is not None:
|
860
|
-
out["error"] = err
|
861
|
-
return out
|
862
|
-
|
863
|
-
async def web_search(agent_state: "AgentState", query: str) -> str:
|
864
|
-
"""
|
865
|
-
Search the web for information.
|
866
|
-
Args:
|
867
|
-
query (str): The query to search the web for.
|
868
|
-
Returns:
|
869
|
-
str: The search results.
|
870
|
-
"""
|
871
|
-
|
872
|
-
try:
|
873
|
-
from tavily import AsyncTavilyClient
|
874
|
-
except ImportError:
|
875
|
-
raise ImportError("tavily is not installed in the tool execution environment")
|
876
|
-
|
877
|
-
# Check if the API key exists
|
878
|
-
if tool_settings.tavily_api_key is None:
|
879
|
-
raise ValueError("TAVILY_API_KEY is not set")
|
880
|
-
|
881
|
-
# Instantiate client and search
|
882
|
-
tavily_client = AsyncTavilyClient(api_key=tool_settings.tavily_api_key)
|
883
|
-
search_results = await tavily_client.search(query=query, auto_parameters=True)
|
884
|
-
|
885
|
-
results = search_results.get("results", [])
|
886
|
-
if not results:
|
887
|
-
return "No search results found."
|
888
|
-
|
889
|
-
# ---- format for the LLM -------------------------------------------------
|
890
|
-
formatted_blocks = []
|
891
|
-
for idx, item in enumerate(results, start=1):
|
892
|
-
title = item.get("title") or "Untitled"
|
893
|
-
url = item.get("url") or "Unknown URL"
|
894
|
-
# keep each content snippet reasonably short so you don’t blow up context
|
895
|
-
content = (
|
896
|
-
shorten(item.get("content", "").strip(), width=600, placeholder=" …")
|
897
|
-
if WEB_SEARCH_CLIP_CONTENT
|
898
|
-
else item.get("content", "").strip()
|
899
|
-
)
|
900
|
-
score = item.get("score")
|
901
|
-
if WEB_SEARCH_INCLUDE_SCORE:
|
902
|
-
block = f"\nRESULT {idx}:\n" f"Title: {title}\n" f"URL: {url}\n" f"Relevance score: {score:.4f}\n" f"Content: {content}\n"
|
903
|
-
else:
|
904
|
-
block = f"\nRESULT {idx}:\n" f"Title: {title}\n" f"URL: {url}\n" f"Content: {content}\n"
|
905
|
-
formatted_blocks.append(block)
|
906
|
-
|
907
|
-
return WEB_SEARCH_SEPARATOR.join(formatted_blocks)
|