letta-nightly 0.8.8.dev20250703104323__py3-none-any.whl → 0.8.9.dev20250703191231__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 (69) hide show
  1. letta/__init__.py +6 -1
  2. letta/agent.py +1 -0
  3. letta/agents/base_agent.py +8 -2
  4. letta/agents/ephemeral_summary_agent.py +33 -33
  5. letta/agents/letta_agent.py +104 -53
  6. letta/agents/voice_agent.py +2 -1
  7. letta/constants.py +8 -4
  8. letta/functions/function_sets/files.py +22 -7
  9. letta/functions/function_sets/multi_agent.py +34 -0
  10. letta/functions/types.py +1 -1
  11. letta/groups/helpers.py +8 -5
  12. letta/groups/sleeptime_multi_agent_v2.py +20 -15
  13. letta/interface.py +1 -1
  14. letta/interfaces/anthropic_streaming_interface.py +15 -8
  15. letta/interfaces/openai_chat_completions_streaming_interface.py +9 -6
  16. letta/interfaces/openai_streaming_interface.py +17 -11
  17. letta/llm_api/openai_client.py +2 -1
  18. letta/orm/agent.py +1 -0
  19. letta/orm/file.py +8 -2
  20. letta/orm/files_agents.py +36 -11
  21. letta/orm/mcp_server.py +3 -0
  22. letta/orm/source.py +2 -1
  23. letta/orm/step.py +3 -0
  24. letta/prompts/system/memgpt_v2_chat.txt +5 -8
  25. letta/schemas/agent.py +58 -23
  26. letta/schemas/embedding_config.py +3 -2
  27. letta/schemas/enums.py +4 -0
  28. letta/schemas/file.py +1 -0
  29. letta/schemas/letta_stop_reason.py +18 -0
  30. letta/schemas/mcp.py +15 -10
  31. letta/schemas/memory.py +35 -5
  32. letta/schemas/providers.py +11 -0
  33. letta/schemas/step.py +1 -0
  34. letta/schemas/tool.py +2 -1
  35. letta/server/rest_api/routers/v1/agents.py +320 -184
  36. letta/server/rest_api/routers/v1/groups.py +6 -2
  37. letta/server/rest_api/routers/v1/identities.py +6 -2
  38. letta/server/rest_api/routers/v1/jobs.py +49 -1
  39. letta/server/rest_api/routers/v1/sources.py +28 -19
  40. letta/server/rest_api/routers/v1/steps.py +7 -2
  41. letta/server/rest_api/routers/v1/tools.py +40 -9
  42. letta/server/rest_api/streaming_response.py +88 -0
  43. letta/server/server.py +61 -55
  44. letta/services/agent_manager.py +28 -16
  45. letta/services/file_manager.py +58 -9
  46. letta/services/file_processor/chunker/llama_index_chunker.py +2 -0
  47. letta/services/file_processor/embedder/openai_embedder.py +54 -10
  48. letta/services/file_processor/file_processor.py +59 -0
  49. letta/services/file_processor/parser/mistral_parser.py +2 -0
  50. letta/services/files_agents_manager.py +120 -2
  51. letta/services/helpers/agent_manager_helper.py +21 -4
  52. letta/services/job_manager.py +57 -6
  53. letta/services/mcp/base_client.py +1 -0
  54. letta/services/mcp_manager.py +13 -1
  55. letta/services/step_manager.py +14 -5
  56. letta/services/summarizer/summarizer.py +6 -22
  57. letta/services/tool_executor/builtin_tool_executor.py +0 -1
  58. letta/services/tool_executor/files_tool_executor.py +2 -2
  59. letta/services/tool_executor/multi_agent_tool_executor.py +23 -0
  60. letta/services/tool_manager.py +7 -7
  61. letta/settings.py +11 -2
  62. letta/templates/summary_request_text.j2 +19 -0
  63. letta/utils.py +95 -14
  64. {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/METADATA +2 -2
  65. {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/RECORD +69 -68
  66. /letta/{agents/prompts → prompts/system}/summary_system_prompt.txt +0 -0
  67. {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/LICENSE +0 -0
  68. {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/WHEEL +0 -0
  69. {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/entry_points.txt +0 -0
@@ -15,7 +15,6 @@ from letta.constants import (
15
15
  LETTA_TOOL_MODULE_NAMES,
16
16
  LETTA_TOOL_SET,
17
17
  MCP_TOOL_TAG_NAME_PREFIX,
18
- MULTI_AGENT_TOOLS,
19
18
  )
20
19
  from letta.functions.functions import derive_openai_json_schema, load_function_set
21
20
  from letta.log import get_logger
@@ -29,6 +28,7 @@ from letta.schemas.tool import Tool as PydanticTool
29
28
  from letta.schemas.tool import ToolCreate, ToolUpdate
30
29
  from letta.schemas.user import User as PydanticUser
31
30
  from letta.server.db import db_registry
31
+ from letta.services.helpers.agent_manager_helper import calculate_multi_agent_tools
32
32
  from letta.services.mcp.types import SSEServerConfig, StdioServerConfig
33
33
  from letta.utils import enforce_types, printd
34
34
 
@@ -419,7 +419,7 @@ class ToolManager:
419
419
  elif name in BASE_MEMORY_TOOLS:
420
420
  tool_type = ToolType.LETTA_MEMORY_CORE
421
421
  tags = [tool_type.value]
422
- elif name in MULTI_AGENT_TOOLS:
422
+ elif name in calculate_multi_agent_tools():
423
423
  tool_type = ToolType.LETTA_MULTI_AGENT_CORE
424
424
  tags = [tool_type.value]
425
425
  elif name in BASE_SLEEPTIME_TOOLS:
@@ -435,9 +435,8 @@ class ToolManager:
435
435
  tool_type = ToolType.LETTA_FILES_CORE
436
436
  tags = [tool_type.value]
437
437
  else:
438
- raise ValueError(
439
- f"Tool name {name} is not in the list of base tool names: {BASE_TOOLS + BASE_MEMORY_TOOLS + MULTI_AGENT_TOOLS + BASE_SLEEPTIME_TOOLS + BASE_VOICE_SLEEPTIME_TOOLS + BASE_VOICE_SLEEPTIME_CHAT_TOOLS}"
440
- )
438
+ logger.warning(f"Tool name {name} is not in any known base tool set, skipping")
439
+ continue
441
440
 
442
441
  # create to tool
443
442
  tools.append(
@@ -486,7 +485,7 @@ class ToolManager:
486
485
  tool_type = ToolType.LETTA_MEMORY_CORE
487
486
  elif name in BASE_SLEEPTIME_TOOLS:
488
487
  tool_type = ToolType.LETTA_SLEEPTIME_CORE
489
- elif name in MULTI_AGENT_TOOLS:
488
+ elif name in calculate_multi_agent_tools():
490
489
  tool_type = ToolType.LETTA_MULTI_AGENT_CORE
491
490
  elif name in BASE_VOICE_SLEEPTIME_TOOLS or name in BASE_VOICE_SLEEPTIME_CHAT_TOOLS:
492
491
  tool_type = ToolType.LETTA_VOICE_SLEEPTIME_CORE
@@ -495,7 +494,8 @@ class ToolManager:
495
494
  elif name in FILES_TOOLS:
496
495
  tool_type = ToolType.LETTA_FILES_CORE
497
496
  else:
498
- raise ValueError(f"Tool name {name} is not recognized in any known base tool set.")
497
+ logger.warning(f"Tool name {name} is not in any known base tool set, skipping")
498
+ continue
499
499
 
500
500
  if allowed_types is not None and tool_type not in allowed_types:
501
501
  continue
letta/settings.py CHANGED
@@ -6,6 +6,7 @@ from pydantic import AliasChoices, Field
6
6
  from pydantic_settings import BaseSettings, SettingsConfigDict
7
7
 
8
8
  from letta.local_llm.constants import DEFAULT_WRAPPER_NAME
9
+ from letta.services.summarizer.enums import SummarizationMode
9
10
 
10
11
 
11
12
  class ToolSettings(BaseSettings):
@@ -38,6 +39,13 @@ class ToolSettings(BaseSettings):
38
39
  class SummarizerSettings(BaseSettings):
39
40
  model_config = SettingsConfigDict(env_prefix="letta_summarizer_", extra="ignore")
40
41
 
42
+ mode: SummarizationMode = SummarizationMode.STATIC_MESSAGE_BUFFER
43
+ message_buffer_limit: int = 60
44
+ message_buffer_min: int = 15
45
+ enable_summarization: bool = True
46
+ max_summarization_retries: int = 3
47
+
48
+ # TODO(cliandy): the below settings are tied to old summarization and should be deprecated or moved
41
49
  # Controls if we should evict all messages
42
50
  # TODO: Can refactor this into an enum if we have a bunch of different kinds of summarizers
43
51
  evict_all_messages: bool = False
@@ -211,8 +219,9 @@ class Settings(BaseSettings):
211
219
  otel_preferred_temporality: Optional[int] = Field(
212
220
  default=1, ge=0, le=2, description="Exported metric temporality. {0: UNSPECIFIED, 1: DELTA, 2: CUMULATIVE}"
213
221
  )
214
- disable_tracing: bool = False
215
- llm_api_logging: bool = True
222
+ disable_tracing: bool = Field(default=False, description="Disable OTEL Tracing")
223
+ llm_api_logging: bool = Field(default=True, description="Enable LLM API logging at each step")
224
+ track_last_agent_run: bool = Field(default=False, description="Update last agent run metrics")
216
225
 
217
226
  # uvicorn settings
218
227
  uvicorn_workers: int = 1
@@ -0,0 +1,19 @@
1
+ {% if retain_count == 0 %}
2
+ You’re a memory-recall helper for an AI that is about to forget all prior messages. Scan the conversation history and write crisp notes that capture any important facts or insights about the conversation history.
3
+ {% else %}
4
+ You’re a memory-recall helper for an AI that can only keep the last {{ retain_count }} 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.
5
+ {% endif %}
6
+
7
+ {% if evicted_messages %}
8
+ (Older) Evicted Messages:
9
+ {% for item in evicted_messages %}
10
+ {{ item }}
11
+ {% endfor %}
12
+ {% endif %}
13
+
14
+ {% if retain_count > 0 and in_context_messages %}
15
+ (Newer) In-Context Messages:
16
+ {% for item in in_context_messages %}
17
+ {{ item }}
18
+ {% endfor %}
19
+ {% endif %}
letta/utils.py CHANGED
@@ -12,11 +12,12 @@ import re
12
12
  import subprocess
13
13
  import sys
14
14
  import uuid
15
+ from collections.abc import Coroutine
15
16
  from contextlib import contextmanager
16
17
  from datetime import datetime, timezone
17
18
  from functools import wraps
18
19
  from logging import Logger
19
- from typing import Any, Coroutine, List, Union, _GenericAlias, get_args, get_origin, get_type_hints
20
+ from typing import Any, Coroutine, Union, _GenericAlias, get_args, get_origin, get_type_hints
20
21
  from urllib.parse import urljoin, urlparse
21
22
 
22
23
  import demjson3 as demjson
@@ -519,7 +520,7 @@ def enforce_types(func):
519
520
  arg_names = inspect.getfullargspec(func).args
520
521
 
521
522
  # Pair each argument with its corresponding type hint
522
- args_with_hints = dict(zip(arg_names[1:], args[1:])) # Skipping 'self'
523
+ args_with_hints = dict(zip(arg_names[1:], args[1:], strict=False)) # Skipping 'self'
523
524
 
524
525
  # Function to check if a value matches a given type hint
525
526
  def matches_type(value, hint):
@@ -557,7 +558,7 @@ def enforce_types(func):
557
558
  return wrapper
558
559
 
559
560
 
560
- def annotate_message_json_list_with_tool_calls(messages: List[dict], allow_tool_roles: bool = False):
561
+ def annotate_message_json_list_with_tool_calls(messages: list[dict], allow_tool_roles: bool = False):
561
562
  """Add in missing tool_call_id fields to a list of messages using function call style
562
563
 
563
564
  Walk through the list forwards:
@@ -946,7 +947,7 @@ def get_human_text(name: str, enforce_limit=True):
946
947
  for file_path in list_human_files():
947
948
  file = os.path.basename(file_path)
948
949
  if f"{name}.txt" == file or name == file:
949
- human_text = open(file_path, "r", encoding="utf-8").read().strip()
950
+ human_text = open(file_path, encoding="utf-8").read().strip()
950
951
  if enforce_limit and len(human_text) > CORE_MEMORY_HUMAN_CHAR_LIMIT:
951
952
  raise ValueError(f"Contents of {name}.txt is over the character limit ({len(human_text)} > {CORE_MEMORY_HUMAN_CHAR_LIMIT})")
952
953
  return human_text
@@ -958,7 +959,7 @@ def get_persona_text(name: str, enforce_limit=True):
958
959
  for file_path in list_persona_files():
959
960
  file = os.path.basename(file_path)
960
961
  if f"{name}.txt" == file or name == file:
961
- persona_text = open(file_path, "r", encoding="utf-8").read().strip()
962
+ persona_text = open(file_path, encoding="utf-8").read().strip()
962
963
  if enforce_limit and len(persona_text) > CORE_MEMORY_PERSONA_CHAR_LIMIT:
963
964
  raise ValueError(
964
965
  f"Contents of {name}.txt is over the character limit ({len(persona_text)} > {CORE_MEMORY_PERSONA_CHAR_LIMIT})"
@@ -991,16 +992,17 @@ def create_uuid_from_string(val: str):
991
992
  return uuid.UUID(hex=hex_string)
992
993
 
993
994
 
994
- def sanitize_filename(filename: str) -> str:
995
+ def sanitize_filename(filename: str, add_uuid_suffix: bool = False) -> str:
995
996
  """
996
997
  Sanitize the given filename to prevent directory traversal, invalid characters,
997
998
  and reserved names while ensuring it fits within the maximum length allowed by the filesystem.
998
999
 
999
1000
  Parameters:
1000
1001
  filename (str): The user-provided filename.
1002
+ add_uuid_suffix (bool): If True, adds a UUID suffix for uniqueness (legacy behavior).
1001
1003
 
1002
1004
  Returns:
1003
- str: A sanitized filename that is unique and safe for use.
1005
+ str: A sanitized filename.
1004
1006
  """
1005
1007
  # Extract the base filename to avoid directory components
1006
1008
  filename = os.path.basename(filename)
@@ -1015,14 +1017,21 @@ def sanitize_filename(filename: str) -> str:
1015
1017
  if base.startswith("."):
1016
1018
  raise ValueError(f"Invalid filename - derived file name {base} cannot start with '.'")
1017
1019
 
1018
- # Truncate the base name to fit within the maximum allowed length
1019
- max_base_length = MAX_FILENAME_LENGTH - len(ext) - 33 # 32 for UUID + 1 for `_`
1020
- if len(base) > max_base_length:
1021
- base = base[:max_base_length]
1020
+ if add_uuid_suffix:
1021
+ # Legacy behavior: Truncate the base name to fit within the maximum allowed length
1022
+ max_base_length = MAX_FILENAME_LENGTH - len(ext) - 33 # 32 for UUID + 1 for `_`
1023
+ if len(base) > max_base_length:
1024
+ base = base[:max_base_length]
1022
1025
 
1023
- # Append a unique UUID suffix for uniqueness
1024
- unique_suffix = uuid.uuid4().hex[:4]
1025
- sanitized_filename = f"{base}_{unique_suffix}{ext}"
1026
+ # Append a unique UUID suffix for uniqueness
1027
+ unique_suffix = uuid.uuid4().hex[:4]
1028
+ sanitized_filename = f"{base}_{unique_suffix}{ext}"
1029
+ else:
1030
+ max_base_length = MAX_FILENAME_LENGTH - len(ext)
1031
+ if len(base) > max_base_length:
1032
+ base = base[:max_base_length]
1033
+
1034
+ sanitized_filename = f"{base}{ext}"
1026
1035
 
1027
1036
  # Return the sanitized filename
1028
1037
  return sanitized_filename
@@ -1101,3 +1110,75 @@ def safe_create_task(coro, logger: Logger, label: str = "background task"):
1101
1110
  logger.exception(f"{label} failed with {type(e).__name__}: {e}")
1102
1111
 
1103
1112
  return asyncio.create_task(wrapper())
1113
+
1114
+
1115
+ class CancellationSignal:
1116
+ """
1117
+ A signal that can be checked for cancellation during streaming operations.
1118
+
1119
+ This provides a lightweight way to check if an operation should be cancelled
1120
+ without having to pass job managers and other dependencies through every method.
1121
+ """
1122
+
1123
+ def __init__(self, job_manager=None, job_id=None, actor=None):
1124
+
1125
+ from letta.log import get_logger
1126
+ from letta.schemas.user import User
1127
+ from letta.services.job_manager import JobManager
1128
+
1129
+ self.job_manager: JobManager | None = job_manager
1130
+ self.job_id: str | None = job_id
1131
+ self.actor: User | None = actor
1132
+ self._is_cancelled = False
1133
+ self.logger = get_logger(__name__)
1134
+
1135
+ async def is_cancelled(self) -> bool:
1136
+ """
1137
+ Check if the operation has been cancelled.
1138
+
1139
+ Returns:
1140
+ True if cancelled, False otherwise
1141
+ """
1142
+ from letta.schemas.enums import JobStatus
1143
+
1144
+ if self._is_cancelled:
1145
+ return True
1146
+
1147
+ if not self.job_manager or not self.job_id or not self.actor:
1148
+ return False
1149
+
1150
+ try:
1151
+ job = await self.job_manager.get_job_by_id_async(job_id=self.job_id, actor=self.actor)
1152
+ self._is_cancelled = job.status == JobStatus.cancelled
1153
+ return self._is_cancelled
1154
+ except Exception as e:
1155
+ self.logger.warning(f"Failed to check cancellation status for job {self.job_id}: {e}")
1156
+ return False
1157
+
1158
+ def cancel(self):
1159
+ """Mark this signal as cancelled locally (for testing or direct cancellation)."""
1160
+ self._is_cancelled = True
1161
+
1162
+ async def check_and_raise_if_cancelled(self):
1163
+ """
1164
+ Check for cancellation and raise CancelledError if cancelled.
1165
+
1166
+ Raises:
1167
+ asyncio.CancelledError: If the operation has been cancelled
1168
+ """
1169
+ if await self.is_cancelled():
1170
+ self.logger.info(f"Operation cancelled for job {self.job_id}")
1171
+ raise asyncio.CancelledError(f"Job {self.job_id} was cancelled")
1172
+
1173
+
1174
+ class NullCancellationSignal(CancellationSignal):
1175
+ """A null cancellation signal that is never cancelled."""
1176
+
1177
+ def __init__(self):
1178
+ super().__init__()
1179
+
1180
+ async def is_cancelled(self) -> bool:
1181
+ return False
1182
+
1183
+ async def check_and_raise_if_cancelled(self):
1184
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: letta-nightly
3
- Version: 0.8.8.dev20250703104323
3
+ Version: 0.8.9.dev20250703191231
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  License: Apache License
6
6
  Author: Letta Team
@@ -56,7 +56,7 @@ Requires-Dist: isort (>=5.13.2,<6.0.0) ; extra == "dev" or extra == "all"
56
56
  Requires-Dist: jinja2 (>=3.1.5,<4.0.0)
57
57
  Requires-Dist: langchain (>=0.3.7,<0.4.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
58
58
  Requires-Dist: langchain-community (>=0.3.7,<0.4.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
59
- Requires-Dist: letta_client (>=0.1.173,<0.2.0)
59
+ Requires-Dist: letta_client (>=0.1.183,<0.2.0)
60
60
  Requires-Dist: llama-index (>=0.12.2,<0.13.0)
61
61
  Requires-Dist: llama-index-embeddings-openai (>=0.3.1,<0.4.0)
62
62
  Requires-Dist: locust (>=2.31.5,<3.0.0) ; extra == "dev" or extra == "desktop" or extra == "all"