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
@@ -27,6 +27,7 @@ from letta.schemas.llm_batch_job import LLMBatchItem
27
27
  from letta.schemas.message import Message, MessageCreate
28
28
  from letta.schemas.openai.chat_completion_response import ToolCall as OpenAIToolCall
29
29
  from letta.schemas.sandbox_config import SandboxConfig, SandboxType
30
+ from letta.schemas.tool_execution_result import ToolExecutionResult
30
31
  from letta.schemas.user import User
31
32
  from letta.server.rest_api.utils import create_heartbeat_system_message, create_letta_messages_from_llm_response
32
33
  from letta.services.agent_manager import AgentManager
@@ -66,15 +67,17 @@ class _ResumeContext:
66
67
  request_status_updates: List[RequestStatusUpdateInfo]
67
68
 
68
69
 
69
- async def execute_tool_wrapper(params: ToolExecutionParams) -> Tuple[str, Tuple[str, bool]]:
70
+ async def execute_tool_wrapper(params: ToolExecutionParams) -> tuple[str, ToolExecutionResult]:
70
71
  """
71
72
  Executes the tool in an out‑of‑process worker and returns:
72
73
  (agent_id, (tool_result:str, success_flag:bool))
73
74
  """
75
+ from letta.schemas.tool_execution_result import ToolExecutionResult
76
+
74
77
  # locate the tool on the agent
75
78
  target_tool = next((t for t in params.agent_state.tools if t.name == params.tool_call_name), None)
76
79
  if not target_tool:
77
- return params.agent_id, (f"Tool not found: {params.tool_call_name}", False)
80
+ return params.agent_id, ToolExecutionResult(func_return=f"Tool not found: {params.tool_call_name}", status="error")
78
81
 
79
82
  try:
