letta-nightly 0.7.21.dev20250522104246__py3-none-any.whl → 0.7.22.dev20250523104244__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 (50) hide show
  1. letta/__init__.py +2 -2
  2. letta/agents/base_agent.py +4 -2
  3. letta/agents/letta_agent.py +3 -10
  4. letta/agents/letta_agent_batch.py +6 -6
  5. letta/cli/cli.py +0 -316
  6. letta/cli/cli_load.py +0 -52
  7. letta/client/client.py +2 -1554
  8. letta/data_sources/connectors.py +4 -2
  9. letta/functions/ast_parsers.py +33 -43
  10. letta/groups/sleeptime_multi_agent_v2.py +49 -13
  11. letta/jobs/llm_batch_job_polling.py +3 -3
  12. letta/jobs/scheduler.py +20 -19
  13. letta/llm_api/anthropic_client.py +3 -0
  14. letta/llm_api/google_vertex_client.py +5 -0
  15. letta/llm_api/openai_client.py +5 -0
  16. letta/main.py +2 -362
  17. letta/server/db.py +5 -0
  18. letta/server/rest_api/routers/v1/agents.py +72 -43
  19. letta/server/rest_api/routers/v1/llms.py +2 -2
  20. letta/server/rest_api/routers/v1/messages.py +5 -3
  21. letta/server/rest_api/routers/v1/sandbox_configs.py +18 -18
  22. letta/server/rest_api/routers/v1/sources.py +49 -36
  23. letta/server/server.py +53 -22
  24. letta/services/agent_manager.py +797 -124
  25. letta/services/block_manager.py +14 -62
  26. letta/services/group_manager.py +37 -0
  27. letta/services/identity_manager.py +9 -0
  28. letta/services/job_manager.py +17 -0
  29. letta/services/llm_batch_manager.py +88 -64
  30. letta/services/message_manager.py +19 -0
  31. letta/services/organization_manager.py +10 -0
  32. letta/services/passage_manager.py +13 -0
  33. letta/services/per_agent_lock_manager.py +4 -0
  34. letta/services/provider_manager.py +34 -0
  35. letta/services/sandbox_config_manager.py +130 -0
  36. letta/services/source_manager.py +59 -44
  37. letta/services/step_manager.py +8 -1
  38. letta/services/tool_manager.py +21 -0
  39. letta/services/tool_sandbox/e2b_sandbox.py +4 -2
  40. letta/services/tool_sandbox/local_sandbox.py +7 -3
  41. letta/services/user_manager.py +16 -0
  42. {letta_nightly-0.7.21.dev20250522104246.dist-info → letta_nightly-0.7.22.dev20250523104244.dist-info}/METADATA +1 -1
  43. {letta_nightly-0.7.21.dev20250522104246.dist-info → letta_nightly-0.7.22.dev20250523104244.dist-info}/RECORD +46 -50
  44. letta/__main__.py +0 -3
  45. letta/benchmark/benchmark.py +0 -98
  46. letta/benchmark/constants.py +0 -14
  47. letta/cli/cli_config.py +0 -227
  48. {letta_nightly-0.7.21.dev20250522104246.dist-info → letta_nightly-0.7.22.dev20250523104244.dist-info}/LICENSE +0 -0
  49. {letta_nightly-0.7.21.dev20250522104246.dist-info → letta_nightly-0.7.22.dev20250523104244.dist-info}/WHEEL +0 -0
  50. {letta_nightly-0.7.21.dev20250522104246.dist-info → letta_nightly-0.7.22.dev20250523104244.dist-info}/entry_points.txt +0 -0
