letta-nightly 0.11.3.dev20250819104229__py3-none-any.whl → 0.11.4.dev20250820213507__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/agents/helpers.py +4 -0
- letta/agents/letta_agent.py +142 -5
- letta/constants.py +10 -7
- letta/data_sources/connectors.py +70 -53
- letta/embeddings.py +3 -240
- letta/errors.py +28 -0
- letta/functions/function_sets/base.py +4 -4
- letta/functions/functions.py +287 -32
- letta/functions/mcp_client/types.py +11 -0
- letta/functions/schema_validator.py +187 -0
- letta/functions/typescript_parser.py +196 -0
- letta/helpers/datetime_helpers.py +8 -4
- letta/helpers/tool_execution_helper.py +25 -2
- letta/llm_api/anthropic_client.py +23 -18
- letta/llm_api/azure_client.py +73 -0
- letta/llm_api/bedrock_client.py +8 -4
- letta/llm_api/google_vertex_client.py +14 -5
- letta/llm_api/llm_api_tools.py +2 -217
- letta/llm_api/llm_client.py +15 -1
- letta/llm_api/llm_client_base.py +32 -1
- letta/llm_api/openai.py +1 -0
- letta/llm_api/openai_client.py +18 -28
- letta/llm_api/together_client.py +55 -0
- letta/orm/provider.py +1 -0
- letta/orm/step_metrics.py +40 -1
- letta/otel/db_pool_monitoring.py +1 -1
- letta/schemas/agent.py +3 -4
- letta/schemas/agent_file.py +2 -0
- letta/schemas/block.py +11 -5
- letta/schemas/embedding_config.py +4 -5
- letta/schemas/enums.py +1 -1
- letta/schemas/job.py +2 -3
- letta/schemas/llm_config.py +79 -7
- letta/schemas/mcp.py +0 -24
- letta/schemas/message.py +0 -108
- letta/schemas/openai/chat_completion_request.py +1 -0
- letta/schemas/providers/__init__.py +0 -2
- letta/schemas/providers/anthropic.py +106 -8
- letta/schemas/providers/azure.py +102 -8
- letta/schemas/providers/base.py +10 -3
- letta/schemas/providers/bedrock.py +28 -16
- letta/schemas/providers/letta.py +3 -3
- letta/schemas/providers/ollama.py +2 -12
- letta/schemas/providers/openai.py +4 -4
- letta/schemas/providers/together.py +14 -2
- letta/schemas/sandbox_config.py +2 -1
- letta/schemas/tool.py +46 -22
- letta/server/rest_api/routers/v1/agents.py +179 -38
- letta/server/rest_api/routers/v1/folders.py +13 -8
- letta/server/rest_api/routers/v1/providers.py +10 -3
- letta/server/rest_api/routers/v1/sources.py +14 -8
- letta/server/rest_api/routers/v1/steps.py +17 -1
- letta/server/rest_api/routers/v1/tools.py +96 -5
- letta/server/rest_api/streaming_response.py +91 -45
- letta/server/server.py +27 -38
- letta/services/agent_manager.py +92 -20
- letta/services/agent_serialization_manager.py +11 -7
- letta/services/context_window_calculator/context_window_calculator.py +40 -2
- letta/services/helpers/agent_manager_helper.py +73 -12
- letta/services/mcp_manager.py +109 -15
- letta/services/passage_manager.py +28 -109
- letta/services/provider_manager.py +24 -0
- letta/services/step_manager.py +68 -0
- letta/services/summarizer/summarizer.py +1 -4
- letta/services/tool_executor/core_tool_executor.py +1 -1
- letta/services/tool_executor/sandbox_tool_executor.py +26 -9
- letta/services/tool_manager.py +82 -5
- letta/services/tool_sandbox/base.py +3 -11
- letta/services/tool_sandbox/modal_constants.py +17 -0
- letta/services/tool_sandbox/modal_deployment_manager.py +242 -0
- letta/services/tool_sandbox/modal_sandbox.py +218 -3
- letta/services/tool_sandbox/modal_sandbox_v2.py +429 -0
- letta/services/tool_sandbox/modal_version_manager.py +273 -0
- letta/services/tool_sandbox/safe_pickle.py +193 -0
- letta/settings.py +5 -3
- letta/templates/sandbox_code_file.py.j2 +2 -4
- letta/templates/sandbox_code_file_async.py.j2 +2 -4
- letta/utils.py +1 -1
- {letta_nightly-0.11.3.dev20250819104229.dist-info → letta_nightly-0.11.4.dev20250820213507.dist-info}/METADATA +2 -2
- {letta_nightly-0.11.3.dev20250819104229.dist-info → letta_nightly-0.11.4.dev20250820213507.dist-info}/RECORD +84 -81
- letta/llm_api/anthropic.py +0 -1206
- letta/llm_api/aws_bedrock.py +0 -104
- letta/llm_api/azure_openai.py +0 -118
- letta/llm_api/azure_openai_constants.py +0 -11
- letta/llm_api/cohere.py +0 -391
- letta/schemas/providers/cohere.py +0 -18
- {letta_nightly-0.11.3.dev20250819104229.dist-info → letta_nightly-0.11.4.dev20250820213507.dist-info}/LICENSE +0 -0
- {letta_nightly-0.11.3.dev20250819104229.dist-info → letta_nightly-0.11.4.dev20250820213507.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.3.dev20250819104229.dist-info → letta_nightly-0.11.4.dev20250820213507.dist-info}/entry_points.txt +0 -0
@@ -6,6 +6,7 @@ import asyncio
|
|
6
6
|
import json
|
7
7
|
from collections.abc import AsyncIterator
|
8
8
|
|
9
|
+
import anyio
|
9
10
|
from fastapi.responses import StreamingResponse
|
10
11
|
from starlette.types import Send
|
11
12
|
|
@@ -15,6 +16,7 @@ from letta.schemas.letta_ping import LettaPing
|
|
15
16
|
from letta.schemas.user import User
|
16
17
|
from letta.server.rest_api.utils import capture_sentry_exception
|
17
18
|
from letta.services.job_manager import JobManager
|
19
|
+
from letta.settings import settings
|
18
20
|
|
19
21
|
logger = get_logger(__name__)
|
20
22
|
|
@@ -175,8 +177,24 @@ class StreamingResponseWithStatusCode(StreamingResponse):
|
|
175
177
|
|
176
178
|
body_iterator: AsyncIterator[str | bytes]
|
177
179
|
response_started: bool = False
|
180
|
+
_client_connected: bool = True
|
178
181
|
|
179
182
|
async def stream_response(self, send: Send) -> None:
|
183
|
+
if settings.use_asyncio_shield:
|
184
|
+
try:
|
185
|
+
await asyncio.shield(self._protected_stream_response(send))
|
186
|
+
except asyncio.CancelledError:
|
187
|
+
logger.info(f"Stream response was cancelled, but shielded task should continue")
|
188
|
+
except anyio.ClosedResourceError:
|
189
|
+
logger.info("Client disconnected, but shielded task should continue")
|
190
|
+
self._client_connected = False
|
191
|
+
except Exception as e:
|
192
|
+
logger.error(f"Error in protected stream response: {e}")
|
193
|
+
raise
|
194
|
+
else:
|
195
|
+
await self._protected_stream_response(send)
|
196
|
+
|
197
|
+
async def _protected_stream_response(self, send: Send) -> None:
|
180
198
|
more_body = True
|
181
199
|
try:
|
182
200
|
first_chunk = await self.body_iterator.__anext__()
|
@@ -188,21 +206,25 @@ class StreamingResponseWithStatusCode(StreamingResponse):
|
|
188
206
|
if isinstance(first_chunk_content, str):
|
189
207
|
first_chunk_content = first_chunk_content.encode(self.charset)
|
190
208
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
209
|
+
try:
|
210
|
+
await send(
|
211
|
+
{
|
212
|
+
"type": "http.response.start",
|
213
|
+
"status": self.status_code,
|
214
|
+
"headers": self.raw_headers,
|
215
|
+
}
|
216
|
+
)
|
217
|
+
self.response_started = True
|
218
|
+
await send(
|
219
|
+
{
|
220
|
+
"type": "http.response.body",
|
221
|
+
"body": first_chunk_content,
|
222
|
+
"more_body": more_body,
|
223
|
+
}
|
224
|
+
)
|
225
|
+
except anyio.ClosedResourceError:
|
226
|
+
logger.info("Client disconnected during initial response, continuing processing without sending more chunks")
|
227
|
+
self._client_connected = False
|
206
228
|
|
207
229
|
async for chunk in self.body_iterator:
|
208
230
|
if isinstance(chunk, tuple):
|
@@ -219,13 +241,21 @@ class StreamingResponseWithStatusCode(StreamingResponse):
|
|
219
241
|
if isinstance(content, str):
|
220
242
|
content = content.encode(self.charset)
|
221
243
|
more_body = True
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
244
|
+
|
245
|
+
# Only attempt to send if client is still connected
|
246
|
+
if self._client_connected:
|
247
|
+
try:
|
248
|
+
await send(
|
249
|
+
{
|
250
|
+
"type": "http.response.body",
|
251
|
+
"body": content,
|
252
|
+
"more_body": more_body,
|
253
|
+
}
|
254
|
+
)
|
255
|
+
except anyio.ClosedResourceError:
|
256
|
+
logger.info("Client disconnected, continuing processing without sending more data")
|
257
|
+
self._client_connected = False
|
258
|
+
# Continue processing but don't try to send more data
|
229
259
|
|
230
260
|
# Handle explicit job cancellations (should not throw error)
|
231
261
|
except JobCancelledException as exc:
|
@@ -243,13 +273,17 @@ class StreamingResponseWithStatusCode(StreamingResponse):
|
|
243
273
|
}
|
244
274
|
)
|
245
275
|
raise
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
276
|
+
if self._client_connected:
|
277
|
+
try:
|
278
|
+
await send(
|
279
|
+
{
|
280
|
+
"type": "http.response.body",
|
281
|
+
"body": cancellation_event,
|
282
|
+
"more_body": more_body,
|
283
|
+
}
|
284
|
+
)
|
285
|
+
except anyio.ClosedResourceError:
|
286
|
+
self._client_connected = False
|
253
287
|
return
|
254
288
|
|
255
289
|
# Handle client timeouts (should throw error to inform user)
|
@@ -268,13 +302,17 @@ class StreamingResponseWithStatusCode(StreamingResponse):
|
|
268
302
|
}
|
269
303
|
)
|
270
304
|
raise
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
305
|
+
if self._client_connected:
|
306
|
+
try:
|
307
|
+
await send(
|
308
|
+
{
|
309
|
+
"type": "http.response.body",
|
310
|
+
"body": error_event,
|
311
|
+
"more_body": more_body,
|
312
|
+
}
|
313
|
+
)
|
314
|
+
except anyio.ClosedResourceError:
|
315
|
+
self._client_connected = False
|
278
316
|
capture_sentry_exception(exc)
|
279
317
|
return
|
280
318
|
|
@@ -293,14 +331,22 @@ class StreamingResponseWithStatusCode(StreamingResponse):
|
|
293
331
|
}
|
294
332
|
)
|
295
333
|
raise
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
334
|
+
if self._client_connected:
|
335
|
+
try:
|
336
|
+
await send(
|
337
|
+
{
|
338
|
+
"type": "http.response.body",
|
339
|
+
"body": error_event,
|
340
|
+
"more_body": more_body,
|
341
|
+
}
|
342
|
+
)
|
343
|
+
except anyio.ClosedResourceError:
|
344
|
+
self._client_connected = False
|
345
|
+
|
303
346
|
capture_sentry_exception(exc)
|
304
347
|
return
|
305
|
-
if more_body:
|
306
|
-
|
348
|
+
if more_body and self._client_connected:
|
349
|
+
try:
|
350
|
+
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
351
|
+
except anyio.ClosedResourceError:
|
352
|
+
self._client_connected = False
|
letta/server/server.py
CHANGED
@@ -23,7 +23,8 @@ from letta.config import LettaConfig
|
|
23
23
|
from letta.constants import LETTA_TOOL_EXECUTION_DIR
|
24
24
|
from letta.data_sources.connectors import DataConnector, load_data
|
25
25
|
from letta.errors import HandleNotFoundError
|
26
|
-
from letta.functions.mcp_client.types import MCPServerType, MCPTool, SSEServerConfig, StdioServerConfig
|
26
|
+
from letta.functions.mcp_client.types import MCPServerType, MCPTool, MCPToolHealth, SSEServerConfig, StdioServerConfig
|
27
|
+
from letta.functions.schema_validator import validate_complete_json_schema
|
27
28
|
from letta.groups.helpers import load_multi_agent
|
28
29
|
from letta.helpers.datetime_helpers import get_utc_time
|
29
30
|
from letta.helpers.json_helpers import json_dumps, json_loads
|
@@ -40,7 +41,7 @@ from letta.schemas.block import Block, BlockUpdate, CreateBlock
|
|
40
41
|
from letta.schemas.embedding_config import EmbeddingConfig
|
41
42
|
|
42
43
|
# openai schemas
|
43
|
-
from letta.schemas.enums import JobStatus, MessageStreamStatus, ProviderCategory, ProviderType, SandboxType
|
44
|
+
from letta.schemas.enums import JobStatus, MessageStreamStatus, ProviderCategory, ProviderType, SandboxType, ToolSourceType
|
44
45
|
from letta.schemas.environment_variables import SandboxEnvironmentVariableCreate
|
45
46
|
from letta.schemas.group import GroupCreate, ManagerType, SleeptimeManager, VoiceSleeptimeManager
|
46
47
|
from letta.schemas.job import Job, JobUpdate
|
@@ -856,6 +857,9 @@ class SyncServer(Server):
|
|
856
857
|
request.llm_config = await self.get_cached_llm_config_async(actor=actor, **config_params)
|
857
858
|
log_event(name="end get_cached_llm_config", attributes=config_params)
|
858
859
|
|
860
|
+
if request.reasoning is None:
|
861
|
+
request.reasoning = request.llm_config.enable_reasoner or request.llm_config.put_inner_thoughts_in_kwargs
|
862
|
+
|
859
863
|
if request.embedding_config is None:
|
860
864
|
if request.embedding is None:
|
861
865
|
if settings.default_embedding_handle is None:
|
@@ -1099,33 +1103,6 @@ class SyncServer(Server):
|
|
1099
1103
|
def get_recall_memory_summary(self, agent_id: str, actor: User) -> RecallMemorySummary:
|
1100
1104
|
return RecallMemorySummary(size=self.message_manager.size(actor=actor, agent_id=agent_id))
|
1101
1105
|
|
1102
|
-
def get_agent_archival(
|
1103
|
-
self,
|
1104
|
-
user_id: str,
|
1105
|
-
agent_id: str,
|
1106
|
-
after: Optional[str] = None,
|
1107
|
-
before: Optional[str] = None,
|
1108
|
-
limit: Optional[int] = 100,
|
1109
|
-
order_by: Optional[str] = "created_at",
|
1110
|
-
reverse: Optional[bool] = False,
|
1111
|
-
query_text: Optional[str] = None,
|
1112
|
-
ascending: Optional[bool] = True,
|
1113
|
-
) -> List[Passage]:
|
1114
|
-
# TODO: Thread actor directly through this function, since the top level caller most likely already retrieved the user
|
1115
|
-
actor = self.user_manager.get_user_or_default(user_id=user_id)
|
1116
|
-
|
1117
|
-
# iterate over records
|
1118
|
-
records = self.agent_manager.list_passages(
|
1119
|
-
actor=actor,
|
1120
|
-
agent_id=agent_id,
|
1121
|
-
after=after,
|
1122
|
-
query_text=query_text,
|
1123
|
-
before=before,
|
1124
|
-
ascending=ascending,
|
1125
|
-
limit=limit,
|
1126
|
-
)
|
1127
|
-
return records
|
1128
|
-
|
1129
1106
|
async def get_agent_archival_async(
|
1130
1107
|
self,
|
1131
1108
|
agent_id: str,
|
@@ -1153,7 +1130,7 @@ class SyncServer(Server):
|
|
1153
1130
|
agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor)
|
1154
1131
|
|
1155
1132
|
# Insert passages into the archive
|
1156
|
-
passages = await self.passage_manager.
|
1133
|
+
passages = await self.passage_manager.insert_passage(agent_state=agent_state, text=memory_contents, actor=actor)
|
1157
1134
|
|
1158
1135
|
return passages
|
1159
1136
|
|
@@ -1471,10 +1448,6 @@ class SyncServer(Server):
|
|
1471
1448
|
passage_count, document_count = await load_data(connector, source, self.passage_manager, self.file_manager, actor=actor)
|
1472
1449
|
return passage_count, document_count
|
1473
1450
|
|
1474
|
-
def list_data_source_passages(self, user_id: str, source_id: str) -> List[Passage]:
|
1475
|
-
# TODO: move this query into PassageManager
|
1476
|
-
return self.agent_manager.list_passages(actor=self.user_manager.get_user_or_default(user_id=user_id), source_id=source_id)
|
1477
|
-
|
1478
1451
|
def list_all_sources(self, actor: User) -> List[Source]:
|
1479
1452
|
# TODO: legacy: remove
|
1480
1453
|
"""List all sources (w/ extra metadata) belonging to a user"""
|
@@ -1934,12 +1907,19 @@ class SyncServer(Server):
|
|
1934
1907
|
pip_requirements: Optional[List[PipRequirement]] = None,
|
1935
1908
|
) -> ToolReturnMessage:
|
1936
1909
|
"""Run a tool from source code"""
|
1937
|
-
|
1938
|
-
|
1910
|
+
|
1911
|
+
if tool_source_type not in (None, ToolSourceType.python, ToolSourceType.typescript):
|
1912
|
+
raise ValueError("Tool source type is not supported at this time. Found {tool_source_type}")
|
1939
1913
|
|
1940
1914
|
# If tools_json_schema is explicitly passed in, override it on the created Tool object
|
1941
1915
|
if tool_json_schema:
|
1942
|
-
tool = Tool(
|
1916
|
+
tool = Tool(
|
1917
|
+
name=tool_name,
|
1918
|
+
source_code=tool_source,
|
1919
|
+
json_schema=tool_json_schema,
|
1920
|
+
pip_requirements=pip_requirements,
|
1921
|
+
source_type=tool_source_type,
|
1922
|
+
)
|
1943
1923
|
else:
|
1944
1924
|
# NOTE: we're creating a floating Tool object and NOT persisting to DB
|
1945
1925
|
tool = Tool(
|
@@ -1947,6 +1927,7 @@ class SyncServer(Server):
|
|
1947
1927
|
source_code=tool_source,
|
1948
1928
|
args_json_schema=tool_args_json_schema,
|
1949
1929
|
pip_requirements=pip_requirements,
|
1930
|
+
source_type=tool_source_type,
|
1950
1931
|
)
|
1951
1932
|
|
1952
1933
|
assert tool.name is not None, "Failed to create tool object"
|
@@ -2086,7 +2067,15 @@ class SyncServer(Server):
|
|
2086
2067
|
if mcp_server_name not in self.mcp_clients:
|
2087
2068
|
raise ValueError(f"No client was created for MCP server: {mcp_server_name}")
|
2088
2069
|
|
2089
|
-
|
2070
|
+
tools = await self.mcp_clients[mcp_server_name].list_tools()
|
2071
|
+
|
2072
|
+
# Add health information to each tool
|
2073
|
+
for tool in tools:
|
2074
|
+
if tool.inputSchema:
|
2075
|
+
health_status, reasons = validate_complete_json_schema(tool.inputSchema)
|
2076
|
+
tool.health = MCPToolHealth(status=health_status.value, reasons=reasons)
|
2077
|
+
|
2078
|
+
return tools
|
2090
2079
|
|
2091
2080
|
async def add_mcp_server_to_config(
|
2092
2081
|
self, server_config: Union[SSEServerConfig, StdioServerConfig], allow_upsert: bool = True
|
letta/services/agent_manager.py
CHANGED
@@ -19,8 +19,9 @@ from letta.constants import (
|
|
19
19
|
DEFAULT_MAX_FILES_OPEN,
|
20
20
|
DEFAULT_TIMEZONE,
|
21
21
|
DEPRECATED_LETTA_TOOLS,
|
22
|
-
|
22
|
+
EXCLUDE_MODEL_KEYWORDS_FROM_BASE_TOOL_RULES,
|
23
23
|
FILES_TOOLS,
|
24
|
+
INCLUDE_MODEL_KEYWORDS_BASE_TOOL_RULES,
|
24
25
|
)
|
25
26
|
from letta.helpers import ToolRulesSolver
|
26
27
|
from letta.helpers.datetime_helpers import get_utc_time
|
@@ -117,6 +118,21 @@ class AgentManager:
|
|
117
118
|
self.identity_manager = IdentityManager()
|
118
119
|
self.file_agent_manager = FileAgentManager()
|
119
120
|
|
121
|
+
@staticmethod
|
122
|
+
def _should_exclude_model_from_base_tool_rules(model: str) -> bool:
|
123
|
+
"""Check if a model should be excluded from base tool rules based on model keywords."""
|
124
|
+
# First check if model contains any include keywords (overrides exclusion)
|
125
|
+
for include_keyword in INCLUDE_MODEL_KEYWORDS_BASE_TOOL_RULES:
|
126
|
+
if include_keyword in model:
|
127
|
+
return False
|
128
|
+
|
129
|
+
# Then check if model contains any exclude keywords
|
130
|
+
for exclude_keyword in EXCLUDE_MODEL_KEYWORDS_FROM_BASE_TOOL_RULES:
|
131
|
+
if exclude_keyword in model:
|
132
|
+
return True
|
133
|
+
|
134
|
+
return False
|
135
|
+
|
120
136
|
@staticmethod
|
121
137
|
def _resolve_tools(session, names: Set[str], ids: Set[str], org_id: str) -> Tuple[Dict[str, str], Dict[str, str]]:
|
122
138
|
"""
|
@@ -334,16 +350,16 @@ class AgentManager:
|
|
334
350
|
|
335
351
|
tool_rules = list(agent_create.tool_rules or [])
|
336
352
|
|
337
|
-
# Override include_base_tool_rules to False if
|
353
|
+
# Override include_base_tool_rules to False if model matches exclusion keywords and include_base_tool_rules is not explicitly set to True
|
338
354
|
if (
|
339
355
|
(
|
340
|
-
agent_create.llm_config.
|
356
|
+
self._should_exclude_model_from_base_tool_rules(agent_create.llm_config.model)
|
341
357
|
and agent_create.include_base_tool_rules is None
|
342
358
|
)
|
343
359
|
and agent_create.agent_type != AgentType.sleeptime_agent
|
344
360
|
) or agent_create.include_base_tool_rules is False:
|
345
361
|
agent_create.include_base_tool_rules = False
|
346
|
-
logger.info(f"Overriding include_base_tool_rules to False for
|
362
|
+
logger.info(f"Overriding include_base_tool_rules to False for model: {agent_create.llm_config.model}")
|
347
363
|
else:
|
348
364
|
agent_create.include_base_tool_rules = True
|
349
365
|
|
@@ -543,16 +559,16 @@ class AgentManager:
|
|
543
559
|
tool_names = set(name_to_id.keys()) # now canonical
|
544
560
|
tool_rules = list(agent_create.tool_rules or [])
|
545
561
|
|
546
|
-
# Override include_base_tool_rules to False if
|
562
|
+
# Override include_base_tool_rules to False if model matches exclusion keywords and include_base_tool_rules is not explicitly set to True
|
547
563
|
if (
|
548
564
|
(
|
549
|
-
agent_create.llm_config.
|
565
|
+
self._should_exclude_model_from_base_tool_rules(agent_create.llm_config.model)
|
550
566
|
and agent_create.include_base_tool_rules is None
|
551
567
|
)
|
552
568
|
and agent_create.agent_type != AgentType.sleeptime_agent
|
553
569
|
) or agent_create.include_base_tool_rules is False:
|
554
570
|
agent_create.include_base_tool_rules = False
|
555
|
-
logger.info(f"Overriding include_base_tool_rules to False for
|
571
|
+
logger.info(f"Overriding include_base_tool_rules to False for model: {agent_create.llm_config.model}")
|
556
572
|
else:
|
557
573
|
agent_create.include_base_tool_rules = True
|
558
574
|
|
@@ -630,6 +646,7 @@ class AgentManager:
|
|
630
646
|
[{"agent_id": aid, "identity_id": iid} for iid in identity_ids],
|
631
647
|
)
|
632
648
|
|
649
|
+
env_rows = []
|
633
650
|
if agent_create.tool_exec_environment_variables:
|
634
651
|
env_rows = [
|
635
652
|
{
|
@@ -640,7 +657,8 @@ class AgentManager:
|
|
640
657
|
}
|
641
658
|
for key, val in agent_create.tool_exec_environment_variables.items()
|
642
659
|
]
|
643
|
-
await session.execute(insert(AgentEnvironmentVariable).values(env_rows))
|
660
|
+
result = await session.execute(insert(AgentEnvironmentVariable).values(env_rows).returning(AgentEnvironmentVariable.id))
|
661
|
+
env_rows = [{**row, "id": env_var_id} for row, env_var_id in zip(env_rows, result.scalars().all())]
|
644
662
|
|
645
663
|
include_relationships = []
|
646
664
|
if tool_ids:
|
@@ -656,6 +674,9 @@ class AgentManager:
|
|
656
674
|
|
657
675
|
result = await new_agent.to_pydantic_async(include_relationships=include_relationships)
|
658
676
|
|
677
|
+
if agent_create.tool_exec_environment_variables and env_rows:
|
678
|
+
result.tool_exec_environment_variables = [AgentEnvironmentVariable(**row) for row in env_rows]
|
679
|
+
|
659
680
|
# initial message sequence (skip if _init_with_no_messages is True)
|
660
681
|
if not _init_with_no_messages:
|
661
682
|
init_messages = await self._generate_initial_message_sequence_async(
|
@@ -1986,6 +2007,26 @@ class AgentManager:
|
|
1986
2007
|
@enforce_types
|
1987
2008
|
@trace_method
|
1988
2009
|
async def refresh_file_blocks(self, agent_state: PydanticAgentState, actor: PydanticUser) -> PydanticAgentState:
|
2010
|
+
"""
|
2011
|
+
Refresh the file blocks in an agent's memory with current file content.
|
2012
|
+
|
2013
|
+
This method synchronizes the agent's in-memory file blocks with the actual
|
2014
|
+
file content from attached sources. It respects the per-file view window
|
2015
|
+
limit to prevent excessive memory usage.
|
2016
|
+
|
2017
|
+
Args:
|
2018
|
+
agent_state: The current agent state containing memory configuration
|
2019
|
+
actor: The user performing this action (for permission checking)
|
2020
|
+
|
2021
|
+
Returns:
|
2022
|
+
Updated agent state with refreshed file blocks
|
2023
|
+
|
2024
|
+
Important:
|
2025
|
+
- File blocks are truncated based on per_file_view_window_char_limit
|
2026
|
+
- None values are filtered out (files that couldn't be loaded)
|
2027
|
+
- This does NOT persist changes to the database, only updates the state object
|
2028
|
+
- Call this before agent interactions if files may have changed externally
|
2029
|
+
"""
|
1989
2030
|
file_blocks = await self.file_agent_manager.list_files_for_agent(
|
1990
2031
|
agent_id=agent_state.id,
|
1991
2032
|
per_file_view_window_char_limit=agent_state.per_file_view_window_char_limit,
|
@@ -2035,6 +2076,28 @@ class AgentManager:
|
|
2035
2076
|
@enforce_types
|
2036
2077
|
@trace_method
|
2037
2078
|
def append_system_message(self, agent_id: str, content: str, actor: PydanticUser):
|
2079
|
+
"""
|
2080
|
+
Append a system message to an agent's in-context message history.
|
2081
|
+
|
2082
|
+
This method is typically used during agent initialization to add system prompts,
|
2083
|
+
instructions, or context that should be treated as system-level guidance.
|
2084
|
+
Unlike user messages, system messages directly influence the agent's behavior
|
2085
|
+
and understanding of its role.
|
2086
|
+
|
2087
|
+
Args:
|
2088
|
+
agent_id: The ID of the agent to append the message to
|
2089
|
+
content: The system message content (e.g., instructions, context, role definition)
|
2090
|
+
actor: The user performing this action (for permission checking)
|
2091
|
+
|
2092
|
+
Side Effects:
|
2093
|
+
- Creates a new Message object in the database
|
2094
|
+
- Updates the agent's in_context_message_ids list
|
2095
|
+
- The message becomes part of the agent's permanent context window
|
2096
|
+
|
2097
|
+
Note:
|
2098
|
+
System messages consume tokens in the context window and cannot be
|
2099
|
+
removed without rebuilding the agent's message history.
|
2100
|
+
"""
|
2038
2101
|
|
2039
2102
|
# get the agent
|
2040
2103
|
agent = self.get_agent_by_id(agent_id=agent_id, actor=actor)
|
@@ -2048,6 +2111,15 @@ class AgentManager:
|
|
2048
2111
|
@enforce_types
|
2049
2112
|
@trace_method
|
2050
2113
|
async def append_system_message_async(self, agent_id: str, content: str, actor: PydanticUser):
|
2114
|
+
"""
|
2115
|
+
Async version of append_system_message.
|
2116
|
+
|
2117
|
+
Append a system message to an agent's in-context message history.
|
2118
|
+
See append_system_message for detailed documentation.
|
2119
|
+
|
2120
|
+
This async version is preferred for high-throughput scenarios or when
|
2121
|
+
called within other async operations to avoid blocking the event loop.
|
2122
|
+
"""
|
2051
2123
|
|
2052
2124
|
# get the agent
|
2053
2125
|
agent = await self.get_agent_by_id_async(agent_id=agent_id, actor=actor)
|
@@ -2354,7 +2426,7 @@ class AgentManager:
|
|
2354
2426
|
|
2355
2427
|
@enforce_types
|
2356
2428
|
@trace_method
|
2357
|
-
def list_passages(
|
2429
|
+
async def list_passages(
|
2358
2430
|
self,
|
2359
2431
|
actor: PydanticUser,
|
2360
2432
|
agent_id: Optional[str] = None,
|
@@ -2372,8 +2444,8 @@ class AgentManager:
|
|
2372
2444
|
agent_only: bool = False,
|
2373
2445
|
) -> List[PydanticPassage]:
|
2374
2446
|
"""Lists all passages attached to an agent."""
|
2375
|
-
with db_registry.
|
2376
|
-
main_query = build_passage_query(
|
2447
|
+
async with db_registry.async_session() as session:
|
2448
|
+
main_query = await build_passage_query(
|
2377
2449
|
actor=actor,
|
2378
2450
|
agent_id=agent_id,
|
2379
2451
|
file_id=file_id,
|
@@ -2394,7 +2466,7 @@ class AgentManager:
|
|
2394
2466
|
main_query = main_query.limit(limit)
|
2395
2467
|
|
2396
2468
|
# Execute query
|
2397
|
-
result = session.execute(main_query)
|
2469
|
+
result = await session.execute(main_query)
|
2398
2470
|
|
2399
2471
|
passages = []
|
2400
2472
|
for row in result:
|
@@ -2437,7 +2509,7 @@ class AgentManager:
|
|
2437
2509
|
) -> List[PydanticPassage]:
|
2438
2510
|
"""Lists all passages attached to an agent."""
|
2439
2511
|
async with db_registry.async_session() as session:
|
2440
|
-
main_query = build_passage_query(
|
2512
|
+
main_query = await build_passage_query(
|
2441
2513
|
actor=actor,
|
2442
2514
|
agent_id=agent_id,
|
2443
2515
|
file_id=file_id,
|
@@ -2500,7 +2572,7 @@ class AgentManager:
|
|
2500
2572
|
) -> List[PydanticPassage]:
|
2501
2573
|
"""Lists all passages attached to an agent."""
|
2502
2574
|
async with db_registry.async_session() as session:
|
2503
|
-
main_query = build_source_passage_query(
|
2575
|
+
main_query = await build_source_passage_query(
|
2504
2576
|
actor=actor,
|
2505
2577
|
agent_id=agent_id,
|
2506
2578
|
file_id=file_id,
|
@@ -2546,7 +2618,7 @@ class AgentManager:
|
|
2546
2618
|
) -> List[PydanticPassage]:
|
2547
2619
|
"""Lists all passages attached to an agent."""
|
2548
2620
|
async with db_registry.async_session() as session:
|
2549
|
-
main_query = build_agent_passage_query(
|
2621
|
+
main_query = await build_agent_passage_query(
|
2550
2622
|
actor=actor,
|
2551
2623
|
agent_id=agent_id,
|
2552
2624
|
query_text=query_text,
|
@@ -2574,7 +2646,7 @@ class AgentManager:
|
|
2574
2646
|
|
2575
2647
|
@enforce_types
|
2576
2648
|
@trace_method
|
2577
|
-
def passage_size(
|
2649
|
+
async def passage_size(
|
2578
2650
|
self,
|
2579
2651
|
actor: PydanticUser,
|
2580
2652
|
agent_id: Optional[str] = None,
|
@@ -2591,8 +2663,8 @@ class AgentManager:
|
|
2591
2663
|
agent_only: bool = False,
|
2592
2664
|
) -> int:
|
2593
2665
|
"""Returns the count of passages matching the given criteria."""
|
2594
|
-
with db_registry.
|
2595
|
-
main_query = build_passage_query(
|
2666
|
+
async with db_registry.async_session() as session:
|
2667
|
+
main_query = await build_passage_query(
|
2596
2668
|
actor=actor,
|
2597
2669
|
agent_id=agent_id,
|
2598
2670
|
file_id=file_id,
|
@@ -2610,7 +2682,7 @@ class AgentManager:
|
|
2610
2682
|
|
2611
2683
|
# Convert to count query
|
2612
2684
|
count_query = select(func.count()).select_from(main_query.subquery())
|
2613
|
-
return session.scalar(count_query) or 0
|
2685
|
+
return (await session.scalar(count_query)) or 0
|
2614
2686
|
|
2615
2687
|
@enforce_types
|
2616
2688
|
async def passage_size_async(
|
@@ -2630,7 +2702,7 @@ class AgentManager:
|
|
2630
2702
|
agent_only: bool = False,
|
2631
2703
|
) -> int:
|
2632
2704
|
async with db_registry.async_session() as session:
|
2633
|
-
main_query = build_passage_query(
|
2705
|
+
main_query = await build_passage_query(
|
2634
2706
|
actor=actor,
|
2635
2707
|
agent_id=agent_id,
|
2636
2708
|
file_id=file_id,
|
@@ -2,7 +2,7 @@ from datetime import datetime, timezone
|
|
2
2
|
from typing import Any, Dict, List, Optional
|
3
3
|
|
4
4
|
from letta.constants import MCP_TOOL_TAG_NAME_PREFIX
|
5
|
-
from letta.errors import
|
5
|
+
from letta.errors import AgentExportIdMappingError, AgentExportProcessingError, AgentFileImportError, AgentNotFoundForExportError
|
6
6
|
from letta.helpers.pinecone_utils import should_use_pinecone
|
7
7
|
from letta.log import get_logger
|
8
8
|
from letta.schemas.agent import AgentState, CreateAgent
|
@@ -118,10 +118,7 @@ class AgentSerializationManager:
|
|
118
118
|
return self._db_to_file_ids[db_id]
|
119
119
|
|
120
120
|
if not allow_new:
|
121
|
-
raise
|
122
|
-
f"Unexpected new {entity_type} ID '{db_id}' encountered during conversion. "
|
123
|
-
f"All IDs should have been mapped during agent processing."
|
124
|
-
)
|
121
|
+
raise AgentExportIdMappingError(db_id, entity_type)
|
125
122
|
|
126
123
|
file_id = self._generate_file_id(entity_type)
|
127
124
|
self._db_to_file_ids[db_id] = file_id
|
@@ -352,7 +349,7 @@ class AgentSerializationManager:
|
|
352
349
|
if len(agent_states) != len(agent_ids):
|
353
350
|
found_ids = {agent.id for agent in agent_states}
|
354
351
|
missing_ids = [agent_id for agent_id in agent_ids if agent_id not in found_ids]
|
355
|
-
raise
|
352
|
+
raise AgentNotFoundForExportError(missing_ids)
|
356
353
|
|
357
354
|
groups = []
|
358
355
|
group_agent_ids = []
|
@@ -417,7 +414,7 @@ class AgentSerializationManager:
|
|
417
414
|
|
418
415
|
except Exception as e:
|
419
416
|
logger.error(f"Failed to export agent file: {e}")
|
420
|
-
raise
|
417
|
+
raise AgentExportProcessingError(str(e), e) from e
|
421
418
|
|
422
419
|
async def import_file(
|
423
420
|
self,
|
@@ -657,6 +654,12 @@ class AgentSerializationManager:
|
|
657
654
|
)
|
658
655
|
imported_count += len(files_for_agent)
|
659
656
|
|
657
|
+
# Extract the imported agent IDs (database IDs)
|
658
|
+
imported_agent_ids = []
|
659
|
+
for agent_schema in schema.agents:
|
660
|
+
if agent_schema.id in file_to_db_ids:
|
661
|
+
imported_agent_ids.append(file_to_db_ids[agent_schema.id])
|
662
|
+
|
660
663
|
for group in schema.groups:
|
661
664
|
group_data = group.model_dump(exclude={"id"})
|
662
665
|
group_data["agent_ids"] = [file_to_db_ids[agent_id] for agent_id in group_data["agent_ids"]]
|
@@ -670,6 +673,7 @@ class AgentSerializationManager:
|
|
670
673
|
success=True,
|
671
674
|
message=f"Import completed successfully. Imported {imported_count} entities.",
|
672
675
|
imported_count=imported_count,
|
676
|
+
imported_agent_ids=imported_agent_ids,
|
673
677
|
id_mappings=file_to_db_ids,
|
674
678
|
)
|
675
679
|
|