letta-nightly 0.7.0.dev20250423003112__py3-none-any.whl → 0.7.2.dev20250423222439__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 (55) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +113 -81
  3. letta/agents/letta_agent.py +2 -2
  4. letta/agents/letta_agent_batch.py +38 -34
  5. letta/client/client.py +10 -2
  6. letta/constants.py +4 -3
  7. letta/functions/function_sets/multi_agent.py +1 -3
  8. letta/functions/helpers.py +3 -3
  9. letta/groups/dynamic_multi_agent.py +58 -59
  10. letta/groups/round_robin_multi_agent.py +43 -49
  11. letta/groups/sleeptime_multi_agent.py +28 -18
  12. letta/groups/supervisor_multi_agent.py +21 -20
  13. letta/helpers/composio_helpers.py +1 -1
  14. letta/helpers/converters.py +29 -0
  15. letta/helpers/datetime_helpers.py +9 -0
  16. letta/helpers/message_helper.py +1 -0
  17. letta/helpers/tool_execution_helper.py +3 -3
  18. letta/jobs/llm_batch_job_polling.py +2 -1
  19. letta/llm_api/anthropic.py +10 -6
  20. letta/llm_api/anthropic_client.py +2 -2
  21. letta/llm_api/cohere.py +2 -2
  22. letta/llm_api/google_ai_client.py +2 -2
  23. letta/llm_api/google_vertex_client.py +2 -2
  24. letta/llm_api/openai.py +11 -4
  25. letta/llm_api/openai_client.py +34 -2
  26. letta/local_llm/chat_completion_proxy.py +2 -2
  27. letta/orm/agent.py +8 -1
  28. letta/orm/custom_columns.py +15 -0
  29. letta/schemas/agent.py +6 -0
  30. letta/schemas/letta_message_content.py +2 -1
  31. letta/schemas/llm_config.py +12 -2
  32. letta/schemas/message.py +18 -0
  33. letta/schemas/openai/chat_completion_response.py +52 -3
  34. letta/schemas/response_format.py +78 -0
  35. letta/schemas/tool_execution_result.py +14 -0
  36. letta/server/rest_api/chat_completions_interface.py +2 -2
  37. letta/server/rest_api/interface.py +3 -2
  38. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +1 -1
  39. letta/server/rest_api/routers/v1/agents.py +4 -4
  40. letta/server/rest_api/routers/v1/groups.py +2 -2
  41. letta/server/rest_api/routers/v1/messages.py +41 -19
  42. letta/server/server.py +24 -57
  43. letta/services/agent_manager.py +6 -1
  44. letta/services/llm_batch_manager.py +28 -26
  45. letta/services/tool_executor/tool_execution_manager.py +37 -28
  46. letta/services/tool_executor/tool_execution_sandbox.py +35 -16
  47. letta/services/tool_executor/tool_executor.py +299 -68
  48. letta/services/tool_sandbox/base.py +3 -2
  49. letta/services/tool_sandbox/e2b_sandbox.py +5 -4
  50. letta/services/tool_sandbox/local_sandbox.py +11 -6
  51. {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/METADATA +1 -1
  52. {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/RECORD +55 -53
  53. {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/LICENSE +0 -0
  54. {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/WHEEL +0 -0
  55. {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/entry_points.txt +0 -0
@@ -291,9 +291,7 @@ class LLMBatchManager:
291
291
  return [item.to_pydantic() for item in results]
292
292
 
293
293
  def bulk_update_llm_batch_items(
294
- self,
295
- llm_batch_id_agent_id_pairs: List[Tuple[str, str]],
296
- field_updates: List[Dict[str, Any]],
294
+ self, llm_batch_id_agent_id_pairs: List[Tuple[str, str]], field_updates: List[Dict[str, Any]], strict: bool = True
297
295
  ) -> None:
298
296
  """
299
297
  Efficiently update multiple LLMBatchItem rows by (llm_batch_id, agent_id) pairs.
@@ -301,30 +299,43 @@ class LLMBatchManager:
301
299
  Args:
302
300
  llm_batch_id_agent_id_pairs: List of (llm_batch_id, agent_id) tuples identifying items to update
303
301
  field_updates: List of dictionaries containing the fields to update for each item
302
+ strict: Whether to error if any of the requested keys don't exist (default True).
303
+ If False, missing pairs are skipped.
304
304
  """
305
305
  if not llm_batch_id_agent_id_pairs or not field_updates:
306
306
  return
307
307
 
308
308
  if len(llm_batch_id_agent_id_pairs) != len(field_updates):
309
- raise ValueError("batch_id_agent_id_pairs and field_updates must have the same length")
309
+ raise ValueError("llm_batch_id_agent_id_pairs and field_updates must have the same length")
310
310
 
311
311
  with self.session_maker() as session:
312
- # Lookup primary keys
312
+ # Lookup primary keys for all requested (batch_id, agent_id) pairs
313
313
  items = (
314
314
  session.query(LLMBatchItem.id, LLMBatchItem.llm_batch_id, LLMBatchItem.agent_id)
315
315
  .filter(tuple_(LLMBatchItem.llm_batch_id, LLMBatchItem.agent_id).in_(llm_batch_id_agent_id_pairs))
316
316
  .all()
317
317
  )
318
- pair_to_pk = {(b, a): id for id, b, a in items}
319
-
318
+ pair_to_pk = {(batch_id, agent_id): pk for pk, batch_id, agent_id in items}
319
+
320
+ if strict:
321
+ requested = set(llm_batch_id_agent_id_pairs)
322
+ found = set(pair_to_pk.keys())
323
+ missing = requested - found
324
+ if missing:
325
+ raise ValueError(
326
+ f"Cannot bulk-update batch items: no records for the following " f"(llm_batch_id, agent_id) pairs: {missing}"
327
+ )
328
+
329
+ # Build mappings, skipping any missing when strict=False
320
330
  mappings = []
321
- for (llm_batch_id, agent_id), fields in zip(llm_batch_id_agent_id_pairs, field_updates):
322
- pk_id = pair_to_pk.get((llm_batch_id, agent_id))
323
- if not pk_id:
331
+ for (batch_id, agent_id), fields in zip(llm_batch_id_agent_id_pairs, field_updates):
332
+ pk = pair_to_pk.get((batch_id, agent_id))
333
+ if pk is None:
334
+ # skip missing in non-strict mode
324
335
  continue
325
336
 
326
337
  update_fields = fields.copy()
327
- update_fields["id"] = pk_id
338
+ update_fields["id"] = pk
328
339
  mappings.append(update_fields)
329
340
 
330
341
  if mappings:
@@ -332,10 +343,7 @@ class LLMBatchManager:
332
343
  session.commit()
333
344
 
334
345
  @enforce_types
335
- def bulk_update_batch_llm_items_results_by_agent(
336
- self,
337
- updates: List[ItemUpdateInfo],
338
- ) -> None:
346
+ def bulk_update_batch_llm_items_results_by_agent(self, updates: List[ItemUpdateInfo], strict: bool = True) -> None:
339
347
  """Update request status and batch results for multiple batch items."""
340
348
  batch_id_agent_id_pairs = [(update.llm_batch_id, update.agent_id) for update in updates]
341
349
  field_updates = [
@@ -346,29 +354,23 @@ class LLMBatchManager:
346
354
  for update in updates
347
355
  ]
348
356
 
349
- self.bulk_update_llm_batch_items(batch_id_agent_id_pairs, field_updates)
357
+ self.bulk_update_llm_batch_items(batch_id_agent_id_pairs, field_updates, strict=strict)
350
358
 
351
359
  @enforce_types
352
- def bulk_update_llm_batch_items_step_status_by_agent(
353
- self,
354
- updates: List[StepStatusUpdateInfo],
355
- ) -> None:
360
+ def bulk_update_llm_batch_items_step_status_by_agent(self, updates: List[StepStatusUpdateInfo], strict: bool = True) -> None:
356
361
  """Update step status for multiple batch items."""
357
362
  batch_id_agent_id_pairs = [(update.llm_batch_id, update.agent_id) for update in updates]
358
363
  field_updates = [{"step_status": update.step_status} for update in updates]
359
364
 
360
- self.bulk_update_llm_batch_items(batch_id_agent_id_pairs, field_updates)
365
+ self.bulk_update_llm_batch_items(batch_id_agent_id_pairs, field_updates, strict=strict)
361
366
 
362
367
  @enforce_types
363
- def bulk_update_llm_batch_items_request_status_by_agent(
364
- self,
365
- updates: List[RequestStatusUpdateInfo],
366
- ) -> None:
368
+ def bulk_update_llm_batch_items_request_status_by_agent(self, updates: List[RequestStatusUpdateInfo], strict: bool = True) -> None:
367
369
  """Update request status for multiple batch items."""
368
370
  batch_id_agent_id_pairs = [(update.llm_batch_id, update.agent_id) for update in updates]
369
371
  field_updates = [{"request_status": update.request_status} for update in updates]
370
372
 
371
- self.bulk_update_llm_batch_items(batch_id_agent_id_pairs, field_updates)
373
+ self.bulk_update_llm_batch_items(batch_id_agent_id_pairs, field_updates, strict=strict)
372
374
 
373
375
  @enforce_types
374
376
  def delete_llm_batch_item(self, item_id: str, actor: PydanticUser) -> None:
@@ -1,16 +1,17 @@
1
- from typing import Any, Dict, Optional, Tuple, Type
1
+ import traceback
2
+ from typing import Any, Dict, Optional, Type
2
3
 
3
4
  from letta.log import get_logger
4
5
  from letta.orm.enums import ToolType
5
6
  from letta.schemas.agent import AgentState
6
- from letta.schemas.sandbox_config import SandboxConfig, SandboxRunResult
7
+ from letta.schemas.sandbox_config import SandboxConfig
7
8
  from letta.schemas.tool import Tool
9
+ from letta.schemas.tool_execution_result import ToolExecutionResult
8
10
  from letta.schemas.user import User
9
11
  from letta.services.tool_executor.tool_executor import (
10
12
  ExternalComposioToolExecutor,
11
13
  ExternalMCPToolExecutor,
12
14
  LettaCoreToolExecutor,
13
- LettaMemoryToolExecutor,
14
15
  LettaMultiAgentToolExecutor,
15
16
  SandboxToolExecutor,
16
17
  ToolExecutor,
@@ -24,8 +25,9 @@ class ToolExecutorFactory:
24
25
 
25
26
  _executor_map: Dict[ToolType, Type[ToolExecutor]] = {
26
27
  ToolType.LETTA_CORE: LettaCoreToolExecutor,
28
+ ToolType.LETTA_MEMORY_CORE: LettaCoreToolExecutor,
29
+ ToolType.LETTA_SLEEPTIME_CORE: LettaCoreToolExecutor,
27
30
  ToolType.LETTA_MULTI_AGENT_CORE: LettaMultiAgentToolExecutor,
28
- ToolType.LETTA_MEMORY_CORE: LettaMemoryToolExecutor,
29
31
  ToolType.EXTERNAL_COMPOSIO: ExternalComposioToolExecutor,
30
32
  ToolType.EXTERNAL_MCP: ExternalMCPToolExecutor,
31
33
  }
@@ -33,13 +35,8 @@ class ToolExecutorFactory:
33
35
  @classmethod
34
36
  def get_executor(cls, tool_type: ToolType) -> ToolExecutor:
35
37
  """Get the appropriate executor for the given tool type."""
36
- executor_class = cls._executor_map.get(tool_type)
37
-
38
- if executor_class:
39
- return executor_class()
40
-
41
- # Default to sandbox executor for unknown types
42
- return SandboxToolExecutor()
38
+ executor_class = cls._executor_map.get(tool_type, SandboxToolExecutor)
39
+ return executor_class()
43
40
 
44
41
 
45
42
  class ToolExecutionManager:
@@ -58,7 +55,7 @@ class ToolExecutionManager:
58
55
  self.sandbox_config = sandbox_config
59
56
  self.sandbox_env_vars = sandbox_env_vars
60
57
 
61
- def execute_tool(self, function_name: str, function_args: dict, tool: Tool) -> Tuple[Any, Optional[SandboxRunResult]]:
58
+ def execute_tool(self, function_name: str, function_args: dict, tool: Tool) -> ToolExecutionResult:
62
59
  """
63
60
  Execute a tool and persist any state changes.
64
61
 
@@ -71,35 +68,43 @@ class ToolExecutionManager:
71
68
  Tuple containing the function response and sandbox run result (if applicable)
72
69
  """
73
70
  try:
74
- # Get the appropriate executor for this tool type
75
71
  executor = ToolExecutorFactory.get_executor(tool.tool_type)
76
-
77
- # Execute the tool
78
72
  return executor.execute(
79
- function_name, function_args, self.agent_state, tool, self.actor, self.sandbox_config, self.sandbox_env_vars
73
+ function_name,
74
+ function_args,
75
+ self.agent_state,
76
+ tool,
77
+ self.actor,
78
+ self.sandbox_config,
79
+ self.sandbox_env_vars,
80
80
  )
81
81
 
82
82
  except Exception as e:
83
83
  self.logger.error(f"Error executing tool {function_name}: {str(e)}")
84
- error_message = get_friendly_error_msg(function_name=function_name, exception_name=type(e).__name__, exception_message=str(e))
85
- return error_message, SandboxRunResult(status="error")
84
+ error_message = get_friendly_error_msg(
85
+ function_name=function_name,
86
+ exception_name=type(e).__name__,
87
+ exception_message=str(e),
88
+ )
89
+ return ToolExecutionResult(
90
+ status="error",
91
+ func_return=error_message,
92
+ stderr=[traceback.format_exc()],
93
+ )
86
94
 
87
95
  @trace_method
88
- async def execute_tool_async(self, function_name: str, function_args: dict, tool: Tool) -> Tuple[Any, Optional[SandboxRunResult]]:
96
+ async def execute_tool_async(self, function_name: str, function_args: dict, tool: Tool) -> ToolExecutionResult:
89
97
  """
90
98
  Execute a tool asynchronously and persist any state changes.
91
99
  """
92
100
  try:
93
- # Get the appropriate executor for this tool type
101
+ executor = ToolExecutorFactory.get_executor(tool.tool_type)
94
102
  # TODO: Extend this async model to composio
95
-
96
- if tool.tool_type == ToolType.CUSTOM:
97
- executor = SandboxToolExecutor()
98
- result_tuple = await executor.execute(function_name, function_args, self.agent_state, tool, self.actor)
103
+ if isinstance(executor, SandboxToolExecutor):
104
+ result = await executor.execute(function_name, function_args, self.agent_state, tool, self.actor)
99
105
  else:
100
- executor = ToolExecutorFactory.get_executor(tool.tool_type)
101
- result_tuple = executor.execute(function_name, function_args, self.agent_state, tool, self.actor)
102
- return result_tuple
106
+ result = executor.execute(function_name, function_args, self.agent_state, tool, self.actor)
107
+ return result
103
108
 
104
109
  except Exception as e:
105
110
  self.logger.error(f"Error executing tool {function_name}: {str(e)}")
@@ -108,4 +113,8 @@ class ToolExecutionManager:
108
113
  exception_name=type(e).__name__,
109
114
  exception_message=str(e),
110
115
  )
111
- return error_message, SandboxRunResult(status="error")
116
+ return ToolExecutionResult(
117
+ status="error",
118
+ func_return=error_message,
119
+ stderr=[traceback.format_exc()],
120
+ )
@@ -13,8 +13,9 @@ from typing import Any, Dict, Optional
13
13
  from letta.functions.helpers import generate_model_from_args_json_schema
14
14
  from letta.log import get_logger
15
15
  from letta.schemas.agent import AgentState
16
- from letta.schemas.sandbox_config import SandboxConfig, SandboxRunResult, SandboxType
16
+ from letta.schemas.sandbox_config import SandboxConfig, SandboxType
17
17
  from letta.schemas.tool import Tool
18
+ from letta.schemas.tool_execution_result import ToolExecutionResult
18
19
  from letta.schemas.user import User
19
20
  from letta.services.helpers.tool_execution_helper import (
20
21
  add_imports_and_pydantic_schemas_for_args,
@@ -72,7 +73,11 @@ class ToolExecutionSandbox:
72
73
  self.force_recreate = force_recreate
73
74
  self.force_recreate_venv = force_recreate_venv
74
75
 
75
- def run(self, agent_state: Optional[AgentState] = None, additional_env_vars: Optional[Dict] = None) -> SandboxRunResult:
76
+ def run(
77
+ self,
78
+ agent_state: Optional[AgentState] = None,
79
+ additional_env_vars: Optional[Dict] = None,
80
+ ) -> ToolExecutionResult:
76
81
  """
77
82
  Run the tool in a sandbox environment.
78
83
 
@@ -81,7 +86,7 @@ class ToolExecutionSandbox:
81
86
  additional_env_vars (Optional[Dict]): Environment variables to inject into the sandbox
82
87
 
83
88
  Returns:
84
- Tuple[Any, Optional[AgentState]]: Tuple containing (tool_result, agent_state)
89
+ ToolExecutionResult: Object containing tool execution outcome (e.g. status, response)
85
90
  """
86
91
  if tool_settings.e2b_api_key and not self.privileged_tools:
87
92
  logger.debug(f"Using e2b sandbox to execute {self.tool_name}")
@@ -115,7 +120,7 @@ class ToolExecutionSandbox:
115
120
  @trace_method
116
121
  def run_local_dir_sandbox(
117
122
  self, agent_state: Optional[AgentState] = None, additional_env_vars: Optional[Dict] = None
118
- ) -> SandboxRunResult:
123
+ ) -> ToolExecutionResult:
119
124
  sbx_config = self.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.LOCAL, actor=self.user)
120
125
  local_configs = sbx_config.get_local_config()
121
126
 
@@ -162,7 +167,12 @@ class ToolExecutionSandbox:
162
167
  os.remove(temp_file_path)
163
168
 
164
169
  @trace_method
165
- def run_local_dir_sandbox_venv(self, sbx_config: SandboxConfig, env: Dict[str, str], temp_file_path: str) -> SandboxRunResult:
170
+ def run_local_dir_sandbox_venv(
171
+ self,
172
+ sbx_config: SandboxConfig,
173
+ env: Dict[str, str],
174
+ temp_file_path: str,
175
+ ) -> ToolExecutionResult:
166
176
  local_configs = sbx_config.get_local_config()
167
177
  sandbox_dir = os.path.expanduser(local_configs.sandbox_dir) # Expand tilde
168
178
  venv_path = os.path.join(sandbox_dir, local_configs.venv_name)
@@ -205,12 +215,12 @@ class ToolExecutionSandbox:
205
215
  func_result, stdout = self.parse_out_function_results_markers(result.stdout)
206
216
  func_return, agent_state = self.parse_best_effort(func_result)
207
217
 
208
- return SandboxRunResult(
218
+ return ToolExecutionResult(
219
+ status="success",
209
220
  func_return=func_return,
210
221
  agent_state=agent_state,
211
222
  stdout=[stdout] if stdout else [],
212
223
  stderr=[result.stderr] if result.stderr else [],
213
- status="success",
214
224
  sandbox_config_fingerprint=sbx_config.fingerprint(),
215
225
  )
216
226
 
@@ -221,12 +231,12 @@ class ToolExecutionSandbox:
221
231
  exception_name=type(e).__name__,
222
232
  exception_message=str(e),
223
233
  )
224
- return SandboxRunResult(
234
+ return ToolExecutionResult(
235
+ status="error",
225
236
  func_return=func_return,
226
237
  agent_state=None,
227
238
  stdout=[e.stdout] if e.stdout else [],
228
239
  stderr=[e.stderr] if e.stderr else [],
229
- status="error",
230
240
  sandbox_config_fingerprint=sbx_config.fingerprint(),
231
241
  )
232
242
 
@@ -238,7 +248,12 @@ class ToolExecutionSandbox:
238
248
  raise e
239
249
 
240
250
  @trace_method
241
- def run_local_dir_sandbox_directly(self, sbx_config: SandboxConfig, env: Dict[str, str], temp_file_path: str) -> SandboxRunResult:
251
+ def run_local_dir_sandbox_directly(
252
+ self,
253
+ sbx_config: SandboxConfig,
254
+ env: Dict[str, str],
255
+ temp_file_path: str,
256
+ ) -> ToolExecutionResult:
242
257
  status = "success"
243
258
  func_return, agent_state, stderr = None, None, None
244
259
 
@@ -288,12 +303,12 @@ class ToolExecutionSandbox:
288
303
  stdout_output = [captured_stdout.getvalue()] if captured_stdout.getvalue() else []
289
304
  stderr_output = [captured_stderr.getvalue()] if captured_stderr.getvalue() else []
290
305
 
291
- return SandboxRunResult(
306
+ return ToolExecutionResult(
307
+ status=status,
292
308
  func_return=func_return,
293
309
  agent_state=agent_state,
294
310
  stdout=stdout_output,
295
311
  stderr=stderr_output,
296
- status=status,
297
312
  sandbox_config_fingerprint=sbx_config.fingerprint(),
298
313
  )
299
314
 
@@ -307,7 +322,11 @@ class ToolExecutionSandbox:
307
322
 
308
323
  # e2b sandbox specific functions
309
324
 
310
- def run_e2b_sandbox(self, agent_state: Optional[AgentState] = None, additional_env_vars: Optional[Dict] = None) -> SandboxRunResult:
325
+ def run_e2b_sandbox(
326
+ self,
327
+ agent_state: Optional[AgentState] = None,
328
+ additional_env_vars: Optional[Dict] = None,
329
+ ) -> ToolExecutionResult:
311
330
  sbx_config = self.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.E2B, actor=self.user)
312
331
  sbx = self.get_running_e2b_sandbox_with_same_state(sbx_config)
313
332
  if not sbx or self.force_recreate:
@@ -348,12 +367,12 @@ class ToolExecutionSandbox:
348
367
  else:
349
368
  raise ValueError(f"Tool {self.tool_name} returned execution with None")
350
369
 
351
- return SandboxRunResult(
370
+ return ToolExecutionResult(
371
+ status="error" if execution.error else "success",
352
372
  func_return=func_return,
353
373
  agent_state=agent_state,
354
374
  stdout=execution.logs.stdout,
355
375
  stderr=execution.logs.stderr,
356
- status="error" if execution.error else "success",
357
376
  sandbox_config_fingerprint=sbx_config.fingerprint(),
358
377
  )
359
378
 
@@ -535,7 +554,7 @@ class ToolExecutionSandbox:
535
554
  Generate the code string to call the function.
536
555
 
537
556
  Args:
538
- inject_agent_state (bool): Whether to inject the axgent's state as an input into the tool
557
+ inject_agent_state (bool): Whether to inject the agent's state as an input into the tool
539
558
 
540
559
  Returns:
541
560
  str: Generated code string for calling the tool