80
83
  mgr = ToolExecutionManager(
@@ -88,9 +91,9 @@ async def execute_tool_wrapper(params: ToolExecutionParams) -> Tuple[str, Tuple[
88
91
  function_args=params.tool_args,
89
92
  tool=target_tool,
90
93
  )
91
- return params.agent_id, (tool_execution_result.func_return, True)
94
+ return params.agent_id, tool_execution_result
92
95
  except Exception as e:
93
- return params.agent_id, (f"Failed to call tool. Error: {e}", False)
96
+ return params.agent_id, ToolExecutionResult(func_return=f"Failed to call tool. Error: {e}", status="error")
94
97
 
95
98
 
96
99
  # TODO: Limitations ->
@@ -245,7 +248,7 @@ class LettaAgentBatch(BaseAgent):
245
248
  await self._mark_steps_complete_async(llm_batch_id, ctx.agent_ids)
246
249
 
247
250
  log_event(name="prepare_next")
248
- next_reqs, next_step_state = self._prepare_next_iteration(exec_results, ctx, msg_map)
251
+ next_reqs, next_step_state = await self._prepare_next_iteration_async(exec_results, ctx, msg_map)
249
252
  if len(next_reqs) == 0:
250
253
  await self.job_manager.update_job_by_id_async(
251
254
  job_id=letta_batch_id, job_update=JobUpdate(status=JobStatus.completed), actor=self.actor
@@ -393,7 +396,7 @@ class LettaAgentBatch(BaseAgent):
393
396
  return cfg, env
394
397
 
395
398
  @trace_method
396
- async def _execute_tools(self, ctx: _ResumeContext) -> Sequence[Tuple[str, Tuple[str, bool]]]:
399
+ async def _execute_tools(self, ctx: _ResumeContext) -> Sequence[tuple[str, ToolExecutionResult]]:
397
400
  sbx_cfg, sbx_env = await self._build_sandbox()
398
401
  rethink_memory_tool_name = "rethink_memory"
399
402
  tool_params = []
@@ -424,7 +427,7 @@ class LettaAgentBatch(BaseAgent):
424
427
  return await pool.map(execute_tool_wrapper, tool_params)
425
428
 
426
429
  @trace_method
427
- async def _bulk_rethink_memory_async(self, params: List[ToolExecutionParams]) -> Sequence[Tuple[str, Tuple[str, bool]]]:
430
+ async def _bulk_rethink_memory_async(self, params: List[ToolExecutionParams]) -> Sequence[tuple[str, ToolExecutionResult]]:
428
431
  updates = {}
429
432
  result = []
430
433
  for param in params:
@@ -443,7 +446,7 @@ class LettaAgentBatch(BaseAgent):
443
446
  updates[block_id] = new_value
444
447
 
445
448
  # TODO: This is quite ugly and confusing - this is mostly to align with the returns of other tools
446
- result.append((param.agent_id, ("", True)))
449
+ result.append((param.agent_id, ToolExecutionResult(status="success")))
447
450
 
448
451
  await self.block_manager.bulk_update_block_values_async(updates=updates, actor=self.actor)
449
452
 
@@ -451,7 +454,7 @@ class LettaAgentBatch(BaseAgent):
451
454
 
452
455
  async def _persist_tool_messages(
453
456
  self,
454
- exec_results: Sequence[Tuple[str, Tuple[str, bool]]],
457
+ exec_results: Sequence[Tuple[str, "ToolExecutionResult"]],
455
458
  ctx: _ResumeContext,
456
459
  ) -> Dict[str, List[Message]]:
457
460
  # TODO: This is redundant, we should have this ready on the ctx
@@ -459,14 +462,15 @@ class LettaAgentBatch(BaseAgent):
459
462
  agent_item_map: Dict[str, LLMBatchItem] = {item.agent_id: item for item in ctx.batch_items}
460
463
 
461
464
  msg_map: Dict[str, List[Message]] = {}
462
- for aid, (tool_res, success) in exec_results:
465
+ for aid, tool_exec_result in exec_results:
463
466
  msgs = self._create_tool_call_messages(
464
467
  llm_batch_item_id=agent_item_map[aid].id,
465
468
  agent_state=ctx.agent_state_map[aid],
466
469
  tool_call_name=ctx.tool_call_name_map[aid],
467
470
  tool_call_args=ctx.tool_call_args_map[aid],
468
- tool_exec_result=tool_res,
469
- success_flag=success,
471
+ tool_exec_result=tool_exec_result.func_return,
472
+ success_flag=tool_exec_result.success_flag,
473
+ tool_exec_result_obj=tool_exec_result,
470
474
  reasoning_content=None,
471
475
  )
472
476
  msg_map[aid] = msgs
@@ -480,16 +484,16 @@ class LettaAgentBatch(BaseAgent):
480
484
  ]
481
485
  await self.batch_manager.bulk_update_llm_batch_items_step_status_by_agent_async(updates)
482
486
 
483
- def _prepare_next_iteration(
487
+ async def _prepare_next_iteration_async(
484
488
  self,
485
- exec_results: Sequence[Tuple[str, Tuple[str, bool]]],
489
+ exec_results: Sequence[Tuple[str, "ToolExecutionResult"]],
486
490
  ctx: _ResumeContext,
487
491
  msg_map: Dict[str, List[Message]],
488
492
  ) -> Tuple[List[LettaBatchRequest], Dict[str, AgentStepState]]:
489
493
  # who continues?
490
494
  continues = [aid for aid, cont in ctx.should_continue_map.items() if cont]
491
495
 
492
- success_flag_map = {aid: flag for aid, (_res, flag) in exec_results}
496
+ success_flag_map = {aid: result.success_flag for aid, result in exec_results}
493
497
 
494
498
  batch_reqs: List[LettaBatchRequest] = []
495
499
  for aid in continues:
@@ -509,7 +513,7 @@ class LettaAgentBatch(BaseAgent):
509
513
  for aid, new_msgs in msg_map.items():
510
514
  ast = ctx.agent_state_map[aid]
511
515
  if not ast.message_buffer_autoclear:
512
- self.agent_manager.set_in_context_messages(
516
+ await self.agent_manager.set_in_context_messages_async(
513
517
  agent_id=aid,
514
518
  message_ids=ast.message_ids + [m.id for m in new_msgs],
515
519
  actor=self.actor,
@@ -528,6 +532,7 @@ class LettaAgentBatch(BaseAgent):
528
532
  tool_call_name: str,
529
533
  tool_call_args: Dict[str, Any],
530
534
  tool_exec_result: str,
535
+ tool_exec_result_obj: "ToolExecutionResult",
531
536
  success_flag: bool,
532
537
  reasoning_content: Optional[List[Union[TextContent, ReasoningContent, RedactedReasoningContent, OmittedReasoningContent]]] = None,
533
538
  ) -> List[Message]:
@@ -541,6 +546,7 @@ class LettaAgentBatch(BaseAgent):
541
546
  tool_call_id=tool_call_id,
542
547
  function_call_success=success_flag,
543
548
  function_response=tool_exec_result,
549
+ tool_execution_result=tool_exec_result_obj,
544
550
  actor=self.actor,
545
551
  add_heartbeat_request_system_message=False,
546
552
  reasoning_content=reasoning_content,
@@ -0,0 +1,62 @@
1
+ You are a memory-recall assistant that preserves conversational context as messages exit the AI's context window.
2
+
3
+ <core_function>
4
+ Extract and preserve information that would be lost when messages are evicted, enabling continuity across conversations.
5
+ </core_function>
6
+
7
+ <detail_adaptation>
8
+ Analyze content type and apply appropriate detail level:
9
+
10
+ <high_detail>
11
+ Apply to: episodic content, code, artifacts, documents, technical discussions
12
+ - Capture specific facts, sequences, and technical details
13
+ - Preserve exact names, dates, numbers, specifications
14
+ - Document code snippets, artifact IDs, document structures
15
+ - Note precise steps in procedures or narratives
16
+ - Include verbatim quotes for critical commitments
17
+ </high_detail>
18
+
19
+ <medium_detail>
20
+ Apply to: ongoing projects, established preferences, multi-message threads
21
+ - Summarize key decisions, milestones, progress
22
+ - Record personal preferences and patterns
23
+ - Track commitments and action items
24
+ - Maintain project context and dependencies
25
+ </medium_detail>
26
+
27
+ <low_detail>
28
+ Apply to: high-level discussions, philosophical topics, general preferences
29
+ - Capture main themes and conclusions
30
+ - Note relationship dynamics and communication style
31
+ - Summarize positions and general goals
32
+ - Record broad aspirations
33
+ </low_detail>
34
+ </detail_adaptation>
35
+
36
+ <information_priority>
37
+ <critical>Commitments, deadlines, medical/legal information, explicit requests</critical>
38
+ <important>Personal details, project status, technical specifications, decisions</important>
39
+ <contextual>Preferences, opinions, relationship dynamics, emotional tone</contextual>
40
+ <background>General topics, themes, conversational patterns</background>
41
+ </information_priority>
42
+
43
+ <format_rules>
44
+ - Use bullet points for discrete facts
45
+ - Write prose for narratives or complex relationships
46
+ - **Bold** key terms and identifiers
47
+ - Include temporal markers: [ongoing], [mentioned DATE], [since TIME]
48
+ - Group under clear headers when multiple topics present
49
+ - Use consistent terminology for searchability
50
+ </format_rules>
51
+
52
+ <exclusions>
53
+ - Information in remaining context
54
+ - Generic pleasantries
55
+ - Inferrable details
56
+ - Redundant restatements
57
+ - Conversational filler
58
+ </exclusions>
59
+
60
+ <critical_reminder>
61
+ Your notes are the sole record of evicted messages. Every word should enable future continuity.
62
+ </critical_reminder>
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  import uuid
3
3
  from datetime import datetime, timedelta, timezone
4
- from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple
4
+ from typing import Any, AsyncGenerator, Dict, List, Optional
5
5
 
6
6
  import openai
7
7
 
@@ -118,6 +118,7 @@ class VoiceAgent(BaseAgent):
118
118
  Main streaming loop that yields partial tokens.
119
119
  Whenever we detect a tool call, we yield from _handle_ai_response as well.
120
120
  """
121
+ print("CALL STREAM")
121
122
  if len(input_messages) != 1 or input_messages[0].role != MessageRole.user:
122
123
  raise ValueError(f"Voice Agent was invoked with multiple input messages or message did not have role `user`: {input_messages}")
123
124
 
@@ -238,14 +239,17 @@ class VoiceAgent(BaseAgent):
238
239
  )
239
240
  in_memory_message_history.append(assistant_tool_call_msg.model_dump())
240
241
 
241
- tool_result, success_flag = await self._execute_tool(
242
+ tool_execution_result = await self._execute_tool(
242
243
  user_query=user_query,
243
244
  tool_name=tool_call_name,
244
245
  tool_args=tool_args,
245
246
  agent_state=agent_state,
246
247
  )
248
+ tool_result = tool_execution_result.func_return
249
+ success_flag = tool_execution_result.success_flag
247
250
 
248
251
  # 3. Provide function_call response back into the conversation
252
+ # TODO: fix this tool format
249
253
  tool_message = ToolMessage(
250
254
  content=json.dumps({"result": tool_result}),
251
255
  tool_call_id=tool_call_id,
@@ -267,6 +271,7 @@ class VoiceAgent(BaseAgent):
267
271
  tool_call_id=tool_call_id,
268
272
  function_call_success=success_flag,
269
273
  function_response=tool_result,
274
+ tool_execution_result=tool_execution_result,
270
275
  actor=self.actor,
271
276
  add_heartbeat_request_system_message=True,
272
277
  )
@@ -388,10 +393,14 @@ class VoiceAgent(BaseAgent):
388
393
  for t in tools
389
394
  ]
390
395
 
391
- async def _execute_tool(self, user_query: str, tool_name: str, tool_args: dict, agent_state: AgentState) -> Tuple[str, bool]:
396
+ async def _execute_tool(self, user_query: str, tool_name: str, tool_args: dict, agent_state: AgentState) -> "ToolExecutionResult":
392
397
  """
393
398
  Executes a tool and returns (result, success_flag).
394
399
  """
400
+ from letta.schemas.tool_execution_result import ToolExecutionResult
401
+
402
+ print("EXECUTING TOOL")
403
+
395
404
  # Special memory case
396
405
  if tool_name == "search_memory":
397
406
  tool_result = await self._search_memory(
@@ -401,11 +410,17 @@ class VoiceAgent(BaseAgent):
401
410
  end_minutes_ago=tool_args["end_minutes_ago"],
402
411
  agent_state=agent_state,
403
412
  )
404
- return tool_result, True
413
+ return ToolExecutionResult(
414
+ func_return=tool_result,
415
+ status="success",
416
+ )
405
417
  else:
406
418
  target_tool = next((x for x in agent_state.tools if x.name == tool_name), None)
407
419
  if not target_tool:
408
- return f"Tool not found: {tool_name}", False
420
+ return ToolExecutionResult(
421
+ func_return=f"Tool not found: {tool_name}",
422
+ status="error",
423
+ )
409
424
 
410
425
  try:
411
426
  tool_result, _ = execute_external_tool(
@@ -416,9 +431,9 @@ class VoiceAgent(BaseAgent):
416
431
  actor=self.actor,
417
432
  allow_agent_state_modifications=False,
418
433
  )
419
- return tool_result, True
434
+ return ToolExecutionResult(func_return=tool_result, status="success")
420
435
  except Exception as e:
421
- return f"Failed to call tool. Error: {e}", False
436
+ return ToolExecutionResult(func_return=f"Failed to call tool. Error: {e}", status="error")
422
437
 
423
438
  async def _search_memory(
424
439
  self,
@@ -1,4 +1,4 @@
1
- from typing import AsyncGenerator, List, Tuple, Union
1
+ from typing import AsyncGenerator, List, Optional, Tuple, Union
2
2
 
3
3
  from letta.agents.helpers import _create_letta_response, serialize_message_history
4
4
  from letta.agents.letta_agent import LettaAgent
@@ -89,20 +89,23 @@ class VoiceSleeptimeAgent(LettaAgent):
89
89
  )
90
90
 
91
91
  @trace_method
92
- async def _execute_tool(self, tool_name: str, tool_args: dict, agent_state: AgentState) -> Tuple[str, bool]:
92
+ async def _execute_tool(self, tool_name: str, tool_args: dict, agent_state: AgentState, agent_step_span: Optional["Span"] = None):
93
93
  """
94
94
  Executes a tool and returns (result, success_flag).
95
95
  """
96
+ from letta.schemas.tool_execution_result import ToolExecutionResult
97
+
96
98
  # Special memory case
97
99
  target_tool = next((x for x in agent_state.tools if x.name == tool_name), None)
98
100
  if not target_tool:
99
- return f"Tool not found: {tool_name}", False
101
+ return ToolExecutionResult(status="error", func_return=f"Tool not found: {tool_name}")
100
102
 
101
103
  try:
102
104
  if target_tool.name == "rethink_user_memory" and target_tool.tool_type == ToolType.LETTA_VOICE_SLEEPTIME_CORE:
103
- return self.rethink_user_memory(agent_state=agent_state, **tool_args)
105
+ func_return, success_flag = self.rethink_user_memory(agent_state=agent_state, **tool_args)
106
+ return ToolExecutionResult(func_return=func_return, status="success" if success_flag else "error")
104
107
  elif target_tool.name == "finish_rethinking_memory" and target_tool.tool_type == ToolType.LETTA_VOICE_SLEEPTIME_CORE:
105
- return "", True
108
+ return ToolExecutionResult(func_return="", status="success")
106
109
  elif target_tool.name == "store_memories" and target_tool.tool_type == ToolType.LETTA_VOICE_SLEEPTIME_CORE:
107
110
  chunks = tool_args.get("chunks", [])
108
111
  results = [self.store_memory(agent_state=self.convo_agent_state, **chunk_args) for chunk_args in chunks]
@@ -110,12 +113,14 @@ class VoiceSleeptimeAgent(LettaAgent):
110
113
  aggregated_result = next((res for res, _ in results if res is not None), None)
111
114
  aggregated_success = all(success for _, success in results)
112
115
 
113
- return aggregated_result, aggregated_success # Note that here we store to the convo agent's archival memory
116
+ return ToolExecutionResult(
117
+ func_return=aggregated_result, status="success" if aggregated_success else "error"
118
+ ) # Note that here we store to the convo agent's archival memory
114
119
  else:
115
120
  result = f"Voice sleeptime agent tried invoking invalid tool with type {target_tool.tool_type}: {target_tool}"
116
- return result, False
121
+ return ToolExecutionResult(func_return=result, status="error")
117
122
  except Exception as e:
118
- return f"Failed to call tool. Error: {e}", False
123
+ return ToolExecutionResult(func_return=f"Failed to call tool. Error: {e}", status="error")
119
124
 
120
125
  def rethink_user_memory(self, new_memory: str, agent_state: AgentState) -> Tuple[str, bool]:
121
126
  if agent_state.memory.get_block(self.target_block_label) is None:
letta/constants.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import re
2
3
  from logging import CRITICAL, DEBUG, ERROR, INFO, NOTSET, WARN, WARNING
3
4
 
4
5
  LETTA_DIR = os.path.join(os.path.expanduser("~"), ".letta")
@@ -36,6 +37,9 @@ TOOL_CALL_ID_MAX_LEN = 29
36
37
  # minimum context window size
37
38
  MIN_CONTEXT_WINDOW = 4096
38
39
 
40
+ # number of concurrent embedding requests to sent
41
+ EMBEDDING_BATCH_SIZE = 200
42
+
39
43
  # Voice Sleeptime message buffer lengths
40
44
  DEFAULT_MAX_MESSAGE_BUFFER_LENGTH = 30
41
45
  DEFAULT_MIN_MESSAGE_BUFFER_LENGTH = 15
@@ -56,12 +60,23 @@ DEFAULT_PERSONA = "sam_pov"
56
60
  DEFAULT_HUMAN = "basic"
57
61
  DEFAULT_PRESET = "memgpt_chat"
58
62
 
63
+ DEFAULT_PERSONA_BLOCK_DESCRIPTION = "The persona block: Stores details about your current persona, guiding how you behave and respond. This helps you to maintain consistency and personality in your interactions."
64
+ DEFAULT_HUMAN_BLOCK_DESCRIPTION = "The human block: Stores key details about the person you are conversing with, allowing for more personalized and friend-like conversation."
65
+
59
66
  SEND_MESSAGE_TOOL_NAME = "send_message"
60
67
  # Base tools that cannot be edited, as they access agent state directly
61
68
  # Note that we don't include "conversation_search_date" for now
62
69
  BASE_TOOLS = [SEND_MESSAGE_TOOL_NAME, "conversation_search", "archival_memory_insert", "archival_memory_search"]
63
70
  # Base memory tools CAN be edited, and are added by default by the server
64
71
  BASE_MEMORY_TOOLS = ["core_memory_append", "core_memory_replace"]
72
+ # New v2 collection of the base memory tools (effecitvely same as sleeptime set), to pair with memgpt_v2 prompt
73
+ BASE_MEMORY_TOOLS_V2 = [
74
+ "memory_replace",
75
+ "memory_insert",
76
+ # NOTE: leaving these ones out to simply the set? Can have these reserved for sleep-time
77
+ # "memory_rethink",
78
+ # "memory_finish_edits",
79
+ ]
65
80
  # Base tools if the memgpt agent has enable_sleeptime on
66
81
  BASE_SLEEPTIME_CHAT_TOOLS = [SEND_MESSAGE_TOOL_NAME, "conversation_search", "archival_memory_search"]
67
82
  # Base memory tools for sleeptime agent
@@ -85,6 +100,15 @@ BASE_VOICE_SLEEPTIME_TOOLS = [
85
100
  # Multi agent tools
86
101
  MULTI_AGENT_TOOLS = ["send_message_to_agent_and_wait_for_reply", "send_message_to_agents_matching_tags", "send_message_to_agent_async"]
87
102
 
103
+ # Used to catch if line numbers are pushed in
104
+ # MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX = re.compile(r"^Line \d+: ", re.MULTILINE)
105
+ # More "robust" version that handles different kinds of whitespace
106
+ # shared constant for both memory_insert and memory_replace
107
+ MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX = re.compile(
108
+ r"^[ \t]*Line[ \t]+\d+[ \t]*:", # allow any leading whitespace and flexible spacing
109
+ re.MULTILINE,
110
+ )
111
+
88
112
  # Built in tools
89
113
  BUILTIN_TOOLS = ["run_code", "web_search"]
90
114
 
@@ -99,6 +123,13 @@ LETTA_TOOL_SET = set(
99
123
  + BUILTIN_TOOLS
100
124
  )
101
125
 
126
+
127
+ def FUNCTION_RETURN_VALUE_TRUNCATED(return_str, return_char: int, return_char_limit: int):
128
+ return (
129
+ f"{return_str}... [NOTE: function output was truncated since it exceeded the character limit: {return_char} > {return_char_limit}]"
130
+ )
131
+
132
+
102
133
  # The name of the tool used to send message to the user
103
134
  # May not be relevant in cases where the agent has multiple ways to message to user (send_imessage, send_discord_mesasge, ...)
104
135
  # or in cases where the agent has no concept of messaging a user (e.g. a workflow agent)
@@ -108,6 +139,7 @@ DEFAULT_MESSAGE_TOOL_KWARG = "message"
108
139
  PRE_EXECUTION_MESSAGE_ARG = "pre_exec_msg"
109
140
 
110
141
  REQUEST_HEARTBEAT_PARAM = "request_heartbeat"
142
+ REQUEST_HEARTBEAT_DESCRIPTION = "Request an immediate heartbeat after function execution. Set to `True` if you want to send a follow-up message or run a follow-up function."
111
143
 
112
144
 
113
145
  # Structured output models
@@ -258,7 +290,7 @@ MAX_ERROR_MESSAGE_CHAR_LIMIT = 500
258
290
  CORE_MEMORY_PERSONA_CHAR_LIMIT: int = 5000
259
291
  CORE_MEMORY_HUMAN_CHAR_LIMIT: int = 5000
260
292
  CORE_MEMORY_BLOCK_CHAR_LIMIT: int = 5000
261
-
293
+ CORE_MEMORY_SOURCE_CHAR_LIMIT: int = 5000
262
294
  # Function return limits
263
295
  FUNCTION_RETURN_CHAR_LIMIT = 6000 # ~300 words
264
296
  BASE_FUNCTION_RETURN_CHAR_LIMIT = 1000000 # very high (we rely on implementation)
@@ -2,6 +2,7 @@ from typing import Dict, Iterator, List, Tuple
2
2
 
3
3
  import typer
4
4
 
5
+ from letta.constants import EMBEDDING_BATCH_SIZE
5
6
  from letta.data_sources.connectors_helper import assert_all_files_exist_locally, extract_metadata_from_files, get_filenames_in_dir
6
7
  from letta.embeddings import embedding_model
7
8
  from letta.schemas.file import FileMetadata
@@ -40,43 +41,37 @@ class DataConnector:
40
41
  async def load_data(
41
42
  connector: DataConnector, source: Source, passage_manager: PassageManager, source_manager: SourceManager, actor: "User"
42
43
  ):
44
+ from letta.llm_api.llm_client import LLMClient
45
+ from letta.schemas.embedding_config import EmbeddingConfig
46
+
43
47
  """Load data from a connector (generates file and passages) into a specified source_id, associated with a user_id."""
44
48
  embedding_config = source.embedding_config
45
49
 
46
- # embedding model
47
- embed_model = embedding_model(embedding_config)
48
-
49
50
  # insert passages/file
50
- passages = []
51
+ texts = []
51
52
  embedding_to_document_name = {}
52
53
  passage_count = 0
53
54
  file_count = 0
54
- for file_metadata in connector.find_files(source):
55
- file_count += 1
56
- await source_manager.create_file(file_metadata, actor)
57
55
 
58
- # generate passages
59
- for passage_text, passage_metadata in connector.generate_passages(file_metadata, chunk_size=embedding_config.embedding_chunk_size):
60
- # for some reason, llama index parsers sometimes return empty strings
61
- if len(passage_text) == 0:
62
- typer.secho(
63
- f"Warning: Llama index parser returned empty string, skipping insert of passage with metadata '{passage_metadata}' into VectorDB. You can usually ignore this warning.",
64
- fg=typer.colors.YELLOW,
65
- )
66
- continue
56
+ async def generate_embeddings(texts: List[str], embedding_config: EmbeddingConfig) -> List[Passage]:
57
+ passages = []
58
+ if embedding_config.embedding_endpoint_type == "openai":
59
+ texts.append(passage_text)
67
60
 
68
- # get embedding
69
- try:
70
- embedding = embed_model.get_text_embedding(passage_text)
71
- except Exception as e:
72
- typer.secho(
73
- f"Warning: Failed to get embedding for {passage_text} (error: {str(e)}), skipping insert into VectorDB.",
74
- fg=typer.colors.YELLOW,
75
- )
76
- continue
61
+ client = LLMClient.create(
62
+ provider_type=embedding_config.embedding_endpoint_type,
63
+ actor=actor,
64
+ )
65
+ embeddings = await client.request_embeddings(texts, embedding_config)
77
66
 
67
+ else:
68
+ embed_model = embedding_model(embedding_config)
69
+ embeddings = [embed_model.get_text_embedding(text) for text in texts]
70
+
71
+ # collate passage and embedding
72
+ for text, embedding in zip(texts, embeddings):
78
73
  passage = Passage(
79
- text=passage_text,
74
+ text=text,
80
75
  file_id=file_metadata.id,
81
76
  source_id=source.id,
82
77
  metadata=passage_metadata,
@@ -84,7 +79,6 @@ async def load_data(
84
79
  embedding_config=source.embedding_config,
85
80
  embedding=embedding,
86
81
  )
87
-
88
82
  hashable_embedding = tuple(passage.embedding)
89
83
  file_name = file_metadata.file_name
90
84
  if hashable_embedding in embedding_to_document_name:
@@ -96,16 +90,38 @@ async def load_data(
96
90
 
97
91
  passages.append(passage)
98
92
  embedding_to_document_name[hashable_embedding] = file_name
99
- if len(passages) >= 100:
100
- # insert passages into passage store
101
- passage_manager.create_many_passages(passages, actor)
93
+ return passages
94
+
95
+ for file_metadata in connector.find_files(source):
96
+ file_count += 1
97
+ await source_manager.create_file(file_metadata, actor)
98
+
99
+ # generate passages
100
+ for passage_text, passage_metadata in connector.generate_passages(file_metadata, chunk_size=embedding_config.embedding_chunk_size):
101
+ # for some reason, llama index parsers sometimes return empty strings
102
+ if len(passage_text) == 0:
103
+ typer.secho(
104
+ f"Warning: Llama index parser returned empty string, skipping insert of passage with metadata '{passage_metadata}' into VectorDB. You can usually ignore this warning.",
105
+ fg=typer.colors.YELLOW,
106
+ )
107
+ continue
108
+
109
+ # get embedding
110
+ texts.append(passage_text)
111
+ if len(texts) >= EMBEDDING_BATCH_SIZE:
112
+ passages = await generate_embeddings(texts, embedding_config)
113
+ texts = []
114
+ else:
115
+ continue
102
116
 
103
- passage_count += len(passages)
104
- passages = []
117
+ # insert passages into passage store
118
+ await passage_manager.create_many_passages_async(passages, actor)
119
+ passage_count += len(passages)
105
120
 
106
- if len(passages) > 0:
107
- # insert passages into passage store
108
- passage_manager.create_many_passages(passages, actor)
121
+ # final remaining
122
+ if len(texts) > 0:
123
+ passages = await generate_embeddings(texts, embedding_config)
124
+ await passage_manager.create_many_passages_async(passages, actor)
109
125
  passage_count += len(passages)
110
126
 
111
127
  return passage_count, file_count
@@ -128,7 +144,7 @@ class DirectoryConnector(DataConnector):
128
144
  self.recursive = recursive
129
145
  self.extensions = extensions
130
146
 
131
- if self.recursive == True:
147
+ if self.recursive:
132
148
  assert self.input_directory is not None, "Must provide input directory if recursive is True."
133
149
 
134
150
  def find_files(self, source: Source) -> Iterator[FileMetadata]:
letta/errors.py CHANGED
@@ -88,6 +88,10 @@ class LLMPermissionDeniedError(LLMError):
88
88
  """Error when permission is denied by LLM service"""
89
89
 
90
90
 
91
+ class LLMContextWindowExceededError(LLMError):
92
+ """Error when the context length is exceeded."""
93
+
94
+
91
95
  class LLMNotFoundError(LLMError):
92
96
  """Error when requested resource is not found"""
93
97