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.
Files changed (96) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +1 -1
  3. letta/agents/letta_agent.py +49 -29
  4. letta/agents/letta_agent_batch.py +1 -2
  5. letta/agents/voice_agent.py +19 -13
  6. letta/agents/voice_sleeptime_agent.py +11 -3
  7. letta/constants.py +18 -0
  8. letta/data_sources/__init__.py +0 -0
  9. letta/data_sources/redis_client.py +282 -0
  10. letta/errors.py +0 -4
  11. letta/functions/function_sets/files.py +58 -0
  12. letta/functions/schema_generator.py +18 -1
  13. letta/groups/sleeptime_multi_agent_v2.py +1 -1
  14. letta/helpers/datetime_helpers.py +47 -3
  15. letta/helpers/decorators.py +69 -0
  16. letta/{services/helpers/noop_helper.py → helpers/singleton.py} +5 -0
  17. letta/interfaces/anthropic_streaming_interface.py +43 -24
  18. letta/interfaces/openai_streaming_interface.py +21 -19
  19. letta/llm_api/anthropic.py +1 -1
  20. letta/llm_api/anthropic_client.py +22 -14
  21. letta/llm_api/google_vertex_client.py +1 -1
  22. letta/llm_api/helpers.py +36 -30
  23. letta/llm_api/llm_api_tools.py +1 -1
  24. letta/llm_api/llm_client_base.py +29 -1
  25. letta/llm_api/openai.py +1 -1
  26. letta/llm_api/openai_client.py +6 -8
  27. letta/local_llm/chat_completion_proxy.py +1 -1
  28. letta/memory.py +1 -1
  29. letta/orm/enums.py +1 -0
  30. letta/orm/file.py +80 -3
  31. letta/orm/files_agents.py +13 -0
  32. letta/orm/sqlalchemy_base.py +34 -11
  33. letta/otel/__init__.py +0 -0
  34. letta/otel/context.py +25 -0
  35. letta/otel/events.py +0 -0
  36. letta/otel/metric_registry.py +122 -0
  37. letta/otel/metrics.py +66 -0
  38. letta/otel/resource.py +26 -0
  39. letta/{tracing.py → otel/tracing.py} +55 -78
  40. letta/plugins/README.md +22 -0
  41. letta/plugins/__init__.py +0 -0
  42. letta/plugins/defaults.py +11 -0
  43. letta/plugins/plugins.py +72 -0
  44. letta/schemas/enums.py +8 -0
  45. letta/schemas/file.py +12 -0
  46. letta/schemas/tool.py +4 -0
  47. letta/server/db.py +7 -7
  48. letta/server/rest_api/app.py +8 -6
  49. letta/server/rest_api/routers/v1/agents.py +37 -36
  50. letta/server/rest_api/routers/v1/groups.py +3 -3
  51. letta/server/rest_api/routers/v1/sources.py +26 -3
  52. letta/server/rest_api/utils.py +9 -6
  53. letta/server/server.py +18 -12
  54. letta/services/agent_manager.py +185 -193
  55. letta/services/block_manager.py +1 -1
  56. letta/services/context_window_calculator/token_counter.py +3 -2
  57. letta/services/file_processor/chunker/line_chunker.py +34 -0
  58. letta/services/file_processor/file_processor.py +40 -11
  59. letta/services/file_processor/parser/mistral_parser.py +11 -1
  60. letta/services/files_agents_manager.py +96 -7
  61. letta/services/group_manager.py +6 -6
  62. letta/services/helpers/agent_manager_helper.py +373 -3
  63. letta/services/identity_manager.py +1 -1
  64. letta/services/job_manager.py +1 -1
  65. letta/services/llm_batch_manager.py +1 -1
  66. letta/services/message_manager.py +1 -1
  67. letta/services/organization_manager.py +1 -1
  68. letta/services/passage_manager.py +1 -1
  69. letta/services/per_agent_lock_manager.py +1 -1
  70. letta/services/provider_manager.py +1 -1
  71. letta/services/sandbox_config_manager.py +1 -1
  72. letta/services/source_manager.py +178 -19
  73. letta/services/step_manager.py +2 -2
  74. letta/services/summarizer/summarizer.py +1 -1
  75. letta/services/telemetry_manager.py +1 -1
  76. letta/services/tool_executor/builtin_tool_executor.py +117 -0
  77. letta/services/tool_executor/composio_tool_executor.py +53 -0
  78. letta/services/tool_executor/core_tool_executor.py +474 -0
  79. letta/services/tool_executor/files_tool_executor.py +131 -0
  80. letta/services/tool_executor/mcp_tool_executor.py +45 -0
  81. letta/services/tool_executor/multi_agent_tool_executor.py +123 -0
  82. letta/services/tool_executor/tool_execution_manager.py +34 -14
  83. letta/services/tool_executor/tool_execution_sandbox.py +1 -1
  84. letta/services/tool_executor/tool_executor.py +3 -802
  85. letta/services/tool_executor/tool_executor_base.py +43 -0
  86. letta/services/tool_manager.py +55 -59
  87. letta/services/tool_sandbox/e2b_sandbox.py +1 -1
  88. letta/services/tool_sandbox/local_sandbox.py +6 -3
  89. letta/services/user_manager.py +6 -3
  90. letta/settings.py +21 -1
  91. letta/utils.py +7 -2
  92. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/METADATA +4 -2
  93. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/RECORD +96 -74
  94. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/LICENSE +0 -0
  95. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/WHEEL +0 -0
  96. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.2.dev20250606215616.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,474 @@
