letta-nightly 0.8.17.dev20250723104501__py3-none-any.whl → 0.9.0.dev20250724081419__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. letta/__init__.py +5 -3
  2. letta/agent.py +3 -2
  3. letta/agents/base_agent.py +4 -1
  4. letta/agents/voice_agent.py +1 -0
  5. letta/constants.py +4 -2
  6. letta/functions/schema_generator.py +2 -1
  7. letta/groups/dynamic_multi_agent.py +1 -0
  8. letta/helpers/converters.py +13 -5
  9. letta/helpers/json_helpers.py +6 -1
  10. letta/llm_api/anthropic.py +2 -2
  11. letta/llm_api/aws_bedrock.py +24 -94
  12. letta/llm_api/deepseek.py +1 -1
  13. letta/llm_api/google_ai_client.py +0 -38
  14. letta/llm_api/google_constants.py +6 -3
  15. letta/llm_api/helpers.py +1 -1
  16. letta/llm_api/llm_api_tools.py +4 -7
  17. letta/llm_api/mistral.py +12 -37
  18. letta/llm_api/openai.py +17 -17
  19. letta/llm_api/sample_response_jsons/aws_bedrock.json +38 -0
  20. letta/llm_api/sample_response_jsons/lmstudio_embedding_list.json +15 -0
  21. letta/llm_api/sample_response_jsons/lmstudio_model_list.json +15 -0
  22. letta/local_llm/constants.py +2 -23
  23. letta/local_llm/json_parser.py +11 -1
  24. letta/local_llm/llm_chat_completion_wrappers/airoboros.py +9 -9
  25. letta/local_llm/llm_chat_completion_wrappers/chatml.py +7 -8
  26. letta/local_llm/llm_chat_completion_wrappers/configurable_wrapper.py +6 -6
  27. letta/local_llm/llm_chat_completion_wrappers/dolphin.py +3 -3
  28. letta/local_llm/llm_chat_completion_wrappers/simple_summary_wrapper.py +1 -1
  29. letta/local_llm/ollama/api.py +2 -2
  30. letta/orm/__init__.py +1 -0
  31. letta/orm/agent.py +33 -2
  32. letta/orm/files_agents.py +13 -10
  33. letta/orm/mixins.py +8 -0
  34. letta/orm/prompt.py +13 -0
  35. letta/orm/sqlite_functions.py +61 -17
  36. letta/otel/db_pool_monitoring.py +13 -12
  37. letta/schemas/agent.py +69 -4
  38. letta/schemas/agent_file.py +2 -0
  39. letta/schemas/block.py +11 -0
  40. letta/schemas/embedding_config.py +15 -3
  41. letta/schemas/enums.py +2 -0
  42. letta/schemas/file.py +1 -1
  43. letta/schemas/folder.py +74 -0
  44. letta/schemas/memory.py +12 -6
  45. letta/schemas/prompt.py +9 -0
  46. letta/schemas/providers/__init__.py +47 -0
  47. letta/schemas/providers/anthropic.py +78 -0
  48. letta/schemas/providers/azure.py +80 -0
  49. letta/schemas/providers/base.py +201 -0
  50. letta/schemas/providers/bedrock.py +78 -0
  51. letta/schemas/providers/cerebras.py +79 -0
  52. letta/schemas/providers/cohere.py +18 -0
  53. letta/schemas/providers/deepseek.py +63 -0
  54. letta/schemas/providers/google_gemini.py +102 -0
  55. letta/schemas/providers/google_vertex.py +54 -0
  56. letta/schemas/providers/groq.py +35 -0
  57. letta/schemas/providers/letta.py +39 -0
  58. letta/schemas/providers/lmstudio.py +97 -0
  59. letta/schemas/providers/mistral.py +41 -0
  60. letta/schemas/providers/ollama.py +151 -0
  61. letta/schemas/providers/openai.py +241 -0
  62. letta/schemas/providers/together.py +85 -0
  63. letta/schemas/providers/vllm.py +57 -0
  64. letta/schemas/providers/xai.py +66 -0
  65. letta/server/db.py +0 -5
  66. letta/server/rest_api/app.py +4 -3
  67. letta/server/rest_api/routers/v1/__init__.py +2 -0
  68. letta/server/rest_api/routers/v1/agents.py +152 -4
  69. letta/server/rest_api/routers/v1/folders.py +490 -0
  70. letta/server/rest_api/routers/v1/providers.py +2 -2
  71. letta/server/rest_api/routers/v1/sources.py +21 -26
  72. letta/server/rest_api/routers/v1/tools.py +90 -15
  73. letta/server/server.py +50 -95
  74. letta/services/agent_manager.py +420 -81
  75. letta/services/agent_serialization_manager.py +707 -0
  76. letta/services/block_manager.py +132 -11
  77. letta/services/file_manager.py +104 -29
  78. letta/services/file_processor/embedder/pinecone_embedder.py +8 -2
  79. letta/services/file_processor/file_processor.py +75 -24
  80. letta/services/file_processor/parser/markitdown_parser.py +95 -0
  81. letta/services/files_agents_manager.py +57 -17
  82. letta/services/group_manager.py +7 -0
  83. letta/services/helpers/agent_manager_helper.py +25 -15
  84. letta/services/provider_manager.py +2 -2
  85. letta/services/source_manager.py +35 -16
  86. letta/services/tool_executor/files_tool_executor.py +12 -5
  87. letta/services/tool_manager.py +12 -0
  88. letta/services/tool_sandbox/e2b_sandbox.py +52 -48
  89. letta/settings.py +9 -6
  90. letta/streaming_utils.py +2 -1
  91. letta/utils.py +34 -1
  92. {letta_nightly-0.8.17.dev20250723104501.dist-info → letta_nightly-0.9.0.dev20250724081419.dist-info}/METADATA +9 -8
  93. {letta_nightly-0.8.17.dev20250723104501.dist-info → letta_nightly-0.9.0.dev20250724081419.dist-info}/RECORD +96 -68
  94. {letta_nightly-0.8.17.dev20250723104501.dist-info → letta_nightly-0.9.0.dev20250724081419.dist-info}/LICENSE +0 -0
  95. {letta_nightly-0.8.17.dev20250723104501.dist-info → letta_nightly-0.9.0.dev20250724081419.dist-info}/WHEEL +0 -0
  96. {letta_nightly-0.8.17.dev20250723104501.dist-info → letta_nightly-0.9.0.dev20250724081419.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,490 @@
1
+ import asyncio
2
+ import mimetypes
3
+ import os
4
+ import tempfile
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+
8
+ from fastapi import APIRouter, Depends, Header, HTTPException, Query, UploadFile
9
+ from starlette import status
10
+
11
+ import letta.constants as constants
12
+ from letta.helpers.pinecone_utils import (
13
+ delete_file_records_from_pinecone_index,
14
+ delete_source_records_from_pinecone_index,
15
+ should_use_pinecone,
16
+ )
17
+ from letta.log import get_logger
18
+ from letta.otel.tracing import trace_method
19
+ from letta.schemas.agent import AgentState
20
+ from letta.schemas.embedding_config import EmbeddingConfig
21
+ from letta.schemas.enums import DuplicateFileHandling, FileProcessingStatus
22
+ from letta.schemas.file import FileMetadata
23
+ from letta.schemas.folder import Folder
24
+ from letta.schemas.passage import Passage
25
+ from letta.schemas.source import Source, SourceCreate, SourceUpdate
26
+ from letta.schemas.source_metadata import OrganizationSourcesStats
27
+ from letta.schemas.user import User
28
+ from letta.server.rest_api.utils import get_letta_server
29
+ from letta.server.server import SyncServer
30
+ from letta.services.file_processor.embedder.openai_embedder import OpenAIEmbedder
31
+ from letta.services.file_processor.embedder.pinecone_embedder import PineconeEmbedder
32
+ from letta.services.file_processor.file_processor import FileProcessor
33
+ from letta.services.file_processor.file_types import get_allowed_media_types, get_extension_to_mime_type_map, register_mime_types
34
+ from letta.services.file_processor.parser.markitdown_parser import MarkitdownFileParser
35
+ from letta.services.file_processor.parser.mistral_parser import MistralFileParser
36
+ from letta.settings import settings
37
+ from letta.utils import safe_create_task, sanitize_filename
38
+
39
+ logger = get_logger(__name__)
40
+
41
+ # Register all supported file types with Python's mimetypes module
42
+ register_mime_types()
43
+
44
+
45
+ router = APIRouter(prefix="/folders", tags=["folders"])
46
+
47
+
48
+ @router.get("/count", response_model=int, operation_id="count_folders")
49
+ async def count_folders(
50
+ server: "SyncServer" = Depends(get_letta_server),
51
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
52
+ ):
53
+ """
54
+ Count all data folders created by a user.
55
+ """
56
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
57
+ return await server.source_manager.size_async(actor=actor)
58
+
59
+
60
+ @router.get("/{folder_id}", response_model=Folder, operation_id="retrieve_folder")
61
+ async def retrieve_folder(
62
+ folder_id: str,
63
+ server: "SyncServer" = Depends(get_letta_server),
64
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
65
+ ):
66
+ """
67
+ Get a folder by ID
68
+ """
69
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
70
+
71
+ folder = await server.source_manager.get_source_by_id(source_id=folder_id, actor=actor)
72
+ if not folder:
73
+ raise HTTPException(status_code=404, detail=f"Folder with id={folder_id} not found.")
74
+ return folder
75
+
76
+
77
+ @router.get("/name/{folder_name}", response_model=str, operation_id="get_folder_id_by_name")
78
+ async def get_folder_id_by_name(
79
+ folder_name: str,
80
+ server: "SyncServer" = Depends(get_letta_server),
81
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
82
+ ):
83
+ """
84
+ Get a folder by name
85
+ """
86
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
87
+
88
+ folder = await server.source_manager.get_source_by_name(source_name=folder_name, actor=actor)
89
+ if not folder:
90
+ raise HTTPException(status_code=404, detail=f"Folder with name={folder_name} not found.")
91
+ return folder.id
92
+
93
+
94
+ @router.get("/metadata", response_model=OrganizationSourcesStats, operation_id="get_folders_metadata")
95
+ async def get_folders_metadata(
96
+ server: "SyncServer" = Depends(get_letta_server),
97
+ actor_id: Optional[str] = Header(None, alias="user_id"),
98
+ include_detailed_per_source_metadata: bool = False,
99
+ ):
100
+ """
101
+ Get aggregated metadata for all folders in an organization.
102
+
103
+ Returns structured metadata including:
104
+ - Total number of folders
105
+ - Total number of files across all folders
106
+ - Total size of all files
107
+ - Per-source breakdown with file details (file_name, file_size per file) if include_detailed_per_source_metadata is True
108
+ """
109
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
110
+ return await server.file_manager.get_organization_sources_metadata(
111
+ actor=actor, include_detailed_per_source_metadata=include_detailed_per_source_metadata
112
+ )
113
+
114
+
115
+ @router.get("/", response_model=List[Folder], operation_id="list_folders")
116
+ async def list_folders(
117
+ server: "SyncServer" = Depends(get_letta_server),
118
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
119
+ ):
120
+ """
121
+ List all data folders created by a user.
122
+ """
123
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
124
+ return await server.source_manager.list_sources(actor=actor)
125
+
126
+
127
+ @router.post("/", response_model=Folder, operation_id="create_folder")
128
+ async def create_folder(
129
+ folder_create: SourceCreate,
130
+ server: "SyncServer" = Depends(get_letta_server),
131
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
132
+ ):
133
+ """
134
+ Create a new data folder.
135
+ """
136
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
137
+
138
+ # TODO: need to asyncify this
139
+ if not folder_create.embedding_config:
140
+ if not folder_create.embedding:
141
+ # TODO: modify error type
142
+ raise ValueError("Must specify either embedding or embedding_config in request")
143
+ folder_create.embedding_config = await server.get_embedding_config_from_handle_async(
144
+ handle=folder_create.embedding,
145
+ embedding_chunk_size=folder_create.embedding_chunk_size or constants.DEFAULT_EMBEDDING_CHUNK_SIZE,
146
+ actor=actor,
147
+ )
148
+ folder = Source(
149
+ name=folder_create.name,
150
+ embedding_config=folder_create.embedding_config,
151
+ description=folder_create.description,
152
+ instructions=folder_create.instructions,
153
+ metadata=folder_create.metadata,
154
+ )
155
+ return await server.source_manager.create_source(source=folder, actor=actor)
156
+
157
+
158
+ @router.patch("/{folder_id}", response_model=Folder, operation_id="modify_folder")
159
+ async def modify_folder(
160
+ folder_id: str,
161
+ folder: SourceUpdate,
162
+ server: "SyncServer" = Depends(get_letta_server),
163
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
164
+ ):
165
+ """
166
+ Update the name or documentation of an existing data folder.
167
+ """
168
+ # TODO: allow updating the handle/embedding config
169
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
170
+ if not await server.source_manager.get_source_by_id(source_id=folder_id, actor=actor):
171
+ raise HTTPException(status_code=404, detail=f"Folder with id={folder_id} does not exist.")
172
+ return await server.source_manager.update_source(source_id=folder_id, source_update=folder, actor=actor)
173
+
174
+
175
+ @router.delete("/{folder_id}", response_model=None, operation_id="delete_folder")
176
+ async def delete_folder(
177
+ folder_id: str,
178
+ server: "SyncServer" = Depends(get_letta_server),
179
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
180
+ ):
181
+ """
182
+ Delete a data folder.
183
+ """
184
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
185
+ folder = await server.source_manager.get_source_by_id(source_id=folder_id, actor=actor)
186
+ agent_states = await server.source_manager.list_attached_agents(source_id=folder_id, actor=actor)
187
+ files = await server.file_manager.list_files(folder_id, actor)
188
+ file_ids = [f.id for f in files]
189
+
190
+ if should_use_pinecone():
191
+ logger.info(f"Deleting folder {folder_id} from pinecone index")
192
+ await delete_source_records_from_pinecone_index(source_id=folder_id, actor=actor)
193
+
194
+ for agent_state in agent_states:
195
+ await server.remove_files_from_context_window(agent_state=agent_state, file_ids=file_ids, actor=actor)
196
+
197
+ if agent_state.enable_sleeptime:
198
+ try:
199
+ block = await server.agent_manager.get_block_with_label_async(agent_id=agent_state.id, block_label=folder.name, actor=actor)
200
+ await server.block_manager.delete_block_async(block.id, actor)
201
+ except:
202
+ pass
203
+ await server.delete_source(source_id=folder_id, actor=actor)
204
+
205
+
206
+ @router.post("/{folder_id}/upload", response_model=FileMetadata, operation_id="upload_file_to_folder")
207
+ async def upload_file_to_folder(
208
+ file: UploadFile,
209
+ folder_id: str,
210
+ duplicate_handling: DuplicateFileHandling = Query(DuplicateFileHandling.SUFFIX, description="How to handle duplicate filenames"),
211
+ server: "SyncServer" = Depends(get_letta_server),
212
+ actor_id: Optional[str] = Header(None, alias="user_id"),
213
+ ):
214
+ """
215
+ Upload a file to a data folder.
216
+ """
217
+ # NEW: Cloud based file processing
218
+ # Determine file's MIME type
219
+ mimetypes.guess_type(file.filename)[0] or "application/octet-stream"
220
+
221
+ allowed_media_types = get_allowed_media_types()
222
+
223
+ # Normalize incoming Content-Type header (strip charset or any parameters).
224
+ raw_ct = file.content_type or ""
225
+ media_type = raw_ct.split(";", 1)[0].strip().lower()
226
+
227
+ # If client didn't supply a Content-Type or it's not one of the allowed types,
228
+ # attempt to infer from filename extension.
229
+ if media_type not in allowed_media_types and file.filename:
230
+ guessed, _ = mimetypes.guess_type(file.filename)
231
+ media_type = (guessed or "").lower()
232
+
233
+ if media_type not in allowed_media_types:
234
+ ext = Path(file.filename).suffix.lower()
235
+ ext_map = get_extension_to_mime_type_map()
236
+ media_type = ext_map.get(ext, media_type)
237
+
238
+ # If still not allowed, reject with 415.
239
+ if media_type not in allowed_media_types:
240
+ raise HTTPException(
241
+ status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
242
+ detail=(
243
+ f"Unsupported file type: {media_type or 'unknown'} "
244
+ f"(filename: {file.filename}). "
245
+ f"Supported types: PDF, text files (.txt, .md), JSON, and code files (.py, .js, .java, etc.)."
246
+ ),
247
+ )
248
+
249
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
250
+
251
+ folder = await server.source_manager.get_source_by_id(source_id=folder_id, actor=actor)
252
+ if folder is None:
253
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Folder with id={folder_id} not found.")
254
+
255
+ content = await file.read()
256
+
257
+ # Store original filename and handle duplicate logic
258
+ original_filename = sanitize_filename(file.filename) # Basic sanitization only
259
+
260
+ # Check if duplicate exists
261
+ existing_file = await server.file_manager.get_file_by_original_name_and_source(
262
+ original_filename=original_filename, source_id=folder_id, actor=actor
263
+ )
264
+
265
+ if existing_file:
266
+ # Duplicate found, handle based on strategy
267
+ if duplicate_handling == DuplicateFileHandling.ERROR:
268
+ raise HTTPException(
269
+ status_code=status.HTTP_409_CONFLICT, detail=f"File '{original_filename}' already exists in folder '{folder.name}'"
270
+ )
271
+ elif duplicate_handling == DuplicateFileHandling.SKIP:
272
+ # Return existing file metadata with custom header to indicate it was skipped
273
+ from fastapi import Response
274
+
275
+ response = Response(
276
+ content=existing_file.model_dump_json(), media_type="application/json", headers={"X-Upload-Result": "skipped"}
277
+ )
278
+ return response
279
+ # For SUFFIX, continue to generate unique filename
280
+
281
+ # Generate unique filename (adds suffix if needed)
282
+ unique_filename = await server.file_manager.generate_unique_filename(
283
+ original_filename=original_filename, source=folder, organization_id=actor.organization_id
284
+ )
285
+
286
+ # create file metadata
287
+ file_metadata = FileMetadata(
288
+ source_id=folder_id,
289
+ file_name=unique_filename,
290
+ original_file_name=original_filename,
291
+ file_path=None,
292
+ file_type=mimetypes.guess_type(original_filename)[0] or file.content_type or "unknown",
293
+ file_size=file.size if file.size is not None else None,
294
+ processing_status=FileProcessingStatus.PARSING,
295
+ )
296
+ file_metadata = await server.file_manager.create_file(file_metadata, actor=actor)
297
+
298
+ # TODO: Do we need to pull in the full agent_states? Can probably simplify here right?
299
+ agent_states = await server.source_manager.list_attached_agents(source_id=folder_id, actor=actor)
300
+
301
+ # Use cloud processing for all files (simple files always, complex files with Mistral key)
302
+ logger.info("Running experimental cloud based file processing...")
303
+ safe_create_task(
304
+ load_file_to_source_cloud(server, agent_states, content, folder_id, actor, folder.embedding_config, file_metadata),
305
+ logger=logger,
306
+ label="file_processor.process",
307
+ )
308
+ safe_create_task(sleeptime_document_ingest_async(server, folder_id, actor), logger=logger, label="sleeptime_document_ingest_async")
309
+
310
+ return file_metadata
311
+
312
+
313
+ @router.get("/{folder_id}/agents", response_model=List[str], operation_id="get_agents_for_folder")
314
+ async def get_agents_for_folder(
315
+ folder_id: str,
316
+ server: SyncServer = Depends(get_letta_server),
317
+ actor_id: Optional[str] = Header(None, alias="user_id"),
318
+ ):
319
+ """
320
+ Get all agent IDs that have the specified folder attached.
321
+ """
322
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
323
+ return await server.source_manager.get_agents_for_source_id(source_id=folder_id, actor=actor)
324
+
325
+
326
+ @router.get("/{folder_id}/passages", response_model=List[Passage], operation_id="list_folder_passages")
327
+ async def list_folder_passages(
328
+ folder_id: str,
329
+ after: Optional[str] = Query(None, description="Message after which to retrieve the returned messages."),
330
+ before: Optional[str] = Query(None, description="Message before which to retrieve the returned messages."),
331
+ limit: int = Query(100, description="Maximum number of messages to retrieve."),
332
+ server: SyncServer = Depends(get_letta_server),
333
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
334
+ ):
335
+ """
336
+ List all passages associated with a data folder.
337
+ """
338
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
339
+ return await server.agent_manager.list_passages_async(
340
+ actor=actor,
341
+ source_id=folder_id,
342
+ after=after,
343
+ before=before,
344
+ limit=limit,
345
+ )
346
+
347
+
348
+ @router.get("/{folder_id}/files", response_model=List[FileMetadata], operation_id="list_folder_files")
349
+ async def list_folder_files(
350
+ folder_id: str,
351
+ limit: int = Query(1000, description="Number of files to return"),
352
+ after: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
353
+ include_content: bool = Query(False, description="Whether to include full file content"),
354
+ server: "SyncServer" = Depends(get_letta_server),
355
+ actor_id: Optional[str] = Header(None, alias="user_id"),
356
+ ):
357
+ """
358
+ List paginated files associated with a data folder.
359
+ """
360
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
361
+ return await server.file_manager.list_files(
362
+ source_id=folder_id,
363
+ limit=limit,
364
+ after=after,
365
+ actor=actor,
366
+ include_content=include_content,
367
+ strip_directory_prefix=True, # TODO: Reconsider this. This is purely for aesthetics.
368
+ )
369
+
370
+
371
+ # @router.get("/{folder_id}/files/{file_id}", response_model=FileMetadata, operation_id="get_file_metadata")
372
+ # async def get_file_metadata(
373
+ # folder_id: str,
374
+ # file_id: str,
375
+ # include_content: bool = Query(False, description="Whether to include full file content"),
376
+ # server: "SyncServer" = Depends(get_letta_server),
377
+ # actor_id: Optional[str] = Header(None, alias="user_id"),
378
+ # ):
379
+ # """
380
+ # Retrieve metadata for a specific file by its ID.
381
+ # """
382
+ # actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
383
+ #
384
+ # # Get file metadata using the file manager
385
+ # file_metadata = await server.file_manager.get_file_by_id(
386
+ # file_id=file_id, actor=actor, include_content=include_content, strip_directory_prefix=True
387
+ # )
388
+ #
389
+ # if not file_metadata:
390
+ # raise HTTPException(status_code=404, detail=f"File with id={file_id} not found.")
391
+ #
392
+ # # Verify the file belongs to the specified folder
393
+ # if file_metadata.source_id != folder_id:
394
+ # raise HTTPException(status_code=404, detail=f"File with id={file_id} not found in folder {folder_id}.")
395
+ #
396
+ # if should_use_pinecone() and file_metadata.processing_status == FileProcessingStatus.EMBEDDING:
397
+ # ids = await list_pinecone_index_for_files(file_id=file_id, actor=actor)
398
+ # logger.info(
399
+ # f"Embedded chunks {len(ids)}/{file_metadata.total_chunks} for {file_id} ({file_metadata.file_name}) in organization {actor.organization_id}"
400
+ # )
401
+ #
402
+ # if len(ids) != file_metadata.chunks_embedded or len(ids) == file_metadata.total_chunks:
403
+ # if len(ids) != file_metadata.total_chunks:
404
+ # file_status = file_metadata.processing_status
405
+ # else:
406
+ # file_status = FileProcessingStatus.COMPLETED
407
+ # try:
408
+ # file_metadata = await server.file_manager.update_file_status(
409
+ # file_id=file_metadata.id, actor=actor, chunks_embedded=len(ids), processing_status=file_status
410
+ # )
411
+ # except ValueError as e:
412
+ # # state transition was blocked - this is a race condition
413
+ # # log it but don't fail the request since we're just reading metadata
414
+ # logger.warning(f"Race condition detected in get_file_metadata: {str(e)}")
415
+ # # return the current file state without updating
416
+ #
417
+ # return file_metadata
418
+
419
+
420
+ # it's redundant to include /delete in the URL path. The HTTP verb DELETE already implies that action.
421
+ # it's still good practice to return a status indicating the success or failure of the deletion
422
+ @router.delete("/{folder_id}/{file_id}", status_code=204, operation_id="delete_file_from_folder")
423
+ async def delete_file_from_folder(
424
+ folder_id: str,
425
+ file_id: str,
426
+ server: "SyncServer" = Depends(get_letta_server),
427
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
428
+ ):
429
+ """
430
+ Delete a file from a folder.
431
+ """
432
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
433
+
434
+ deleted_file = await server.file_manager.delete_file(file_id=file_id, actor=actor)
435
+
436
+ await server.remove_file_from_context_windows(source_id=folder_id, file_id=deleted_file.id, actor=actor)
437
+
438
+ if should_use_pinecone():
439
+ logger.info(f"Deleting file {file_id} from pinecone index")
440
+ await delete_file_records_from_pinecone_index(file_id=file_id, actor=actor)
441
+
442
+ asyncio.create_task(sleeptime_document_ingest_async(server, folder_id, actor, clear_history=True))
443
+ if deleted_file is None:
444
+ raise HTTPException(status_code=404, detail=f"File with id={file_id} not found.")
445
+
446
+
447
+ async def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str, filename: str, bytes: bytes, actor: User):
448
+ # Create a temporary directory (deleted after the context manager exits)
449
+ with tempfile.TemporaryDirectory() as tmpdirname:
450
+ file_path = os.path.join(tmpdirname, filename)
451
+
452
+ # Write the file to the sanitized path
453
+ with open(file_path, "wb") as buffer:
454
+ buffer.write(bytes)
455
+
456
+ # Pass the file to load_file_to_source
457
+ await server.load_file_to_source(source_id, file_path, job_id, actor)
458
+
459
+
460
+ async def sleeptime_document_ingest_async(server: SyncServer, source_id: str, actor: User, clear_history: bool = False):
461
+ source = await server.source_manager.get_source_by_id(source_id=source_id)
462
+ agents = await server.source_manager.list_attached_agents(source_id=source_id, actor=actor)
463
+ for agent in agents:
464
+ if agent.enable_sleeptime:
465
+ await server.sleeptime_document_ingest_async(agent, source, actor, clear_history)
466
+
467
+
468
+ @trace_method
469
+ async def load_file_to_source_cloud(
470
+ server: SyncServer,
471
+ agent_states: List[AgentState],
472
+ content: bytes,
473
+ source_id: str,
474
+ actor: User,
475
+ embedding_config: EmbeddingConfig,
476
+ file_metadata: FileMetadata,
477
+ ):
478
+ # Choose parser based on mistral API key availability
479
+ if settings.mistral_api_key:
480
+ file_parser = MistralFileParser()
481
+ else:
482
+ file_parser = MarkitdownFileParser()
483
+
484
+ using_pinecone = should_use_pinecone()
485
+ if using_pinecone:
486
+ embedder = PineconeEmbedder(embedding_config=embedding_config)
487
+ else:
488
+ embedder = OpenAIEmbedder(embedding_config=embedding_config)
489
+ file_processor = FileProcessor(file_parser=file_parser, embedder=embedder, actor=actor, using_pinecone=using_pinecone)
490
+ await file_processor.process(agent_states=agent_states, source_id=source_id, content=content, file_metadata=file_metadata)
@@ -71,12 +71,12 @@ async def modify_provider(
71
71
 
72
72
 
73
73
  @router.get("/check", response_model=None, operation_id="check_provider")
74
- def check_provider(
74
+ async def check_provider(
75
75
  request: ProviderCheck = Body(...),
76
76
  server: "SyncServer" = Depends(get_letta_server),
77
77
  ):
78
78
  try:
79
- server.provider_manager.check_provider_api_key(provider_check=request)
79
+ await server.provider_manager.check_provider_api_key(provider_check=request)
80
80
  return JSONResponse(
81
81
  status_code=status.HTTP_200_OK, content={"message": f"Valid api key for provider_type={request.provider_type.value}"}
82
82
  )
@@ -30,12 +30,8 @@ from letta.server.server import SyncServer
30
30
  from letta.services.file_processor.embedder.openai_embedder import OpenAIEmbedder
31
31
  from letta.services.file_processor.embedder.pinecone_embedder import PineconeEmbedder
32
32
  from letta.services.file_processor.file_processor import FileProcessor
33
- from letta.services.file_processor.file_types import (
34
- get_allowed_media_types,
35
- get_extension_to_mime_type_map,
36
- is_simple_text_mime_type,
37
- register_mime_types,
38
- )
33
+ from letta.services.file_processor.file_types import get_allowed_media_types, get_extension_to_mime_type_map, register_mime_types
34
+ from letta.services.file_processor.parser.markitdown_parser import MarkitdownFileParser
39
35
  from letta.services.file_processor.parser.mistral_parser import MistralFileParser
40
36
  from letta.settings import settings
41
37
  from letta.utils import safe_create_task, sanitize_filename
@@ -220,17 +216,7 @@ async def upload_file_to_source(
220
216
  """
221
217
  # NEW: Cloud based file processing
222
218
  # Determine file's MIME type
223
- file_mime_type = mimetypes.guess_type(file.filename)[0] or "application/octet-stream"
224
-
225
- # Check if it's a simple text file
226
- is_simple_file = is_simple_text_mime_type(file_mime_type)
227
-
228
- # For complex files, require Mistral API key
229
- if not is_simple_file and not settings.mistral_api_key:
230
- raise HTTPException(
231
- status_code=status.HTTP_400_BAD_REQUEST,
232
- detail=f"Mistral API key is required to process this file type {file_mime_type}. Please configure your Mistral API key to upload complex file formats.",
233
- )
219
+ mimetypes.guess_type(file.filename)[0] or "application/octet-stream"
234
220
 
235
221
  allowed_media_types = get_allowed_media_types()
236
222
 
@@ -418,9 +404,15 @@ async def get_file_metadata(
418
404
  file_status = file_metadata.processing_status
419
405
  else:
420
406
  file_status = FileProcessingStatus.COMPLETED
421
- file_metadata = await server.file_manager.update_file_status(
422
- file_id=file_metadata.id, actor=actor, chunks_embedded=len(ids), processing_status=file_status
423
- )
407
+ try:
408
+ file_metadata = await server.file_manager.update_file_status(
409
+ file_id=file_metadata.id, actor=actor, chunks_embedded=len(ids), processing_status=file_status
410
+ )
411
+ except ValueError as e:
412
+ # state transition was blocked - this is a race condition
413
+ # log it but don't fail the request since we're just reading metadata
414
+ logger.warning(f"Race condition detected in get_file_metadata: {str(e)}")
415
+ # return the current file state without updating
424
416
 
425
417
  return file_metadata
426
418
 
@@ -483,13 +475,16 @@ async def load_file_to_source_cloud(
483
475
  embedding_config: EmbeddingConfig,
484
476
  file_metadata: FileMetadata,
485
477
  ):
486
- file_processor = MistralFileParser()
478
+ # Choose parser based on mistral API key availability
479
+ if settings.mistral_api_key:
480
+ file_parser = MistralFileParser()
481
+ else:
482
+ file_parser = MarkitdownFileParser()
483
+
487
484
  using_pinecone = should_use_pinecone()
488
485
  if using_pinecone:
489
- embedder = PineconeEmbedder()
486
+ embedder = PineconeEmbedder(embedding_config=embedding_config)
490
487
  else:
491
488
  embedder = OpenAIEmbedder(embedding_config=embedding_config)
492
- file_processor = FileProcessor(file_parser=file_processor, embedder=embedder, actor=actor, using_pinecone=using_pinecone)
493
- await file_processor.process(
494
- server=server, agent_states=agent_states, source_id=source_id, content=content, file_metadata=file_metadata
495
- )
489
+ file_processor = FileProcessor(file_parser=file_parser, embedder=embedder, actor=actor, using_pinecone=using_pinecone)
490
+ await file_processor.process(agent_states=agent_states, source_id=source_id, content=content, file_metadata=file_metadata)