letta-nightly 0.11.6.dev20250903104037__py3-none-any.whl → 0.11.7.dev20250904045700__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 (138) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +10 -14
  3. letta/agents/base_agent.py +18 -0
  4. letta/agents/helpers.py +32 -7
  5. letta/agents/letta_agent.py +953 -762
  6. letta/agents/voice_agent.py +1 -1
  7. letta/client/streaming.py +0 -1
  8. letta/constants.py +11 -8
  9. letta/errors.py +9 -0
  10. letta/functions/function_sets/base.py +77 -69
  11. letta/functions/function_sets/builtin.py +41 -22
  12. letta/functions/function_sets/multi_agent.py +1 -2
  13. letta/functions/schema_generator.py +0 -1
  14. letta/helpers/converters.py +8 -3
  15. letta/helpers/datetime_helpers.py +5 -4
  16. letta/helpers/message_helper.py +1 -2
  17. letta/helpers/pinecone_utils.py +0 -1
  18. letta/helpers/tool_rule_solver.py +10 -0
  19. letta/helpers/tpuf_client.py +848 -0
  20. letta/interface.py +8 -8
  21. letta/interfaces/anthropic_streaming_interface.py +7 -0
  22. letta/interfaces/openai_streaming_interface.py +29 -6
  23. letta/llm_api/anthropic_client.py +188 -18
  24. letta/llm_api/azure_client.py +0 -1
  25. letta/llm_api/bedrock_client.py +1 -2
  26. letta/llm_api/deepseek_client.py +319 -5
  27. letta/llm_api/google_vertex_client.py +75 -17
  28. letta/llm_api/groq_client.py +0 -1
  29. letta/llm_api/helpers.py +2 -2
  30. letta/llm_api/llm_api_tools.py +1 -50
  31. letta/llm_api/llm_client.py +6 -8
  32. letta/llm_api/mistral.py +1 -1
  33. letta/llm_api/openai.py +16 -13
  34. letta/llm_api/openai_client.py +31 -16
  35. letta/llm_api/together_client.py +0 -1
  36. letta/llm_api/xai_client.py +0 -1
  37. letta/local_llm/chat_completion_proxy.py +7 -6
  38. letta/local_llm/settings/settings.py +1 -1
  39. letta/orm/__init__.py +1 -0
  40. letta/orm/agent.py +8 -6
  41. letta/orm/archive.py +9 -1
  42. letta/orm/block.py +3 -4
  43. letta/orm/block_history.py +3 -1
  44. letta/orm/group.py +2 -3
  45. letta/orm/identity.py +1 -2
  46. letta/orm/job.py +1 -2
  47. letta/orm/llm_batch_items.py +1 -2
  48. letta/orm/message.py +8 -4
  49. letta/orm/mixins.py +18 -0
  50. letta/orm/organization.py +2 -0
  51. letta/orm/passage.py +8 -1
  52. letta/orm/passage_tag.py +55 -0
  53. letta/orm/sandbox_config.py +1 -3
  54. letta/orm/step.py +1 -2
  55. letta/orm/tool.py +1 -0
  56. letta/otel/resource.py +2 -2
  57. letta/plugins/plugins.py +1 -1
  58. letta/prompts/prompt_generator.py +10 -2
  59. letta/schemas/agent.py +11 -0
  60. letta/schemas/archive.py +4 -0
  61. letta/schemas/block.py +13 -0
  62. letta/schemas/embedding_config.py +0 -1
  63. letta/schemas/enums.py +24 -7
  64. letta/schemas/group.py +12 -0
  65. letta/schemas/letta_message.py +55 -1
  66. letta/schemas/letta_message_content.py +28 -0
  67. letta/schemas/letta_request.py +21 -4
  68. letta/schemas/letta_stop_reason.py +9 -1
  69. letta/schemas/llm_config.py +24 -8
  70. letta/schemas/mcp.py +0 -3
  71. letta/schemas/memory.py +14 -0
  72. letta/schemas/message.py +245 -141
  73. letta/schemas/openai/chat_completion_request.py +2 -1
  74. letta/schemas/passage.py +1 -0
  75. letta/schemas/providers/bedrock.py +1 -1
  76. letta/schemas/providers/openai.py +2 -2
  77. letta/schemas/tool.py +11 -5
  78. letta/schemas/tool_execution_result.py +0 -1
  79. letta/schemas/tool_rule.py +71 -0
  80. letta/serialize_schemas/marshmallow_agent.py +1 -2
  81. letta/server/rest_api/app.py +3 -3
  82. letta/server/rest_api/auth/index.py +0 -1
  83. letta/server/rest_api/interface.py +3 -11
  84. letta/server/rest_api/redis_stream_manager.py +3 -4
  85. letta/server/rest_api/routers/v1/agents.py +143 -84
  86. letta/server/rest_api/routers/v1/blocks.py +1 -1
  87. letta/server/rest_api/routers/v1/folders.py +1 -1
  88. letta/server/rest_api/routers/v1/groups.py +23 -22
  89. letta/server/rest_api/routers/v1/internal_templates.py +68 -0
  90. letta/server/rest_api/routers/v1/sandbox_configs.py +11 -5
  91. letta/server/rest_api/routers/v1/sources.py +1 -1
  92. letta/server/rest_api/routers/v1/tools.py +167 -15
  93. letta/server/rest_api/streaming_response.py +4 -3
  94. letta/server/rest_api/utils.py +75 -18
  95. letta/server/server.py +24 -35
  96. letta/services/agent_manager.py +359 -45
  97. letta/services/agent_serialization_manager.py +23 -3
  98. letta/services/archive_manager.py +72 -3
  99. letta/services/block_manager.py +1 -2
  100. letta/services/context_window_calculator/token_counter.py +11 -6
  101. letta/services/file_manager.py +1 -3
  102. letta/services/files_agents_manager.py +2 -4
  103. letta/services/group_manager.py +73 -12
  104. letta/services/helpers/agent_manager_helper.py +5 -5
  105. letta/services/identity_manager.py +8 -3
  106. letta/services/job_manager.py +2 -14
  107. letta/services/llm_batch_manager.py +1 -3
  108. letta/services/mcp/base_client.py +1 -2
  109. letta/services/mcp_manager.py +5 -6
  110. letta/services/message_manager.py +536 -15
  111. letta/services/organization_manager.py +1 -2
  112. letta/services/passage_manager.py +287 -12
  113. letta/services/provider_manager.py +1 -3
  114. letta/services/sandbox_config_manager.py +12 -7
  115. letta/services/source_manager.py +1 -2
  116. letta/services/step_manager.py +0 -1
  117. letta/services/summarizer/summarizer.py +4 -2
  118. letta/services/telemetry_manager.py +1 -3
  119. letta/services/tool_executor/builtin_tool_executor.py +136 -316
  120. letta/services/tool_executor/core_tool_executor.py +231 -74
  121. letta/services/tool_executor/files_tool_executor.py +2 -2
  122. letta/services/tool_executor/mcp_tool_executor.py +0 -1
  123. letta/services/tool_executor/multi_agent_tool_executor.py +2 -2
  124. letta/services/tool_executor/sandbox_tool_executor.py +0 -1
  125. letta/services/tool_executor/tool_execution_sandbox.py +2 -3
  126. letta/services/tool_manager.py +181 -64
  127. letta/services/tool_sandbox/modal_deployment_manager.py +2 -2
  128. letta/services/user_manager.py +1 -2
  129. letta/settings.py +5 -3
  130. letta/streaming_interface.py +3 -3
  131. letta/system.py +1 -1
  132. letta/utils.py +0 -1
  133. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/METADATA +11 -7
  134. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/RECORD +137 -135
  135. letta/llm_api/deepseek.py +0 -303
  136. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/WHEEL +0 -0
  137. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/entry_points.txt +0 -0
  138. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,6 @@
