letta-nightly 0.11.6.dev20250903104037__py3-none-any.whl → 0.11.7.dev20250904104046__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 +10 -14
- letta/agents/base_agent.py +18 -0
- letta/agents/helpers.py +32 -7
- letta/agents/letta_agent.py +953 -762
- letta/agents/voice_agent.py +1 -1
- letta/client/streaming.py +0 -1
- letta/constants.py +11 -8
- letta/errors.py +9 -0
- letta/functions/function_sets/base.py +77 -69
- letta/functions/function_sets/builtin.py +41 -22
- letta/functions/function_sets/multi_agent.py +1 -2
- letta/functions/schema_generator.py +0 -1
- letta/helpers/converters.py +8 -3
- letta/helpers/datetime_helpers.py +5 -4
- letta/helpers/message_helper.py +1 -2
- letta/helpers/pinecone_utils.py +0 -1
- letta/helpers/tool_rule_solver.py +10 -0
- letta/helpers/tpuf_client.py +848 -0
- letta/interface.py +8 -8
- letta/interfaces/anthropic_streaming_interface.py +7 -0
- letta/interfaces/openai_streaming_interface.py +29 -6
- letta/llm_api/anthropic_client.py +188 -18
- letta/llm_api/azure_client.py +0 -1
- letta/llm_api/bedrock_client.py +1 -2
- letta/llm_api/deepseek_client.py +319 -5
- letta/llm_api/google_vertex_client.py +75 -17
- letta/llm_api/groq_client.py +0 -1
- letta/llm_api/helpers.py +2 -2
- letta/llm_api/llm_api_tools.py +1 -50
- letta/llm_api/llm_client.py +6 -8
- letta/llm_api/mistral.py +1 -1
- letta/llm_api/openai.py +16 -13
- letta/llm_api/openai_client.py +31 -16
- letta/llm_api/together_client.py +0 -1
- letta/llm_api/xai_client.py +0 -1
- letta/local_llm/chat_completion_proxy.py +7 -6
- letta/local_llm/settings/settings.py +1 -1
- letta/orm/__init__.py +1 -0
- letta/orm/agent.py +8 -6
- letta/orm/archive.py +9 -1
- letta/orm/block.py +3 -4
- letta/orm/block_history.py +3 -1
- letta/orm/group.py +2 -3
- letta/orm/identity.py +1 -2
- letta/orm/job.py +1 -2
- letta/orm/llm_batch_items.py +1 -2
- letta/orm/message.py +8 -4
- letta/orm/mixins.py +18 -0
- letta/orm/organization.py +2 -0
- letta/orm/passage.py +8 -1
- letta/orm/passage_tag.py +55 -0
- letta/orm/sandbox_config.py +1 -3
- letta/orm/step.py +1 -2
- letta/orm/tool.py +1 -0
- letta/otel/resource.py +2 -2
- letta/plugins/plugins.py +1 -1
- letta/prompts/prompt_generator.py +10 -2
- letta/schemas/agent.py +11 -0
- letta/schemas/archive.py +4 -0
- letta/schemas/block.py +13 -0
- letta/schemas/embedding_config.py +0 -1
- letta/schemas/enums.py +24 -7
- letta/schemas/group.py +12 -0
- letta/schemas/letta_message.py +55 -1
- letta/schemas/letta_message_content.py +28 -0
- letta/schemas/letta_request.py +21 -4
- letta/schemas/letta_stop_reason.py +9 -1
- letta/schemas/llm_config.py +24 -8
- letta/schemas/mcp.py +0 -3
- letta/schemas/memory.py +14 -0
- letta/schemas/message.py +245 -141
- letta/schemas/openai/chat_completion_request.py +2 -1
- letta/schemas/passage.py +1 -0
- letta/schemas/providers/bedrock.py +1 -1
- letta/schemas/providers/openai.py +2 -2
- letta/schemas/tool.py +11 -5
- letta/schemas/tool_execution_result.py +0 -1
- letta/schemas/tool_rule.py +71 -0
- letta/serialize_schemas/marshmallow_agent.py +1 -2
- letta/server/rest_api/app.py +3 -3
- letta/server/rest_api/auth/index.py +0 -1
- letta/server/rest_api/interface.py +3 -11
- letta/server/rest_api/redis_stream_manager.py +3 -4
- letta/server/rest_api/routers/v1/agents.py +143 -84
- letta/server/rest_api/routers/v1/blocks.py +1 -1
- letta/server/rest_api/routers/v1/folders.py +1 -1
- letta/server/rest_api/routers/v1/groups.py +23 -22
- letta/server/rest_api/routers/v1/internal_templates.py +68 -0
- letta/server/rest_api/routers/v1/sandbox_configs.py +11 -5
- letta/server/rest_api/routers/v1/sources.py +1 -1
- letta/server/rest_api/routers/v1/tools.py +167 -15
- letta/server/rest_api/streaming_response.py +4 -3
- letta/server/rest_api/utils.py +75 -18
- letta/server/server.py +24 -35
- letta/services/agent_manager.py +359 -45
- letta/services/agent_serialization_manager.py +23 -3
- letta/services/archive_manager.py +72 -3
- letta/services/block_manager.py +1 -2
- letta/services/context_window_calculator/token_counter.py +11 -6
- letta/services/file_manager.py +1 -3
- letta/services/files_agents_manager.py +2 -4
- letta/services/group_manager.py +73 -12
- letta/services/helpers/agent_manager_helper.py +5 -5
- letta/services/identity_manager.py +8 -3
- letta/services/job_manager.py +2 -14
- letta/services/llm_batch_manager.py +1 -3
- letta/services/mcp/base_client.py +1 -2
- letta/services/mcp_manager.py +5 -6
- letta/services/message_manager.py +536 -15
- letta/services/organization_manager.py +1 -2
- letta/services/passage_manager.py +287 -12
- letta/services/provider_manager.py +1 -3
- letta/services/sandbox_config_manager.py +12 -7
- letta/services/source_manager.py +1 -2
- letta/services/step_manager.py +0 -1
- letta/services/summarizer/summarizer.py +4 -2
- letta/services/telemetry_manager.py +1 -3
- letta/services/tool_executor/builtin_tool_executor.py +136 -316
- letta/services/tool_executor/core_tool_executor.py +231 -74
- letta/services/tool_executor/files_tool_executor.py +2 -2
- letta/services/tool_executor/mcp_tool_executor.py +0 -1
- letta/services/tool_executor/multi_agent_tool_executor.py +2 -2
- letta/services/tool_executor/sandbox_tool_executor.py +0 -1
- letta/services/tool_executor/tool_execution_sandbox.py +2 -3
- letta/services/tool_manager.py +181 -64
- letta/services/tool_sandbox/modal_deployment_manager.py +2 -2
- letta/services/user_manager.py +1 -2
- letta/settings.py +5 -3
- letta/streaming_interface.py +3 -3
- letta/system.py +1 -1
- letta/utils.py +0 -1
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/METADATA +11 -7
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/RECORD +137 -135
- letta/llm_api/deepseek.py +0 -303
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,6 @@
|
|
1
|
-
import
|
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(
|
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
|
96
|
+
Search prior conversation history using hybrid search (text + semantic similarity).
|
86
97
|
|
87
98
|
Args:
|
88
|
-
query (str): String to search for.
|
89
|
-
|
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
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
275
|
+
except Exception as e:
|
276
|
+
raise e
|
120
277
|
|
121
278
|
async def archival_memory_search(
|
122
|
-
self,
|
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
|
-
|
130
|
-
|
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
|
-
#
|
146
|
-
|
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
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
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(
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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.
|
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
|
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(
|
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
|
477
|
+
if result["agent_state"] is not None:
|
479
478
|
agent_state = result["agent_state"]
|
480
479
|
return result["results"], agent_state
|
481
480
|
|