letta-nightly 0.8.0.dev20250606195656__py3-none-any.whl → 0.8.3.dev20250607000559__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 +1 -1
- letta/agent.py +16 -12
- letta/agents/base_agent.py +1 -1
- letta/agents/helpers.py +13 -2
- letta/agents/letta_agent.py +72 -34
- letta/agents/letta_agent_batch.py +1 -2
- letta/agents/voice_agent.py +19 -13
- letta/agents/voice_sleeptime_agent.py +23 -6
- letta/constants.py +18 -0
- letta/data_sources/__init__.py +0 -0
- letta/data_sources/redis_client.py +282 -0
- letta/errors.py +0 -4
- letta/functions/function_sets/files.py +58 -0
- letta/functions/schema_generator.py +18 -1
- letta/groups/sleeptime_multi_agent_v2.py +13 -3
- letta/helpers/datetime_helpers.py +47 -3
- letta/helpers/decorators.py +69 -0
- letta/{services/helpers/noop_helper.py → helpers/singleton.py} +5 -0
- letta/interfaces/anthropic_streaming_interface.py +43 -24
- letta/interfaces/openai_streaming_interface.py +21 -19
- letta/llm_api/anthropic.py +1 -1
- letta/llm_api/anthropic_client.py +30 -16
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/helpers.py +36 -30
- letta/llm_api/llm_api_tools.py +1 -1
- letta/llm_api/llm_client_base.py +29 -1
- letta/llm_api/openai.py +1 -1
- letta/llm_api/openai_client.py +6 -8
- letta/local_llm/chat_completion_proxy.py +1 -1
- letta/memory.py +1 -1
- letta/orm/enums.py +1 -0
- letta/orm/file.py +80 -3
- letta/orm/files_agents.py +13 -0
- letta/orm/passage.py +2 -0
- letta/orm/sqlalchemy_base.py +34 -11
- letta/otel/__init__.py +0 -0
- letta/otel/context.py +25 -0
- letta/otel/events.py +0 -0
- letta/otel/metric_registry.py +122 -0
- letta/otel/metrics.py +66 -0
- letta/otel/resource.py +26 -0
- letta/{tracing.py → otel/tracing.py} +55 -78
- letta/plugins/README.md +22 -0
- letta/plugins/__init__.py +0 -0
- letta/plugins/defaults.py +11 -0
- letta/plugins/plugins.py +72 -0
- letta/schemas/enums.py +8 -0
- letta/schemas/file.py +12 -0
- letta/schemas/letta_request.py +6 -0
- letta/schemas/passage.py +1 -0
- letta/schemas/tool.py +4 -0
- letta/server/db.py +7 -7
- letta/server/rest_api/app.py +8 -6
- letta/server/rest_api/routers/v1/agents.py +46 -37
- letta/server/rest_api/routers/v1/groups.py +3 -3
- letta/server/rest_api/routers/v1/sources.py +26 -3
- letta/server/rest_api/routers/v1/tools.py +7 -2
- letta/server/rest_api/utils.py +9 -6
- letta/server/server.py +25 -13
- letta/services/agent_manager.py +186 -194
- letta/services/block_manager.py +1 -1
- letta/services/context_window_calculator/context_window_calculator.py +1 -1
- letta/services/context_window_calculator/token_counter.py +3 -2
- letta/services/file_processor/chunker/line_chunker.py +34 -0
- letta/services/file_processor/file_processor.py +43 -12
- letta/services/file_processor/parser/mistral_parser.py +11 -1
- letta/services/files_agents_manager.py +96 -7
- letta/services/group_manager.py +6 -6
- letta/services/helpers/agent_manager_helper.py +404 -3
- letta/services/identity_manager.py +1 -1
- letta/services/job_manager.py +1 -1
- letta/services/llm_batch_manager.py +1 -1
- letta/services/mcp/stdio_client.py +5 -1
- letta/services/mcp_manager.py +4 -4
- letta/services/message_manager.py +1 -1
- letta/services/organization_manager.py +1 -1
- letta/services/passage_manager.py +604 -19
- letta/services/per_agent_lock_manager.py +1 -1
- letta/services/provider_manager.py +1 -1
- letta/services/sandbox_config_manager.py +1 -1
- letta/services/source_manager.py +178 -19
- letta/services/step_manager.py +2 -2
- letta/services/summarizer/summarizer.py +1 -1
- letta/services/telemetry_manager.py +1 -1
- letta/services/tool_executor/builtin_tool_executor.py +117 -0
- letta/services/tool_executor/composio_tool_executor.py +53 -0
- letta/services/tool_executor/core_tool_executor.py +474 -0
- letta/services/tool_executor/files_tool_executor.py +138 -0
- letta/services/tool_executor/mcp_tool_executor.py +45 -0
- letta/services/tool_executor/multi_agent_tool_executor.py +123 -0
- letta/services/tool_executor/tool_execution_manager.py +34 -14
- letta/services/tool_executor/tool_execution_sandbox.py +1 -1
- letta/services/tool_executor/tool_executor.py +3 -802
- letta/services/tool_executor/tool_executor_base.py +43 -0
- letta/services/tool_manager.py +55 -59
- letta/services/tool_sandbox/e2b_sandbox.py +1 -1
- letta/services/tool_sandbox/local_sandbox.py +6 -3
- letta/services/user_manager.py +6 -3
- letta/settings.py +23 -2
- letta/utils.py +7 -2
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/METADATA +4 -2
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/RECORD +105 -83
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/entry_points.txt +0 -0
@@ -3,10 +3,11 @@ from typing import AsyncGenerator, List, Optional, Tuple, Union
|
|
3
3
|
from letta.agents.helpers import _create_letta_response, serialize_message_history
|
4
4
|
from letta.agents.letta_agent import LettaAgent
|
5
5
|
from letta.orm.enums import ToolType
|
6
|
+
from letta.otel.tracing import trace_method
|
6
7
|
from letta.schemas.agent import AgentState
|
7
8
|
from letta.schemas.block import BlockUpdate
|
8
9
|
from letta.schemas.enums import MessageStreamStatus
|
9
|
-
from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage
|
10
|
+
from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage, MessageType
|
10
11
|
from letta.schemas.letta_response import LettaResponse
|
11
12
|
from letta.schemas.message import MessageCreate
|
12
13
|
from letta.schemas.tool_rule import ChildToolRule, ContinueToolRule, InitToolRule, TerminalToolRule
|
@@ -17,7 +18,7 @@ from letta.services.message_manager import MessageManager
|
|
17
18
|
from letta.services.passage_manager import PassageManager
|
18
19
|
from letta.services.summarizer.enums import SummarizationMode
|
19
20
|
from letta.services.summarizer.summarizer import Summarizer
|
20
|
-
from letta.
|
21
|
+
from letta.types import JsonDict
|
21
22
|
|
22
23
|
|
23
24
|
class VoiceSleeptimeAgent(LettaAgent):
|
@@ -58,7 +59,13 @@ class VoiceSleeptimeAgent(LettaAgent):
|
|
58
59
|
def update_message_transcript(self, message_transcripts: List[str]):
|
59
60
|
self.message_transcripts = message_transcripts
|
60
61
|
|
61
|
-
async def step(
|
62
|
+
async def step(
|
63
|
+
self,
|
64
|
+
input_messages: List[MessageCreate],
|
65
|
+
max_steps: int = 20,
|
66
|
+
use_assistant_message: bool = True,
|
67
|
+
include_return_message_types: Optional[List[MessageType]] = None,
|
68
|
+
) -> LettaResponse:
|
62
69
|
"""
|
63
70
|
Process the user's input message, allowing the model to call memory-related tools
|
64
71
|
until it decides to stop and provide a final response.
|
@@ -85,13 +92,23 @@ class VoiceSleeptimeAgent(LettaAgent):
|
|
85
92
|
)
|
86
93
|
|
87
94
|
return _create_letta_response(
|
88
|
-
new_in_context_messages=new_in_context_messages,
|
95
|
+
new_in_context_messages=new_in_context_messages,
|
96
|
+
use_assistant_message=use_assistant_message,
|
97
|
+
usage=usage,
|
98
|
+
include_return_message_types=include_return_message_types,
|
89
99
|
)
|
90
100
|
|
91
101
|
@trace_method
|
92
|
-
async def _execute_tool(
|
102
|
+
async def _execute_tool(
|
103
|
+
self,
|
104
|
+
tool_name: str,
|
105
|
+
tool_args: JsonDict,
|
106
|
+
agent_state: AgentState,
|
107
|
+
agent_step_span: Optional["Span"] = None,
|
108
|
+
step_id: str | None = None,
|
109
|
+
) -> "ToolExecutionResult":
|
93
110
|
"""
|
94
|
-
Executes a tool and returns
|
111
|
+
Executes a tool and returns the ToolExecutionResult
|
95
112
|
"""
|
96
113
|
from letta.schemas.tool_execution_result import ToolExecutionResult
|
97
114
|
|
letta/constants.py
CHANGED
@@ -21,6 +21,15 @@ LETTA_CORE_TOOL_MODULE_NAME = "letta.functions.function_sets.base"
|
|
21
21
|
LETTA_MULTI_AGENT_TOOL_MODULE_NAME = "letta.functions.function_sets.multi_agent"
|
22
22
|
LETTA_VOICE_TOOL_MODULE_NAME = "letta.functions.function_sets.voice"
|
23
23
|
LETTA_BUILTIN_TOOL_MODULE_NAME = "letta.functions.function_sets.builtin"
|
24
|
+
LETTA_FILES_TOOL_MODULE_NAME = "letta.functions.function_sets.files"
|
25
|
+
|
26
|
+
LETTA_TOOL_MODULE_NAMES = [
|
27
|
+
LETTA_CORE_TOOL_MODULE_NAME,
|
28
|
+
LETTA_MULTI_AGENT_TOOL_MODULE_NAME,
|
29
|
+
LETTA_VOICE_TOOL_MODULE_NAME,
|
30
|
+
LETTA_BUILTIN_TOOL_MODULE_NAME,
|
31
|
+
LETTA_FILES_TOOL_MODULE_NAME,
|
32
|
+
]
|
24
33
|
|
25
34
|
|
26
35
|
# String in the error message for when the context window is too large
|
@@ -112,6 +121,9 @@ MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX = re.compile(
|
|
112
121
|
# Built in tools
|
113
122
|
BUILTIN_TOOLS = ["run_code", "web_search"]
|
114
123
|
|
124
|
+
# Built in tools
|
125
|
+
FILES_TOOLS = ["open_file", "close_file", "grep", "search_files"]
|
126
|
+
|
115
127
|
# Set of all built-in Letta tools
|
116
128
|
LETTA_TOOL_SET = set(
|
117
129
|
BASE_TOOLS
|
@@ -121,6 +133,7 @@ LETTA_TOOL_SET = set(
|
|
121
133
|
+ BASE_VOICE_SLEEPTIME_TOOLS
|
122
134
|
+ BASE_VOICE_SLEEPTIME_CHAT_TOOLS
|
123
135
|
+ BUILTIN_TOOLS
|
136
|
+
+ FILES_TOOLS
|
124
137
|
)
|
125
138
|
|
126
139
|
|
@@ -294,6 +307,7 @@ CORE_MEMORY_SOURCE_CHAR_LIMIT: int = 5000
|
|
294
307
|
# Function return limits
|
295
308
|
FUNCTION_RETURN_CHAR_LIMIT = 6000 # ~300 words
|
296
309
|
BASE_FUNCTION_RETURN_CHAR_LIMIT = 1000000 # very high (we rely on implementation)
|
310
|
+
FILE_IS_TRUNCATED_WARNING = "# NOTE: This block is truncated, use functions to view the full content."
|
297
311
|
|
298
312
|
MAX_PAUSE_HEARTBEATS = 360 # in min
|
299
313
|
|
@@ -316,3 +330,7 @@ RESERVED_FILENAMES = {"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "LPT1", "LPT2"
|
|
316
330
|
WEB_SEARCH_CLIP_CONTENT = False
|
317
331
|
WEB_SEARCH_INCLUDE_SCORE = False
|
318
332
|
WEB_SEARCH_SEPARATOR = "\n" + "-" * 40 + "\n"
|
333
|
+
|
334
|
+
REDIS_INCLUDE = "INCLUDE"
|
335
|
+
REDIS_EXCLUDE = "EXCLUDE"
|
336
|
+
REDIS_SET_DEFAULT_VAL = "None"
|
File without changes
|
@@ -0,0 +1,282 @@
|
|
1
|
+
import asyncio
|
2
|
+
from functools import wraps
|
3
|
+
from typing import Any, Optional, Set, Union
|
4
|
+
|
5
|
+
import redis.asyncio as redis
|
6
|
+
from redis import RedisError
|
7
|
+
|
8
|
+
from letta.constants import REDIS_EXCLUDE, REDIS_INCLUDE, REDIS_SET_DEFAULT_VAL
|
9
|
+
from letta.log import get_logger
|
10
|
+
|
11
|
+
logger = get_logger(__name__)
|
12
|
+
|
13
|
+
_client_instance = None
|
14
|
+
|
15
|
+
|
16
|
+
class AsyncRedisClient:
|
17
|
+
"""Async Redis client with connection pooling and error handling"""
|
18
|
+
|
19
|
+
def __init__(
|
20
|
+
self,
|
21
|
+
host: str = "localhost",
|
22
|
+
port: int = 6379,
|
23
|
+
db: int = 0,
|
24
|
+
password: Optional[str] = None,
|
25
|
+
max_connections: int = 50,
|
26
|
+
decode_responses: bool = True,
|
27
|
+
socket_timeout: int = 5,
|
28
|
+
socket_connect_timeout: int = 5,
|
29
|
+
retry_on_timeout: bool = True,
|
30
|
+
health_check_interval: int = 30,
|
31
|
+
):
|
32
|
+
"""
|
33
|
+
Initialize Redis client with connection pool.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
host: Redis server hostname
|
37
|
+
port: Redis server port
|
38
|
+
db: Database number
|
39
|
+
password: Redis password if required
|
40
|
+
max_connections: Maximum number of connections in pool
|
41
|
+
decode_responses: Decode byte responses to strings
|
42
|
+
socket_timeout: Socket timeout in seconds
|
43
|
+
socket_connect_timeout: Socket connection timeout
|
44
|
+
retry_on_timeout: Retry operations on timeout
|
45
|
+
health_check_interval: Seconds between health checks
|
46
|
+
"""
|
47
|
+
self.pool = redis.ConnectionPool(
|
48
|
+
host=host,
|
49
|
+
port=port,
|
50
|
+
db=db,
|
51
|
+
password=password,
|
52
|
+
max_connections=max_connections,
|
53
|
+
decode_responses=decode_responses,
|
54
|
+
socket_timeout=socket_timeout,
|
55
|
+
socket_connect_timeout=socket_connect_timeout,
|
56
|
+
retry_on_timeout=retry_on_timeout,
|
57
|
+
health_check_interval=health_check_interval,
|
58
|
+
)
|
59
|
+
self._client = None
|
60
|
+
self._lock = asyncio.Lock()
|
61
|
+
|
62
|
+
async def get_client(self) -> redis.Redis:
|
63
|
+
"""Get or create Redis client instance."""
|
64
|
+
if self._client is None:
|
65
|
+
async with self._lock:
|
66
|
+
if self._client is None:
|
67
|
+
self._client = redis.Redis(connection_pool=self.pool)
|
68
|
+
return self._client
|
69
|
+
|
70
|
+
async def close(self):
|
71
|
+
"""Close Redis connection and cleanup."""
|
72
|
+
if self._client:
|
73
|
+
await self._client.close()
|
74
|
+
await self.pool.disconnect()
|
75
|
+
self._client = None
|
76
|
+
|
77
|
+
async def __aenter__(self):
|
78
|
+
"""Async context manager entry."""
|
79
|
+
await self.get_client()
|
80
|
+
return self
|
81
|
+
|
82
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
83
|
+
"""Async context manager exit."""
|
84
|
+
await self.close()
|
85
|
+
|
86
|
+
# Health check and connection management
|
87
|
+
async def ping(self) -> bool:
|
88
|
+
"""Check if Redis is accessible."""
|
89
|
+
try:
|
90
|
+
client = await self.get_client()
|
91
|
+
await client.ping()
|
92
|
+
return True
|
93
|
+
except RedisError:
|
94
|
+
logger.exception("Redis ping failed")
|
95
|
+
return False
|
96
|
+
|
97
|
+
async def wait_for_ready(self, timeout: int = 30, interval: float = 0.5):
|
98
|
+
"""Wait for Redis to be ready."""
|
99
|
+
start_time = asyncio.get_event_loop().time()
|
100
|
+
while (asyncio.get_event_loop().time() - start_time) < timeout:
|
101
|
+
if await self.ping():
|
102
|
+
return
|
103
|
+
await asyncio.sleep(interval)
|
104
|
+
raise ConnectionError(f"Redis not ready after {timeout} seconds")
|
105
|
+
|
106
|
+
# Retry decorator for resilience
|
107
|
+
def with_retry(max_attempts: int = 3, delay: float = 0.1):
|
108
|
+
"""Decorator to retry Redis operations on failure."""
|
109
|
+
|
110
|
+
def decorator(func):
|
111
|
+
@wraps(func)
|
112
|
+
async def wrapper(self, *args, **kwargs):
|
113
|
+
last_error = None
|
114
|
+
for attempt in range(max_attempts):
|
115
|
+
try:
|
116
|
+
return await func(self, *args, **kwargs)
|
117
|
+
except (ConnectionError, TimeoutError) as e:
|
118
|
+
last_error = e
|
119
|
+
if attempt < max_attempts - 1:
|
120
|
+
await asyncio.sleep(delay * (2**attempt))
|
121
|
+
logger.warning(f"Retry {attempt + 1}/{max_attempts} for {func.__name__}: {e}")
|
122
|
+
raise last_error
|
123
|
+
|
124
|
+
return wrapper
|
125
|
+
|
126
|
+
return decorator
|
127
|
+
|
128
|
+
# Basic operations with error handling
|
129
|
+
@with_retry()
|
130
|
+
async def get(self, key: str, default: Any = None) -> Any:
|
131
|
+
"""Get value by key."""
|
132
|
+
try:
|
133
|
+
client = await self.get_client()
|
134
|
+
return await client.get(key)
|
135
|
+
except:
|
136
|
+
return default
|
137
|
+
|
138
|
+
@with_retry()
|
139
|
+
async def set(
|
140
|
+
self,
|
141
|
+
key: str,
|
142
|
+
value: Union[str, int, float],
|
143
|
+
ex: Optional[int] = None,
|
144
|
+
px: Optional[int] = None,
|
145
|
+
nx: bool = False,
|
146
|
+
xx: bool = False,
|
147
|
+
) -> bool:
|
148
|
+
"""
|
149
|
+
Set key-value with options.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
key: Redis key
|
153
|
+
value: Value to store
|
154
|
+
ex: Expire time in seconds
|
155
|
+
px: Expire time in milliseconds
|
156
|
+
nx: Only set if key doesn't exist
|
157
|
+
xx: Only set if key exists
|
158
|
+
"""
|
159
|
+
client = await self.get_client()
|
160
|
+
return await client.set(key, value, ex=ex, px=px, nx=nx, xx=xx)
|
161
|
+
|
162
|
+
@with_retry()
|
163
|
+
async def delete(self, *keys: str) -> int:
|
164
|
+
"""Delete one or more keys."""
|
165
|
+
client = await self.get_client()
|
166
|
+
return await client.delete(*keys)
|
167
|
+
|
168
|
+
@with_retry()
|
169
|
+
async def exists(self, *keys: str) -> int:
|
170
|
+
"""Check if keys exist."""
|
171
|
+
client = await self.get_client()
|
172
|
+
return await client.exists(*keys)
|
173
|
+
|
174
|
+
# Set operations
|
175
|
+
async def sadd(self, key: str, *members: Union[str, int, float]) -> int:
|
176
|
+
"""Add members to set."""
|
177
|
+
client = await self.get_client()
|
178
|
+
return await client.sadd(key, *members)
|
179
|
+
|
180
|
+
async def smembers(self, key: str) -> Set[str]:
|
181
|
+
"""Get all set members."""
|
182
|
+
client = await self.get_client()
|
183
|
+
return await client.smembers(key)
|
184
|
+
|
185
|
+
@with_retry()
|
186
|
+
async def smismember(self, key: str, values: list[Any] | Any) -> list[int] | int:
|
187
|
+
"""clever!: set member is member"""
|
188
|
+
try:
|
189
|
+
client = await self.get_client()
|
190
|
+
result = await client.smismember(key, values)
|
191
|
+
return result if isinstance(values, list) else result[0]
|
192
|
+
except:
|
193
|
+
return [0] * len(values) if isinstance(values, list) else 0
|
194
|
+
|
195
|
+
async def srem(self, key: str, *members: Union[str, int, float]) -> int:
|
196
|
+
"""Remove members from set."""
|
197
|
+
client = await self.get_client()
|
198
|
+
return await client.srem(key, *members)
|
199
|
+
|
200
|
+
async def scard(self, key: str) -> int:
|
201
|
+
client = await self.get_client()
|
202
|
+
return await client.scard(key)
|
203
|
+
|
204
|
+
# Atomic operations
|
205
|
+
async def incr(self, key: str) -> int:
|
206
|
+
"""Increment key value."""
|
207
|
+
client = await self.get_client()
|
208
|
+
return await client.incr(key)
|
209
|
+
|
210
|
+
async def decr(self, key: str) -> int:
|
211
|
+
"""Decrement key value."""
|
212
|
+
client = await self.get_client()
|
213
|
+
return await client.decr(key)
|
214
|
+
|
215
|
+
async def check_inclusion_and_exclusion(self, member: str, group: str) -> bool:
|
216
|
+
exclude_key = f"{group}_{REDIS_EXCLUDE}"
|
217
|
+
include_key = f"{group}_{REDIS_INCLUDE}"
|
218
|
+
# 1. if the member IS excluded from the group
|
219
|
+
if self.exists(exclude_key) and await self.scard(exclude_key) > 1:
|
220
|
+
return bool(await self.smismember(exclude_key, member))
|
221
|
+
# 2. if the group HAS an include set, is the member in that set?
|
222
|
+
if self.exists(include_key) and await self.scard(include_key) > 1:
|
223
|
+
return bool(await self.smismember(include_key, member))
|
224
|
+
# 3. if the group does NOT HAVE an include set and member NOT excluded
|
225
|
+
return True
|
226
|
+
|
227
|
+
async def create_inclusion_exclusion_keys(self, group: str) -> None:
|
228
|
+
redis_client = await self.get_client()
|
229
|
+
await redis_client.sadd(self._get_group_inclusion_key(group), REDIS_SET_DEFAULT_VAL)
|
230
|
+
await redis_client.sadd(self._get_group_exclusion_key(group), REDIS_SET_DEFAULT_VAL)
|
231
|
+
|
232
|
+
@staticmethod
|
233
|
+
def _get_group_inclusion_key(group: str) -> str:
|
234
|
+
return f"{group}_{REDIS_INCLUDE}"
|
235
|
+
|
236
|
+
@staticmethod
|
237
|
+
def _get_group_exclusion_key(group: str) -> str:
|
238
|
+
return f"{group}_{REDIS_EXCLUDE}"
|
239
|
+
|
240
|
+
|
241
|
+
class NoopAsyncRedisClient(AsyncRedisClient):
|
242
|
+
async def get(self, key: str, default: Any = None) -> Any:
|
243
|
+
return default
|
244
|
+
|
245
|
+
async def exists(self, *keys: str) -> int:
|
246
|
+
return 0
|
247
|
+
|
248
|
+
async def sadd(self, key: str, *members: Union[str, int, float]) -> int:
|
249
|
+
return 0
|
250
|
+
|
251
|
+
async def smismember(self, key: str, values: list[Any] | Any) -> list[int] | int:
|
252
|
+
return [0] * len(values) if isinstance(values, list) else 0
|
253
|
+
|
254
|
+
async def delete(self, *keys: str) -> int:
|
255
|
+
return 0
|
256
|
+
|
257
|
+
async def check_inclusion_and_exclusion(self, member: str, group: str) -> bool:
|
258
|
+
return False
|
259
|
+
|
260
|
+
async def create_inclusion_exclusion_keys(self, group: str) -> None:
|
261
|
+
return None
|
262
|
+
|
263
|
+
async def scard(self, key: str) -> int:
|
264
|
+
return 0
|
265
|
+
|
266
|
+
|
267
|
+
async def get_redis_client() -> AsyncRedisClient:
|
268
|
+
global _client_instance
|
269
|
+
if _client_instance is None:
|
270
|
+
try:
|
271
|
+
from letta.settings import settings
|
272
|
+
|
273
|
+
_client_instance = AsyncRedisClient(
|
274
|
+
host=settings.redis_host or "localhost",
|
275
|
+
port=settings.redis_port or 6379,
|
276
|
+
)
|
277
|
+
await _client_instance.wait_for_ready(timeout=5)
|
278
|
+
logger.info("Redis client initialized")
|
279
|
+
except Exception as e:
|
280
|
+
logger.warning(f"Failed to initialize Redis: {e}")
|
281
|
+
_client_instance = NoopAsyncRedisClient()
|
282
|
+
return _client_instance
|
letta/errors.py
CHANGED
@@ -88,10 +88,6 @@ 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
|
-
|
95
91
|
class LLMNotFoundError(LLMError):
|
96
92
|
"""Error when requested resource is not found"""
|
97
93
|
|
@@ -0,0 +1,58 @@
|
|
1
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple
|
2
|
+
|
3
|
+
if TYPE_CHECKING:
|
4
|
+
from letta.schemas.agent import AgentState
|
5
|
+
from letta.schemas.file import FileMetadata
|
6
|
+
|
7
|
+
|
8
|
+
async def open_file(agent_state: "AgentState", file_name: str, view_range: Optional[Tuple[int, int]]) -> str:
|
9
|
+
"""
|
10
|
+
Open up a file in core memory.
|
11
|
+
|
12
|
+
Args:
|
13
|
+
file_name (str): Name of the file to view.
|
14
|
+
view_range (Optional[Tuple[int, int]]): Optional tuple indicating range to view.
|
15
|
+
|
16
|
+
Returns:
|
17
|
+
str: A status message
|
18
|
+
"""
|
19
|
+
raise NotImplementedError("Tool not implemented. Please contact the Letta team.")
|
20
|
+
|
21
|
+
|
22
|
+
async def close_file(agent_state: "AgentState", file_name: str) -> str:
|
23
|
+
"""
|
24
|
+
Close a file in core memory.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
file_name (str): Name of the file to close.
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
str: A status message
|
31
|
+
"""
|
32
|
+
raise NotImplementedError("Tool not implemented. Please contact the Letta team.")
|
33
|
+
|
34
|
+
|
35
|
+
async def grep(agent_state: "AgentState", pattern: str) -> str:
|
36
|
+
"""
|
37
|
+
Grep tool to search files across data sources with keywords.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
pattern (str): Keyword or regex pattern to search.
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
str: Matching lines or summary output.
|
44
|
+
"""
|
45
|
+
raise NotImplementedError("Tool not implemented. Please contact the Letta team.")
|
46
|
+
|
47
|
+
|
48
|
+
async def search_files(agent_state: "AgentState", query: str) -> List["FileMetadata"]:
|
49
|
+
"""
|
50
|
+
Get list of most relevant files across all data sources.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
query (str): The search query.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
List[FileMetadata]: List of matching files.
|
57
|
+
"""
|
58
|
+
raise NotImplementedError("Tool not implemented. Please contact the Letta team.")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import inspect
|
2
2
|
import warnings
|
3
|
-
from typing import Any, Dict, List, Optional, Type, Union, get_args, get_origin
|
3
|
+
from typing import Any, Dict, List, Optional, Tuple, Type, Union, get_args, get_origin
|
4
4
|
|
5
5
|
from composio.client.collections import ActionParametersModel
|
6
6
|
from docstring_parser import parse
|
@@ -76,6 +76,23 @@ def type_to_json_schema_type(py_type) -> dict:
|
|
76
76
|
if get_origin(py_type) is Literal:
|
77
77
|
return {"type": "string", "enum": get_args(py_type)}
|
78
78
|
|
79
|
+
# Handle tuple types (specifically fixed-length like Tuple[int, int])
|
80
|
+
if origin in (tuple, Tuple):
|
81
|
+
args = get_args(py_type)
|
82
|
+
if len(args) == 0:
|
83
|
+
raise ValueError("Tuple type must have at least one element")
|
84
|
+
|
85
|
+
# Support only fixed-length tuples like Tuple[int, int], not variable-length like Tuple[int, ...]
|
86
|
+
if len(args) == 2 and args[1] is Ellipsis:
|
87
|
+
raise NotImplementedError("Variable-length tuples (e.g., Tuple[int, ...]) are not supported")
|
88
|
+
|
89
|
+
return {
|
90
|
+
"type": "array",
|
91
|
+
"prefixItems": [type_to_json_schema_type(arg) for arg in args],
|
92
|
+
"minItems": len(args),
|
93
|
+
"maxItems": len(args),
|
94
|
+
}
|
95
|
+
|
79
96
|
# Handle object types
|
80
97
|
if py_type == dict or origin in (dict, Dict):
|
81
98
|
args = get_args(py_type)
|
@@ -5,9 +5,11 @@ from typing import AsyncGenerator, List, Optional
|
|
5
5
|
from letta.agents.base_agent import BaseAgent
|
6
6
|
from letta.agents.letta_agent import LettaAgent
|
7
7
|
from letta.groups.helpers import stringify_message
|
8
|
+
from letta.otel.tracing import trace_method
|
8
9
|
from letta.schemas.enums import JobStatus
|
9
10
|
from letta.schemas.group import Group, ManagerType
|
10
11
|
from letta.schemas.job import JobUpdate
|
12
|
+
from letta.schemas.letta_message import MessageType
|
11
13
|
from letta.schemas.letta_message_content import TextContent
|
12
14
|
from letta.schemas.letta_response import LettaResponse
|
13
15
|
from letta.schemas.message import Message, MessageCreate
|
@@ -21,7 +23,6 @@ from letta.services.message_manager import MessageManager
|
|
21
23
|
from letta.services.passage_manager import PassageManager
|
22
24
|
from letta.services.step_manager import NoopStepManager, StepManager
|
23
25
|
from letta.services.telemetry_manager import NoopTelemetryManager, TelemetryManager
|
24
|
-
from letta.tracing import trace_method
|
25
26
|
|
26
27
|
|
27
28
|
class SleeptimeMultiAgentV2(BaseAgent):
|
@@ -63,6 +64,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
|
|
63
64
|
max_steps: int = 10,
|
64
65
|
use_assistant_message: bool = True,
|
65
66
|
request_start_timestamp_ns: Optional[int] = None,
|
67
|
+
include_return_message_types: Optional[List[MessageType]] = None,
|
66
68
|
) -> LettaResponse:
|
67
69
|
run_ids = []
|
68
70
|
|
@@ -87,7 +89,10 @@ class SleeptimeMultiAgentV2(BaseAgent):
|
|
87
89
|
)
|
88
90
|
# Perform foreground agent step
|
89
91
|
response = await foreground_agent.step(
|
90
|
-
input_messages=new_messages,
|
92
|
+
input_messages=new_messages,
|
93
|
+
max_steps=max_steps,
|
94
|
+
use_assistant_message=use_assistant_message,
|
95
|
+
include_return_message_types=include_return_message_types,
|
91
96
|
)
|
92
97
|
|
93
98
|
# Get last response messages
|
@@ -129,8 +134,11 @@ class SleeptimeMultiAgentV2(BaseAgent):
|
|
129
134
|
max_steps: int = 10,
|
130
135
|
use_assistant_message: bool = True,
|
131
136
|
request_start_timestamp_ns: Optional[int] = None,
|
137
|
+
include_return_message_types: Optional[List[MessageType]] = None,
|
132
138
|
):
|
133
|
-
response = await self.step(
|
139
|
+
response = await self.step(
|
140
|
+
input_messages, max_steps, use_assistant_message, request_start_timestamp_ns, include_return_message_types
|
141
|
+
)
|
134
142
|
|
135
143
|
for message in response.messages:
|
136
144
|
yield f"data: {message.model_dump_json()}\n\n"
|
@@ -144,6 +152,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
|
|
144
152
|
max_steps: int = 10,
|
145
153
|
use_assistant_message: bool = True,
|
146
154
|
request_start_timestamp_ns: Optional[int] = None,
|
155
|
+
include_return_message_types: Optional[List[MessageType]] = None,
|
147
156
|
) -> AsyncGenerator[str, None]:
|
148
157
|
# Prepare new messages
|
149
158
|
new_messages = []
|
@@ -170,6 +179,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
|
|
170
179
|
max_steps=max_steps,
|
171
180
|
use_assistant_message=use_assistant_message,
|
172
181
|
request_start_timestamp_ns=request_start_timestamp_ns,
|
182
|
+
include_return_message_types=include_return_message_types,
|
173
183
|
):
|
174
184
|
yield chunk
|
175
185
|
|
@@ -1,7 +1,9 @@
|
|
1
1
|
import re
|
2
2
|
import time
|
3
|
-
from datetime import datetime, timedelta
|
3
|
+
from datetime import datetime, timedelta
|
4
|
+
from datetime import timezone as dt_timezone
|
4
5
|
from time import strftime
|
6
|
+
from typing import Callable
|
5
7
|
|
6
8
|
import pytz
|
7
9
|
|
@@ -66,7 +68,7 @@ def get_local_time(timezone=None):
|
|
66
68
|
def get_utc_time() -> datetime:
|
67
69
|
"""Get the current UTC time"""
|
68
70
|
# return datetime.now(pytz.utc)
|
69
|
-
return datetime.now(
|
71
|
+
return datetime.now(dt_timezone.utc)
|
70
72
|
|
71
73
|
|
72
74
|
def get_utc_time_int() -> int:
|
@@ -78,9 +80,13 @@ def get_utc_timestamp_ns() -> int:
|
|
78
80
|
return int(time.time_ns())
|
79
81
|
|
80
82
|
|
83
|
+
def ns_to_ms(ns: int) -> int:
|
84
|
+
return ns // 1_000_000
|
85
|
+
|
86
|
+
|
81
87
|
def timestamp_to_datetime(timestamp_seconds: int) -> datetime:
|
82
88
|
"""Convert Unix timestamp in seconds to UTC datetime object"""
|
83
|
-
return datetime.fromtimestamp(timestamp_seconds, tz=
|
89
|
+
return datetime.fromtimestamp(timestamp_seconds, tz=dt_timezone.utc)
|
84
90
|
|
85
91
|
|
86
92
|
def format_datetime(dt):
|
@@ -105,3 +111,41 @@ def extract_date_from_timestamp(timestamp):
|
|
105
111
|
|
106
112
|
def is_utc_datetime(dt: datetime) -> bool:
|
107
113
|
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) == timedelta(0)
|
114
|
+
|
115
|
+
|
116
|
+
class AsyncTimer:
|
117
|
+
"""An async context manager for timing async code execution.
|
118
|
+
|
119
|
+
Takes in an optional callback_func to call on exit with arguments
|
120
|
+
taking in the elapsed_ms and exc if present.
|
121
|
+
|
122
|
+
Do not use the start and end times outside of this function as they are relative.
|
123
|
+
"""
|
124
|
+
|
125
|
+
def __init__(self, callback_func: Callable | None = None):
|
126
|
+
self._start_time_ns = None
|
127
|
+
self._end_time_ns = None
|
128
|
+
self.elapsed_ns = None
|
129
|
+
self.callback_func = callback_func
|
130
|
+
|
131
|
+
async def __aenter__(self):
|
132
|
+
self._start_time_ns = time.perf_counter_ns()
|
133
|
+
return self
|
134
|
+
|
135
|
+
async def __aexit__(self, exc_type, exc, tb):
|
136
|
+
self._end_time_ns = time.perf_counter_ns()
|
137
|
+
self.elapsed_ns = self._end_time_ns - self._start_time_ns
|
138
|
+
if self.callback_func:
|
139
|
+
from asyncio import iscoroutinefunction
|
140
|
+
|
141
|
+
if iscoroutinefunction(self.callback_func):
|
142
|
+
await self.callback_func(self.elapsed_ms, exc)
|
143
|
+
else:
|
144
|
+
self.callback_func(self.elapsed_ms, exc)
|
145
|
+
return False
|
146
|
+
|
147
|
+
@property
|
148
|
+
def elapsed_ms(self):
|
149
|
+
if self.elapsed_ns is not None:
|
150
|
+
return ns_to_ms(self.elapsed_ns)
|
151
|
+
return None
|