1
- import math
2
- from typing import Any, Dict, Optional
1
+ from datetime import datetime
2
+ from typing import Any, Dict, List, Literal, Optional
3
+ from zoneinfo import ZoneInfo
3
4
 
4
5
  from letta.constants import (
5
6
  CORE_MEMORY_LINE_NUMBER_WARNING,
@@ -8,17 +9,18 @@ from letta.constants import (
8
9
  RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE,
9
10
  )
10
11
  from letta.helpers.json_helpers import json_dumps
12
+ from letta.log import get_logger
11
13
  from letta.schemas.agent import AgentState
14
+ from letta.schemas.enums import MessageRole, TagMatchMode
12
15
  from letta.schemas.sandbox_config import SandboxConfig
13
16
  from letta.schemas.tool import Tool
14
17
  from letta.schemas.tool_execution_result import ToolExecutionResult
15
18
  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
19
  from letta.services.tool_executor.tool_executor_base import ToolExecutor
20
20
  from letta.utils import get_friendly_error_msg
21
21
 
22
+ logger = get_logger(__name__)
23
+
22
24
 
23
25
  class LettaCoreToolExecutor(ToolExecutor):
24
26
  """Executor for LETTA core tools with direct implementation of functions."""
@@ -80,106 +82,262 @@ class LettaCoreToolExecutor(ToolExecutor):
80
82
  """
81
83
  return "Sent message successfully."
82
84
 
83
- async def conversation_search(self, agent_state: AgentState, actor: User, query: str, page: Optional[int] = 0) -> Optional[str]:
85
+ async def conversation_search(
86
+ self,
87
+ agent_state: AgentState,
88
+ actor: User,
89
+ query: str,
90
+ roles: Optional[List[Literal["assistant", "user", "tool"]]] = None,
91
+ limit: Optional[int] = None,
92
+ start_date: Optional[str] = None,
93
+ end_date: Optional[str] = None,
94
+ ) -> Optional[str]:
84
95
  """
85
- Search prior conversation history using case-insensitive string matching.
96
+ Search prior conversation history using hybrid search (text + semantic similarity).
86
97
 
87
98
  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).
