letta-nightly 0.7.15.dev20250515104317__py3-none-any.whl → 0.7.17.dev20250516090339__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 (43) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +12 -0
  3. letta/agents/helpers.py +48 -5
  4. letta/agents/letta_agent.py +64 -28
  5. letta/agents/letta_agent_batch.py +44 -26
  6. letta/agents/voice_sleeptime_agent.py +6 -4
  7. letta/client/client.py +16 -1
  8. letta/constants.py +3 -0
  9. letta/functions/async_composio_toolset.py +1 -1
  10. letta/interfaces/anthropic_streaming_interface.py +40 -6
  11. letta/interfaces/openai_streaming_interface.py +303 -0
  12. letta/jobs/llm_batch_job_polling.py +6 -2
  13. letta/orm/agent.py +102 -1
  14. letta/orm/block.py +3 -0
  15. letta/orm/sqlalchemy_base.py +459 -158
  16. letta/schemas/agent.py +10 -2
  17. letta/schemas/block.py +3 -0
  18. letta/schemas/memory.py +7 -2
  19. letta/server/rest_api/routers/v1/agents.py +29 -27
  20. letta/server/rest_api/routers/v1/blocks.py +1 -1
  21. letta/server/rest_api/routers/v1/groups.py +2 -2
  22. letta/server/rest_api/routers/v1/messages.py +11 -11
  23. letta/server/rest_api/routers/v1/runs.py +2 -2
  24. letta/server/rest_api/routers/v1/tools.py +4 -4
  25. letta/server/rest_api/routers/v1/users.py +9 -9
  26. letta/server/rest_api/routers/v1/voice.py +1 -1
  27. letta/server/server.py +74 -0
  28. letta/services/agent_manager.py +417 -7
  29. letta/services/block_manager.py +12 -8
  30. letta/services/helpers/agent_manager_helper.py +19 -0
  31. letta/services/job_manager.py +99 -0
  32. letta/services/llm_batch_manager.py +28 -27
  33. letta/services/message_manager.py +66 -19
  34. letta/services/passage_manager.py +14 -0
  35. letta/services/tool_executor/tool_executor.py +19 -1
  36. letta/services/tool_manager.py +13 -3
  37. letta/services/user_manager.py +70 -0
  38. letta/types/__init__.py +0 -0
  39. {letta_nightly-0.7.15.dev20250515104317.dist-info → letta_nightly-0.7.17.dev20250516090339.dist-info}/METADATA +3 -3
  40. {letta_nightly-0.7.15.dev20250515104317.dist-info → letta_nightly-0.7.17.dev20250516090339.dist-info}/RECORD +43 -41
  41. {letta_nightly-0.7.15.dev20250515104317.dist-info → letta_nightly-0.7.17.dev20250516090339.dist-info}/LICENSE +0 -0
  42. {letta_nightly-0.7.15.dev20250515104317.dist-info → letta_nightly-0.7.17.dev20250516090339.dist-info}/WHEEL +0 -0
  43. {letta_nightly-0.7.15.dev20250515104317.dist-info → letta_nightly-0.7.17.dev20250516090339.dist-info}/entry_points.txt +0 -0
letta/schemas/agent.py CHANGED
@@ -312,9 +312,17 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
312
312
  )
313
313
  return (
314
314
  "{% for block in blocks %}"
315
- '<{{ block.label }} characters="{{ block.value|length }}/{{ block.limit }}">\n'
315
+ "<{{ block.label }}>\n"
316
+ "<description>\n"
317
+ "{{ block.description }}\n"
318
+ "</description>\n"
319
+ "<metadata>\n"
320
+ '{% if block.read_only %}read_only="true" {% endif %}chars_current="{{ block.value|length }}" chars_limit="{{ block.limit }}"\n'
321
+ "</metadata>\n"
322
+ "<value>\n"
316
323
  "{{ block.value }}\n"
317
- "</{{ block.label }}>"
324
+ "</value>\n"
325
+ "</{{ block.label }}>\n"
318
326
  "{% if not loop.last %}\n{% endif %}"
319
327
  "{% endfor %}"
320
328
  )
