letta-nightly 0.7.30.dev20250603104343__py3-none-any.whl → 0.8.0.dev20250604201135__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 (136) hide show
  1. letta/__init__.py +7 -1
  2. letta/agent.py +14 -7
  3. letta/agents/base_agent.py +1 -0
  4. letta/agents/ephemeral_summary_agent.py +104 -0
  5. letta/agents/helpers.py +35 -3
  6. letta/agents/letta_agent.py +492 -176
  7. letta/agents/letta_agent_batch.py +22 -16
  8. letta/agents/prompts/summary_system_prompt.txt +62 -0
  9. letta/agents/voice_agent.py +22 -7
  10. letta/agents/voice_sleeptime_agent.py +13 -8
  11. letta/constants.py +33 -1
  12. letta/data_sources/connectors.py +52 -36
  13. letta/errors.py +4 -0
  14. letta/functions/ast_parsers.py +13 -30
  15. letta/functions/function_sets/base.py +3 -1
  16. letta/functions/functions.py +2 -0
  17. letta/functions/mcp_client/base_client.py +151 -97
  18. letta/functions/mcp_client/sse_client.py +49 -31
  19. letta/functions/mcp_client/stdio_client.py +107 -106
  20. letta/functions/schema_generator.py +22 -22
  21. letta/groups/helpers.py +3 -4
  22. letta/groups/sleeptime_multi_agent.py +4 -4
  23. letta/groups/sleeptime_multi_agent_v2.py +22 -0
  24. letta/helpers/composio_helpers.py +16 -0
  25. letta/helpers/converters.py +20 -0
  26. letta/helpers/datetime_helpers.py +1 -6
  27. letta/helpers/tool_rule_solver.py +2 -1
  28. letta/interfaces/anthropic_streaming_interface.py +17 -2
  29. letta/interfaces/openai_chat_completions_streaming_interface.py +1 -0
  30. letta/interfaces/openai_streaming_interface.py +18 -2
  31. letta/llm_api/anthropic_client.py +24 -3
  32. letta/llm_api/google_ai_client.py +0 -15
  33. letta/llm_api/google_vertex_client.py +6 -5
  34. letta/llm_api/llm_client_base.py +15 -0
  35. letta/llm_api/openai.py +2 -2
  36. letta/llm_api/openai_client.py +60 -8
  37. letta/orm/__init__.py +2 -0
  38. letta/orm/agent.py +45 -43
  39. letta/orm/base.py +0 -2
  40. letta/orm/block.py +1 -0
  41. letta/orm/custom_columns.py +13 -0
  42. letta/orm/enums.py +5 -0
  43. letta/orm/file.py +3 -1
  44. letta/orm/files_agents.py +68 -0
  45. letta/orm/mcp_server.py +48 -0
  46. letta/orm/message.py +1 -0
  47. letta/orm/organization.py +11 -2
  48. letta/orm/passage.py +25 -10
  49. letta/orm/sandbox_config.py +5 -2
  50. letta/orm/sqlalchemy_base.py +171 -110
  51. letta/prompts/system/memgpt_base.txt +6 -1
  52. letta/prompts/system/memgpt_v2_chat.txt +57 -0
  53. letta/prompts/system/sleeptime.txt +2 -0
  54. letta/prompts/system/sleeptime_v2.txt +28 -0
  55. letta/schemas/agent.py +87 -20
  56. letta/schemas/block.py +7 -1
  57. letta/schemas/file.py +57 -0
  58. letta/schemas/mcp.py +74 -0
  59. letta/schemas/memory.py +5 -2
  60. letta/schemas/message.py +9 -0
  61. letta/schemas/openai/openai.py +0 -6
  62. letta/schemas/providers.py +33 -4
  63. letta/schemas/tool.py +26 -21
  64. letta/schemas/tool_execution_result.py +5 -0
  65. letta/server/db.py +23 -8
  66. letta/server/rest_api/app.py +73 -56
  67. letta/server/rest_api/interface.py +4 -4
  68. letta/server/rest_api/routers/v1/agents.py +132 -47
  69. letta/server/rest_api/routers/v1/blocks.py +3 -2
  70. letta/server/rest_api/routers/v1/embeddings.py +3 -3
  71. letta/server/rest_api/routers/v1/groups.py +3 -3
  72. letta/server/rest_api/routers/v1/jobs.py +14 -17
  73. letta/server/rest_api/routers/v1/organizations.py +10 -10
  74. letta/server/rest_api/routers/v1/providers.py +12 -10
  75. letta/server/rest_api/routers/v1/runs.py +3 -3
  76. letta/server/rest_api/routers/v1/sandbox_configs.py +12 -12
  77. letta/server/rest_api/routers/v1/sources.py +108 -43
  78. letta/server/rest_api/routers/v1/steps.py +8 -6
  79. letta/server/rest_api/routers/v1/tools.py +134 -95
  80. letta/server/rest_api/utils.py +12 -1
  81. letta/server/server.py +272 -73
  82. letta/services/agent_manager.py +246 -313
  83. letta/services/block_manager.py +30 -9
  84. letta/services/context_window_calculator/__init__.py +0 -0
  85. letta/services/context_window_calculator/context_window_calculator.py +150 -0
  86. letta/services/context_window_calculator/token_counter.py +82 -0
  87. letta/services/file_processor/__init__.py +0 -0
  88. letta/services/file_processor/chunker/__init__.py +0 -0
  89. letta/services/file_processor/chunker/llama_index_chunker.py +29 -0
  90. letta/services/file_processor/embedder/__init__.py +0 -0
  91. letta/services/file_processor/embedder/openai_embedder.py +84 -0
  92. letta/services/file_processor/file_processor.py +123 -0
  93. letta/services/file_processor/parser/__init__.py +0 -0
  94. letta/services/file_processor/parser/base_parser.py +9 -0
  95. letta/services/file_processor/parser/mistral_parser.py +54 -0
  96. letta/services/file_processor/types.py +0 -0
  97. letta/services/files_agents_manager.py +184 -0
  98. letta/services/group_manager.py +118 -0
  99. letta/services/helpers/agent_manager_helper.py +76 -21
  100. letta/services/helpers/tool_execution_helper.py +3 -0
  101. letta/services/helpers/tool_parser_helper.py +100 -0
  102. letta/services/identity_manager.py +44 -42
  103. letta/services/job_manager.py +21 -10
  104. letta/services/mcp/base_client.py +5 -2
  105. letta/services/mcp/sse_client.py +3 -5
  106. letta/services/mcp/stdio_client.py +3 -5
  107. letta/services/mcp_manager.py +281 -0
  108. letta/services/message_manager.py +40 -26
  109. letta/services/organization_manager.py +55 -19
  110. letta/services/passage_manager.py +211 -13
  111. letta/services/provider_manager.py +48 -2
  112. letta/services/sandbox_config_manager.py +105 -0
  113. letta/services/source_manager.py +4 -5
  114. letta/services/step_manager.py +9 -6
  115. letta/services/summarizer/summarizer.py +50 -23
  116. letta/services/telemetry_manager.py +7 -0
  117. letta/services/tool_executor/tool_execution_manager.py +11 -52
  118. letta/services/tool_executor/tool_execution_sandbox.py +4 -34
  119. letta/services/tool_executor/tool_executor.py +107 -105
  120. letta/services/tool_manager.py +56 -17
  121. letta/services/tool_sandbox/base.py +39 -92
  122. letta/services/tool_sandbox/e2b_sandbox.py +16 -11
  123. letta/services/tool_sandbox/local_sandbox.py +51 -23
  124. letta/services/user_manager.py +36 -3
  125. letta/settings.py +10 -3
  126. letta/templates/__init__.py +0 -0
  127. letta/templates/sandbox_code_file.py.j2 +47 -0
  128. letta/templates/template_helper.py +16 -0
  129. letta/tracing.py +30 -1
  130. letta/types/__init__.py +7 -0
  131. letta/utils.py +25 -1
  132. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604201135.dist-info}/METADATA +7 -2
  133. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604201135.dist-info}/RECORD +136 -110
  134. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604201135.dist-info}/LICENSE +0 -0
  135. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604201135.dist-info}/WHEEL +0 -0
  136. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604201135.dist-info}/entry_points.txt +0 -0