99
+ query (str): String to search for using both text matching and semantic similarity.
100
+ roles (Optional[List[Literal["assistant", "user", "tool"]]]): Optional list of message roles to filter by.
101
+ limit (Optional[int]): Maximum number of results to return. Uses system default if not specified.
102
+ start_date (Optional[str]): Filter results to messages created after this date. ISO 8601 format: "YYYY-MM-DD" or "YYYY-MM-DDTHH:MM". Examples: "2024-01-15", "2024-01-15T14:30".
103
+ end_date (Optional[str]): Filter results to messages created before this date. ISO 8601 format: "YYYY-MM-DD" or "YYYY-MM-DDTHH:MM". Examples: "2024-01-20", "2024-01-20T17:00".
90
104
 
91
105
  Returns:
92
- str: Query result string
106
+ str: Query result string containing matching messages with timestamps and content.
93
107
  """
94
- if page is None or (isinstance(page, str) and page.lower().strip() == "none"):
95
- page = 0
96
108
  try:
97
- page = int(page)
98
- except:
99
- raise ValueError("'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
109
+ # Parse datetime parameters if provided
110
+ start_datetime = None
111
+ end_datetime = None
112
+
113
+ if start_date:
114
+ try:
115
+ # Try parsing as full datetime first (with time)
116
+ start_datetime = datetime.fromisoformat(start_date)
117
+ except ValueError:
118
+ try:
119
+ # Fall back to date-only format
120
+ start_datetime = datetime.strptime(start_date, "%Y-%m-%d")
121
+ # Set to beginning of day
122
+ start_datetime = start_datetime.replace(hour=0, minute=0, second=0, microsecond=0)
123
+ except ValueError:
124
+ raise ValueError(f"Invalid start_date format: {start_date}. Use ISO 8601 format (YYYY-MM-DD or YYYY-MM-DDTHH:MM)")
125
+
126
+ # Apply agent's timezone if datetime is naive
127
+ if start_datetime.tzinfo is None and agent_state.timezone:
128
+ tz = ZoneInfo(agent_state.timezone)
129
+ start_datetime = start_datetime.replace(tzinfo=tz)
130
+
131
+ if end_date:
132
+ try:
133
+ # Try parsing as full datetime first (with time)
134
+ end_datetime = datetime.fromisoformat(end_date)
135
+ except ValueError:
136
+ try:
137
+ # Fall back to date-only format
138
+ end_datetime = datetime.strptime(end_date, "%Y-%m-%d")
139
+ # Set to end of day for end dates
140
+ end_datetime = end_datetime.replace(hour=23, minute=59, second=59, microsecond=999999)
141
+ except ValueError:
142
+ raise ValueError(f"Invalid end_date format: {end_date}. Use ISO 8601 format (YYYY-MM-DD or YYYY-MM-DDTHH:MM)")
143
+
144
+ # Apply agent's timezone if datetime is naive
145
+ if end_datetime.tzinfo is None and agent_state.timezone:
146
+ tz = ZoneInfo(agent_state.timezone)
147
+ end_datetime = end_datetime.replace(tzinfo=tz)
148
+
149
+ # Convert string roles to MessageRole enum if provided
150
+ message_roles = None
151
+ if roles:
152
+ message_roles = [MessageRole(role) for role in roles]
153
+
154
+ # Use provided limit or default
155
+ search_limit = limit if limit is not None else RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
156
+
157
+ # Search using the message manager's search_messages_async method
158
+ message_results = await self.message_manager.search_messages_async(
159
+ agent_id=agent_state.id,
160
+ actor=actor,
161
+ query_text=query,
162
+ roles=message_roles,
163
+ limit=search_limit,
164
+ start_date=start_datetime,
165
+ end_date=end_datetime,
166
+ embedding_config=agent_state.embedding_config,
167
+ )
111
168
 
112
- if len(messages) == 0:
113
- results_str = "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)}"
169
+ if len(message_results) == 0:
170
+ results_str = "No results found."
171
+ else:
172
+ results_pref = f"Showing {len(message_results)} results:"
173
+ results_formatted = []
174
+ # get current time in UTC, then convert to agent timezone for consistent comparison
175
+ from datetime import timezone
176
+
177
+ now_utc = datetime.now(timezone.utc)
178
+ if agent_state.timezone:
179
+ try:
180
+ tz = ZoneInfo(agent_state.timezone)
181
+ now = now_utc.astimezone(tz)
182
+ except Exception:
183
+ now = now_utc
184
+ else:
185
+ now = now_utc
186
+
187
+ for message, metadata in message_results:
188
+ # Format timestamp in agent's timezone if available
189
+ timestamp = message.created_at
190
+ time_delta_str = ""
191
+
192
+ if timestamp and agent_state.timezone:
193
+ try:
194
+ # Convert to agent's timezone
195
+ tz = ZoneInfo(agent_state.timezone)
196
+ local_time = timestamp.astimezone(tz)
197
+ # Format as ISO string with timezone
198
+ formatted_timestamp = local_time.isoformat()
199
+
200
+ # Calculate time delta
201
+ delta = now - local_time
202
+ total_seconds = int(delta.total_seconds())
203
+
204
+ if total_seconds < 60:
205
+ time_delta_str = f"{total_seconds}s ago"
206
+ elif total_seconds < 3600:
207
+ minutes = total_seconds // 60
208
+ time_delta_str = f"{minutes}m ago"
209
+ elif total_seconds < 86400:
210
+ hours = total_seconds // 3600
211
+ time_delta_str = f"{hours}h ago"
212
+ else:
213
+ days = total_seconds // 86400
214
+ time_delta_str = f"{days}d ago"
215
+
216
+ except Exception:
217
+ # Fallback to ISO format if timezone conversion fails
218
+ formatted_timestamp = str(timestamp)
219
+ else:
220
+ # Use ISO format if no timezone is set
221
+ formatted_timestamp = str(timestamp) if timestamp else "Unknown"
222
+
223
+ content = self.message_manager._extract_message_text(message)
224
+
225
+ # Create the base result dict
226
+ result_dict = {
227
+ "timestamp": formatted_timestamp,
228
+ "time_ago": time_delta_str,
229
+ "role": message.role,
230
+ }
231
+
232
+ # Add search relevance metadata if available
233
+ if metadata:
234
+ # Only include non-None values
235
+ relevance_info = {
236
+ k: v
237
+ for k, v in {
238
+ "rrf_score": metadata.get("combined_score"),
239
+ "vector_rank": metadata.get("vector_rank"),
240
+ "fts_rank": metadata.get("fts_rank"),
241
+ "search_mode": metadata.get("search_mode"),
242
+ }.items()
243
+ if v is not None
244
+ }
245
+
246
+ if relevance_info: # Only add if we have metadata
247
+ result_dict["relevance"] = relevance_info
248
+
249
+ # _extract_message_text returns already JSON-encoded strings
250
+ # We need to parse them to get the actual content structure
251
+ if content:
252
+ try:
253
+ import json
254
+
255
+ parsed_content = json.loads(content)
256
+
257
+ # Add the parsed content directly to avoid double JSON encoding
258
+ if isinstance(parsed_content, dict):
259
+ # Merge the parsed content into result_dict
260
+ result_dict.update(parsed_content)
261
+ else:
262
+ # If it's not a dict, add as content
263
+ result_dict["content"] = parsed_content
264
+ except (json.JSONDecodeError, ValueError):
265
+ # if not valid JSON, add as plain content
266
+ result_dict["content"] = content
267
+
268
+ results_formatted.append(result_dict)
269
+
270
+ # Don't double-encode - results_formatted already has the parsed content
271
+ results_str = f"{results_pref} {json_dumps(results_formatted)}"
272
+
273
+ return results_str
118
274
 
119
- return results_str
275
+ except Exception as e:
276
+ raise e
120
277
 
121
278
  async def archival_memory_search(
122
- self, agent_state: AgentState, actor: User, query: str, page: Optional[int] = 0, start: Optional[int] = 0
279
+ self,
280
+ agent_state: AgentState,
281
+ actor: User,
282
+ query: str,
283
+ tags: Optional[list[str]] = None,
284
+ tag_match_mode: Literal["any", "all"] = "any",
285
+ top_k: Optional[int] = None,
286
+ start_datetime: Optional[str] = None,
287
+ end_datetime: Optional[str] = None,
123
288
  ) -> Optional[str]:
124
289
  """