@@ -161,7 +161,7 @@ async def list_batch_messages(
161
161
 
162
162
  # Get messages directly using our efficient method
163
163
  # We'll need to update the underlying implementation to use message_id as cursor
164
- messages = server.batch_manager.get_messages_for_letta_batch(
164
+ messages = await server.batch_manager.get_messages_for_letta_batch_async(
165
165
  letta_batch_job_id=batch_id, limit=limit, actor=actor, agent_id=agent_id, sort_descending=sort_descending, cursor=cursor
166
166
  )
167
167
 
@@ -184,7 +184,7 @@ async def cancel_batch_run(
184
184
  job = await server.job_manager.update_job_by_id_async(job_id=job.id, job_update=JobUpdate(status=JobStatus.cancelled), actor=actor)
185
185
 
186
186
  # Get related llm batch jobs
187
- llm_batch_jobs = server.batch_manager.list_llm_batch_jobs(letta_batch_id=job.id, actor=actor)
187
+ llm_batch_jobs = await server.batch_manager.list_llm_batch_jobs_async(letta_batch_id=job.id, actor=actor)
188
188
  for llm_batch_job in llm_batch_jobs:
189
189
  if llm_batch_job.status in {JobStatus.running, JobStatus.created}:
190
190
  # TODO: Extend to providers beyond anthropic
@@ -194,6 +194,8 @@ async def cancel_batch_run(
194
194
  await server.anthropic_async_client.messages.batches.cancel(anthropic_batch_id)
195
195
 
196
196
  # Update all the batch_job statuses
197
- server.batch_manager.update_llm_batch_status(llm_batch_id=llm_batch_job.id, status=JobStatus.cancelled, actor=actor)
197
+ await server.batch_manager.update_llm_batch_status_async(
198
+ llm_batch_id=llm_batch_job.id, status=JobStatus.cancelled, actor=actor
199
+ )
198
200
  except NoResultFound:
199
201
  raise HTTPException(status_code=404, detail="Run not found")
@@ -22,36 +22,36 @@ logger = get_logger(__name__)
22
22
 
23
23
 
24
24
  @router.post("/", response_model=PydanticSandboxConfig)
25
- def create_sandbox_config(
25
+ async def create_sandbox_config(
26
26
  config_create: SandboxConfigCreate,
27
27
  server: SyncServer = Depends(get_letta_server),
28
28
  actor_id: str = Depends(get_user_id),
29
29
  ):
30
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
30
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
31
31
 
32
- return server.sandbox_config_manager.create_or_update_sandbox_config(config_create, actor)
32
+ return await server.sandbox_config_manager.create_or_update_sandbox_config_async(config_create, actor)
33
33
 
34
34
 
35
35
  @router.post("/e2b/default", response_model=PydanticSandboxConfig)
36
- def create_default_e2b_sandbox_config(
36
+ async def create_default_e2b_sandbox_config(
37
37
  server: SyncServer = Depends(get_letta_server),
38
38
  actor_id: str = Depends(get_user_id),
39
39
  ):
40
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
41
- return server.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.E2B, actor=actor)
40
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
41
+ return await server.sandbox_config_manager.get_or_create_default_sandbox_config_async(sandbox_type=SandboxType.E2B, actor=actor)
42
42
 
43
43
 
44
44
  @router.post("/local/default", response_model=PydanticSandboxConfig)
45
- def create_default_local_sandbox_config(
45
+ async def create_default_local_sandbox_config(
46
46
  server: SyncServer = Depends(get_letta_server),
47
47
  actor_id: str = Depends(get_user_id),
48
48
  ):
49
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
50
- return server.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.LOCAL, actor=actor)
49
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
50
+ return await server.sandbox_config_manager.get_or_create_default_sandbox_config_async(sandbox_type=SandboxType.LOCAL, actor=actor)
51
51
 
52
52
 
53
53
  @router.post("/local", response_model=PydanticSandboxConfig)
54
- def create_custom_local_sandbox_config(
54
+ async def create_custom_local_sandbox_config(
55
55
  local_sandbox_config: LocalSandboxConfig,
56
56
  server: SyncServer = Depends(get_letta_server),
57
57
  actor_id: str = Depends(get_user_id),
@@ -67,26 +67,26 @@ def create_custom_local_sandbox_config(
67
67
  )
68
68
 
69
69
  # Retrieve the user (actor)
70
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
70
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
71
71
 
72
72
  # Wrap the LocalSandboxConfig into a SandboxConfigCreate
73
73
  sandbox_config_create = SandboxConfigCreate(config=local_sandbox_config)
74
74
 
75
75
  # Use the manager to create or update the sandbox config
76
- sandbox_config = server.sandbox_config_manager.create_or_update_sandbox_config(sandbox_config_create, actor=actor)
76
+ sandbox_config = await server.sandbox_config_manager.create_or_update_sandbox_config_async(sandbox_config_create, actor=actor)
77
77
 
78
78
  return sandbox_config
79
79
 
80
80
 
81
81
  @router.patch("/{sandbox_config_id}", response_model=PydanticSandboxConfig)
82
- def update_sandbox_config(
82
+ async def update_sandbox_config(
83
83
  sandbox_config_id: str,
84
84
  config_update: SandboxConfigUpdate,
85
85
  server: SyncServer = Depends(get_letta_server),
86
86
  actor_id: str = Depends(get_user_id),
87
87
  ):
88
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
89
- return server.sandbox_config_manager.update_sandbox_config(sandbox_config_id, config_update, actor)
88
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
89
+ return await server.sandbox_config_manager.update_sandbox_config_async(sandbox_config_id, config_update, actor)
90
90
 
91
91
 
92
92
  @router.delete("/{sandbox_config_id}", status_code=204)
@@ -112,7 +112,7 @@ async def list_sandbox_configs(
112
112
 
113
113
 
114
114
  @router.post("/local/recreate-venv", response_model=PydanticSandboxConfig)
115
- def force_recreate_local_sandbox_venv(
115
+ async def force_recreate_local_sandbox_venv(
116
116
  server: SyncServer = Depends(get_letta_server),
117
117
  actor_id: str = Depends(get_user_id),
118
118
  ):
@@ -120,10 +120,10 @@ def force_recreate_local_sandbox_venv(
120
120
  Forcefully recreate the virtual environment for the local sandbox.
121
121
  Deletes and recreates the venv, then reinstalls required dependencies.
122
122
  """
123
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
123
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
124
124
 
125
125
  # Retrieve the local sandbox config
126
- sbx_config = server.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.LOCAL, actor=actor)
126
+ sbx_config = await server.sandbox_config_manager.get_or_create_default_sandbox_config_async(sandbox_type=SandboxType.LOCAL, actor=actor)
127
127
 
128
128
  local_configs = sbx_config.get_local_config()
129
129
  sandbox_dir = os.path.expanduser(local_configs.sandbox_dir) # Expand tilde
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import os
2
3
  import tempfile
3
4
  from typing import List, Optional
@@ -21,18 +22,18 @@ router = APIRouter(prefix="/sources", tags=["sources"])
21
22
 
22
23
 
23
24
  @router.get("/count", response_model=int, operation_id="count_sources")
24
- def count_sources(
25
+ async def count_sources(
25
26
  server: "SyncServer" = Depends(get_letta_server),
26
27
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
27
28
  ):
28
29
  """
29
30
  Count all data sources created by a user.
30
31
  """
31
- return server.source_manager.size(actor=server.user_manager.get_user_or_default(user_id=actor_id))
32
+ return await server.source_manager.size(actor=server.user_manager.get_user_or_default(user_id=actor_id))
32
33
 
33
34
 
34
35
  @router.get("/{source_id}", response_model=Source, operation_id="retrieve_source")
35
- def retrieve_source(
36
+ async def retrieve_source(
36
37
  source_id: str,
37
38
  server: "SyncServer" = Depends(get_letta_server),
38
39
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -42,14 +43,14 @@ def retrieve_source(
42
43
  """
43
44
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
44
45
 
45
- source = server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
46
+ source = await server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
46
47
  if not source:
47
48
  raise HTTPException(status_code=404, detail=f"Source with id={source_id} not found.")
48
49
  return source
49
50
 
50
51
 
51
52
  @router.get("/name/{source_name}", response_model=str, operation_id="get_source_id_by_name")
52
- def get_source_id_by_name(
53
+ async def get_source_id_by_name(
53
54
  source_name: str,
54
55
  server: "SyncServer" = Depends(get_letta_server),
55
56
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -59,14 +60,14 @@ def get_source_id_by_name(
59
60
  """
60
61
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
61
62
 
62
- source = server.source_manager.get_source_by_name(source_name=source_name, actor=actor)
63
+ source = await server.source_manager.get_source_by_name(source_name=source_name, actor=actor)
63
64
  if not source:
64
65
  raise HTTPException(status_code=404, detail=f"Source with name={source_name} not found.")
65
66
  return source.id
66
67
 
67
68
 
68
69
  @router.get("/", response_model=List[Source], operation_id="list_sources")
69
- def list_sources(
70
+ async def list_sources(
70
71
  server: "SyncServer" = Depends(get_letta_server),
71
72
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
72
73
  ):
@@ -74,8 +75,7 @@ def list_sources(
74
75
  List all data sources created by a user.
75
76
  """
76
77
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
77
-
78
- return server.list_all_sources(actor=actor)
78
+ return await server.source_manager.list_sources(actor=actor)
79
79
 
80
80
 
81
81
  @router.get("/count", response_model=int, operation_id="count_sources")
@@ -90,7 +90,7 @@ def count_sources(
90
90
 
91
91
 
92
92
  @router.post("/", response_model=Source, operation_id="create_source")
93
- def create_source(
93
+ async def create_source(
94
94
  source_create: SourceCreate,
95
95
  server: "SyncServer" = Depends(get_letta_server),
96
96
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -99,6 +99,8 @@ def create_source(
99
99
  Create a new data source.
100
100
  """
101
101
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
102
+
103
+ # TODO: need to asyncify this
102
104
  if not source_create.embedding_config:
103
105
  if not source_create.embedding:
104
106
  # TODO: modify error type
@@ -115,11 +117,11 @@ def create_source(
115
117
  instructions=source_create.instructions,
116
118
  metadata=source_create.metadata,
117
119
  )
118
- return server.source_manager.create_source(source=source, actor=actor)
120
+ return await server.source_manager.create_source(source=source, actor=actor)
119
121
 
120
122
 
121
123
  @router.patch("/{source_id}", response_model=Source, operation_id="modify_source")
122
- def modify_source(
124
+ async def modify_source(
123
125
  source_id: str,
124
126
  source: SourceUpdate,
125
127
  server: "SyncServer" = Depends(get_letta_server),
@@ -130,13 +132,13 @@ def modify_source(
130
132
  """
131
133
  # TODO: allow updating the handle/embedding config
132
134
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
133
- if not server.source_manager.get_source_by_id(source_id=source_id, actor=actor):
135
+ if not await server.source_manager.get_source_by_id(source_id=source_id, actor=actor):
134
136
  raise HTTPException(status_code=404, detail=f"Source with id={source_id} does not exist.")
135
- return server.source_manager.update_source(source_id=source_id, source_update=source, actor=actor)
137
+ return await server.source_manager.update_source(source_id=source_id, source_update=source, actor=actor)
136
138
 
137
139
 
138
140
  @router.delete("/{source_id}", response_model=None, operation_id="delete_source")
139
- def delete_source(
141
+ async def delete_source(
140
142
  source_id: str,
141
143
  server: "SyncServer" = Depends(get_letta_server),
142
144
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -145,20 +147,21 @@ def delete_source(
145
147
  Delete a data source.
146
148
  """
147
149
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
148
- source = server.source_manager.get_source_by_id(source_id=source_id)
149
- agents = server.source_manager.list_attached_agents(source_id=source_id, actor=actor)
150
+ source = await server.source_manager.get_source_by_id(source_id=source_id)
151
+ agents = await server.source_manager.list_attached_agents(source_id=source_id, actor=actor)
150
152
  for agent in agents:
151
153
  if agent.enable_sleeptime:
152
154
  try:
155
+ # TODO: make async
153
156
  block = server.agent_manager.get_block_with_label(agent_id=agent.id, block_label=source.name, actor=actor)
154
157
  server.block_manager.delete_block(block.id, actor)
155
158
  except:
156
159
  pass
157
- server.delete_source(source_id=source_id, actor=actor)
160
+ await server.delete_source(source_id=source_id, actor=actor)
158
161
 
159
162
 
160
163
  @router.post("/{source_id}/upload", response_model=Job, operation_id="upload_file_to_source")
161
- def upload_file_to_source(
164
+ async def upload_file_to_source(
162
165
  file: UploadFile,
163
166
  source_id: str,
164
167
  background_tasks: BackgroundTasks,
@@ -170,7 +173,7 @@ def upload_file_to_source(
170
173
  """
171
174
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
172
175
 
173
- source = server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
176
+ source = await server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
174
177
  assert source is not None, f"Source with id={source_id} not found."
175
178
  bytes = file.file.read()
176
179
 
@@ -184,8 +187,8 @@ def upload_file_to_source(
184
187
  server.job_manager.create_job(job, actor=actor)
185
188
 
186
189
  # create background tasks
187
- background_tasks.add_task(load_file_to_source_async, server, source_id=source.id, file=file, job_id=job.id, bytes=bytes, actor=actor)
188
- background_tasks.add_task(sleeptime_document_ingest_async, server, source_id, actor)
190
+ asyncio.create_task(load_file_to_source_async(server, source_id=source.id, file=file, job_id=job.id, bytes=bytes, actor=actor))
191
+ asyncio.create_task(sleeptime_document_ingest_async(server, source_id, actor))
189
192
 
190
193
  # return job information
191
194
  # Is this necessary? Can we just return the job from create_job?
@@ -195,8 +198,11 @@ def upload_file_to_source(
195
198
 
196
199
 
197
200
  @router.get("/{source_id}/passages", response_model=List[Passage], operation_id="list_source_passages")
198
- def list_source_passages(
201
+ async def list_source_passages(
199
202
  source_id: str,
203
+ after: Optional[str] = Query(None, description="Message after which to retrieve the returned messages."),
204
+ before: Optional[str] = Query(None, description="Message before which to retrieve the returned messages."),
205
+ limit: int = Query(100, description="Maximum number of messages to retrieve."),
200
206
  server: SyncServer = Depends(get_letta_server),
201
207
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
202
208
  ):
@@ -204,12 +210,17 @@ def list_source_passages(
204
210
  List all passages associated with a data source.
205
211
  """
206
212
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
207
- passages = server.list_data_source_passages(user_id=actor.id, source_id=source_id)
208
- return passages
213
+ return await server.agent_manager.list_passages_async(
214
+ actor=actor,
215
+ source_id=source_id,
216
+ after=after,
217
+ before=before,
218
+ limit=limit,
219
+ )
209
220
 
210
221
 
211
222
  @router.get("/{source_id}/files", response_model=List[FileMetadata], operation_id="list_source_files")
212
- def list_source_files(
223
+ async def list_source_files(
213
224
  source_id: str,
214
225
  limit: int = Query(1000, description="Number of files to return"),
215
226
  after: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
@@ -220,13 +231,13 @@ def list_source_files(
220
231
  List paginated files associated with a data source.
221
232
  """
222
233
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
223
- return server.source_manager.list_files(source_id=source_id, limit=limit, after=after, actor=actor)
234
+ return await server.source_manager.list_files(source_id=source_id, limit=limit, after=after, actor=actor)
224
235
 
225
236
 
226
237
  # it's redundant to include /delete in the URL path. The HTTP verb DELETE already implies that action.
227
238
  # it's still good practice to return a status indicating the success or failure of the deletion
228
239
  @router.delete("/{source_id}/{file_id}", status_code=204, operation_id="delete_file_from_source")
229
- def delete_file_from_source(
240
+ async def delete_file_from_source(
230
241
  source_id: str,
231
242
  file_id: str,
232
243
  background_tasks: BackgroundTasks,
@@ -238,13 +249,15 @@ def delete_file_from_source(
238
249
  """
239
250
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
240
251
 
241
- deleted_file = server.source_manager.delete_file(file_id=file_id, actor=actor)
242
- background_tasks.add_task(sleeptime_document_ingest_async, server, source_id, actor, clear_history=True)
252
+ deleted_file = await server.source_manager.delete_file(file_id=file_id, actor=actor)
253
+
254
+ # TODO: make async
255
+ asyncio.create_task(sleeptime_document_ingest_async(server, source_id, actor, clear_history=True))
243
256
  if deleted_file is None:
244
257
  raise HTTPException(status_code=404, detail=f"File with id={file_id} not found.")
245
258
 
246
259
 
247
- def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str, file: UploadFile, bytes: bytes, actor: User):
260
+ async def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str, file: UploadFile, bytes: bytes, actor: User):
248
261
  # Create a temporary directory (deleted after the context manager exits)
249
262
  with tempfile.TemporaryDirectory() as tmpdirname:
250
263
  # Sanitize the filename
@@ -256,12 +269,12 @@ def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str, f
256
269
  buffer.write(bytes)
257
270
 
258
271
  # Pass the file to load_file_to_source
259
- server.load_file_to_source(source_id, file_path, job_id, actor)
272
+ await server.load_file_to_source(source_id, file_path, job_id, actor)
260
273
 
261
274
 
262
- def sleeptime_document_ingest_async(server: SyncServer, source_id: str, actor: User, clear_history: bool = False):
263
- source = server.source_manager.get_source_by_id(source_id=source_id)
264
- agents = server.source_manager.list_attached_agents(source_id=source_id, actor=actor)
275
+ async def sleeptime_document_ingest_async(server: SyncServer, source_id: str, actor: User, clear_history: bool = False):
276
+ source = await server.source_manager.get_source_by_id(source_id=source_id)
277
+ agents = await server.source_manager.list_attached_agents(source_id=source_id, actor=actor)
265
278
  for agent in agents:
266
279
  if agent.enable_sleeptime:
267
- server.sleeptime_document_ingest(agent, source, actor, clear_history)
280
+ server.sleeptime_document_ingest(agent, source, actor, clear_history) # TODO: make async
letta/server/server.py CHANGED
@@ -50,7 +50,7 @@ from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage, ToolRe
50
50
  from letta.schemas.letta_message_content import TextContent
51
51
  from letta.schemas.letta_response import LettaResponse
52
52
  from letta.schemas.llm_config import LLMConfig
53
- from letta.schemas.memory import ArchivalMemorySummary, ContextWindowOverview, Memory, RecallMemorySummary
53
+ from letta.schemas.memory import ArchivalMemorySummary, Memory, RecallMemorySummary
54
54
  from letta.schemas.message import Message, MessageCreate, MessageUpdate
55
55
  from letta.schemas.organization import Organization
56
56
  from letta.schemas.passage import Passage, PassageUpdate
@@ -969,6 +969,11 @@ class SyncServer(Server):
969
969
  """Return the memory of an agent (core memory)"""
970
970
  return self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor).memory
971
971
 
972
+ async def get_agent_memory_async(self, agent_id: str, actor: User) -> Memory:
973
+ """Return the memory of an agent (core memory)"""
974
+ agent = await self.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor)
975
+ return agent.memory
976
+
972
977
  def get_archival_memory_summary(self, agent_id: str, actor: User) -> ArchivalMemorySummary:
973
978
  return ArchivalMemorySummary(size=self.agent_manager.passage_size(actor=actor, agent_id=agent_id))
974
979
 
@@ -1169,17 +1174,20 @@ class SyncServer(Server):
1169
1174
  # rebuild system prompt for agent, potentially changed
1170
1175
  return self.agent_manager.rebuild_system_prompt(agent_id=agent_id, actor=actor).memory
1171
1176
 
1172
- def delete_source(self, source_id: str, actor: User):
1177
+ async def delete_source(self, source_id: str, actor: User):
1173
1178
  """Delete a data source"""
1174
- self.source_manager.delete_source(source_id=source_id, actor=actor)
1179
+ await self.source_manager.delete_source(source_id=source_id, actor=actor)
1175
1180
 
1176
1181
  # delete data from passage store
1182
+ # TODO: make async
1177
1183
  passages_to_be_deleted = self.agent_manager.list_passages(actor=actor, source_id=source_id, limit=None)
1184
+
1185
+ # TODO: make this async
1178
1186
  self.passage_manager.delete_passages(actor=actor, passages=passages_to_be_deleted)
1179
1187
 
1180
1188
  # TODO: delete data from agent passage stores (?)
1181
1189
 
1182
- def load_file_to_source(self, source_id: str, file_path: str, job_id: str, actor: User) -> Job:
1190
+ async def load_file_to_source(self, source_id: str, file_path: str, job_id: str, actor: User) -> Job:
1183
1191
 
1184
1192
  # update job
1185
1193
  job = self.job_manager.get_job_by_id(job_id, actor=actor)
@@ -1189,21 +1197,22 @@ class SyncServer(Server):
1189
1197
  # try:
1190
1198
  from letta.data_sources.connectors import DirectoryConnector
1191
1199
 
1192
- source = self.source_manager.get_source_by_id(source_id=source_id)
1200
+ # TODO: move this into a thread
1201
+ source = await self.source_manager.get_source_by_id(source_id=source_id)
1193
1202
  if source is None:
1194
1203
  raise ValueError(f"Source {source_id} does not exist")
1195
1204
  connector = DirectoryConnector(input_files=[file_path])
1196
- num_passages, num_documents = self.load_data(user_id=source.created_by_id, source_name=source.name, connector=connector)
1205
+ num_passages, num_documents = await self.load_data(user_id=source.created_by_id, source_name=source.name, connector=connector)
1197
1206
 
1198
1207
  # update all agents who have this source attached
1199
- agent_states = self.source_manager.list_attached_agents(source_id=source_id, actor=actor)
1208
+ agent_states = await self.source_manager.list_attached_agents(source_id=source_id, actor=actor)
1200
1209
  for agent_state in agent_states:
1201
1210
  agent_id = agent_state.id
1202
1211
 
1203
1212
  # Attach source to agent
1204
- curr_passage_size = self.agent_manager.passage_size(actor=actor, agent_id=agent_id)
1213
+ curr_passage_size = await self.agent_manager.passage_size_async(actor=actor, agent_id=agent_id)
1205
1214
  agent_state = self.agent_manager.attach_source(agent_id=agent_state.id, source_id=source_id, actor=actor)
1206
- new_passage_size = self.agent_manager.passage_size(actor=actor, agent_id=agent_id)
1215
+ new_passage_size = await self.agent_manager.passage_size_async(actor=actor, agent_id=agent_id)
1207
1216
  assert new_passage_size >= curr_passage_size # in case empty files are added
1208
1217
 
1209
1218
  # rebuild system prompt and force
@@ -1266,7 +1275,7 @@ class SyncServer(Server):
1266
1275
  actor=actor,
1267
1276
  )
1268
1277
 
1269
- def load_data(
1278
+ async def load_data(
1270
1279
  self,
1271
1280
  user_id: str,
1272
1281
  connector: DataConnector,
@@ -1277,12 +1286,12 @@ class SyncServer(Server):
1277
1286
 
1278
1287
  # load data from a data source into the document store
1279
1288
  user = self.user_manager.get_user_by_id(user_id=user_id)
1280
- source = self.source_manager.get_source_by_name(source_name=source_name, actor=user)
1289
+ source = await self.source_manager.get_source_by_name(source_name=source_name, actor=user)
1281
1290
  if source is None:
1282
1291
  raise ValueError(f"Data source {source_name} does not exist for user {user_id}")
1283
1292
 
1284
1293
  # load data into the document store
1285
- passage_count, document_count = load_data(connector, source, self.passage_manager, self.source_manager, actor=user)
1294
+ passage_count, document_count = await load_data(connector, source, self.passage_manager, self.source_manager, actor=user)
1286
1295
  return passage_count, document_count
1287
1296
 
1288
1297
  def list_data_source_passages(self, user_id: str, source_id: str) -> List[Passage]:
@@ -1290,6 +1299,7 @@ class SyncServer(Server):
1290
1299
  return self.agent_manager.list_passages(actor=self.user_manager.get_user_or_default(user_id=user_id), source_id=source_id)
1291
1300
 
1292
1301
  def list_all_sources(self, actor: User) -> List[Source]:
1302
+ # TODO: legacy: remove
1293
1303
  """List all sources (w/ extra metadata) belonging to a user"""
1294
1304
 
1295
1305
  sources = self.source_manager.list_sources(actor=actor)
@@ -1376,7 +1386,7 @@ class SyncServer(Server):
1376
1386
  """Asynchronously list available models with maximum concurrency"""
1377
1387
  import asyncio
1378
1388
 
1379
- providers = self.get_enabled_providers(
1389
+ providers = await self.get_enabled_providers_async(
1380
1390
  provider_category=provider_category,
1381
1391
  provider_name=provider_name,
1382
1392
  provider_type=provider_type,
@@ -1422,7 +1432,7 @@ class SyncServer(Server):
1422
1432
  import asyncio
1423
1433
 
1424
1434
  # Get all eligible providers first
1425
- providers = self.get_enabled_providers(actor=actor)
1435
+ providers = await self.get_enabled_providers_async(actor=actor)
1426
1436
 
1427
1437
  # Fetch embedding models from each provider concurrently
1428
1438
  async def get_provider_embedding_models(provider):
@@ -1475,6 +1485,35 @@ class SyncServer(Server):
1475
1485
 
1476
1486
  return providers
1477
1487
 
1488
+ async def get_enabled_providers_async(
1489
+ self,
1490
+ actor: User,
1491
+ provider_category: Optional[List[ProviderCategory]] = None,
1492
+ provider_name: Optional[str] = None,
1493
+ provider_type: Optional[ProviderType] = None,
1494
+ ) -> List[Provider]:
1495
+ providers = []
1496
+ if not provider_category or ProviderCategory.base in provider_category:
1497
+ providers_from_env = [p for p in self._enabled_providers]
1498
+ providers.extend(providers_from_env)
1499
+
1500
+ if not provider_category or ProviderCategory.byok in provider_category:
1501
+ providers_from_db = await self.provider_manager.list_providers_async(
1502
+ name=provider_name,
1503
+ provider_type=provider_type,
1504
+ actor=actor,
1505
+ )
1506
+ providers_from_db = [p.cast_to_subtype() for p in providers_from_db]
1507
+ providers.extend(providers_from_db)
1508
+
1509
+ if provider_name is not None:
1510
+ providers = [p for p in providers if p.name == provider_name]
1511
+
1512
+ if provider_type is not None:
1513
+ providers = [p for p in providers if p.provider_type == provider_type]
1514
+
1515
+ return providers
1516
+
1478
1517
  @trace_method
1479
1518
  def get_llm_config_from_handle(
1480
1519
  self,
@@ -1613,14 +1652,6 @@ class SyncServer(Server):
1613
1652
  def add_embedding_model(self, request: EmbeddingConfig) -> EmbeddingConfig:
1614
1653
  """Add a new embedding model"""
1615
1654
 
1616
- def get_agent_context_window(self, agent_id: str, actor: User) -> ContextWindowOverview:
1617
- letta_agent = self.load_agent(agent_id=agent_id, actor=actor)
1618
- return letta_agent.get_context_window()
1619
-
1620
- async def get_agent_context_window_async(self, agent_id: str, actor: User) -> ContextWindowOverview:
1621
- letta_agent = self.load_agent(agent_id=agent_id, actor=actor)
1622
- return await letta_agent.get_context_window_async()
1623
-
1624
1655
  def run_tool_from_source(
1625
1656
  self,
1626
1657
  actor: User,