letta-nightly 0.8.17.dev20250723104501__py3-none-any.whl → 0.9.0.dev20250724104456__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 +5 -3
- letta/agent.py +3 -2
- letta/agents/base_agent.py +4 -1
- letta/agents/voice_agent.py +1 -0
- letta/constants.py +4 -2
- letta/functions/schema_generator.py +2 -1
- letta/groups/dynamic_multi_agent.py +1 -0
- letta/helpers/converters.py +13 -5
- letta/helpers/json_helpers.py +6 -1
- letta/llm_api/anthropic.py +2 -2
- letta/llm_api/aws_bedrock.py +24 -94
- letta/llm_api/deepseek.py +1 -1
- letta/llm_api/google_ai_client.py +0 -38
- letta/llm_api/google_constants.py +6 -3
- letta/llm_api/helpers.py +1 -1
- letta/llm_api/llm_api_tools.py +4 -7
- letta/llm_api/mistral.py +12 -37
- letta/llm_api/openai.py +17 -17
- letta/llm_api/sample_response_jsons/aws_bedrock.json +38 -0
- letta/llm_api/sample_response_jsons/lmstudio_embedding_list.json +15 -0
- letta/llm_api/sample_response_jsons/lmstudio_model_list.json +15 -0
- letta/local_llm/constants.py +2 -23
- letta/local_llm/json_parser.py +11 -1
- letta/local_llm/llm_chat_completion_wrappers/airoboros.py +9 -9
- letta/local_llm/llm_chat_completion_wrappers/chatml.py +7 -8
- letta/local_llm/llm_chat_completion_wrappers/configurable_wrapper.py +6 -6
- letta/local_llm/llm_chat_completion_wrappers/dolphin.py +3 -3
- letta/local_llm/llm_chat_completion_wrappers/simple_summary_wrapper.py +1 -1
- letta/local_llm/ollama/api.py +2 -2
- letta/orm/__init__.py +1 -0
- letta/orm/agent.py +33 -2
- letta/orm/files_agents.py +13 -10
- letta/orm/mixins.py +8 -0
- letta/orm/prompt.py +13 -0
- letta/orm/sqlite_functions.py +61 -17
- letta/otel/db_pool_monitoring.py +13 -12
- letta/schemas/agent.py +69 -4
- letta/schemas/agent_file.py +2 -0
- letta/schemas/block.py +11 -0
- letta/schemas/embedding_config.py +15 -3
- letta/schemas/enums.py +2 -0
- letta/schemas/file.py +1 -1
- letta/schemas/folder.py +74 -0
- letta/schemas/memory.py +12 -6
- letta/schemas/prompt.py +9 -0
- letta/schemas/providers/__init__.py +47 -0
- letta/schemas/providers/anthropic.py +78 -0
- letta/schemas/providers/azure.py +80 -0
- letta/schemas/providers/base.py +201 -0
- letta/schemas/providers/bedrock.py +78 -0
- letta/schemas/providers/cerebras.py +79 -0
- letta/schemas/providers/cohere.py +18 -0
- letta/schemas/providers/deepseek.py +63 -0
- letta/schemas/providers/google_gemini.py +102 -0
- letta/schemas/providers/google_vertex.py +54 -0
- letta/schemas/providers/groq.py +35 -0
- letta/schemas/providers/letta.py +39 -0
- letta/schemas/providers/lmstudio.py +97 -0
- letta/schemas/providers/mistral.py +41 -0
- letta/schemas/providers/ollama.py +151 -0
- letta/schemas/providers/openai.py +241 -0
- letta/schemas/providers/together.py +85 -0
- letta/schemas/providers/vllm.py +57 -0
- letta/schemas/providers/xai.py +66 -0
- letta/server/db.py +0 -5
- letta/server/rest_api/app.py +4 -3
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +152 -4
- letta/server/rest_api/routers/v1/folders.py +490 -0
- letta/server/rest_api/routers/v1/providers.py +2 -2
- letta/server/rest_api/routers/v1/sources.py +21 -26
- letta/server/rest_api/routers/v1/tools.py +90 -15
- letta/server/server.py +50 -95
- letta/services/agent_manager.py +420 -81
- letta/services/agent_serialization_manager.py +707 -0
- letta/services/block_manager.py +132 -11
- letta/services/file_manager.py +104 -29
- letta/services/file_processor/embedder/pinecone_embedder.py +8 -2
- letta/services/file_processor/file_processor.py +75 -24
- letta/services/file_processor/parser/markitdown_parser.py +95 -0
- letta/services/files_agents_manager.py +57 -17
- letta/services/group_manager.py +7 -0
- letta/services/helpers/agent_manager_helper.py +25 -15
- letta/services/provider_manager.py +2 -2
- letta/services/source_manager.py +35 -16
- letta/services/tool_executor/files_tool_executor.py +12 -5
- letta/services/tool_manager.py +12 -0
- letta/services/tool_sandbox/e2b_sandbox.py +52 -48
- letta/settings.py +9 -6
- letta/streaming_utils.py +2 -1
- letta/utils.py +34 -1
- {letta_nightly-0.8.17.dev20250723104501.dist-info → letta_nightly-0.9.0.dev20250724104456.dist-info}/METADATA +9 -8
- {letta_nightly-0.8.17.dev20250723104501.dist-info → letta_nightly-0.9.0.dev20250724104456.dist-info}/RECORD +96 -68
- {letta_nightly-0.8.17.dev20250723104501.dist-info → letta_nightly-0.9.0.dev20250724104456.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.17.dev20250723104501.dist-info → letta_nightly-0.9.0.dev20250724104456.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.17.dev20250723104501.dist-info → letta_nightly-0.9.0.dev20250724104456.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
import tempfile
|
4
|
+
|
5
|
+
from markitdown import MarkItDown
|
6
|
+
from mistralai import OCRPageObject, OCRResponse, OCRUsageInfo
|
7
|
+
|
8
|
+
from letta.log import get_logger
|
9
|
+
from letta.otel.tracing import trace_method
|
10
|
+
from letta.services.file_processor.file_types import is_simple_text_mime_type
|
11
|
+
from letta.services.file_processor.parser.base_parser import FileParser
|
12
|
+
|
13
|
+
logger = get_logger(__name__)
|
14
|
+
|
15
|
+
# Suppress pdfminer warnings that occur during PDF processing
|
16
|
+
logging.getLogger("pdfminer.pdffont").setLevel(logging.ERROR)
|
17
|
+
logging.getLogger("pdfminer.pdfinterp").setLevel(logging.ERROR)
|
18
|
+
logging.getLogger("pdfminer.pdfpage").setLevel(logging.ERROR)
|
19
|
+
logging.getLogger("pdfminer.converter").setLevel(logging.ERROR)
|
20
|
+
|
21
|
+
|
22
|
+
class MarkitdownFileParser(FileParser):
|
23
|
+
"""Markitdown-based file parsing for documents"""
|
24
|
+
|
25
|
+
def __init__(self, model: str = "markitdown"):
|
26
|
+
self.model = model
|
27
|
+
|
28
|
+
@trace_method
|
29
|
+
async def extract_text(self, content: bytes, mime_type: str) -> OCRResponse:
|
30
|
+
"""Extract text using markitdown."""
|
31
|
+
try:
|
32
|
+
# Handle simple text files directly
|
33
|
+
if is_simple_text_mime_type(mime_type):
|
34
|
+
logger.info(f"Extracting text directly (no processing needed): {self.model}")
|
35
|
+
text = content.decode("utf-8", errors="replace")
|
36
|
+
return OCRResponse(
|
37
|
+
model=self.model,
|
38
|
+
pages=[
|
39
|
+
OCRPageObject(
|
40
|
+
index=0,
|
41
|
+
markdown=text,
|
42
|
+
images=[],
|
43
|
+
dimensions=None,
|
44
|
+
)
|
45
|
+
],
|
46
|
+
usage_info=OCRUsageInfo(pages_processed=1),
|
47
|
+
document_annotation=None,
|
48
|
+
)
|
49
|
+
|
50
|
+
logger.info(f"Extracting text using markitdown: {self.model}")
|
51
|
+
|
52
|
+
# Create temporary file to pass to markitdown
|
53
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=self._get_file_extension(mime_type)) as temp_file:
|
54
|
+
temp_file.write(content)
|
55
|
+
temp_file_path = temp_file.name
|
56
|
+
|
57
|
+
try:
|
58
|
+
md = MarkItDown(enable_plugins=False)
|
59
|
+
result = md.convert(temp_file_path)
|
60
|
+
|
61
|
+
return OCRResponse(
|
62
|
+
model=self.model,
|
63
|
+
pages=[
|
64
|
+
OCRPageObject(
|
65
|
+
index=0,
|
66
|
+
markdown=result.text_content,
|
67
|
+
images=[],
|
68
|
+
dimensions=None,
|
69
|
+
)
|
70
|
+
],
|
71
|
+
usage_info=OCRUsageInfo(pages_processed=1),
|
72
|
+
document_annotation=None,
|
73
|
+
)
|
74
|
+
finally:
|
75
|
+
# Clean up temporary file
|
76
|
+
os.unlink(temp_file_path)
|
77
|
+
|
78
|
+
except Exception as e:
|
79
|
+
logger.error(f"Markitdown text extraction failed: {str(e)}")
|
80
|
+
raise
|
81
|
+
|
82
|
+
def _get_file_extension(self, mime_type: str) -> str:
|
83
|
+
"""Get file extension based on MIME type for markitdown processing."""
|
84
|
+
mime_to_ext = {
|
85
|
+
"application/pdf": ".pdf",
|
86
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
87
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
88
|
+
"application/vnd.ms-excel": ".xls",
|
89
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
90
|
+
"text/csv": ".csv",
|
91
|
+
"application/json": ".json",
|
92
|
+
"text/xml": ".xml",
|
93
|
+
"application/xml": ".xml",
|
94
|
+
}
|
95
|
+
return mime_to_ext.get(mime_type, ".txt")
|
@@ -1,14 +1,14 @@
|
|
1
1
|
from datetime import datetime, timezone
|
2
|
-
from typing import List, Optional
|
2
|
+
from typing import List, Optional, Union
|
3
3
|
|
4
|
-
from sqlalchemy import and_, func, select, update
|
4
|
+
from sqlalchemy import and_, delete, func, or_, select, update
|
5
5
|
|
6
|
-
from letta.constants import MAX_FILES_OPEN
|
7
6
|
from letta.log import get_logger
|
8
7
|
from letta.orm.errors import NoResultFound
|
9
8
|
from letta.orm.files_agents import FileAgent as FileAgentModel
|
10
9
|
from letta.otel.tracing import trace_method
|
11
10
|
from letta.schemas.block import Block as PydanticBlock
|
11
|
+
from letta.schemas.block import FileBlock as PydanticFileBlock
|
12
12
|
from letta.schemas.file import FileAgent as PydanticFileAgent
|
13
13
|
from letta.schemas.file import FileMetadata
|
14
14
|
from letta.schemas.user import User as PydanticUser
|
@@ -31,6 +31,7 @@ class FileAgentManager:
|
|
31
31
|
file_name: str,
|
32
32
|
source_id: str,
|
33
33
|
actor: PydanticUser,
|
34
|
+
max_files_open: int,
|
34
35
|
is_open: bool = True,
|
35
36
|
visible_content: Optional[str] = None,
|
36
37
|
) -> tuple[PydanticFileAgent, List[str]]:
|
@@ -40,7 +41,7 @@ class FileAgentManager:
|
|
40
41
|
• If the row already exists → update `is_open`, `visible_content`
|
41
42
|
and always refresh `last_accessed_at`.
|
42
43
|
• Otherwise create a brand-new association.
|
43
|
-
• If is_open=True, enforces
|
44
|
+
• If is_open=True, enforces max_files_open using LRU eviction.
|
44
45
|
|
45
46
|
Returns:
|
46
47
|
Tuple of (file_agent, closed_file_names)
|
@@ -54,6 +55,7 @@ class FileAgentManager:
|
|
54
55
|
source_id=source_id,
|
55
56
|
actor=actor,
|
56
57
|
visible_content=visible_content or "",
|
58
|
+
max_files_open=max_files_open,
|
57
59
|
)
|
58
60
|
|
59
61
|
# Get the updated file agent to return
|
@@ -160,6 +162,36 @@ class FileAgentManager:
|
|
160
162
|
assoc = await self._get_association_by_file_id(session, agent_id, file_id, actor)
|
161
163
|
await assoc.hard_delete_async(session, actor=actor)
|
162
164
|
|
165
|
+
@enforce_types
|
166
|
+
@trace_method
|
167
|
+
async def detach_file_bulk(self, *, agent_file_pairs: List, actor: PydanticUser) -> int: # List of (agent_id, file_id) tuples
|
168
|
+
"""
|
169
|
+
Bulk delete multiple agent-file associations in a single query.
|
170
|
+
|
171
|
+
Args:
|
172
|
+
agent_file_pairs: List of (agent_id, file_id) tuples to delete
|
173
|
+
actor: User performing the action
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
Number of rows deleted
|
177
|
+
"""
|
178
|
+
if not agent_file_pairs:
|
179
|
+
return 0
|
180
|
+
|
181
|
+
async with db_registry.async_session() as session:
|
182
|
+
# Build compound OR conditions for each agent-file pair
|
183
|
+
conditions = []
|
184
|
+
for agent_id, file_id in agent_file_pairs:
|
185
|
+
conditions.append(and_(FileAgentModel.agent_id == agent_id, FileAgentModel.file_id == file_id))
|
186
|
+
|
187
|
+
# Create delete statement with all conditions
|
188
|
+
stmt = delete(FileAgentModel).where(and_(or_(*conditions), FileAgentModel.organization_id == actor.organization_id))
|
189
|
+
|
190
|
+
result = await session.execute(stmt)
|
191
|
+
await session.commit()
|
192
|
+
|
193
|
+
return result.rowcount
|
194
|
+
|
163
195
|
@enforce_types
|
164
196
|
@trace_method
|
165
197
|
async def get_file_agent_by_id(self, *, agent_id: str, file_id: str, actor: PydanticUser) -> Optional[PydanticFileAgent]:
|
@@ -177,6 +209,7 @@ class FileAgentManager:
|
|
177
209
|
*,
|
178
210
|
file_names: List[str],
|
179
211
|
agent_id: str,
|
212
|
+
per_file_view_window_char_limit: int,
|
180
213
|
actor: PydanticUser,
|
181
214
|
) -> List[PydanticBlock]:
|
182
215
|
"""
|
@@ -185,6 +218,7 @@ class FileAgentManager:
|
|
185
218
|
Args:
|
186
219
|
file_names: List of file names to retrieve
|
187
220
|
agent_id: ID of the agent to retrieve file blocks for
|
221
|
+
per_file_view_window_char_limit: The per-file view window char limit
|
188
222
|
actor: The user making the request
|
189
223
|
|
190
224
|
Returns:
|
@@ -207,7 +241,7 @@ class FileAgentManager:
|
|
207
241
|
rows = (await session.execute(query)).scalars().all()
|
208
242
|
|
209
243
|
# Convert to Pydantic models
|
210
|
-
return [row.to_pydantic_block() for row in rows]
|
244
|
+
return [row.to_pydantic_block(per_file_view_window_char_limit=per_file_view_window_char_limit) for row in rows]
|
211
245
|
|
212
246
|
@enforce_types
|
213
247
|
@trace_method
|
@@ -222,8 +256,13 @@ class FileAgentManager:
|
|
222
256
|
@enforce_types
|
223
257
|
@trace_method
|
224
258
|
async def list_files_for_agent(
|
225
|
-
self,
|
226
|
-
|
259
|
+
self,
|
260
|
+
agent_id: str,
|
261
|
+
per_file_view_window_char_limit: int,
|
262
|
+
actor: PydanticUser,
|
263
|
+
is_open_only: bool = False,
|
264
|
+
return_as_blocks: bool = False,
|
265
|
+
) -> Union[List[PydanticFileAgent], List[PydanticFileBlock]]:
|
227
266
|
"""Return associations for *agent_id* (filtering by `is_open` if asked)."""
|
228
267
|
async with db_registry.async_session() as session:
|
229
268
|
conditions = [
|
@@ -236,7 +275,7 @@ class FileAgentManager:
|
|
236
275
|
rows = (await session.execute(select(FileAgentModel).where(and_(*conditions)))).scalars().all()
|
237
276
|
|
238
277
|
if return_as_blocks:
|
239
|
-
return [r.to_pydantic_block() for r in rows]
|
278
|
+
return [r.to_pydantic_block(per_file_view_window_char_limit=per_file_view_window_char_limit) for r in rows]
|
240
279
|
else:
|
241
280
|
return [r.to_pydantic() for r in rows]
|
242
281
|
|
@@ -334,7 +373,7 @@ class FileAgentManager:
|
|
334
373
|
@enforce_types
|
335
374
|
@trace_method
|
336
375
|
async def enforce_max_open_files_and_open(
|
337
|
-
self, *, agent_id: str, file_id: str, file_name: str, source_id: str, actor: PydanticUser, visible_content: str
|
376
|
+
self, *, agent_id: str, file_id: str, file_name: str, source_id: str, actor: PydanticUser, visible_content: str, max_files_open: int
|
338
377
|
) -> tuple[List[str], bool]:
|
339
378
|
"""
|
340
379
|
Efficiently handle LRU eviction and file opening in a single transaction.
|
@@ -343,7 +382,7 @@ class FileAgentManager:
|
|
343
382
|
agent_id: ID of the agent
|
344
383
|
file_id: ID of the file to open
|
345
384
|
file_name: Name of the file to open
|
346
|
-
source_id: ID of the source
|
385
|
+
source_id: ID of the source
|
347
386
|
actor: User performing the action
|
348
387
|
visible_content: Content to set for the opened file
|
349
388
|
|
@@ -386,7 +425,7 @@ class FileAgentManager:
|
|
386
425
|
|
387
426
|
# Calculate how many files need to be closed
|
388
427
|
current_other_count = len(other_open_files)
|
389
|
-
target_other_count =
|
428
|
+
target_other_count = max_files_open - 1 # Reserve 1 slot for file we're opening
|
390
429
|
|
391
430
|
closed_file_names = []
|
392
431
|
if current_other_count > target_other_count:
|
@@ -443,6 +482,7 @@ class FileAgentManager:
|
|
443
482
|
*,
|
444
483
|
agent_id: str,
|
445
484
|
files_metadata: list[FileMetadata],
|
485
|
+
max_files_open: int,
|
446
486
|
visible_content_map: Optional[dict[str, str]] = None,
|
447
487
|
actor: PydanticUser,
|
448
488
|
) -> list[str]:
|
@@ -496,17 +536,17 @@ class FileAgentManager:
|
|
496
536
|
still_open_names = [r.file_name for r in currently_open if r.file_name not in new_names_set]
|
497
537
|
|
498
538
|
# decide final open set
|
499
|
-
if len(new_names) >=
|
500
|
-
final_open = new_names[:
|
539
|
+
if len(new_names) >= max_files_open:
|
540
|
+
final_open = new_names[:max_files_open]
|
501
541
|
else:
|
502
|
-
room_for_old =
|
542
|
+
room_for_old = max_files_open - len(new_names)
|
503
543
|
final_open = new_names + still_open_names[-room_for_old:]
|
504
544
|
final_open_set = set(final_open)
|
505
545
|
|
506
546
|
closed_file_names = [r.file_name for r in currently_open if r.file_name not in final_open_set]
|
507
|
-
# Add new files that won't be opened due to
|
508
|
-
if len(new_names) >=
|
509
|
-
closed_file_names.extend(new_names[
|
547
|
+
# Add new files that won't be opened due to max_files_open limit
|
548
|
+
if len(new_names) >= max_files_open:
|
549
|
+
closed_file_names.extend(new_names[max_files_open:])
|
510
550
|
evicted_ids = [r.file_id for r in currently_open if r.file_name in closed_file_names]
|
511
551
|
|
512
552
|
# upsert requested files
|
letta/services/group_manager.py
CHANGED
@@ -220,6 +220,13 @@ class GroupManager:
|
|
220
220
|
group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
|
221
221
|
group.hard_delete(session)
|
222
222
|
|
223
|
+
@enforce_types
|
224
|
+
@trace_method
|
225
|
+
async def delete_group_async(self, group_id: str, actor: PydanticUser) -> None:
|
226
|
+
async with db_registry.async_session() as session:
|
227
|
+
group = await GroupModel.read_async(db_session=session, identifier=group_id, actor=actor)
|
228
|
+
await group.hard_delete_async(session)
|
229
|
+
|
223
230
|
@enforce_types
|
224
231
|
@trace_method
|
225
232
|
def list_group_messages(
|
@@ -22,11 +22,12 @@ from letta.constants import (
|
|
22
22
|
from letta.embeddings import embedding_model
|
23
23
|
from letta.helpers import ToolRulesSolver
|
24
24
|
from letta.helpers.datetime_helpers import format_datetime, get_local_time, get_local_time_fast
|
25
|
-
from letta.orm import AgentPassage, SourcePassage, SourcesAgents
|
26
25
|
from letta.orm.agent import Agent as AgentModel
|
27
26
|
from letta.orm.agents_tags import AgentsTags
|
28
27
|
from letta.orm.errors import NoResultFound
|
29
28
|
from letta.orm.identity import Identity
|
29
|
+
from letta.orm.passage import AgentPassage, SourcePassage
|
30
|
+
from letta.orm.sources_agents import SourcesAgents
|
30
31
|
from letta.orm.sqlite_functions import adapt_array
|
31
32
|
from letta.otel.tracing import trace_method
|
32
33
|
from letta.prompts import gpt_system
|
@@ -45,7 +46,7 @@ from letta.system import get_initial_boot_messages, get_login_event, package_fun
|
|
45
46
|
# Static methods
|
46
47
|
@trace_method
|
47
48
|
def _process_relationship(
|
48
|
-
session, agent: AgentModel, relationship_name: str, model_class, item_ids: List[str], allow_partial=False, replace=True
|
49
|
+
session, agent: "AgentModel", relationship_name: str, model_class, item_ids: List[str], allow_partial=False, replace=True
|
49
50
|
):
|
50
51
|
"""
|
51
52
|
Generalized function to handle relationships like tools, sources, and blocks using item IDs.
|
@@ -88,7 +89,7 @@ def _process_relationship(
|
|
88
89
|
|
89
90
|
@trace_method
|
90
91
|
async def _process_relationship_async(
|
91
|
-
session, agent: AgentModel, relationship_name: str, model_class, item_ids: List[str], allow_partial=False, replace=True
|
92
|
+
session, agent: "AgentModel", relationship_name: str, model_class, item_ids: List[str], allow_partial=False, replace=True
|
92
93
|
):
|
93
94
|
"""
|
94
95
|
Generalized function to handle relationships like tools, sources, and blocks using item IDs.
|
@@ -130,7 +131,7 @@ async def _process_relationship_async(
|
|
130
131
|
current_relationship.extend(new_items)
|
131
132
|
|
132
133
|
|
133
|
-
def _process_tags(agent: AgentModel, tags: List[str], replace=True):
|
134
|
+
def _process_tags(agent: "AgentModel", tags: List[str], replace=True):
|
134
135
|
"""
|
135
136
|
Handles tags for an agent.
|
136
137
|
|
@@ -207,16 +208,21 @@ def compile_memory_metadata_block(
|
|
207
208
|
timestamp_str = format_datetime(memory_edit_timestamp, timezone)
|
208
209
|
|
209
210
|
# Create a metadata block of info so the agent knows about the metadata of out-of-context memories
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
211
|
+
metadata_lines = [
|
212
|
+
"<memory_metadata>",
|
213
|
+
f"- The current time is: {get_local_time_fast(timezone)}",
|
214
|
+
f"- Memory blocks were last modified: {timestamp_str}",
|
215
|
+
f"- {previous_message_count} previous messages between you and the user are stored in recall memory (use tools to access them)",
|
216
|
+
]
|
217
|
+
|
218
|
+
# Only include archival memory line if there are archival memories
|
219
|
+
if archival_memory_size > 0:
|
220
|
+
metadata_lines.append(
|
221
|
+
f"- {archival_memory_size} total memories you created are stored in archival memory (use tools to access them)"
|
222
|
+
)
|
223
|
+
|
224
|
+
metadata_lines.append("</memory_metadata>")
|
225
|
+
memory_metadata_block = "\n".join(metadata_lines)
|
220
226
|
return memory_metadata_block
|
221
227
|
|
222
228
|
|
@@ -253,6 +259,7 @@ def compile_system_message(
|
|
253
259
|
archival_memory_size: int = 0,
|
254
260
|
tool_rules_solver: Optional[ToolRulesSolver] = None,
|
255
261
|
sources: Optional[List] = None,
|
262
|
+
max_files_open: Optional[int] = None,
|
256
263
|
) -> str:
|
257
264
|
"""Prepare the final/full system message that will be fed into the LLM API
|
258
265
|
|
@@ -285,7 +292,9 @@ def compile_system_message(
|
|
285
292
|
timezone=timezone,
|
286
293
|
)
|
287
294
|
|
288
|
-
memory_with_sources = in_context_memory.compile(
|
295
|
+
memory_with_sources = in_context_memory.compile(
|
296
|
+
tool_usage_rules=tool_constraint_block, sources=sources, max_files_open=max_files_open
|
297
|
+
)
|
289
298
|
full_memory_string = memory_with_sources + "\n\n" + memory_metadata_string
|
290
299
|
|
291
300
|
# Add to the variables list to inject
|
@@ -337,6 +346,7 @@ def initialize_message_sequence(
|
|
337
346
|
previous_message_count=previous_message_count,
|
338
347
|
archival_memory_size=archival_memory_size,
|
339
348
|
sources=agent_state.sources,
|
349
|
+
max_files_open=agent_state.max_files_open,
|
340
350
|
)
|
341
351
|
first_user_message = get_login_event(agent_state.timezone) # event letting Letta know the user just logged in
|
342
352
|
|
@@ -207,7 +207,7 @@ class ProviderManager:
|
|
207
207
|
|
208
208
|
@enforce_types
|
209
209
|
@trace_method
|
210
|
-
def check_provider_api_key(self, provider_check: ProviderCheck) -> None:
|
210
|
+
async def check_provider_api_key(self, provider_check: ProviderCheck) -> None:
|
211
211
|
provider = PydanticProvider(
|
212
212
|
name=provider_check.provider_type.value,
|
213
213
|
provider_type=provider_check.provider_type,
|
@@ -221,4 +221,4 @@ class ProviderManager:
|
|
221
221
|
if not provider.api_key:
|
222
222
|
raise ValueError("API key is required")
|
223
223
|
|
224
|
-
provider.check_api_key()
|
224
|
+
await provider.check_api_key()
|
letta/services/source_manager.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import asyncio
|
2
|
-
from typing import List, Optional
|
2
|
+
from typing import List, Optional, Union
|
3
3
|
|
4
4
|
from sqlalchemy import and_, exists, select
|
5
5
|
|
@@ -234,37 +234,56 @@ class SourceManager:
|
|
234
234
|
|
235
235
|
@enforce_types
|
236
236
|
@trace_method
|
237
|
-
async def list_attached_agents(
|
237
|
+
async def list_attached_agents(
|
238
|
+
self, source_id: str, actor: PydanticUser, ids_only: bool = False
|
239
|
+
) -> Union[List[PydanticAgentState], List[str]]:
|
238
240
|
"""
|
239
241
|
Lists all agents that have the specified source attached.
|
240
242
|
|
241
243
|
Args:
|
242
244
|
source_id: ID of the source to find attached agents for
|
243
245
|
actor: User performing the action
|
246
|
+
ids_only: If True, return only agent IDs instead of full agent states
|
244
247
|
|
245
248
|
Returns:
|
246
|
-
List[PydanticAgentState]: List of agents that have this source attached
|
249
|
+
List[PydanticAgentState] | List[str]: List of agents or agent IDs that have this source attached
|
247
250
|
"""
|
248
251
|
async with db_registry.async_session() as session:
|
249
252
|
# Verify source exists and user has permission to access it
|
250
253
|
await self._validate_source_exists_async(session, source_id, actor)
|
251
254
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
255
|
+
if ids_only:
|
256
|
+
# Query only agent IDs for performance
|
257
|
+
query = (
|
258
|
+
select(AgentModel.id)
|
259
|
+
.join(SourcesAgents, AgentModel.id == SourcesAgents.agent_id)
|
260
|
+
.where(
|
261
|
+
SourcesAgents.source_id == source_id,
|
262
|
+
AgentModel.organization_id == actor.organization_id,
|
263
|
+
AgentModel.is_deleted == False,
|
264
|
+
)
|
265
|
+
.order_by(AgentModel.created_at.desc(), AgentModel.id)
|
260
266
|
)
|
261
|
-
.order_by(AgentModel.created_at.desc(), AgentModel.id)
|
262
|
-
)
|
263
267
|
|
264
|
-
|
265
|
-
|
268
|
+
result = await session.execute(query)
|
269
|
+
return list(result.scalars().all())
|
270
|
+
else:
|
271
|
+
# Use junction table query instead of relationship to avoid performance issues
|
272
|
+
query = (
|
273
|
+
select(AgentModel)
|
274
|
+
.join(SourcesAgents, AgentModel.id == SourcesAgents.agent_id)
|
275
|
+
.where(
|
276
|
+
SourcesAgents.source_id == source_id,
|
277
|
+
AgentModel.organization_id == actor.organization_id,
|
278
|
+
AgentModel.is_deleted == False,
|
279
|
+
)
|
280
|
+
.order_by(AgentModel.created_at.desc(), AgentModel.id)
|
281
|
+
)
|
282
|
+
|
283
|
+
result = await session.execute(query)
|
284
|
+
agents_orm = result.scalars().all()
|
266
285
|
|
267
|
-
|
286
|
+
return await asyncio.gather(*[agent.to_pydantic_async() for agent in agents_orm])
|
268
287
|
|
269
288
|
@enforce_types
|
270
289
|
@trace_method
|
@@ -2,7 +2,7 @@ import asyncio
|
|
2
2
|
import re
|
3
3
|
from typing import Any, Dict, List, Optional
|
4
4
|
|
5
|
-
from letta.constants import
|
5
|
+
from letta.constants import PINECONE_TEXT_FIELD_NAME
|
6
6
|
from letta.functions.types import FileOpenRequest
|
7
7
|
from letta.helpers.pinecone_utils import search_pinecone_index, should_use_pinecone
|
8
8
|
from letta.log import get_logger
|
@@ -117,8 +117,10 @@ class LettaFileToolExecutor(ToolExecutor):
|
|
117
117
|
file_requests = parsed_requests
|
118
118
|
|
119
119
|
# Validate file count first
|
120
|
-
if len(file_requests) >
|
121
|
-
raise ValueError(
|
120
|
+
if len(file_requests) > agent_state.max_files_open:
|
121
|
+
raise ValueError(
|
122
|
+
f"Cannot open {len(file_requests)} files: exceeds configured maximum limit of {agent_state.max_files_open} files"
|
123
|
+
)
|
122
124
|
|
123
125
|
if not file_requests:
|
124
126
|
raise ValueError("No file requests provided")
|
@@ -186,6 +188,7 @@ class LettaFileToolExecutor(ToolExecutor):
|
|
186
188
|
source_id=file.source_id,
|
187
189
|
actor=self.actor,
|
188
190
|
visible_content=visible_content,
|
191
|
+
max_files_open=agent_state.max_files_open,
|
189
192
|
)
|
190
193
|
|
191
194
|
opened_files.append(file_name)
|
@@ -329,7 +332,9 @@ class LettaFileToolExecutor(ToolExecutor):
|
|
329
332
|
include_regex = re.compile(include_pattern, re.IGNORECASE)
|
330
333
|
|
331
334
|
# Get all attached files for this agent
|
332
|
-
file_agents = await self.files_agents_manager.list_files_for_agent(
|
335
|
+
file_agents = await self.files_agents_manager.list_files_for_agent(
|
336
|
+
agent_id=agent_state.id, per_file_view_window_char_limit=agent_state.per_file_view_window_char_limit, actor=self.actor
|
337
|
+
)
|
333
338
|
|
334
339
|
if not file_agents:
|
335
340
|
return "No files are currently attached to search"
|
@@ -509,7 +514,9 @@ class LettaFileToolExecutor(ToolExecutor):
|
|
509
514
|
return f"No valid source IDs found for attached files"
|
510
515
|
|
511
516
|
# Get all attached files for this agent
|
512
|
-
file_agents = await self.files_agents_manager.list_files_for_agent(
|
517
|
+
file_agents = await self.files_agents_manager.list_files_for_agent(
|
518
|
+
agent_id=agent_state.id, per_file_view_window_char_limit=agent_state.per_file_view_window_char_limit, actor=self.actor
|
519
|
+
)
|
513
520
|
if not file_agents:
|
514
521
|
return "No files are currently attached to search"
|
515
522
|
|
letta/services/tool_manager.py
CHANGED
@@ -3,6 +3,8 @@ import os
|
|
3
3
|
import warnings
|
4
4
|
from typing import List, Optional, Set, Union
|
5
5
|
|
6
|
+
from sqlalchemy import func, select
|
7
|
+
|
6
8
|
from letta.constants import (
|
7
9
|
BASE_FUNCTION_RETURN_CHAR_LIMIT,
|
8
10
|
BASE_MEMORY_TOOLS,
|
@@ -290,6 +292,16 @@ class ToolManager:
|
|
290
292
|
except NoResultFound:
|
291
293
|
return None
|
292
294
|
|
295
|
+
@enforce_types
|
296
|
+
@trace_method
|
297
|
+
async def tool_exists_async(self, tool_id: str, actor: PydanticUser) -> bool:
|
298
|
+
"""Check if a tool exists and belongs to the user's organization (lightweight check)."""
|
299
|
+
async with db_registry.async_session() as session:
|
300
|
+
query = select(func.count(ToolModel.id)).where(ToolModel.id == tool_id, ToolModel.organization_id == actor.organization_id)
|
301
|
+
result = await session.execute(query)
|
302
|
+
count = result.scalar()
|
303
|
+
return count > 0
|
304
|
+
|
293
305
|
@enforce_types
|
294
306
|
@trace_method
|
295
307
|
async def list_tools_async(
|