125
- Search archival memory using semantic (embedding-based) search.
290
+ Search archival memory using semantic (embedding-based) search with optional temporal filtering.
126
291
 
127
292
  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.
293
+ query (str): String to search for using semantic similarity.
294
+ tags (Optional[list[str]]): Optional list of tags to filter search results. Only passages with these tags will be returned.
295
+ tag_match_mode (Literal["any", "all"]): How to match tags - "any" to match passages with any of the tags, "all" to match only passages with all tags. Defaults to "any".
296
+ top_k (Optional[int]): Maximum number of results to return. Uses system default if not specified.
297
+ start_datetime (Optional[str]): Filter results to passages created after this datetime. ISO 8601 format.
298
+ end_datetime (Optional[str]): Filter results to passages created before this datetime. ISO 8601 format.
131
299
 
132
300
  Returns:
133
- str: Query result string
301
+ str: Query result string containing matching passages with timestamps, content, and tags.
134
302
  """
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("'page' argument must be an integer")
141
-
142
- count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
143
-
144
303
  try:
145
- # Get results using passage manager
146
- all_results = await AgentManager().list_agent_passages_async(
147
- actor=actor,
304
+ # Use the shared service method to get results
305
+ formatted_results, count = await self.agent_manager.search_agent_archival_memory_async(
148
306
  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,
307
+ actor=actor,
308
+ query=query,
309
+ tags=tags,
310
+ tag_match_mode=tag_match_mode,
311
+ top_k=top_k,
312
+ start_datetime=start_datetime,
313
+ end_datetime=end_datetime,
153
314
  )
154
315
 
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)
316
+ return formatted_results, count
163
317
 
164
318
  except Exception as e:
165
319
  raise e
166
320
 
167
- async def archival_memory_insert(self, agent_state: AgentState, actor: User, content: str) -> Optional[str]:
321
+ async def archival_memory_insert(
322
+ self, agent_state: AgentState, actor: User, content: str, tags: Optional[list[str]] = None
323
+ ) -> Optional[str]:
168
324
  """
