letta-nightly 0.7.30.dev20250603104343__py3-none-any.whl → 0.8.0.dev20250604104349__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.dev20250604104349.dist-info}/METADATA +7 -2
  133. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/RECORD +136 -110
  134. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/LICENSE +0 -0
  135. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/WHEEL +0 -0
  136. {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/entry_points.txt +0 -0
@@ -192,6 +192,15 @@ class SandboxConfigManager:
192
192
  sandbox.hard_delete(db_session=session, actor=actor)
193
193
  return sandbox.to_pydantic()
194
194
 
195
+ @enforce_types
196
+ @trace_method
197
+ async def delete_sandbox_config_async(self, sandbox_config_id: str, actor: PydanticUser) -> PydanticSandboxConfig:
198
+ """Delete a sandbox configuration by its ID."""
199
+ async with db_registry.async_session() as session:
200
+ sandbox = await SandboxConfigModel.read_async(db_session=session, identifier=sandbox_config_id, actor=actor)
201
+ await sandbox.hard_delete_async(db_session=session, actor=actor)
202
+ return sandbox.to_pydantic()
203
+
195
204
  @enforce_types
196
205
  @trace_method
197
206
  def list_sandbox_configs(
@@ -305,6 +314,34 @@ class SandboxConfigManager:
305
314
  env_var.create(session, actor=actor)
306
315
  return env_var.to_pydantic()
307
316
 
317
+ @enforce_types
318
+ @trace_method
319
+ async def create_sandbox_env_var_async(
320
+ self, env_var_create: SandboxEnvironmentVariableCreate, sandbox_config_id: str, actor: PydanticUser
321
+ ) -> PydanticEnvVar:
322
+ """Create a new sandbox environment variable."""
323
+ env_var = PydanticEnvVar(**env_var_create.model_dump(), sandbox_config_id=sandbox_config_id, organization_id=actor.organization_id)
324
+
325
+ db_env_var = await self.get_sandbox_env_var_by_key_and_sandbox_config_id_async(env_var.key, env_var.sandbox_config_id, actor=actor)
326
+ if db_env_var:
327
+ update_data = env_var.model_dump(exclude_unset=True, exclude_none=True)
328
+ update_data = {key: value for key, value in update_data.items() if getattr(db_env_var, key) != value}
329
+ # If there are changes, update the environment variable
330
+ if update_data:
331
+ db_env_var = await self.update_sandbox_env_var_async(db_env_var.id, SandboxEnvironmentVariableUpdate(**update_data), actor)
332
+ else:
333
+ printd(
334
+ f"`create_or_update_sandbox_env_var` was called with user_id={actor.id}, organization_id={actor.organization_id}, "
335
+ f"key={env_var.key}, but found existing variable with nothing to update."
336
+ )
337
+
338
+ return db_env_var
339
+ else:
340
+ async with db_registry.async_session() as session:
341
+ env_var = SandboxEnvVarModel(**env_var.model_dump(to_orm=True, exclude_none=True))
342
+ await env_var.create_async(session, actor=actor)
343
+ return env_var.to_pydantic()
344
+
308
345
  @enforce_types
309
346
  @trace_method
310
347
  def update_sandbox_env_var(
@@ -327,6 +364,28 @@ class SandboxConfigManager:
327
364
  )
328
365
  return env_var.to_pydantic()
329
366
 
367
+ @enforce_types
368
+ @trace_method
369
+ async def update_sandbox_env_var_async(
370
+ self, env_var_id: str, env_var_update: SandboxEnvironmentVariableUpdate, actor: PydanticUser
371
+ ) -> PydanticEnvVar:
372
+ """Update an existing sandbox environment variable."""
373
+ async with db_registry.async_session() as session:
374
+ env_var = await SandboxEnvVarModel.read_async(db_session=session, identifier=env_var_id, actor=actor)
375
+ update_data = env_var_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
376
+ update_data = {key: value for key, value in update_data.items() if getattr(env_var, key) != value}
377
+
378
+ if update_data:
379
+ for key, value in update_data.items():
380
+ setattr(env_var, key, value)
381
+ await env_var.update_async(db_session=session, actor=actor)
382
+ else:
383
+ printd(
384
+ f"`update_sandbox_env_var` called with user_id={actor.id}, organization_id={actor.organization_id}, "
385
+ f"key={env_var.key}, but nothing to update."
386
+ )
387
+ return env_var.to_pydantic()
388
+
330
389
  @enforce_types
331
390
  @trace_method
332
391
  def delete_sandbox_env_var(self, env_var_id: str, actor: PydanticUser) -> PydanticEnvVar:
@@ -336,6 +395,15 @@ class SandboxConfigManager:
336
395
  env_var.hard_delete(db_session=session, actor=actor)
337
396
  return env_var.to_pydantic()
338
397
 
398
+ @enforce_types
399
+ @trace_method
400
+ async def delete_sandbox_env_var_async(self, env_var_id: str, actor: PydanticUser) -> PydanticEnvVar:
401
+ """Delete a sandbox environment variable by its ID."""
402
+ async with db_registry.async_session() as session:
403
+ env_var = await SandboxEnvVarModel.read_async(db_session=session, identifier=env_var_id, actor=actor)
404
+ await env_var.hard_delete_async(db_session=session, actor=actor)
405
+ return env_var.to_pydantic()
406
+
339
407
  @enforce_types
340
408
  @trace_method
341
409
  def list_sandbox_env_vars(
@@ -392,6 +460,22 @@ class SandboxConfigManager:
392
460
  )
393
461
  return [env_var.to_pydantic() for env_var in env_vars]
394
462
 
463
+ @enforce_types
464
+ @trace_method
465
+ async def list_sandbox_env_vars_by_key_async(
466
+ self, key: str, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50
467
+ ) -> List[PydanticEnvVar]:
468
+ """List all sandbox environment variables with optional pagination."""
469
+ async with db_registry.async_session() as session:
470
+ env_vars = await SandboxEnvVarModel.list_async(
471
+ db_session=session,
472
+ after=after,
473
+ limit=limit,
474
+ organization_id=actor.organization_id,
475
+ key=key,
476
+ )
477
+ return [env_var.to_pydantic() for env_var in env_vars]
478
+
395
479
  @enforce_types
396
480
  @trace_method
397
481
  def get_sandbox_env_vars_as_dict(
@@ -434,3 +518,24 @@ class SandboxConfigManager:
434
518
  return None
435
519
  except NoResultFound:
436
520
  return None
521
+
522
+ @enforce_types
523
+ @trace_method
524
+ async def get_sandbox_env_var_by_key_and_sandbox_config_id_async(
525
+ self, key: str, sandbox_config_id: str, actor: Optional[PydanticUser] = None
526
+ ) -> Optional[PydanticEnvVar]:
527
+ """Retrieve a sandbox environment variable by its key and sandbox_config_id."""
528
+ async with db_registry.async_session() as session:
529
+ try:
530
+ env_var = await SandboxEnvVarModel.list_async(
531
+ db_session=session,
532
+ key=key,
533
+ sandbox_config_id=sandbox_config_id,
534
+ organization_id=actor.organization_id,
535
+ limit=1,
536
+ )
537
+ if env_var:
538
+ return env_var[0].to_pydantic()
539
+ return None
540
+ except NoResultFound:
541
+ return None
@@ -31,7 +31,7 @@ class SourceManager:
31
31
  source.organization_id = actor.organization_id
32
32
  source = SourceModel(**source.model_dump(to_orm=True, exclude_none=True))
33
33
  await source.create_async(session, actor=actor)
34
- return source.to_pydantic()
34
+ return source.to_pydantic()
35
35
 
36
36
  @enforce_types
37
37
  @trace_method
@@ -48,7 +48,7 @@ class SourceManager:
48
48
  if update_data:
49
49
  for key, value in update_data.items():
50
50
  setattr(source, key, value)
51
- source.update(db_session=session, actor=actor)
51
+ await source.update_async(db_session=session, actor=actor)
52
52
  else:
53
53
  printd(
54
54
  f"`update_source` was called with user_id={actor.id}, organization_id={actor.organization_id}, name={source.name}, but found existing source with nothing to update."
@@ -83,7 +83,7 @@ class SourceManager:
83
83
 
84
84
  @enforce_types
85
85
  @trace_method
86
- async def size(self, actor: PydanticUser) -> int:
86
+ async def size_async(self, actor: PydanticUser) -> int:
87
87
  """
88
88
  Get the total count of sources for the given user.
89
89
  """
@@ -152,7 +152,7 @@ class SourceManager:
152
152
  file_metadata.organization_id = actor.organization_id
153
153
  file_metadata = FileMetadataModel(**file_metadata.model_dump(to_orm=True, exclude_none=True))
154
154
  await file_metadata.create_async(session, actor=actor)
155
- return file_metadata.to_pydantic()
155
+ return file_metadata.to_pydantic()
156
156
 
157
157
  # TODO: We make actor optional for now, but should most likely be enforced due to security reasons
158
158
  @enforce_types
@@ -173,7 +173,6 @@ class SourceManager:
173
173
  ) -> List[PydanticFileMetadata]:
174
174
  """List all files with optional pagination."""
175
175
  async with db_registry.async_session() as session:
176
- files_all = await FileMetadataModel.list_async(db_session=session, organization_id=actor.organization_id, source_id=source_id)
177
176
  files = await FileMetadataModel.list_async(
178
177
  db_session=session, after=after, limit=limit, organization_id=actor.organization_id, source_id=source_id
179
178
  )
@@ -22,7 +22,7 @@ class StepManager:
22
22
 
23
23
  @enforce_types
24
24
  @trace_method
25
- def list_steps(
25
+ async def list_steps_async(
26
26
  self,
27
27
  actor: PydanticUser,
28
28
  before: Optional[str] = None,
@@ -33,16 +33,19 @@ class StepManager:
33
33
  order: Optional[str] = None,
34
34
  model: Optional[str] = None,
35
35
  agent_id: Optional[str] = None,
36
+ trace_ids: Optional[list[str]] = None,
36
37
  ) -> List[PydanticStep]:
37
38
  """List all jobs with optional pagination and status filter."""
38
- with db_registry.session() as session:
39
+ async with db_registry.async_session() as session:
39
40
  filter_kwargs = {"organization_id": actor.organization_id}
40
41
  if model:
41
42
  filter_kwargs["model"] = model
42
43
  if agent_id:
43
44
  filter_kwargs["agent_id"] = agent_id
45
+ if trace_ids:
46
+ filter_kwargs["trace_id"] = trace_ids
44
47
 
45
- steps = StepModel.list(
48
+ steps = await StepModel.list_async(
46
49
  db_session=session,
47
50
  before=before,
48
51
  after=after,
@@ -142,9 +145,9 @@ class StepManager:
142
145
 
143
146
  @enforce_types
144
147
  @trace_method
145
- def get_step(self, step_id: str, actor: PydanticUser) -> PydanticStep:
146
- with db_registry.session() as session:
147
- step = StepModel.read(db_session=session, identifier=step_id, actor=actor)
148
+ async def get_step_async(self, step_id: str, actor: PydanticUser) -> PydanticStep:
149
+ async with db_registry.async_session() as session:
150
+ step = await StepModel.read_async(db_session=session, identifier=step_id, actor=actor)
148
151
  return step.to_pydantic()
149
152
 
150
153
  @enforce_types
@@ -1,14 +1,16 @@
1
1
  import asyncio
2
2
  import json
3
3
  import traceback
4
- from typing import List, Optional, Tuple
4
+ from typing import List, Optional, Tuple, Union
5
5
 
6
+ from letta.agents.ephemeral_summary_agent import EphemeralSummaryAgent
6
7
  from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
7
8
  from letta.log import get_logger
8
9
  from letta.schemas.enums import MessageRole
9
10
  from letta.schemas.letta_message_content import TextContent
10
11
  from letta.schemas.message import Message, MessageCreate
11
12
  from letta.services.summarizer.enums import SummarizationMode
13
+ from letta.tracing import trace_method
12
14
 
13
15
  logger = get_logger(__name__)
14
16
 
@@ -23,7 +25,7 @@ class Summarizer:
23
25
  def __init__(
24
26
  self,
25
27
  mode: SummarizationMode,
26
- summarizer_agent: Optional["VoiceSleeptimeAgent"] = None,
28
+ summarizer_agent: Optional[Union[EphemeralSummaryAgent, "VoiceSleeptimeAgent"]] = None,
27
29
  message_buffer_limit: int = 10,
28
30
  message_buffer_min: int = 3,
29
31
  ):
@@ -35,7 +37,10 @@ class Summarizer:
35
37
  self.summarizer_agent = summarizer_agent
36
38
  # TODO: Move this to config
37
39
 
38
- def summarize(self, in_context_messages: List[Message], new_letta_messages: List[Message]) -> Tuple[List[Message], bool]:
40
+ @trace_method
41
+ def summarize(
42
+ self, in_context_messages: List[Message], new_letta_messages: List[Message], force: bool = False, clear: bool = False
43
+ ) -> Tuple[List[Message], bool]:
39
44
  """
40
45
  Summarizes or trims in_context_messages according to the chosen mode,
41
46
  and returns the updated messages plus any optional "summary message".
@@ -43,6 +48,7 @@ class Summarizer:
43
48
  Args:
44
49
  in_context_messages: The existing messages in the conversation's context.
45
50
  new_letta_messages: The newly added Letta messages (just appended).
51
+ force: Force summarize even if the criteria is not met
46
52
 
47
53
  Returns:
48
54
  (updated_messages, summary_message)
@@ -51,7 +57,7 @@ class Summarizer:
51
57
  (could be appended to the conversation if desired)
52
58
  """
53
59
  if self.mode == SummarizationMode.STATIC_MESSAGE_BUFFER:
54
- return self._static_buffer_summarization(in_context_messages, new_letta_messages)
60
+ return self._static_buffer_summarization(in_context_messages, new_letta_messages, force=force, clear=clear)
55
61
  else:
56
62
  # Fallback or future logic
57
63
  return in_context_messages, False
@@ -69,42 +75,48 @@ class Summarizer:
69
75
  return task
70
76
 
71
77
  def _static_buffer_summarization(
72
- self, in_context_messages: List[Message], new_letta_messages: List[Message]
78
+ self, in_context_messages: List[Message], new_letta_messages: List[Message], force: bool = False, clear: bool = False
73
79
  ) -> Tuple[List[Message], bool]:
74
80
  all_in_context_messages = in_context_messages + new_letta_messages
75
81
 
76
- if len(all_in_context_messages) <= self.message_buffer_limit:
82
+ if len(all_in_context_messages) <= self.message_buffer_limit and not force:
77
83
  logger.info(
78
84
  f"Nothing to evict, returning in context messages as is. Current buffer length is {len(all_in_context_messages)}, limit is {self.message_buffer_limit}."
79
85
  )
80
86
  return all_in_context_messages, False
81
87
 
82
- logger.info("Buffer length hit, evicting messages.")
88
+ retain_count = 0 if clear else self.message_buffer_min
89
+
90
+ if not force:
91
+ logger.info(f"Buffer length hit {self.message_buffer_limit}, evicting until we retain only {retain_count} messages.")
92
+ else:
93
+ logger.info(f"Requested force summarization, evicting until we retain only {retain_count} messages.")
83
94
 
84
- target_trim_index = len(all_in_context_messages) - self.message_buffer_min
95
+ target_trim_index = max(1, len(all_in_context_messages) - retain_count)
85
96
 
86
97
  while target_trim_index < len(all_in_context_messages) and all_in_context_messages[target_trim_index].role != MessageRole.user:
87
98
  target_trim_index += 1
88
99
 
89
- updated_in_context_messages = all_in_context_messages[target_trim_index:]
100
+ evicted_messages = all_in_context_messages[1:target_trim_index] # everything except sys msg
101
+ updated_in_context_messages = all_in_context_messages[target_trim_index:] # may be empty
90
102
 
91
- # Target trim index went beyond end of all_in_context_messages
92
- if not updated_in_context_messages:
93
- logger.info("Nothing to evict, returning in context messages as is.")
103
+ # If *no* messages were evicted we really have nothing to do
104
+ if not evicted_messages:
105
+ logger.info("Nothing to evict, returning in-context messages as-is.")
94
106
  return all_in_context_messages, False
95
107
 
96
108
  if self.summarizer_agent:
97
109
  # Only invoke if summarizer agent is passed in
98
-
99
- evicted_messages = all_in_context_messages[1:target_trim_index]
100
-
101
110
  # Format
102
111
  formatted_evicted_messages = format_transcript(evicted_messages)
103
112
  formatted_in_context_messages = format_transcript(updated_in_context_messages)
104
113
 
105
114
  # TODO: This is hyperspecific to voice, generalize!
106
115
  # Update the message transcript of the memory agent
107
- self.summarizer_agent.update_message_transcript(message_transcripts=formatted_evicted_messages + formatted_in_context_messages)
116
+ if not isinstance(self.summarizer_agent, EphemeralSummaryAgent):
117
+ self.summarizer_agent.update_message_transcript(
118
+ message_transcripts=formatted_evicted_messages + formatted_in_context_messages
119
+ )
108
120
 
109
121
  # Add line numbers to the formatted messages
110
122
  offset = len(formatted_evicted_messages)
@@ -113,14 +125,28 @@ class Summarizer:
113
125
 
114
126
  evicted_messages_str = "\n".join(formatted_evicted_messages)
115
127
  in_context_messages_str = "\n".join(formatted_in_context_messages)
116
- summary_request_text = f"""You’re a memory-recall helper for an AI that can only keep the last {self.message_buffer_min} messages. Scan the conversation history, focusing on messages about to drop out of that window, and write crisp notes that capture any important facts or insights about the human so they aren’t lost.
128
+ # Base prompt
129
+ prompt_header = (
130
+ f"You’re a memory-recall helper for an AI that can only keep the last {retain_count} messages. "
131
+ "Scan the conversation history, focusing on messages about to drop out of that window, "
132
+ "and write crisp notes that capture any important facts or insights about the conversation history so they aren’t lost."
133
+ )
117
134
 
118
- (Older) Evicted Messages:\n
119
- {evicted_messages_str}\n
135
+ # Sections
136
+ evicted_section = f"\n\n(Older) Evicted Messages:\n{evicted_messages_str}" if evicted_messages_str.strip() else ""
137
+ in_context_section = ""
138
+
139
+ if retain_count > 0 and in_context_messages_str.strip():
140
+ in_context_section = f"\n\n(Newer) In-Context Messages:\n{in_context_messages_str}"
141
+ elif retain_count == 0:
142
+ prompt_header = (
143
+ "You’re a memory-recall helper for an AI that is about to forget all prior messages. "
144
+ "Scan the conversation history and write crisp notes that capture any important facts or insights about the conversation history."
145
+ )
146
+
147
+ # Compose final prompt
148
+ summary_request_text = prompt_header + evicted_section + in_context_section
120
149
 
121
- (Newer) In-Context Messages:\n
122
- {in_context_messages_str}
123
- """
124
150
  # Fire-and-forget the summarization task
125
151
  self.fire_and_forget(
126
152
  self.summarizer_agent.step([MessageCreate(role=MessageRole.user, content=[TextContent(text=summary_request_text)])])
@@ -156,7 +182,8 @@ def format_transcript(messages: List[Message], include_system: bool = False) ->
156
182
  # Skip tool messages where the name is "send_message"
157
183
  if msg.role == MessageRole.tool and msg.name == DEFAULT_MESSAGE_TOOL:
158
184
  continue
159
- text = "".join(c.text for c in msg.content).strip()
185
+
186
+ text = "".join(c.text for c in msg.content if isinstance(c, TextContent)).strip()
160
187
 
161
188
  # 2) Otherwise, try extracting from function calls
162
189
  elif msg.tool_calls:
@@ -38,6 +38,13 @@ class TelemetryManager:
38
38
  def create_provider_trace(self, actor: PydanticUser, provider_trace_create: ProviderTraceCreate) -> PydanticProviderTrace:
39
39
  with db_registry.session() as session:
40
40
  provider_trace = ProviderTraceModel(**provider_trace_create.model_dump())
41
+ if provider_trace_create.request_json:
42
+ request_json_str = json_dumps(provider_trace_create.request_json)
43
+ provider_trace.request_json = json_loads(request_json_str)
44
+
45
+ if provider_trace_create.response_json:
46
+ response_json_str = json_dumps(provider_trace_create.response_json)
47
+ provider_trace.response_json = json_loads(response_json_str)
41
48
  provider_trace.create(session, actor=actor)
42
49
  return provider_trace.to_pydantic()
43
50
 
@@ -1,6 +1,7 @@
1
1
  import traceback
2
2
  from typing import Any, Dict, Optional, Type
3
3
 
4
+ from letta.constants import FUNCTION_RETURN_VALUE_TRUNCATED
4
5
  from letta.log import get_logger
5
6
  from letta.orm.enums import ToolType
6
7
  from letta.schemas.agent import AgentState
@@ -68,8 +69,8 @@ class ToolExecutionManager:
68
69
  agent_manager: AgentManager,
69
70
  block_manager: BlockManager,
70
71
  passage_manager: PassageManager,
71
- agent_state: AgentState,
72
72
  actor: User,
73
+ agent_state: Optional[AgentState] = None,
73
74
  sandbox_config: Optional[SandboxConfig] = None,
74
75
  sandbox_env_vars: Optional[Dict[str, Any]] = None,
75
76
  ):
@@ -83,50 +84,6 @@ class ToolExecutionManager:
83
84
  self.sandbox_config = sandbox_config
84
85
  self.sandbox_env_vars = sandbox_env_vars
85
86
 
86
- def execute_tool(self, function_name: str, function_args: dict, tool: Tool) -> ToolExecutionResult:
87
- """
88
- Execute a tool and persist any state changes.
89
-
90
- Args:
91
- function_name: Name of the function to execute
92
- function_args: Arguments to pass to the function
93
- tool: Tool object containing metadata about the tool
94
-
95
- Returns:
96
- Tuple containing the function response and sandbox run result (if applicable)
97
- """
98
- try:
99
- executor = ToolExecutorFactory.get_executor(
100
- tool.tool_type,
101
- message_manager=self.message_manager,
102
- agent_manager=self.agent_manager,
103
- block_manager=self.block_manager,
104
- passage_manager=self.passage_manager,
105
- actor=self.actor,
106
- )
107
- return executor.execute(
108
- function_name,
109
- function_args,
110
- self.agent_state,
111
- tool,
112
- self.actor,
113
- self.sandbox_config,
114
- self.sandbox_env_vars,
115
- )
116
-
117
- except Exception as e:
118
- self.logger.error(f"Error executing tool {function_name}: {str(e)}")
119
- error_message = get_friendly_error_msg(
120
- function_name=function_name,
121
- exception_name=type(e).__name__,
122
- exception_message=str(e),
123
- )
124
- return ToolExecutionResult(
125
- status="error",
126
- func_return=error_message,
127
- stderr=[traceback.format_exc()],
128
- )
129
-
130
87
  @trace_method
131
88
  async def execute_tool_async(self, function_name: str, function_args: dict, tool: Tool) -> ToolExecutionResult:
132
89
  """
@@ -141,13 +98,15 @@ class ToolExecutionManager:
141
98
  passage_manager=self.passage_manager,
142
99
  actor=self.actor,
143
100
  )
144
- # TODO: Extend this async model to composio
145
- if isinstance(
146
- executor, (SandboxToolExecutor, ExternalComposioToolExecutor, LettaBuiltinToolExecutor, LettaMultiAgentToolExecutor)
147
- ):
148
- result = await executor.execute(function_name, function_args, self.agent_state, tool, self.actor)
149
- else:
150
- result = executor.execute(function_name, function_args, self.agent_state, tool, self.actor)
101
+ result = await executor.execute(
102
+ function_name, function_args, tool, self.actor, self.agent_state, self.sandbox_config, self.sandbox_env_vars
103
+ )
104
+
105
+ # trim result
106
+ return_str = str(result.func_return)
107
+ if len(return_str) > tool.return_char_limit:
108
+ # TODO: okay that this become a string?
109
+ result.func_return = FUNCTION_RETURN_VALUE_TRUNCATED(return_str, len(return_str), tool.return_char_limit)
151
110
  return result
152
111
 
153
112
  except Exception as e:
@@ -1,4 +1,3 @@
1
- import ast
2
1
  import base64
3
2
  import io
4
3
  import os
@@ -23,6 +22,7 @@ from letta.services.helpers.tool_execution_helper import (
23
22
  find_python_executable,
24
23
  install_pip_requirements_for_sandbox,
25
24
  )
25
+ from letta.services.helpers.tool_parser_helper import convert_param_to_str_value, parse_function_arguments
26
26
  from letta.services.organization_manager import OrganizationManager
27
27
  from letta.services.sandbox_config_manager import SandboxConfigManager
28
28
  from letta.services.tool_manager import ToolManager
@@ -52,7 +52,6 @@ class ToolExecutionSandbox:
52
52
  self.tool_name = tool_name
53
53
  self.args = args
54
54
  self.user = user
55
- # get organization
56
55
  self.organization = OrganizationManager().get_organization_by_id(self.user.organization_id)
57
56
  self.privileged_tools = self.organization.privileged_tools
58
57
 
@@ -476,16 +475,6 @@ class ToolExecutionSandbox:
476
475
  agent_state = result["agent_state"]
477
476
  return result["results"], agent_state
478
477
 
479
- def parse_function_arguments(self, source_code: str, tool_name: str):
480
- """Get arguments of a function from its source code"""
481
- tree = ast.parse(source_code)
482
- args = []
483
- for node in ast.walk(tree):
484
- if isinstance(node, ast.FunctionDef) and node.name == tool_name:
485
- for arg in node.args.args:
486
- args.append(arg.arg)
487
- return args
488
-
489
478
  def generate_execution_script(self, agent_state: AgentState, wrap_print_with_markers: bool = False) -> str:
490
479
  """
491
480
  Generate code to run inside of execution sandbox.
@@ -498,7 +487,7 @@ class ToolExecutionSandbox:
498
487
  Returns:
499
488
  code (str): The generated code strong
500
489
  """
501
- if "agent_state" in self.parse_function_arguments(self.tool.source_code, self.tool.name):
490
+ if "agent_state" in parse_function_arguments(self.tool.source_code, self.tool.name):
502
491
  inject_agent_state = True
503
492
  else:
504
493
  inject_agent_state = False
@@ -546,7 +535,7 @@ class ToolExecutionSandbox:
546
535
  code += (
547
536
  self.LOCAL_SANDBOX_RESULT_VAR_NAME
548
537
  + ' = {"results": '
549
- + self.invoke_function_call(inject_agent_state=inject_agent_state)
538
+ + self.invoke_function_call(inject_agent_state=inject_agent_state) # this inject_agent_state is the main difference
550
539
  + ', "agent_state": agent_state}\n'
551
540
  )
552
541
  code += (
@@ -562,24 +551,6 @@ class ToolExecutionSandbox:
562
551
 
563
552
  return code
564
553
 
565
- def _convert_param_to_value(self, param_type: str, raw_value: str) -> str:
566
-
567
- if param_type == "string":
568
- value = "pickle.loads(" + str(pickle.dumps(raw_value)) + ")"
569
-
570
- elif param_type == "integer" or param_type == "boolean" or param_type == "number":
571
- value = raw_value
572
-
573
- elif param_type == "array":
574
- value = raw_value
575
-
576
- elif param_type == "object":
577
- value = raw_value
578
-
579
- else:
580
- raise TypeError(f"Unsupported type: {param_type}, raw_value={raw_value}")
581
- return str(value)
582
-
583
554
  def initialize_param(self, name: str, raw_value: str) -> str:
584
555
  params = self.tool.json_schema["parameters"]["properties"]
585
556
  spec = params.get(name)
@@ -591,8 +562,7 @@ class ToolExecutionSandbox:
591
562
  if param_type is None and spec.get("parameters"):
592
563
  param_type = spec["parameters"].get("type")
593
564
 
594
- value = self._convert_param_to_value(param_type, raw_value)
595
-
565
+ value = convert_param_to_str_value(param_type, raw_value)
596
566
  return name + " = " + value + "\n"
597
567
 
598
568
  def invoke_function_call(self, inject_agent_state: bool) -> str: