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.
- letta/__init__.py +6 -1
- letta/agent.py +1 -0
- letta/agents/base_agent.py +8 -2
- letta/agents/ephemeral_summary_agent.py +33 -33
- letta/agents/letta_agent.py +104 -53
- letta/agents/voice_agent.py +2 -1
- letta/constants.py +8 -4
- letta/functions/function_sets/files.py +22 -7
- letta/functions/function_sets/multi_agent.py +34 -0
- letta/functions/types.py +1 -1
- letta/groups/helpers.py +8 -5
- letta/groups/sleeptime_multi_agent_v2.py +20 -15
- letta/interface.py +1 -1
- letta/interfaces/anthropic_streaming_interface.py +15 -8
- letta/interfaces/openai_chat_completions_streaming_interface.py +9 -6
- letta/interfaces/openai_streaming_interface.py +17 -11
- letta/llm_api/openai_client.py +2 -1
- letta/orm/agent.py +1 -0
- letta/orm/file.py +8 -2
- letta/orm/files_agents.py +36 -11
- letta/orm/mcp_server.py +3 -0
- letta/orm/source.py +2 -1
- letta/orm/step.py +3 -0
- letta/prompts/system/memgpt_v2_chat.txt +5 -8
- letta/schemas/agent.py +58 -23
- letta/schemas/embedding_config.py +3 -2
- letta/schemas/enums.py +4 -0
- letta/schemas/file.py +1 -0
- letta/schemas/letta_stop_reason.py +18 -0
- letta/schemas/mcp.py +15 -10
- letta/schemas/memory.py +35 -5
- letta/schemas/providers.py +11 -0
- letta/schemas/step.py +1 -0
- letta/schemas/tool.py +2 -1
- letta/server/rest_api/routers/v1/agents.py +320 -184
- letta/server/rest_api/routers/v1/groups.py +6 -2
- letta/server/rest_api/routers/v1/identities.py +6 -2
- letta/server/rest_api/routers/v1/jobs.py +49 -1
- letta/server/rest_api/routers/v1/sources.py +28 -19
- letta/server/rest_api/routers/v1/steps.py +7 -2
- letta/server/rest_api/routers/v1/tools.py +40 -9
- letta/server/rest_api/streaming_response.py +88 -0
- letta/server/server.py +61 -55
- letta/services/agent_manager.py +28 -16
- letta/services/file_manager.py +58 -9
- letta/services/file_processor/chunker/llama_index_chunker.py +2 -0
- letta/services/file_processor/embedder/openai_embedder.py +54 -10
- letta/services/file_processor/file_processor.py +59 -0
- letta/services/file_processor/parser/mistral_parser.py +2 -0
- letta/services/files_agents_manager.py +120 -2
- letta/services/helpers/agent_manager_helper.py +21 -4
- letta/services/job_manager.py +57 -6
- letta/services/mcp/base_client.py +1 -0
- letta/services/mcp_manager.py +13 -1
- letta/services/step_manager.py +14 -5
- letta/services/summarizer/summarizer.py +6 -22
- letta/services/tool_executor/builtin_tool_executor.py +0 -1
- letta/services/tool_executor/files_tool_executor.py +2 -2
- letta/services/tool_executor/multi_agent_tool_executor.py +23 -0
- letta/services/tool_manager.py +7 -7
- letta/settings.py +11 -2
- letta/templates/summary_request_text.j2 +19 -0
- letta/utils.py +95 -14
- {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/METADATA +2 -2
- {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/RECORD +69 -68
- /letta/{agents/prompts → prompts/system}/summary_system_prompt.txt +0 -0
- {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.8.dev20250703104323.dist-info → letta_nightly-0.8.9.dev20250703191231.dist-info}/entry_points.txt +0 -0
letta/services/tool_manager.py
CHANGED
@@ -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
|
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
|
-
|
439
|
-
|
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
|
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
|
-
|
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,
|
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:
|
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,
|
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,
|
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
|
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
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
base
|
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
|
-
|
1024
|
-
|
1025
|
-
|
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.
|
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.
|
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"
|