169
325
  Add to archival memory. Make sure to phrase the memory contents such that it can be easily queried later.
170
326
 
171
327
  Args:
172
328
  content (str): Content to write to the memory. All unicode (including emojis) are supported.
329
+ tags (Optional[list[str]]): Optional list of tags to associate with this memory for better organization and filtering.
173
330
 
174
331
  Returns:
175
332
  Optional[str]: None is always returned as this function does not produce a response.
176
333
  """
177
- await PassageManager().insert_passage(
334
+ await self.passage_manager.insert_passage(
178
335
  agent_state=agent_state,
179
336
  text=content,
180
337
  actor=actor,
338
+ tags=tags,
181
339
  )
182
- await AgentManager().rebuild_system_prompt_async(agent_id=agent_state.id, actor=actor, force=True)
340
+ await self.agent_manager.rebuild_system_prompt_async(agent_id=agent_state.id, actor=actor, force=True)
183
341
  return None
184
342
 
185
343
  async def core_memory_append(self, agent_state: AgentState, actor: User, label: str, content: str) -> Optional[str]:
@@ -198,7 +356,7 @@ class LettaCoreToolExecutor(ToolExecutor):
198
356
  current_value = str(agent_state.memory.get_block(label).value)
199
357
  new_value = current_value + "\n" + str(content)
200
358
  agent_state.memory.update_block_value(label=label, value=new_value)
201
- await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
359
+ await self.agent_manager.update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
202
360
  return None
203
361
 
204
362
  async def core_memory_replace(
@@ -227,7 +385,7 @@ class LettaCoreToolExecutor(ToolExecutor):
227
385
  raise ValueError(f"Old content '{old_content}' not found in memory block '{label}'")
228
386
  new_value = current_value.replace(str(old_content), str(new_content))
229
387
  agent_state.memory.update_block_value(label=label, value=new_value)
230
- await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
388
+ await self.agent_manager.update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
231
389
  return None
232
390
 
233
391
  async def memory_replace(self, agent_state: AgentState, actor: User, label: str, old_str: str, new_str: str) -> str:
@@ -275,14 +433,13 @@ class LettaCoreToolExecutor(ToolExecutor):
275
433
  occurences = current_value.count(old_str)
276
434
  if occurences == 0:
277
435
  raise ValueError(
278
- f"No replacement was performed, old_str `{old_str}` did not appear " f"verbatim in memory block with label `{label}`."
436
+ f"No replacement was performed, old_str `{old_str}` did not appear verbatim in memory block with label `{label}`."
279
437
  )
280
438
  elif occurences > 1:
281
439
  content_value_lines = current_value.split("\n")
282
440
  lines = [idx + 1 for idx, line in enumerate(content_value_lines) if old_str in line]
283
441
  raise ValueError(
284
- f"No replacement was performed. Multiple occurrences of "
285
- f"old_str `{old_str}` in lines {lines}. Please ensure it is unique."
442
+ f"No replacement was performed. Multiple occurrences of old_str `{old_str}` in lines {lines}. Please ensure it is unique."
286
443
  )
287
444
 
288
445
  # Replace old_str with new_str
@@ -291,7 +448,7 @@ class LettaCoreToolExecutor(ToolExecutor):
291
448
  # Write the new content to the block
292
449
  agent_state.memory.update_block_value(label=label, value=new_value)
293
450
 
294
- await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
451
+ await self.agent_manager.update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
295
452
 
296
453
  # Create a snippet of the edited section
297
454
  SNIPPET_LINES = 3
@@ -384,7 +541,7 @@ class LettaCoreToolExecutor(ToolExecutor):
384
541
  # Write into the block
385
542
  agent_state.memory.update_block_value(label=label, value=new_value)
386
543
 
387
- await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
544
+ await self.agent_manager.update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
388
545
 
389
546
  # Prepare the success message
390
547
  success_msg = f"The core memory block with label `{label}` has been edited. "
@@ -437,7 +594,7 @@ class LettaCoreToolExecutor(ToolExecutor):
437
594
 
438
595
  agent_state.memory.update_block_value(label=label, value=new_memory)
439
596
 
440
- await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
597
+ await self.agent_manager.update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
441
598
 
442
599
  # Prepare the success message
443
600
  success_msg = f"The core memory block with label `{label}` has been edited. "
@@ -568,7 +568,7 @@ class LettaFileToolExecutor(ToolExecutor):
568
568
  attached_sources = await self.agent_manager.list_attached_sources_async(agent_id=agent_state.id, actor=self.actor)
569
569
  source_ids = [source.id for source in attached_sources]
570
570
  if not source_ids:
571
- return f"No valid source IDs found for attached files"
571
+ return "No valid source IDs found for attached files"
572
572
 
573
573
  # Get all attached files for this agent
574
574
  file_agents = await self.files_agents_manager.list_files_for_agent(
@@ -661,7 +661,7 @@ class LettaFileToolExecutor(ToolExecutor):
661
661
  async def _search_files_traditional(self, agent_state: AgentState, query: str, limit: int) -> str:
662
662
  """Traditional search using existing passage manager."""
663
663
  # Get semantic search results
664
- passages = await self.agent_manager.list_source_passages_async(
664
+ passages = await self.agent_manager.query_source_passages_async(
665
665
  actor=self.actor,
666
666
  agent_id=agent_state.id,
667
667
  query_text=query,
@@ -25,7 +25,6 @@ class ExternalMCPToolExecutor(ToolExecutor):
25
25
  sandbox_config: Optional[SandboxConfig] = None,
26
26
  sandbox_env_vars: Optional[Dict[str, Any]] = None,
27
27
  ) -> ToolExecutionResult:
28
-
29
28
  pass
30
29
 
31
30
  mcp_server_tag = [tag for tag in tool.tags if tag.startswith(f"{MCP_TOOL_TAG_NAME_PREFIX}:")]
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import os
3
2
  from typing import Any, Dict, List, Optional
4
3
 
5
4
  from letta.log import get_logger
@@ -13,6 +12,7 @@ from letta.schemas.tool import Tool
13
12
  from letta.schemas.tool_execution_result import ToolExecutionResult
14
13
  from letta.schemas.user import User
15
14
  from letta.services.tool_executor.tool_executor_base import ToolExecutor
15
+ from letta.settings import settings
16
16
 
17
17
  logger = get_logger(__name__)
18
18
 
@@ -112,7 +112,7 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
112
112
  }
113
113
 
114
114
  async def send_message_to_agent_async(self, agent_state: AgentState, message: str, other_agent_id: str) -> str:
115
- if os.getenv("LETTA_ENVIRONMENT") == "PRODUCTION":
115
+ if settings.environment == "PRODUCTION":
116
116
  raise RuntimeError("This tool is not allowed to be run on Letta Cloud.")
117
117
 
118
118
  # 1) Build the prefixed system‐message
@@ -34,7 +34,6 @@ class SandboxToolExecutor(ToolExecutor):
34
34
  sandbox_config: Optional[SandboxConfig] = None,
35
35
  sandbox_env_vars: Optional[Dict[str, Any]] = None,
36
36
  ) -> ToolExecutionResult:
37
-
38
37
  # Store original memory state
39
38
  if agent_state:
40
39
  orig_memory_str = await agent_state.memory.compile_in_thread_async()
@@ -100,7 +100,7 @@ class ToolExecutionSandbox:
100
100
  logger.debug(f"Executed tool '{self.tool_name}', logging output from tool run: \n")
101
101
  for log_line in (result.stdout or []) + (result.stderr or []):
102
102
  logger.debug(f"{log_line}")
103
- logger.debug(f"Ending output log from tool run.")
103
+ logger.debug("Ending output log from tool run.")
104
104
 
105
105
  # Return result
106
106
  return result
@@ -267,7 +267,6 @@ class ToolExecutionSandbox:
267
267
 
268
268
  try:
269
269
  with self.temporary_env_vars(env):
270
-
271
270
  # Read and compile the Python script
272
271
  with open(temp_file_path, "r", encoding="utf-8") as f:
273
272
  source = f.read()
@@ -475,7 +474,7 @@ class ToolExecutionSandbox:
475
474
  return None, None
476
475
  result = pickle.loads(base64.b64decode(text))
477
476
  agent_state = None
478
- if not result["agent_state"] is None:
477
+ if result["agent_state"] is not None:
479
478
  agent_state = result["agent_state"]
480
479
  return result["results"], agent_state
481
480