letta/schemas/block.py CHANGED
@@ -25,6 +25,9 @@ class BaseBlock(LettaBase, validate_assignment=True):
25
25
  # context window label
26
26
  label: Optional[str] = Field(None, description="Label of the block (e.g. 'human', 'persona') in the context window.")
27
27
 
28
+ # permissions of the agent
29
+ read_only: bool = Field(False, description="Whether the agent has read-only access to the block.")
30
+
28
31
  # metadata
29
32
  description: Optional[str] = Field(None, description="Description of the block.")
30
33
  metadata: Optional[dict] = Field({}, description="Metadata of the block.")
letta/schemas/memory.py CHANGED
@@ -69,9 +69,14 @@ class Memory(BaseModel, validate_assignment=True):
69
69
  # Memory.template is a Jinja2 template for compiling memory module into a prompt string.
70
70
  prompt_template: str = Field(
71
71
  default="{% for block in blocks %}"
72
- '<{{ block.label }} characters="{{ block.value|length }}/{{ block.limit }}">\n'
72
+ "<{{ block.label }}>\n"
73
+ "<metadata>"
74
+ 'read_only="{{ block.read_only}}" chars_current="{{ block.value|length }}" chars_limit="{{ block.limit }}"'
75
+ "</metadata>"
76
+ "<value>"
73
77
  "{{ block.value }}\n"
74
- "</{{ block.label }}>"
78
+ "</value>"
79
+ "</{{ block.label }}>\n"
75
80
  "{% if not loop.last %}\n{% endif %}"
76
81
  "{% endfor %}",
77
82
  description="Jinja2 template for compiling memory blocks into a prompt string",
@@ -44,7 +44,7 @@ logger = get_logger(__name__)
44
44
 
45
45
 
46
46
  @router.get("/", response_model=List[AgentState], operation_id="list_agents")
47
- def list_agents(
47
+ async def list_agents(
48
48
  name: Optional[str] = Query(None, description="Name of the agent"),
49
49
  tags: Optional[List[str]] = Query(None, description="List of tags to filter agents by"),
50
50
  match_all_tags: bool = Query(
@@ -83,10 +83,10 @@ def list_agents(
83
83
  """
84
84
 
85
85
  # Retrieve the actor (user) details
86
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
86
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
87
87
 
88
88
  # Call list_agents directly without unnecessary dict handling
89
- return server.agent_manager.list_agents(
89
+ return await server.agent_manager.list_agents_async(
90
90
  actor=actor,
91
91
  name=name,
92
92
  before=before,
@@ -163,7 +163,7 @@ async def import_agent_serialized(
163
163
  """
164
164
  Import a serialized agent file and recreate the agent in the system.
165
165
  """
166
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
166
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
167
167
 
168
168
  try:
169
169
  serialized_data = await file.read()
@@ -223,7 +223,7 @@ class CreateAgentRequest(CreateAgent):
223
223
 
224
224
 
225
225
  @router.post("/", response_model=AgentState, operation_id="create_agent")
226
- def create_agent(
226
+ async def create_agent(
227
227
  agent: CreateAgentRequest = Body(...),
228
228
  server: "SyncServer" = Depends(get_letta_server),
229
229
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -233,23 +233,23 @@ def create_agent(
233
233
  Create a new agent with the specified configuration.
234
234
  """
235
235
  try:
236
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
237
- return server.create_agent(agent, actor=actor)
236
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
237
+ return await server.create_agent_async(agent, actor=actor)
238
238
  except Exception as e:
239
239
  traceback.print_exc()
240
240
  raise HTTPException(status_code=500, detail=str(e))
241
241
 
242
242
 
243
243
  @router.patch("/{agent_id}", response_model=AgentState, operation_id="modify_agent")
244
- def modify_agent(
244
+ async def modify_agent(
245
245
  agent_id: str,
246
246
  update_agent: UpdateAgent = Body(...),
247
247
  server: "SyncServer" = Depends(get_letta_server),
248
248
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
249
249
  ):
250
250
  """Update an existing agent"""
251
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
252
- return server.update_agent(agent_id=agent_id, request=update_agent, actor=actor)
251
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
252
+ return await server.update_agent_async(agent_id=agent_id, request=update_agent, actor=actor)
253
253
 
254
254
 
255
255
  @router.get("/{agent_id}/tools", response_model=List[Tool], operation_id="list_agent_tools")
@@ -333,7 +333,7 @@ def detach_source(
333
333
 
334
334
 
335
335
  @router.get("/{agent_id}", response_model=AgentState, operation_id="retrieve_agent")
336
- def retrieve_agent(
336
+ async def retrieve_agent(
337
337
  agent_id: str,
338
338
  server: "SyncServer" = Depends(get_letta_server),
339
339
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -344,7 +344,7 @@ def retrieve_agent(
344
344
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
345
345
 
346
346
  try:
347
- return server.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor)
347
+ return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor)
348
348
  except NoResultFound as e:
349
349
  raise HTTPException(status_code=404, detail=str(e))
350
350
 
@@ -414,7 +414,7 @@ def retrieve_block(
414
414
 
415
415
 
416
416
  @router.get("/{agent_id}/core-memory/blocks", response_model=List[Block], operation_id="list_core_memory_blocks")
417
- def list_blocks(
417
+ async def list_blocks(
418
418
  agent_id: str,
419
419
  server: "SyncServer" = Depends(get_letta_server),
420
420
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
@@ -424,7 +424,7 @@ def list_blocks(
424
424
  """
425
425
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
426
426
  try:
427
- agent = server.agent_manager.get_agent_by_id(agent_id, actor)
427
+ agent = await server.agent_manager.get_agent_by_id_async(agent_id, actor)
428
428
  return agent.memory.blocks
429
429
  except NoResultFound as e:
430
430
  raise HTTPException(status_code=404, detail=str(e))
@@ -628,12 +628,12 @@ async def send_message(
628
628
  Process a user message and return the agent's response.
629
629
  This endpoint accepts a message from a user and processes it through the agent.
630
630
  """
631
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
631
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
632
632
  # TODO: This is redundant, remove soon
633
- agent = server.agent_manager.get_agent_by_id(agent_id, actor)
633
+ agent = await server.agent_manager.get_agent_by_id_async(agent_id, actor)
634
634
  agent_eligible = not agent.enable_sleeptime and not agent.multi_agent_group and agent.agent_type != AgentType.sleeptime_agent
635
- experimental_header = request_obj.headers.get("x-experimental")
636
- feature_enabled = settings.use_experimental or experimental_header
635
+ experimental_header = request_obj.headers.get("X-EXPERIMENTAL") or "false"
636
+ feature_enabled = settings.use_experimental or experimental_header.lower() == "true"
637
637
  model_compatible = agent.llm_config.model_endpoint_type in ["anthropic", "openai", "google_vertex", "google_ai"]
638
638
 
639
639
  if agent_eligible and feature_enabled and model_compatible:
@@ -646,7 +646,7 @@ async def send_message(
646
646
  actor=actor,
647
647
  )
648
648
 
649
- result = await experimental_agent.step(request.messages, max_steps=10)
649
+ result = await experimental_agent.step(request.messages, max_steps=10, use_assistant_message=request.use_assistant_message)
650
650
  else:
651
651
  result = await server.send_message_to_agent(
652
652
  agent_id=agent_id,
@@ -686,15 +686,15 @@ async def send_message_streaming(
686
686
  It will stream the steps of the response always, and stream the tokens if 'stream_tokens' is set to True.
687
687
  """
688
688
  request_start_timestamp_ns = get_utc_timestamp_ns()
689
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
689
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
690
690
  # TODO: This is redundant, remove soon
691
- agent = server.agent_manager.get_agent_by_id(agent_id, actor)
691
+ agent = await server.agent_manager.get_agent_by_id_async(agent_id, actor)
692
692
  agent_eligible = not agent.enable_sleeptime and not agent.multi_agent_group and agent.agent_type != AgentType.sleeptime_agent
693
- experimental_header = request_obj.headers.get("x-experimental")
694
- feature_enabled = settings.use_experimental or experimental_header
693
+ experimental_header = request_obj.headers.get("X-EXPERIMENTAL") or "false"
694
+ feature_enabled = settings.use_experimental or experimental_header.lower() == "true"
695
695
  model_compatible = agent.llm_config.model_endpoint_type in ["anthropic", "openai"]
696
696
 
697
- if agent_eligible and feature_enabled and model_compatible:
697
+ if agent_eligible and feature_enabled and model_compatible and request.stream_tokens:
698
698
  experimental_agent = LettaAgent(
699
699
  agent_id=agent_id,
700
700
  message_manager=server.message_manager,
@@ -705,7 +705,9 @@ async def send_message_streaming(
705
705
  )
706
706
 
707
707
  result = StreamingResponse(
708
- experimental_agent.step_stream(request.messages, max_steps=10, use_assistant_message=request.use_assistant_message),
708
+ experimental_agent.step_stream(
709
+ request.messages, max_steps=10, use_assistant_message=request.use_assistant_message, stream_tokens=request.stream_tokens
710
+ ),
709
711
  media_type="text/event-stream",
710
712
  )
711
713
  else:
@@ -784,7 +786,7 @@ async def send_message_async(
784
786
  Asynchronously process a user message and return a run object.
785
787
  The actual processing happens in the background, and the status can be checked using the run ID.
786
788
  """
787
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
789
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
788
790
 
789
791
  # Create a new job
790
792
  run = Run(
@@ -838,6 +840,6 @@ async def list_agent_groups(
838
840
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
839
841
  ):
840
842
  """Lists the groups for an agent"""
841
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
843
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
842
844
  print("in list agents with manager_type", manager_type)
843
845
  return server.agent_manager.list_groups(agent_id=agent_id, manager_type=manager_type, actor=actor)
@@ -26,7 +26,7 @@ async def list_blocks(
26
26
  server: SyncServer = Depends(get_letta_server),
27
27
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
28
28
  ):
29
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
29
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
30
30
  return await server.block_manager.get_blocks_async(
31
31
  actor=actor,
32
32
  label=label,
@@ -135,7 +135,7 @@ async def send_group_message(
135
135
  Process a user message and return the group's response.
136
136
  This endpoint accepts a message from a user and processes it through through agents in the group based on the specified pattern
137
137
  """
138
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
138
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
139
139
  result = await server.send_group_message_to_agent(
140
140
  group_id=group_id,
141
141
  actor=actor,
@@ -174,7 +174,7 @@ async def send_group_message_streaming(
174
174
  This endpoint accepts a message from a user and processes it through agents in the group based on the specified pattern.
175
175
  It will stream the steps of the response always, and stream the tokens if 'stream_tokens' is set to True.
176
176
  """
177
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
177
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
178
178
  result = await server.send_group_message_to_agent(
179
179
  group_id=group_id,
180
180
  actor=actor,
@@ -52,7 +52,7 @@ async def create_messages_batch(
52
52
  detail=f"Server misconfiguration: LETTA_ENABLE_BATCH_JOB_POLLING is set to False.",
53
53
  )
54
54
 
55
- 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)
56
56
  batch_job = BatchJob(
57
57
  user_id=actor.id,
58
58
  status=JobStatus.running,
@@ -63,7 +63,7 @@ async def create_messages_batch(
63
63
  )
64
64
 
65
65
  try:
66
- batch_job = server.job_manager.create_job(pydantic_job=batch_job, actor=actor)
66
+ batch_job = await server.job_manager.create_job_async(pydantic_job=batch_job, actor=actor)
67
67
 
68
68
  # create the batch runner
69
69
  batch_runner = LettaAgentBatch(
@@ -86,7 +86,7 @@ async def create_messages_batch(
86
86
  traceback.print_exc()
87
87
 
88
88
  # mark job as failed
89
- server.job_manager.update_job_by_id(job_id=batch_job.id, job=BatchJob(status=JobStatus.failed), actor=actor)
89
+ await server.job_manager.update_job_by_id_async(job_id=batch_job.id, job_update=JobUpdate(status=JobStatus.failed), actor=actor)
90
90
  raise
91
91
  return batch_job
92
92
 
@@ -100,10 +100,10 @@ async def retrieve_batch_run(
100
100
  """
101
101
  Get the status of a batch run.
102
102
  """
103
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
103
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
104
104
 
105
105
  try:
106
- job = server.job_manager.get_job_by_id(job_id=batch_id, actor=actor)
106
+ job = await server.job_manager.get_job_by_id_async(job_id=batch_id, actor=actor)
107
107
  return BatchJob.from_job(job)
108
108
  except NoResultFound:
109
109
  raise HTTPException(status_code=404, detail="Batch not found")
@@ -118,7 +118,7 @@ async def list_batch_runs(
118
118
  List all batch runs.
119
119
  """
120
120
  # TODO: filter
121
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
121
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
122
122
 
123
123
  jobs = server.job_manager.list_jobs(actor=actor, statuses=[JobStatus.created, JobStatus.running], job_type=JobType.BATCH)
124
124
  return [BatchJob.from_job(job) for job in jobs]
@@ -150,11 +150,11 @@ async def list_batch_messages(
150
150
  - For subsequent pages, use the ID of the last message from the previous response as the cursor
151
151
  - Results will include messages before/after the cursor based on sort_descending
152
152
  """
153
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
153
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
154
154
 
155
155
  # First, verify the batch job exists and the user has access to it
156
156
  try:
157
- job = server.job_manager.get_job_by_id(job_id=batch_id, actor=actor)
157
+ job = await server.job_manager.get_job_by_id_async(job_id=batch_id, actor=actor)
158
158
  BatchJob.from_job(job)
159
159
  except NoResultFound:
160
160
  raise HTTPException(status_code=404, detail="Batch not found")
@@ -177,11 +177,11 @@ async def cancel_batch_run(
177
177
  """
178
178
  Cancel a batch run.
179
179
  """
180
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
180
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
181
181
 
182
182
  try:
183
- job = server.job_manager.get_job_by_id(job_id=batch_id, actor=actor)
184
- job = server.job_manager.update_job_by_id(job_id=job.id, job_update=JobUpdate(status=JobStatus.cancelled), actor=actor)
183
+ job = await server.job_manager.get_job_by_id_async(job_id=batch_id, actor=actor)
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
187
  llm_batch_jobs = server.batch_manager.list_llm_batch_jobs(letta_batch_id=job.id, actor=actor)
@@ -115,7 +115,7 @@ async def list_run_messages(
115
115
  if order not in ["asc", "desc"]:
116
116
  raise HTTPException(status_code=400, detail="Order must be 'asc' or 'desc'")
117
117
 
118
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
118
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
119
119
 
120
120
  try:
121
121
  messages = server.job_manager.get_run_messages(
@@ -182,7 +182,7 @@ async def list_run_steps(
182
182
  if order not in ["asc", "desc"]:
183
183
  raise HTTPException(status_code=400, detail="Order must be 'asc' or 'desc'")
184
184
 
185
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
185
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
186
186
 
187
187
  try:
188
188
  steps = server.job_manager.get_job_steps(
@@ -76,7 +76,7 @@ def retrieve_tool(
76
76
 
77
77
 
78
78
  @router.get("/", response_model=List[Tool], operation_id="list_tools")
79
- def list_tools(
79
+ async def list_tools(
80
80
  after: Optional[str] = None,
81
81
  limit: Optional[int] = 50,
82
82
  name: Optional[str] = None,
@@ -87,11 +87,11 @@ def list_tools(
87
87
  Get a list of all tools available to agents belonging to the org of the user
88
88
  """
89
89
  try:
90
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
90
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
91
91
  if name is not None:
92
- tool = server.tool_manager.get_tool_by_name(tool_name=name, actor=actor)
92
+ tool = await server.tool_manager.get_tool_by_name_async(tool_name=name, actor=actor)
93
93
  return [tool] if tool else []
94
- return server.tool_manager.list_tools(actor=actor, after=after, limit=limit)
94
+ return await server.tool_manager.list_tools_async(actor=actor, after=after, limit=limit)
95
95
  except Exception as e:
96
96
  # Log or print the full exception here for debugging
97
97
  print(f"Error occurred: {e}")
@@ -14,7 +14,7 @@ router = APIRouter(prefix="/users", tags=["users", "admin"])
14
14
 
15
15
 
16
16
  @router.get("/", tags=["admin"], response_model=List[User], operation_id="list_users")
17
- def list_users(
17
+ async def list_users(
18
18
  after: Optional[str] = Query(None),
19
19
  limit: Optional[int] = Query(50),
20
20
  server: "SyncServer" = Depends(get_letta_server),
@@ -23,7 +23,7 @@ def list_users(
23
23
  Get a list of all users in the database
24
24
  """
25
25
  try:
26
- users = server.user_manager.list_users(after=after, limit=limit)
26
+ users = await server.user_manager.list_actors_async(after=after, limit=limit)
27
27
  except HTTPException:
28
28
  raise
29
29
  except Exception as e:
@@ -32,7 +32,7 @@ def list_users(
32
32
 
33
33
 
34
34
  @router.post("/", tags=["admin"], response_model=User, operation_id="create_user")
35
- def create_user(
35
+ async def create_user(
36
36
  request: UserCreate = Body(...),
37
37
  server: "SyncServer" = Depends(get_letta_server),
38
38
  ):
@@ -40,33 +40,33 @@ def create_user(
40
40
  Create a new user in the database
41
41
  """
42
42
  user = User(**request.model_dump())
43
- user = server.user_manager.create_user(user)
43
+ user = await server.user_manager.create_actor_async(user)
44
44
  return user
45
45
 
46
46
 
47
47
  @router.put("/", tags=["admin"], response_model=User, operation_id="update_user")
48
- def update_user(
48
+ async def update_user(
49
49
  user: UserUpdate = Body(...),
50
50
  server: "SyncServer" = Depends(get_letta_server),
51
51
  ):
52
52
  """
53
53
  Update a user in the database
54
54
  """
55
- user = server.user_manager.update_user(user)
55
+ user = await server.user_manager.update_actor_async(user)
56
56
  return user
57
57
 
58
58
 
59
59
  @router.delete("/", tags=["admin"], response_model=User, operation_id="delete_user")
60
- def delete_user(
60
+ async def delete_user(
61
61
  user_id: str = Query(..., description="The user_id key to be deleted."),
62
62
  server: "SyncServer" = Depends(get_letta_server),
63
63
  ):
64
64
  # TODO make a soft deletion, instead of a hard deletion
65
65
  try:
66
- user = server.user_manager.get_user_by_id(user_id=user_id)
66
+ user = await server.user_manager.get_actor_by_id_async(actor_id=user_id)
67
67
  if user is None:
68
68
  raise HTTPException(status_code=404, detail=f"User does not exist")
69
- server.user_manager.delete_user_by_id(user_id=user_id)
69
+ await server.user_manager.delete_actor_by_id_async(user_id=user_id)
70
70
  except HTTPException:
71
71
  raise
72
72
  except Exception as e:
@@ -36,7 +36,7 @@ async def create_voice_chat_completions(
36
36
  server: "SyncServer" = Depends(get_letta_server),
37
37
  user_id: Optional[str] = Header(None, alias="user_id"),
38
38
  ):
39
- actor = server.user_manager.get_user_or_default(user_id=user_id)
39
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=user_id)
40
40
 
41
41
  # Create OpenAI async client
42
42
  client = openai.AsyncClient(
letta/server/server.py CHANGED
@@ -794,6 +794,54 @@ class SyncServer(Server):
794
794
 
795
795
  return main_agent
796
796
 
797
+ @trace_method
798
+ async def create_agent_async(
799
+ self,
800
+ request: CreateAgent,
801
+ actor: User,
802
+ # interface
803
+ interface: Union[AgentInterface, None] = None,
804
+ ) -> AgentState:
805
+ if request.llm_config is None:
806
+ if request.model is None:
807
+ raise ValueError("Must specify either model or llm_config in request")
808
+ config_params = {
809
+ "handle": request.model,
810
+ "context_window_limit": request.context_window_limit,
811
+ "max_tokens": request.max_tokens,
812
+ "max_reasoning_tokens": request.max_reasoning_tokens,
813
+ "enable_reasoner": request.enable_reasoner,
814
+ }
815
+ log_event(name="start get_cached_llm_config", attributes=config_params)
816
+ request.llm_config = self.get_cached_llm_config(actor=actor, **config_params)
817
+ log_event(name="end get_cached_llm_config", attributes=config_params)
818
+
819
+ if request.embedding_config is None:
820
+ if request.embedding is None:
821
+ raise ValueError("Must specify either embedding or embedding_config in request")
822
+ embedding_config_params = {
823
+ "handle": request.embedding,
824
+ "embedding_chunk_size": request.embedding_chunk_size or constants.DEFAULT_EMBEDDING_CHUNK_SIZE,
825
+ }
826
+ log_event(name="start get_cached_embedding_config", attributes=embedding_config_params)
827
+ request.embedding_config = self.get_cached_embedding_config(actor=actor, **embedding_config_params)
828
+ log_event(name="end get_cached_embedding_config", attributes=embedding_config_params)
829
+
830
+ log_event(name="start create_agent db")
831
+ main_agent = await self.agent_manager.create_agent_async(
832
+ agent_create=request,
833
+ actor=actor,
834
+ )
835
+ log_event(name="end create_agent db")
836
+
837
+ if request.enable_sleeptime:
838
+ if request.agent_type == AgentType.voice_convo_agent:
839
+ main_agent = self.create_voice_sleeptime_agent(main_agent=main_agent, actor=actor)
840
+ else:
841
+ main_agent = self.create_sleeptime_agent(main_agent=main_agent, actor=actor)
842
+
843
+ return main_agent
844
+
797
845
  def update_agent(
798
846
  self,
799
847
  agent_id: str,
@@ -820,6 +868,32 @@ class SyncServer(Server):
820
868
  actor=actor,
821
869
  )
822
870
 
871
+ async def update_agent_async(
872
+ self,
873
+ agent_id: str,
874
+ request: UpdateAgent,
875
+ actor: User,
876
+ ) -> AgentState:
877
+ if request.model is not None:
878
+ request.llm_config = self.get_llm_config_from_handle(handle=request.model, actor=actor)
879
+
880
+ if request.embedding is not None:
881
+ request.embedding_config = self.get_embedding_config_from_handle(handle=request.embedding, actor=actor)
882
+
883
+ if request.enable_sleeptime:
884
+ agent = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor)
885
+ if agent.multi_agent_group is None:
886
+ if agent.agent_type == AgentType.voice_convo_agent:
887
+ self.create_voice_sleeptime_agent(main_agent=agent, actor=actor)
888
+ else:
889
+ self.create_sleeptime_agent(main_agent=agent, actor=actor)
890
+
891
+ return await self.agent_manager.update_agent_async(
892
+ agent_id=agent_id,
893
+ agent_update=request,
894
+ actor=actor,
895
+ )
896
+
823
897
  def create_sleeptime_agent(self, main_agent: AgentState, actor: User) -> AgentState:
824
898
  request = CreateAgent(
825
899
  name=main_agent.name + "-sleeptime",