@@ -9,7 +9,7 @@ router = APIRouter(prefix="/embeddings", tags=["embeddings"])
9
9
 
10
10
 
11
11
  @router.get("/total_storage_size", response_model=float, operation_id="get_total_storage_size")
12
- def get_embeddings_total_storage_size(
12
+ async def get_embeddings_total_storage_size(
13
13
  server: SyncServer = Depends(get_letta_server),
14
14
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
15
15
  storage_unit: Optional[str] = Header("GB", alias="storage_unit"), # Extract storage unit from header, default to GB
@@ -17,5 +17,5 @@ def get_embeddings_total_storage_size(
17
17
  """
18
18
  Get the total size of all embeddings in the database for a user in the storage unit given.
19
19
  """
20
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
21
- return server.passage_manager.estimate_embeddings_size(actor=actor, storage_unit=storage_unit)
20
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
21
+ return await server.passage_manager.estimate_embeddings_size_async(actor=actor, storage_unit=storage_unit)
@@ -52,7 +52,7 @@ def count_groups(
52
52
 
53
53
 
54
54
  @router.get("/{group_id}", response_model=Group, operation_id="retrieve_group")
55
- def retrieve_group(
55
+ async def retrieve_group(
56
56
  group_id: str,
57
57
  server: "SyncServer" = Depends(get_letta_server),
58
58
  actor_id: Optional[str] = Header(None, alias="user_id"),
@@ -60,10 +60,10 @@ def retrieve_group(
60
60
  """
61
61
  Retrieve the group by id.
62
62
  """
63
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
63
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
64
64
 
65
65
  try:
66
- return server.group_manager.retrieve_group(group_id=group_id, actor=actor)
66
+ return await server.group_manager.retrieve_group_async(group_id=group_id, actor=actor)
67
67
  except NoResultFound as e:
68
68
  raise HTTPException(status_code=404, detail=str(e))
69
69
 
@@ -12,7 +12,7 @@ router = APIRouter(prefix="/jobs", tags=["jobs"])
12
12
 
13
13
 
14
14
  @router.get("/", response_model=List[Job], operation_id="list_jobs")
15
- def list_jobs(
15
+ async def list_jobs(
16
16
  server: "SyncServer" = Depends(get_letta_server),
17
17
  source_id: Optional[str] = Query(None, description="Only list jobs associated with the source."),
18
18
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -20,33 +20,30 @@ def list_jobs(
20
20
  """
21
21
  List all jobs.
22
22
  """
23
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
23
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
24
24
 
25
25
  # TODO: add filtering by status
26
- jobs = server.job_manager.list_jobs(actor=actor)
27
-
28
- if source_id:
29
- # can't be in the ORM since we have source_id stored in the metadata
30
- # TODO: Probably change this
31
- jobs = [job for job in jobs if job.metadata.get("source_id") == source_id]
32
- return jobs
26
+ return await server.job_manager.list_jobs_async(
27
+ actor=actor,
28
+ source_id=source_id,
29
+ )
33
30
 
34
31
 
35
32
  @router.get("/active", response_model=List[Job], operation_id="list_active_jobs")
36
33
  async def list_active_jobs(
37
34
  server: "SyncServer" = Depends(get_letta_server),
38
35
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
36
+ source_id: Optional[str] = Query(None, description="Only list jobs associated with the source."),
39
37
  ):
40
38
  """
41
39
  List all active jobs.
42
40
  """
43
41
  actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
44
-
45
- return await server.job_manager.list_jobs_async(actor=actor, statuses=[JobStatus.created, JobStatus.running])
42
+ return await server.job_manager.list_jobs_async(actor=actor, statuses=[JobStatus.created, JobStatus.running], source_id=source_id)
46
43
 
47
44
 
48
45
  @router.get("/{job_id}", response_model=Job, operation_id="retrieve_job")
49
- def retrieve_job(
46
+ async def retrieve_job(
50
47
  job_id: str,
51
48
  actor_id: Optional[str] = Header(None, alias="user_id"),
52
49
  server: "SyncServer" = Depends(get_letta_server),
@@ -54,16 +51,16 @@ def retrieve_job(
54
51
  """
55
52
  Get the status of a job.
56
53
  """
57
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
54
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
58
55
 
59
56
  try:
60
- return server.job_manager.get_job_by_id(job_id=job_id, actor=actor)
57
+ return await server.job_manager.get_job_by_id_async(job_id=job_id, actor=actor)
61
58
  except NoResultFound:
62
59
  raise HTTPException(status_code=404, detail="Job not found")
63
60
 
64
61
 
65
62
  @router.delete("/{job_id}", response_model=Job, operation_id="delete_job")
66
- def delete_job(
63
+ async def delete_job(
67
64
  job_id: str,
68
65
  actor_id: Optional[str] = Header(None, alias="user_id"),
69
66
  server: "SyncServer" = Depends(get_letta_server),
@@ -71,10 +68,10 @@ def delete_job(
71
68
  """
72
69
  Delete a job by its job_id.
73
70
  """
74
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
71
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
75
72
 
76
73
  try:
77
- job = server.job_manager.delete_job_by_id(job_id=job_id, actor=actor)
74
+ job = await server.job_manager.delete_job_by_id_async(job_id=job_id, actor=actor)
78
75
  return job
79
76
  except NoResultFound:
80
77
  raise HTTPException(status_code=404, detail="Job not found")
@@ -13,7 +13,7 @@ router = APIRouter(prefix="/orgs", tags=["organization", "admin"])
13
13
 
14
14
 
15
15
  @router.get("/", tags=["admin"], response_model=List[Organization], operation_id="list_orgs")
16
- def get_all_orgs(
16
+ async def get_all_orgs(
17
17
  after: Optional[str] = Query(None),
18
18
  limit: Optional[int] = Query(50),
19
19
  server: "SyncServer" = Depends(get_letta_server),
@@ -22,7 +22,7 @@ def get_all_orgs(
22
22
  Get a list of all orgs in the database
23
23
  """
24
24
  try:
25
- orgs = server.organization_manager.list_organizations(after=after, limit=limit)
25
+ orgs = await server.organization_manager.list_organizations_async(after=after, limit=limit)
26
26
  except HTTPException:
27
27
  raise
28
28
  except Exception as e:
@@ -31,7 +31,7 @@ def get_all_orgs(
31
31
 
32
32
 
33
33
  @router.post("/", tags=["admin"], response_model=Organization, operation_id="create_organization")
34
- def create_org(
34
+ async def create_org(
35
35
  request: OrganizationCreate = Body(...),
36
36
  server: "SyncServer" = Depends(get_letta_server),
37
37
  ):
@@ -39,21 +39,21 @@ def create_org(
39
39
  Create a new org in the database
40
40
  """
41
41
  org = Organization(**request.model_dump())
42
- org = server.organization_manager.create_organization(pydantic_org=org)
42
+ org = await server.organization_manager.create_organization_async(pydantic_org=org)
43
43
  return org
44
44
 
45
45
 
46
46
  @router.delete("/", tags=["admin"], response_model=Organization, operation_id="delete_organization_by_id")
47
- def delete_org(
47
+ async def delete_org(
48
48
  org_id: str = Query(..., description="The org_id key to be deleted."),
49
49
  server: "SyncServer" = Depends(get_letta_server),
50
50
  ):
51
51
  # TODO make a soft deletion, instead of a hard deletion
52
52
  try:
53
- org = server.organization_manager.get_organization_by_id(org_id=org_id)
53
+ org = await server.organization_manager.get_organization_by_id_async(org_id=org_id)
54
54
  if org is None:
55
55
  raise HTTPException(status_code=404, detail=f"Organization does not exist")
56
- server.organization_manager.delete_organization_by_id(org_id=org_id)
56
+ await server.organization_manager.delete_organization_by_id_async(org_id=org_id)
57
57
  except HTTPException:
58
58
  raise
59
59
  except Exception as e:
@@ -62,16 +62,16 @@ def delete_org(
62
62
 
63
63
 
64
64
  @router.patch("/", tags=["admin"], response_model=Organization, operation_id="update_organization")
65
- def update_org(
65
+ async def update_org(
66
66
  org_id: str = Query(..., description="The org_id key to be updated."),
67
67
  request: OrganizationUpdate = Body(...),
68
68
  server: "SyncServer" = Depends(get_letta_server),
69
69
  ):
70
70
  try:
71
- org = server.organization_manager.get_organization_by_id(org_id=org_id)
71
+ org = await server.organization_manager.get_organization_by_id_async(org_id=org_id)
72
72
  if org is None:
73
73
  raise HTTPException(status_code=404, detail=f"Organization does not exist")
74
- org = server.organization_manager.update_organization(org_id=org_id, name=request.name)
74
+ org = await server.organization_manager.update_organization_async(org_id=org_id, name=request.name)
75
75
  except HTTPException:
76
76
  raise
77
77
  except Exception as e:
@@ -16,7 +16,7 @@ router = APIRouter(prefix="/providers", tags=["providers"])
16
16
 
17
17
 
18
18
  @router.get("/", response_model=List[Provider], operation_id="list_providers")
19
- def list_providers(
19
+ async def list_providers(
20
20
  name: Optional[str] = Query(None),
21
21
  provider_type: Optional[ProviderType] = Query(None),
22
22
  after: Optional[str] = Query(None),
@@ -28,8 +28,10 @@ def list_providers(
28
28
  Get a list of all custom providers in the database
29
29
  """
30
30
  try:
31
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
32
- providers = server.provider_manager.list_providers(after=after, limit=limit, actor=actor, name=name, provider_type=provider_type)
31
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
32
+ providers = await server.provider_manager.list_providers_async(
33
+ after=after, limit=limit, actor=actor, name=name, provider_type=provider_type
34
+ )
33
35
  except HTTPException:
34
36
  raise
35
37
  except Exception as e:
@@ -38,7 +40,7 @@ def list_providers(
38
40
 
39
41
 
40
42
  @router.post("/", response_model=Provider, operation_id="create_provider")
41
- def create_provider(
43
+ async def create_provider(
42
44
  request: ProviderCreate = Body(...),
43
45
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
44
46
  server: "SyncServer" = Depends(get_letta_server),
@@ -46,16 +48,16 @@ def create_provider(
46
48
  """
47
49
  Create a new custom provider
48
50
  """
49
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
51
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
50
52
 
51
53
  provider = ProviderCreate(**request.model_dump())
52
54
 
53
- provider = server.provider_manager.create_provider(provider, actor=actor)
55
+ provider = await server.provider_manager.create_provider_async(provider, actor=actor)
54
56
  return provider
55
57
 
56
58
 
57
59
  @router.patch("/{provider_id}", response_model=Provider, operation_id="modify_provider")
58
- def modify_provider(
60
+ async def modify_provider(
59
61
  provider_id: str,
60
62
  request: ProviderUpdate = Body(...),
61
63
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -85,7 +87,7 @@ def check_provider(
85
87
 
86
88
 
87
89
  @router.delete("/{provider_id}", response_model=None, operation_id="delete_provider")
88
- def delete_provider(
90
+ async def delete_provider(
89
91
  provider_id: str,
90
92
  actor_id: Optional[str] = Header(None, alias="user_id"),
91
93
  server: "SyncServer" = Depends(get_letta_server),
@@ -94,8 +96,8 @@ def delete_provider(
94
96
  Delete an existing custom provider
95
97
  """
96
98
  try:
97
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
98
- server.provider_manager.delete_provider_by_id(provider_id=provider_id, actor=actor)
99
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
100
+ await server.provider_manager.delete_provider_by_id_async(provider_id=provider_id, actor=actor)
99
101
  return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"Provider id={provider_id} successfully deleted"})
100
102
  except NoResultFound:
101
103
  raise HTTPException(status_code=404, detail=f"Provider provider_id={provider_id} not found for user_id={actor.id}.")
@@ -199,7 +199,7 @@ async def list_run_steps(
199
199
 
200
200
 
201
201
  @router.delete("/{run_id}", response_model=Run, operation_id="delete_run")
202
- def delete_run(
202
+ async def delete_run(
203
203
  run_id: str,
204
204
  actor_id: Optional[str] = Header(None, alias="user_id"),
205
205
  server: "SyncServer" = Depends(get_letta_server),
@@ -207,10 +207,10 @@ def delete_run(
207
207
  """
208
208
  Delete a run by its run_id.
209
209
  """
210
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
210
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
211
211
 
212
212
  try:
213
- job = server.job_manager.delete_job_by_id(job_id=run_id, actor=actor)
213
+ job = await server.job_manager.delete_job_by_id_async(job_id=run_id, actor=actor)
214
214
  return Run.from_job(job)
215
215
  except NoResultFound:
216
216
  raise HTTPException(status_code=404, detail="Run not found")
@@ -90,13 +90,13 @@ async def update_sandbox_config(
90
90
 
91
91
 
92
92
  @router.delete("/{sandbox_config_id}", status_code=204)
93
- def delete_sandbox_config(
93
+ async def delete_sandbox_config(
94
94
  sandbox_config_id: str,
95
95
  server: SyncServer = Depends(get_letta_server),
96
96
  actor_id: str = Depends(get_user_id),
97
97
  ):
98
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
99
- server.sandbox_config_manager.delete_sandbox_config(sandbox_config_id, actor)
98
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
99
+ await server.sandbox_config_manager.delete_sandbox_config_async(sandbox_config_id, actor)
100
100
 
101
101
 
102
102
  @router.get("/", response_model=List[PydanticSandboxConfig])
@@ -158,35 +158,35 @@ async def force_recreate_local_sandbox_venv(
158
158
 
159
159
 
160
160
  @router.post("/{sandbox_config_id}/environment-variable", response_model=PydanticEnvVar)
161
- def create_sandbox_env_var(
161
+ async def create_sandbox_env_var(
162
162
  sandbox_config_id: str,
163
163
  env_var_create: SandboxEnvironmentVariableCreate,
164
164
  server: SyncServer = Depends(get_letta_server),
165
165
  actor_id: str = Depends(get_user_id),
166
166
  ):
167
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
168
- return server.sandbox_config_manager.create_sandbox_env_var(env_var_create, sandbox_config_id, actor)
167
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
168
+ return await server.sandbox_config_manager.create_sandbox_env_var_async(env_var_create, sandbox_config_id, actor)
169
169
 
170
170
 
171
171
  @router.patch("/environment-variable/{env_var_id}", response_model=PydanticEnvVar)
172
- def update_sandbox_env_var(
172
+ async def update_sandbox_env_var(
173
173
  env_var_id: str,
174
174
  env_var_update: SandboxEnvironmentVariableUpdate,
175
175
  server: SyncServer = Depends(get_letta_server),
176
176
  actor_id: str = Depends(get_user_id),
177
177
  ):
178
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
179
- return server.sandbox_config_manager.update_sandbox_env_var(env_var_id, env_var_update, actor)
178
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
179
+ return await server.sandbox_config_manager.update_sandbox_env_var_async(env_var_id, env_var_update, actor)
180
180
 
181
181
 
182
182
  @router.delete("/environment-variable/{env_var_id}", status_code=204)
183
- def delete_sandbox_env_var(
183
+ async def delete_sandbox_env_var(
184
184
  env_var_id: str,
185
185
  server: SyncServer = Depends(get_letta_server),
186
186
  actor_id: str = Depends(get_user_id),
187
187
  ):
188
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
189
- server.sandbox_config_manager.delete_sandbox_env_var(env_var_id, actor)
188
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
189
+ await server.sandbox_config_manager.delete_sandbox_env_var_async(env_var_id, actor)
190
190
 
191
191
 
192
192
  @router.get("/{sandbox_config_id}/environment-variable", response_model=List[PydanticEnvVar])
@@ -1,11 +1,16 @@
1
1
  import asyncio
2
+ import mimetypes
2
3
  import os
3
4
  import tempfile
5
+ from pathlib import Path
4
6
  from typing import List, Optional
5
7
 
6
- from fastapi import APIRouter, BackgroundTasks, Depends, Header, HTTPException, Query, UploadFile
8
+ from fastapi import APIRouter, Depends, Header, HTTPException, Query, UploadFile
9
+ from starlette import status
7
10
 
8
11
  import letta.constants as constants
12
+ from letta.log import get_logger
13
+ from letta.schemas.agent import AgentState
9
14
  from letta.schemas.file import FileMetadata
10
15
  from letta.schemas.job import Job
11
16
  from letta.schemas.passage import Passage
@@ -13,9 +18,14 @@ from letta.schemas.source import Source, SourceCreate, SourceUpdate
13
18
  from letta.schemas.user import User
14
19
  from letta.server.rest_api.utils import get_letta_server
15
20
  from letta.server.server import SyncServer
16
- from letta.utils import sanitize_filename
21
+ from letta.services.file_processor.chunker.llama_index_chunker import LlamaIndexChunker
22
+ from letta.services.file_processor.embedder.openai_embedder import OpenAIEmbedder
23
+ from letta.services.file_processor.file_processor import FileProcessor
24
+ from letta.services.file_processor.parser.mistral_parser import MistralFileParser
25
+ from letta.settings import model_settings, settings
26
+ from letta.utils import safe_create_task, sanitize_filename
17
27
 
18
- # These can be forward refs, but because Fastapi needs them at runtime the must be imported normally
28
+ logger = get_logger(__name__)
19
29
 
20
30
 
21
31
  router = APIRouter(prefix="/sources", tags=["sources"])
@@ -29,7 +39,8 @@ async def count_sources(
29
39
  """
30
40
  Count all data sources created by a user.
31
41
  """
32
- return await server.source_manager.size(actor=server.user_manager.get_user_or_default(user_id=actor_id))
42
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
43
+ return await server.source_manager.size_async(actor=actor)
33
44
 
34
45
 
35
46
  @router.get("/{source_id}", response_model=Source, operation_id="retrieve_source")
@@ -41,7 +52,7 @@ async def retrieve_source(
41
52
  """
42
53
  Get all sources
43
54
  """
44
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
55
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
45
56
 
46
57
  source = await server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
47
58
  if not source:
@@ -58,7 +69,7 @@ async def get_source_id_by_name(
58
69
  """
59
70
  Get a source by name
60
71
  """
61
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
72
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
62
73
 
63
74
  source = await server.source_manager.get_source_by_name(source_name=source_name, actor=actor)
64
75
  if not source:
@@ -74,7 +85,7 @@ async def list_sources(
74
85
  """
75
86
  List all data sources created by a user.
76
87
  """
77
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
88
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
78
89
  return await server.source_manager.list_sources(actor=actor)
79
90
 
80
91
 
@@ -87,14 +98,14 @@ async def create_source(
87
98
  """
88
99
  Create a new data source.
89
100
  """
90
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
101
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
91
102
 
92
103
  # TODO: need to asyncify this
93
104
  if not source_create.embedding_config:
94
105
  if not source_create.embedding:
95
106
  # TODO: modify error type
96
107
  raise ValueError("Must specify either embedding or embedding_config in request")
97
- source_create.embedding_config = server.get_embedding_config_from_handle(
108
+ source_create.embedding_config = await server.get_embedding_config_from_handle_async(
98
109
  handle=source_create.embedding,
99
110
  embedding_chunk_size=source_create.embedding_chunk_size or constants.DEFAULT_EMBEDDING_CHUNK_SIZE,
100
111
  actor=actor,
@@ -120,7 +131,7 @@ async def modify_source(
120
131
  Update the name or documentation of an existing data source.
121
132
  """
122
133
  # TODO: allow updating the handle/embedding config
123
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
134
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
124
135
  if not await server.source_manager.get_source_by_id(source_id=source_id, actor=actor):
125
136
  raise HTTPException(status_code=404, detail=f"Source with id={source_id} does not exist.")
126
137
  return await server.source_manager.update_source(source_id=source_id, source_update=source, actor=actor)
@@ -135,15 +146,19 @@ async def delete_source(
135
146
  """
136
147
  Delete a data source.
137
148
  """
138
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
139
- source = await server.source_manager.get_source_by_id(source_id=source_id)
140
- agents = await server.source_manager.list_attached_agents(source_id=source_id, actor=actor)
141
- for agent in agents:
142
- if agent.enable_sleeptime:
149
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
150
+ source = await server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
151
+ agent_states = await server.source_manager.list_attached_agents(source_id=source_id, actor=actor)
152
+ files = await server.source_manager.list_files(source_id, actor)
153
+ file_ids = [f.id for f in files]
154
+
155
+ for agent_state in agent_states:
156
+ await server.remove_files_from_context_window(agent_state=agent_state, file_ids=file_ids, actor=actor)
157
+
158
+ if agent_state.enable_sleeptime:
143
159
  try:
144
- # TODO: make async
145
- block = server.agent_manager.get_block_with_label(agent_id=agent.id, block_label=source.name, actor=actor)
146
- server.block_manager.delete_block(block.id, actor)
160
+ block = await server.agent_manager.get_block_with_label_async(agent_id=agent_state.id, block_label=source.name, actor=actor)
161
+ await server.block_manager.delete_block_async(block.id, actor)
147
162
  except:
148
163
  pass
149
164
  await server.delete_source(source_id=source_id, actor=actor)
@@ -153,18 +168,49 @@ async def delete_source(
153
168
  async def upload_file_to_source(
154
169
  file: UploadFile,
155
170
  source_id: str,
156
- background_tasks: BackgroundTasks,
157
171
  server: "SyncServer" = Depends(get_letta_server),
158
- actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
172
+ actor_id: Optional[str] = Header(None, alias="user_id"),
159
173
  ):
160
174
  """
161
175
  Upload a file to a data source.
162
176
  """
163
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
177
+ allowed_media_types = {"application/pdf", "text/plain", "application/json"}
178
+
179
+ # Normalize incoming Content-Type header (strip charset or any parameters).
180
+ raw_ct = file.content_type or ""
181
+ media_type = raw_ct.split(";", 1)[0].strip().lower()
182
+
183
+ # If client didn’t supply a Content-Type or it’s not one of the allowed types,
184
+ # attempt to infer from filename extension.
185
+ if media_type not in allowed_media_types and file.filename:
186
+ guessed, _ = mimetypes.guess_type(file.filename)
187
+ media_type = (guessed or "").lower()
188
+
189
+ if media_type not in allowed_media_types:
190
+ ext = Path(file.filename).suffix.lower()
191
+ ext_map = {
192
+ ".pdf": "application/pdf",
193
+ ".txt": "text/plain",
194
+ ".json": "application/json",
195
+ }
196
+ media_type = ext_map.get(ext, media_type)
197
+
198
+ # If still not allowed, reject with 415.
199
+ if media_type not in allowed_media_types:
200
+ raise HTTPException(
201
+ status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
202
+ detail=(f"Unsupported file type: {media_type or 'unknown'} " f"(filename: {file.filename}). Only PDF, .txt, or .json allowed."),
203
+ )
204
+
205
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
164
206
 
165
207
  source = await server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
166
- assert source is not None, f"Source with id={source_id} not found."
167
- bytes = file.file.read()
208
+ if source is None:
209
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Source with id={source_id} not found.")
210
+ content = await file.read()
211
+
212
+ # sanitize filename
213
+ file.filename = sanitize_filename(file.filename)
168
214
 
169
215
  # create job
170
216
  job = Job(
@@ -172,17 +218,28 @@ async def upload_file_to_source(
172
218
  metadata={"type": "embedding", "filename": file.filename, "source_id": source_id},
173
219
  completed_at=None,
174
220
  )
175
- job_id = job.id
176
- server.job_manager.create_job(job, actor=actor)
177
-
178
- # create background tasks
179
- asyncio.create_task(load_file_to_source_async(server, source_id=source.id, file=file, job_id=job.id, bytes=bytes, actor=actor))
180
- asyncio.create_task(sleeptime_document_ingest_async(server, source_id, actor))
221
+ job = await server.job_manager.create_job_async(job, actor=actor)
222
+
223
+ # TODO: Do we need to pull in the full agent_states? Can probably simplify here right?
224
+ agent_states = await server.source_manager.list_attached_agents(source_id=source_id, actor=actor)
225
+
226
+ # NEW: Cloud based file processing
227
+ if settings.mistral_api_key and model_settings.openai_api_key:
228
+ logger.info("Running experimental cloud based file processing...")
229
+ safe_create_task(
230
+ load_file_to_source_cloud(server, agent_states, content, file, job, source_id, actor),
231
+ logger=logger,
232
+ label="file_processor.process",
233
+ )
234
+ else:
235
+ # create background tasks
236
+ safe_create_task(
237
+ load_file_to_source_async(server, source_id=source.id, filename=file.filename, job_id=job.id, bytes=content, actor=actor),
238
+ logger=logger,
239
+ label="load_file_to_source_async",
240
+ )
241
+ safe_create_task(sleeptime_document_ingest_async(server, source_id, actor), logger=logger, label="sleeptime_document_ingest_async")
181
242
 
182
- # return job information
183
- # Is this necessary? Can we just return the job from create_job?
184
- job = server.job_manager.get_job_by_id(job_id=job_id, actor=actor)
185
- assert job is not None, "Job not found"
186
243
  return job
187
244
 
188
245
 
@@ -198,7 +255,7 @@ async def list_source_passages(
198
255
  """
199
256
  List all passages associated with a data source.
200
257
  """
201
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
258
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
202
259
  return await server.agent_manager.list_passages_async(
203
260
  actor=actor,
204
261
  source_id=source_id,
@@ -219,7 +276,7 @@ async def list_source_files(
219
276
  """
220
277
  List paginated files associated with a data source.
221
278
  """
222
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
279
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
223
280
  return await server.source_manager.list_files(source_id=source_id, limit=limit, after=after, actor=actor)
224
281
 
225
282
 
@@ -229,29 +286,27 @@ async def list_source_files(
229
286
  async def delete_file_from_source(
230
287
  source_id: str,
231
288
  file_id: str,
232
- background_tasks: BackgroundTasks,
233
289
  server: "SyncServer" = Depends(get_letta_server),
234
290
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
235
291
  ):
236
292
  """
237
293
  Delete a data source.
238
294
  """
239
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
295
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
240
296
 
241
297
  deleted_file = await server.source_manager.delete_file(file_id=file_id, actor=actor)
242
298
 
243
- # TODO: make async
299
+ await server.remove_file_from_context_windows(source_id=source_id, file_id=deleted_file.id, actor=actor)
300
+
244
301
  asyncio.create_task(sleeptime_document_ingest_async(server, source_id, actor, clear_history=True))
245
302
  if deleted_file is None:
246
303
  raise HTTPException(status_code=404, detail=f"File with id={file_id} not found.")
247
304
 
248
305
 
249
- async def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str, file: UploadFile, bytes: bytes, actor: User):
306
+ async def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str, filename: str, bytes: bytes, actor: User):
250
307
  # Create a temporary directory (deleted after the context manager exits)
251
308
  with tempfile.TemporaryDirectory() as tmpdirname:
252
- # Sanitize the filename
253
- sanitized_filename = sanitize_filename(file.filename)
254
- file_path = os.path.join(tmpdirname, sanitized_filename)
309
+ file_path = os.path.join(tmpdirname, filename)
255
310
 
256
311
  # Write the file to the sanitized path
257
312
  with open(file_path, "wb") as buffer:
@@ -266,4 +321,14 @@ async def sleeptime_document_ingest_async(server: SyncServer, source_id: str, ac
266
321
  agents = await server.source_manager.list_attached_agents(source_id=source_id, actor=actor)
267
322
  for agent in agents:
268
323
  if agent.enable_sleeptime:
269
- server.sleeptime_document_ingest(agent, source, actor, clear_history) # TODO: make async
324
+ await server.sleeptime_document_ingest_async(agent, source, actor, clear_history)
325
+
326
+
327
+ async def load_file_to_source_cloud(
328
+ server: SyncServer, agent_states: List[AgentState], content: bytes, file: UploadFile, job: Job, source_id: str, actor: User
329
+ ):
330
+ file_processor = MistralFileParser()
331
+ text_chunker = LlamaIndexChunker()
332
+ embedder = OpenAIEmbedder()
333
+ file_processor = FileProcessor(file_parser=file_processor, text_chunker=text_chunker, embedder=embedder, actor=actor)
334
+ await file_processor.process(server=server, agent_states=agent_states, source_id=source_id, content=content, file=file, job=job)