1
+ import math
2
+ from typing import Any, Dict, Optional
3
+
4
+ from letta.constants import (
5
+ CORE_MEMORY_LINE_NUMBER_WARNING,
6
+ MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX,
7
+ READ_ONLY_BLOCK_EDIT_ERROR,
8
+ RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE,
9
+ )
10
+ from letta.helpers.json_helpers import json_dumps
11
+ from letta.schemas.agent import AgentState
12
+ from letta.schemas.sandbox_config import SandboxConfig
13
+ from letta.schemas.tool import Tool
14
+ from letta.schemas.tool_execution_result import ToolExecutionResult
15
+ from letta.schemas.user import User
16
+ from letta.services.agent_manager import AgentManager
17
+ from letta.services.message_manager import MessageManager
18
+ from letta.services.passage_manager import PassageManager
19
+ from letta.services.tool_executor.tool_executor_base import ToolExecutor
20
+ from letta.utils import get_friendly_error_msg
21
+
22
+
23
+ class LettaCoreToolExecutor(ToolExecutor):
24
+ """Executor for LETTA core tools with direct implementation of functions."""
25
+
26
+ async def execute(
27
+ self,
28
+ function_name: str,
29
+ function_args: dict,
30
+ tool: Tool,
31
+ actor: User,
32
+ agent_state: Optional[AgentState] = None,
33
+ sandbox_config: Optional[SandboxConfig] = None,
34
+ sandbox_env_vars: Optional[Dict[str, Any]] = None,
35
+ ) -> ToolExecutionResult:
36
+ # Map function names to method calls
37
+ assert agent_state is not None, "Agent state is required for core tools"
38
+ function_map = {
39
+ "send_message": self.send_message,
40
+ "conversation_search": self.conversation_search,
41
+ "archival_memory_search": self.archival_memory_search,
42
+ "archival_memory_insert": self.archival_memory_insert,
43
+ "core_memory_append": self.core_memory_append,
44
+ "core_memory_replace": self.core_memory_replace,
45
+ "memory_replace": self.memory_replace,
46
+ "memory_insert": self.memory_insert,
47
+ "memory_rethink": self.memory_rethink,
48
+ "memory_finish_edits": self.memory_finish_edits,
49
+ }
50
+
51
+ if function_name not in function_map:
52
+ raise ValueError(f"Unknown function: {function_name}")
53
+
54
+ # Execute the appropriate function
55
+ function_args_copy = function_args.copy() # Make a copy to avoid modifying the original
56
+ try:
57
+ function_response = await function_map[function_name](agent_state, actor, **function_args_copy)
58
+ return ToolExecutionResult(
59
+ status="success",
60
+ func_return=function_response,
61
+ agent_state=agent_state,
62
+ )
63
+ except Exception as e:
64
+ return ToolExecutionResult(
65
+ status="error",
66
+ func_return=e,
67
+ agent_state=agent_state,
68
+ stderr=[get_friendly_error_msg(function_name=function_name, exception_name=type(e).__name__, exception_message=str(e))],
69
+ )
70
+
71
+ async def send_message(self, agent_state: AgentState, actor: User, message: str) -> Optional[str]:
72
+ """
73
+ Sends a message to the human user.
74
+
75
+ Args:
76
+ message (str): Message contents. All unicode (including emojis) are supported.
77
+
78
+ Returns:
79
+ Optional[str]: None is always returned as this function does not produce a response.
80
+ """
81
+ return "Sent message successfully."
82
+
83
+ async def conversation_search(self, agent_state: AgentState, actor: User, query: str, page: Optional[int] = 0) -> Optional[str]:
84
+ """
85
+ Search prior conversation history using case-insensitive string matching.
86
+
87
+ Args:
88
+ query (str): String to search for.
89
+ page (int): Allows you to page through results. Only use on a follow-up query. Defaults to 0 (first page).
90
+
91
+ Returns:
92
+ str: Query result string
93
+ """
94
+ if page is None or (isinstance(page, str) and page.lower().strip() == "none"):
95
+ page = 0
96
+ try:
97
+ page = int(page)
98
+ except:
99
+ raise ValueError(f"'page' argument must be an integer")
100
+
101
+ count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
102
+ messages = await MessageManager().list_user_messages_for_agent_async(
103
+ agent_id=agent_state.id,
104
+ actor=actor,
105
+ query_text=query,
106
+ limit=count,
107
+ )
108
+
109
+ total = len(messages)
110
+ num_pages = math.ceil(total / count) - 1 # 0 index
111
+
112
+ if len(messages) == 0:
113
+ results_str = f"No results found."
114
+ else:
115
+ results_pref = f"Showing {len(messages)} of {total} results (page {page}/{num_pages}):"
116
+ results_formatted = [message.content[0].text for message in messages]
117
+ results_str = f"{results_pref} {json_dumps(results_formatted)}"
118
+
119
+ return results_str
120
+
121
+ async def archival_memory_search(
122
+ self, agent_state: AgentState, actor: User, query: str, page: Optional[int] = 0, start: Optional[int] = 0
123
+ ) -> Optional[str]:
124
+ """
125
+ Search archival memory using semantic (embedding-based) search.
126
+
127
+ Args:
128
+ query (str): String to search for.
129
+ page (Optional[int]): Allows you to page through results. Only use on a follow-up query. Defaults to 0 (first page).
130
+ start (Optional[int]): Starting index for the search results. Defaults to 0.
131
+
132
+ Returns:
133
+ str: Query result string
134
+ """
135
+ if page is None or (isinstance(page, str) and page.lower().strip() == "none"):
136
+ page = 0
137
+ try:
138
+ page = int(page)
139
+ except:
140
+ raise ValueError(f"'page' argument must be an integer")
141
+
142
+ count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
143
+
144
+ try:
145
+ # Get results using passage manager
146
+ all_results = await AgentManager().list_passages_async(
147
+ actor=actor,
148
+ agent_id=agent_state.id,
149
+ query_text=query,
150
+ limit=count + start, # Request enough results to handle offset
151
+ embedding_config=agent_state.embedding_config,
152
+ embed_query=True,
153
+ )
154
+
155
+ # Apply pagination
156
+ end = min(count + start, len(all_results))
157
+ paged_results = all_results[start:end]
158
+
159
+ # Format results to match previous implementation
160
+ formatted_results = [{"timestamp": str(result.created_at), "content": result.text} for result in paged_results]
161
+
162
+ return formatted_results, len(formatted_results)
163
+
164
+ except Exception as e:
165
+ raise e
166
+
167
+ async def archival_memory_insert(self, agent_state: AgentState, actor: User, content: str) -> Optional[str]:
168
+ """
169
+ Add to archival memory. Make sure to phrase the memory contents such that it can be easily queried later.
170
+
171
+ Args:
172
+ content (str): Content to write to the memory. All unicode (including emojis) are supported.
173
+
174
+ Returns:
175
+ Optional[str]: None is always returned as this function does not produce a response.
176
+ """
177
+ await PassageManager().insert_passage_async(
178
+ agent_state=agent_state,
179
+ agent_id=agent_state.id,
180
+ text=content,
181
+ actor=actor,
182
+ )
183
+ await AgentManager().rebuild_system_prompt_async(agent_id=agent_state.id, actor=actor, force=True)
184
+ return None
185
+
186
+ async def core_memory_append(self, agent_state: AgentState, actor: User, label: str, content: str) -> Optional[str]:
187
+ """
188
+ Append to the contents of core memory.
189
+
190
+ Args:
191
+ label (str): Section of the memory to be edited (persona or human).
192
+ content (str): Content to write to the memory. All unicode (including emojis) are supported.
193
+
194
+ Returns:
195
+ Optional[str]: None is always returned as this function does not produce a response.
196
+ """
197
+ if agent_state.memory.get_block(label).read_only:
198
+ raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
199
+ current_value = str(agent_state.memory.get_block(label).value)
200
+ new_value = current_value + "\n" + str(content)
201
+ agent_state.memory.update_block_value(label=label, value=new_value)
202
+ await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
203
+ return None
204
+
205
+ async def core_memory_replace(
206
+ self,
207
+ agent_state: AgentState,
208
+ actor: User,
209
+ label: str,
210
+ old_content: str,
211
+ new_content: str,
212
+ ) -> Optional[str]:
213
+ """
214
+ Replace the contents of core memory. To delete memories, use an empty string for new_content.
215
+
216
+ Args:
217
+ label (str): Section of the memory to be edited (persona or human).
218
+ old_content (str): String to replace. Must be an exact match.
219
+ new_content (str): Content to write to the memory. All unicode (including emojis) are supported.
220
+
221
+ Returns:
222
+ Optional[str]: None is always returned as this function does not produce a response.
223
+ """
224
+ if agent_state.memory.get_block(label).read_only:
225
+ raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
226
+ current_value = str(agent_state.memory.get_block(label).value)
227
+ if old_content not in current_value:
228
+ raise ValueError(f"Old content '{old_content}' not found in memory block '{label}'")
229
+ new_value = current_value.replace(str(old_content), str(new_content))
230
+ agent_state.memory.update_block_value(label=label, value=new_value)
231
+ await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
232
+ return None
233
+
234
+ async def memory_replace(
235
+ self,
236
+ agent_state: AgentState,
237
+ actor: User,
238
+ label: str,
239
+ old_str: str,
240
+ new_str: Optional[str] = None,
241
+ ) -> str:
242
+ """
243
+ The memory_replace command allows you to replace a specific string in a memory
244
+ block with a new string. This is used for making precise edits.
245
+
246
+ Args:
247
+ label (str): Section of the memory to be edited, identified by its label.
248
+ old_str (str): The text to replace (must match exactly, including whitespace
249
+ and indentation). Do not include line number prefixes.
250
+ new_str (Optional[str]): The new text to insert in place of the old text.
251
+ Omit this argument to delete the old_str. Do not include line number prefixes.
252
+
253
+ Returns:
254
+ str: The success message
255
+ """
256
+
257
+ if agent_state.memory.get_block(label).read_only:
258
+ raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
259
+
260
+ if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(old_str)):
261
+ raise ValueError(
262
+ "old_str contains a line number prefix, which is not allowed. "
263
+ "Do not include line numbers when calling memory tools (line "
264
+ "numbers are for display purposes only)."
265
+ )
266
+ if CORE_MEMORY_LINE_NUMBER_WARNING in old_str:
267
+ raise ValueError(
268
+ "old_str contains a line number warning, which is not allowed. "
269
+ "Do not include line number information when calling memory tools "
270
+ "(line numbers are for display purposes only)."
271
+ )
272
+ if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(new_str)):
273
+ raise ValueError(
274
+ "new_str contains a line number prefix, which is not allowed. "
275
+ "Do not include line numbers when calling memory tools (line "
276
+ "numbers are for display purposes only)."
277
+ )
278
+
279
+ old_str = str(old_str).expandtabs()
280
+ new_str = str(new_str).expandtabs()
281
+ current_value = str(agent_state.memory.get_block(label).value).expandtabs()
282
+
283
+ # Check if old_str is unique in the block
284
+ occurences = current_value.count(old_str)
285
+ if occurences == 0:
286
+ raise ValueError(
287
+ f"No replacement was performed, old_str `{old_str}` did not appear " f"verbatim in memory block with label `{label}`."
288
+ )
289
+ elif occurences > 1:
290
+ content_value_lines = current_value.split("\n")
291
+ lines = [idx + 1 for idx, line in enumerate(content_value_lines) if old_str in line]
292
+ raise ValueError(
293
+ f"No replacement was performed. Multiple occurrences of "
294
+ f"old_str `{old_str}` in lines {lines}. Please ensure it is unique."
295
+ )
296
+
297
+ # Replace old_str with new_str
298
+ new_value = current_value.replace(str(old_str), str(new_str))
299
+
300
+ # Write the new content to the block
301
+ agent_state.memory.update_block_value(label=label, value=new_value)
302
+
303
+ await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
304
+
305
+ # Create a snippet of the edited section
306
+ SNIPPET_LINES = 3
307
+ replacement_line = current_value.split(old_str)[0].count("\n")
308
+ start_line = max(0, replacement_line - SNIPPET_LINES)
309
+ end_line = replacement_line + SNIPPET_LINES + new_str.count("\n")
310
+ snippet = "\n".join(new_value.split("\n")[start_line : end_line + 1])
311
+
312
+ # Prepare the success message
313
+ success_msg = f"The core memory block with label `{label}` has been edited. "
314
+ # success_msg += self._make_output(
315
+ # snippet, f"a snippet of {path}", start_line + 1
316
+ # )
317
+ # success_msg += f"A snippet of core memory block `{label}`:\n{snippet}\n"
318
+ success_msg += (
319
+ "Review the changes and make sure they are as expected (correct indentation, "
320
+ "no duplicate lines, etc). Edit the memory block again if necessary."
321
+ )
322
+
323
+ # return None
324
+ return success_msg
325
+
326
+ async def memory_insert(
327
+ self,
328
+ agent_state: AgentState,
329
+ actor: User,
330
+ label: str,
331
+ new_str: str,
332
+ insert_line: int = -1,
333
+ ) -> str:
334
+ """
335
+ The memory_insert command allows you to insert text at a specific location
336
+ in a memory block.
337
+
338
+ Args:
339
+ label (str): Section of the memory to be edited, identified by its label.
340
+ new_str (str): The text to insert. Do not include line number prefixes.
341
+ insert_line (int): The line number after which to insert the text (0 for
342
+ beginning of file). Defaults to -1 (end of the file).
343
+
344
+ Returns:
345
+ str: The success message
346
+ """
347
+
348
+ if agent_state.memory.get_block(label).read_only:
349
+ raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
350
+
351
+ if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(new_str)):
352
+ raise ValueError(
353
+ "new_str contains a line number prefix, which is not allowed. Do not "
354
+ "include line numbers when calling memory tools (line numbers are for "
355
+ "display purposes only)."
356
+ )
357
+ if CORE_MEMORY_LINE_NUMBER_WARNING in new_str:
358
+ raise ValueError(
359
+ "new_str contains a line number warning, which is not allowed. Do not "
360
+ "include line number information when calling memory tools (line numbers "
361
+ "are for display purposes only)."
362
+ )
363
+
364
+ current_value = str(agent_state.memory.get_block(label).value).expandtabs()
365
+ new_str = str(new_str).expandtabs()
366
+ current_value_lines = current_value.split("\n")
367
+ n_lines = len(current_value_lines)
368
+
369
+ # Check if we're in range, from 0 (pre-line), to 1 (first line), to n_lines (last line)
370
+ if insert_line == -1:
371
+ insert_line = n_lines
372
+ elif insert_line < 0 or insert_line > n_lines:
373
+ raise ValueError(
374
+ f"Invalid `insert_line` parameter: {insert_line}. It should be within "
375
+ f"the range of lines of the memory block: {[0, n_lines]}, or -1 to "
376
+ f"append to the end of the memory block."
377
+ )
378
+
379
+ # Insert the new string as a line
380
+ SNIPPET_LINES = 3
381
+ new_str_lines = new_str.split("\n")
382
+ new_value_lines = current_value_lines[:insert_line] + new_str_lines + current_value_lines[insert_line:]
383
+ snippet_lines = (
384
+ current_value_lines[max(0, insert_line - SNIPPET_LINES) : insert_line]
385
+ + new_str_lines
386
+ + current_value_lines[insert_line : insert_line + SNIPPET_LINES]
387
+ )
388
+
389
+ # Collate into the new value to update
390
+ new_value = "\n".join(new_value_lines)
391
+ snippet = "\n".join(snippet_lines)
392
+
393
+ # Write into the block
394
+ agent_state.memory.update_block_value(label=label, value=new_value)
395
+
396
+ await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
397
+
398
+ # Prepare the success message
399
+ success_msg = f"The core memory block with label `{label}` has been edited. "
400
+ # success_msg += self._make_output(
401
+ # snippet,
402
+ # "a snippet of the edited file",
403
+ # max(1, insert_line - SNIPPET_LINES + 1),
404
+ # )
405
+ # success_msg += f"A snippet of core memory block `{label}`:\n{snippet}\n"
406
+ success_msg += (
407
+ "Review the changes and make sure they are as expected (correct indentation, "
408
+ "no duplicate lines, etc). Edit the memory block again if necessary."
409
+ )
410
+
411
+ return success_msg
412
+
413
+ async def memory_rethink(self, agent_state: AgentState, actor: User, label: str, new_memory: str) -> str:
414
+ """
415
+ The memory_rethink command allows you to completely rewrite the contents of a
416
+ memory block. Use this tool to make large sweeping changes (e.g. when you want
417
+ to condense or reorganize the memory blocks), do NOT use this tool to make small
418
+ precise edits (e.g. add or remove a line, replace a specific string, etc).
419
+
420
+ Args:
421
+ label (str): The memory block to be rewritten, identified by its label.
422
+ new_memory (str): The new memory contents with information integrated from
423
+ existing memory blocks and the conversation context. Do not include line number prefixes.
424
+
425
+ Returns:
426
+ str: The success message
427
+ """
428
+ if agent_state.memory.get_block(label).read_only:
429
+ raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
430
+
431
+ if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(new_memory)):
432
+ raise ValueError(
433
+ "new_memory contains a line number prefix, which is not allowed. Do not "
434
+ "include line numbers when calling memory tools (line numbers are for "
435
+ "display purposes only)."
436
+ )
437
+ if CORE_MEMORY_LINE_NUMBER_WARNING in new_memory:
438
+ raise ValueError(
439
+ "new_memory contains a line number warning, which is not allowed. Do not "
440
+ "include line number information when calling memory tools (line numbers "
441
+ "are for display purposes only)."
442
+ )
443
+
444
+ if agent_state.memory.get_block(label) is None:
445
+ agent_state.memory.create_block(label=label, value=new_memory)
446
+
447
+ agent_state.memory.update_block_value(label=label, value=new_memory)
448
+
449
+ await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
450
+
451
+ # Prepare the success message
452
+ success_msg = f"The core memory block with label `{label}` has been edited. "
453
+ # success_msg += self._make_output(
454
+ # snippet, f"a snippet of {path}", start_line + 1
455
+ # )
456
+ # success_msg += f"A snippet of core memory block `{label}`:\n{snippet}\n"
457
+ success_msg += (
458
+ "Review the changes and make sure they are as expected (correct indentation, "
459
+ "no duplicate lines, etc). Edit the memory block again if necessary."
460
+ )
461
+
462
+ # return None
463
+ return success_msg
464
+
465
+ async def memory_finish_edits(self, agent_state: AgentState, actor: User) -> None:
466
+ """
467
+ Call the memory_finish_edits command when you are finished making edits
468
+ (integrating all new information) into the memory blocks. This function
469
+ is called when the agent is done rethinking the memory.
470
+
471
+ Returns:
472
+ Optional[str]: None is always returned as this function does not produce a response.
473
+ """
474
+ return None
@@ -0,0 +1,131 @@
1
+ from typing import Any, Dict, List, Optional, Tuple
2
+
3
+ from letta.schemas.agent import AgentState
4
+ from letta.schemas.sandbox_config import SandboxConfig
5
+ from letta.schemas.tool import Tool
6
+ from letta.schemas.tool_execution_result import ToolExecutionResult
7
+ from letta.schemas.user import User
8
+ from letta.services.agent_manager import AgentManager
9
+ from letta.services.block_manager import BlockManager
10
+ from letta.services.file_processor.chunker.line_chunker import LineChunker
11
+ from letta.services.files_agents_manager import FileAgentManager
12
+ from letta.services.message_manager import MessageManager
13
+ from letta.services.passage_manager import PassageManager
14
+ from letta.services.source_manager import SourceManager
15
+ from letta.services.tool_executor.tool_executor_base import ToolExecutor
16
+ from letta.utils import get_friendly_error_msg
17
+
18
+
19
+ class LettaFileToolExecutor(ToolExecutor):
20
+ """Executor for Letta file tools with direct implementation of functions."""
21
+
22
+ def __init__(
23
+ self,
24
+ message_manager: MessageManager,
25
+ agent_manager: AgentManager,
26
+ block_manager: BlockManager,
27
+ passage_manager: PassageManager,
28
+ actor: User,
29
+ ):
30
+ super().__init__(
31
+ message_manager=message_manager,
32
+ agent_manager=agent_manager,
33
+ block_manager=block_manager,
34
+ passage_manager=passage_manager,
35
+ actor=actor,
36
+ )
37
+
38
+ # TODO: This should be passed in to for testing purposes
39
+ self.files_agents_manager = FileAgentManager()
40
+ self.source_manager = SourceManager()
41
+
42
+ async def execute(
43
+ self,
44
+ function_name: str,
45
+ function_args: dict,
46
+ tool: Tool,
47
+ actor: User,
48
+ agent_state: Optional[AgentState] = None,
49
+ sandbox_config: Optional[SandboxConfig] = None,
50
+ sandbox_env_vars: Optional[Dict[str, Any]] = None,
51
+ ) -> ToolExecutionResult:
52
+ if agent_state is None:
53
+ raise ValueError("Agent state is required for file tools")
54
+
55
+ function_map = {
56
+ "open_file": self.open_file,
57
+ "close_file": self.close_file,
58
+ "grep": self.grep,
59
+ "search_files": self.search_files,
60
+ }
61
+
62
+ if function_name not in function_map:
63
+ raise ValueError(f"Unknown function: {function_name}")
64
+
65
+ function_args_copy = function_args.copy()
66
+ try:
67
+ func_return = await function_map[function_name](agent_state, **function_args_copy)
68
+ return ToolExecutionResult(
69
+ status="success",
70
+ func_return=func_return,
71
+ agent_state=agent_state,
72
+ )
73
+ except Exception as e:
74
+ return ToolExecutionResult(
75
+ status="error",
76
+ func_return=e,
77
+ agent_state=agent_state,
78
+ stderr=[get_friendly_error_msg(function_name=function_name, exception_name=type(e).__name__, exception_message=str(e))],
79
+ )
80
+
81
+ async def open_file(self, agent_state: AgentState, file_name: str, view_range: Optional[Tuple[int, int]] = None) -> str:
82
+ """Stub for open_file tool."""
83
+ start, end = None, None
84
+ if view_range:
85
+ start, end = view_range
86
+ if start >= end:
87
+ raise ValueError(f"Provided view range {view_range} is invalid, starting range must be less than ending range.")
88
+
89
+ # TODO: This is inefficient. We can skip the initial DB lookup by preserving on the block metadata what the file_id is
90
+ file_agent = await self.files_agents_manager.get_file_agent_by_file_name(
91
+ agent_id=agent_state.id, file_name=file_name, actor=self.actor
92
+ )
93
+
94
+ if not file_agent:
95
+ file_blocks = agent_state.memory.file_blocks
96
+ file_names = [fb.label for fb in file_blocks]
97
+ raise ValueError(
98
+ f"{file_name} not attached - did you get the filename correct? Currently you have the following files attached: {file_names}"
99
+ )
100
+
101
+ file_id = file_agent.file_id
102
+ file = await self.source_manager.get_file_by_id(file_id=file_id, actor=self.actor, include_content=True)
103
+
104
+ # TODO: Inefficient, maybe we can pre-compute this
105
+ # TODO: This is also not the best way to split things - would be cool to have "content aware" splitting
106
+ # TODO: Split code differently from large text blurbs
107
+ content_lines = LineChunker().chunk_text(text=file.content, start=start, end=end)
108
+ visible_content = "\n".join(content_lines)
109
+
110
+ await self.files_agents_manager.update_file_agent_by_id(
111
+ agent_id=agent_state.id, file_id=file_id, actor=self.actor, is_open=True, visible_content=visible_content
112
+ )
113
+
114
+ return "Success"
115
+
116
+ async def close_file(self, agent_state: AgentState, file_name: str) -> str:
117
+ """Stub for close_file tool."""
118
+ await self.files_agents_manager.update_file_agent_by_name(
119
+ agent_id=agent_state.id, file_name=file_name, actor=self.actor, is_open=False
120
+ )
121
+ return "Success"
122
+
123
+ async def grep(self, agent_state: AgentState, pattern: str) -> str:
124
+ """Stub for grep tool."""
125
+ raise NotImplementedError
126
+
127
+ # TODO: Make this paginated?
128
+ async def search_files(self, agent_state: AgentState, query: str) -> List[str]:
129
+ """Stub for search_files tool."""
130
+ passages = await self.agent_manager.list_source_passages_async(actor=self.actor, agent_id=agent_state.id, query_text=query)
131
+ return [p.text for p in passages]
@@ -0,0 +1,45 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+ from letta.constants import MCP_TOOL_TAG_NAME_PREFIX
4
+ from letta.otel.tracing import trace_method
5
+ from letta.schemas.agent import AgentState
6
+ from letta.schemas.sandbox_config import SandboxConfig
7
+ from letta.schemas.tool import Tool
8
+ from letta.schemas.tool_execution_result import ToolExecutionResult
9
+ from letta.schemas.user import User
10
+ from letta.services.mcp_manager import MCPManager
11
+ from letta.services.tool_executor.tool_executor_base import ToolExecutor
12
+
13
+
14
+ class ExternalMCPToolExecutor(ToolExecutor):
15
+ """Executor for external MCP tools."""
16
+
17
+ @trace_method
18
+ async def execute(
19
+ self,
20
+ function_name: str,
21
+ function_args: dict,
22
+ tool: Tool,
23
+ actor: User,
24
+ agent_state: Optional[AgentState] = None,
25
+ sandbox_config: Optional[SandboxConfig] = None,
26
+ sandbox_env_vars: Optional[Dict[str, Any]] = None,
27
+ ) -> ToolExecutionResult:
28
+
29
+ pass
30
+
31
+ mcp_server_tag = [tag for tag in tool.tags if tag.startswith(f"{MCP_TOOL_TAG_NAME_PREFIX}:")]
32
+ if not mcp_server_tag:
33
+ raise ValueError(f"Tool {tool.name} does not have a valid MCP server tag")
34
+ mcp_server_name = mcp_server_tag[0].split(":")[1]
35
+
36
+ mcp_manager = MCPManager()
37
+ # TODO: may need to have better client connection management
38
+ function_response, success = await mcp_manager.execute_mcp_server_tool(
39
+ mcp_server_name=mcp_server_name, tool_name=function_name, tool_args=function_args, actor=actor
40
+ )
41
+
42
+ return ToolExecutionResult(
43
+ status="success" if success else "error",
44
+ func_return=function_response,
45
+ )