letta-nightly 0.8.5.dev20250625104328__py3-none-any.whl → 0.8.6.dev20250625222533__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/agent.py +16 -12
- letta/agents/base_agent.py +4 -1
- letta/agents/helpers.py +35 -3
- letta/agents/letta_agent.py +132 -106
- letta/agents/letta_agent_batch.py +4 -3
- letta/agents/voice_agent.py +12 -2
- letta/agents/voice_sleeptime_agent.py +12 -2
- letta/constants.py +24 -3
- letta/data_sources/redis_client.py +6 -0
- letta/errors.py +5 -0
- letta/functions/function_sets/files.py +10 -3
- letta/functions/function_sets/multi_agent.py +0 -32
- letta/groups/sleeptime_multi_agent_v2.py +6 -0
- letta/helpers/converters.py +4 -1
- letta/helpers/datetime_helpers.py +16 -23
- letta/helpers/message_helper.py +5 -2
- letta/helpers/tool_rule_solver.py +29 -2
- letta/interfaces/openai_streaming_interface.py +9 -2
- letta/llm_api/anthropic.py +11 -1
- letta/llm_api/anthropic_client.py +14 -3
- letta/llm_api/aws_bedrock.py +29 -15
- letta/llm_api/bedrock_client.py +74 -0
- letta/llm_api/google_ai_client.py +7 -3
- letta/llm_api/google_vertex_client.py +18 -4
- letta/llm_api/llm_client.py +7 -0
- letta/llm_api/openai_client.py +13 -0
- letta/orm/agent.py +5 -0
- letta/orm/block_history.py +1 -1
- letta/orm/enums.py +6 -25
- letta/orm/job.py +1 -2
- letta/orm/llm_batch_items.py +1 -1
- letta/orm/mcp_server.py +1 -1
- letta/orm/passage.py +7 -1
- letta/orm/sqlalchemy_base.py +7 -5
- letta/orm/tool.py +2 -1
- letta/schemas/agent.py +34 -10
- letta/schemas/enums.py +42 -1
- letta/schemas/job.py +6 -3
- letta/schemas/letta_request.py +4 -0
- letta/schemas/llm_batch_job.py +7 -2
- letta/schemas/memory.py +2 -2
- letta/schemas/providers.py +32 -6
- letta/schemas/run.py +1 -1
- letta/schemas/tool_rule.py +40 -12
- letta/serialize_schemas/pydantic_agent_schema.py +9 -2
- letta/server/rest_api/app.py +3 -2
- letta/server/rest_api/routers/v1/agents.py +25 -22
- letta/server/rest_api/routers/v1/runs.py +2 -3
- letta/server/rest_api/routers/v1/sources.py +31 -0
- letta/server/rest_api/routers/v1/voice.py +1 -0
- letta/server/rest_api/utils.py +38 -13
- letta/server/server.py +52 -21
- letta/services/agent_manager.py +58 -7
- letta/services/block_manager.py +1 -1
- letta/services/file_processor/chunker/line_chunker.py +2 -1
- letta/services/file_processor/file_processor.py +2 -9
- letta/services/files_agents_manager.py +177 -37
- letta/services/helpers/agent_manager_helper.py +77 -48
- letta/services/helpers/tool_parser_helper.py +2 -1
- letta/services/job_manager.py +33 -2
- letta/services/llm_batch_manager.py +1 -1
- letta/services/provider_manager.py +6 -4
- letta/services/tool_executor/core_tool_executor.py +1 -1
- letta/services/tool_executor/files_tool_executor.py +99 -30
- letta/services/tool_executor/multi_agent_tool_executor.py +1 -17
- letta/services/tool_executor/tool_execution_manager.py +6 -0
- letta/services/tool_executor/tool_executor_base.py +3 -0
- letta/services/tool_sandbox/base.py +39 -1
- letta/services/tool_sandbox/e2b_sandbox.py +7 -0
- letta/services/user_manager.py +3 -2
- letta/settings.py +8 -14
- letta/system.py +17 -17
- letta/templates/sandbox_code_file_async.py.j2 +59 -0
- {letta_nightly-0.8.5.dev20250625104328.dist-info → letta_nightly-0.8.6.dev20250625222533.dist-info}/METADATA +3 -2
- {letta_nightly-0.8.5.dev20250625104328.dist-info → letta_nightly-0.8.6.dev20250625222533.dist-info}/RECORD +78 -76
- {letta_nightly-0.8.5.dev20250625104328.dist-info → letta_nightly-0.8.6.dev20250625222533.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.5.dev20250625104328.dist-info → letta_nightly-0.8.6.dev20250625222533.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.5.dev20250625104328.dist-info → letta_nightly-0.8.6.dev20250625222533.dist-info}/entry_points.txt +0 -0
@@ -18,14 +18,14 @@ from letta.local_llm.constants import INNER_THOUGHTS_KWARG
|
|
18
18
|
from letta.log import get_logger
|
19
19
|
from letta.orm.enums import ToolType
|
20
20
|
from letta.otel.tracing import log_event, trace_method
|
21
|
-
from letta.schemas.agent import AgentState
|
21
|
+
from letta.schemas.agent import AgentState
|
22
22
|
from letta.schemas.enums import AgentStepStatus, JobStatus, MessageStreamStatus, ProviderType
|
23
23
|
from letta.schemas.job import JobUpdate
|
24
24
|
from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage
|
25
25
|
from letta.schemas.letta_message_content import OmittedReasoningContent, ReasoningContent, RedactedReasoningContent, TextContent
|
26
26
|
from letta.schemas.letta_request import LettaBatchRequest
|
27
27
|
from letta.schemas.letta_response import LettaBatchResponse, LettaResponse
|
28
|
-
from letta.schemas.llm_batch_job import LLMBatchItem
|
28
|
+
from letta.schemas.llm_batch_job import AgentStepState, LLMBatchItem
|
29
29
|
from letta.schemas.message import Message, MessageCreate
|
30
30
|
from letta.schemas.openai.chat_completion_response import ToolCall as OpenAIToolCall
|
31
31
|
from letta.schemas.sandbox_config import SandboxConfig, SandboxType
|
@@ -548,8 +548,9 @@ class LettaAgentBatch(BaseAgent):
|
|
548
548
|
function_call_success=success_flag,
|
549
549
|
function_response=tool_exec_result,
|
550
550
|
tool_execution_result=tool_exec_result_obj,
|
551
|
+
timezone=agent_state.timezone,
|
551
552
|
actor=self.actor,
|
552
|
-
|
553
|
+
continue_stepping=False,
|
553
554
|
reasoning_content=reasoning_content,
|
554
555
|
pre_computed_assistant_message_id=None,
|
555
556
|
llm_batch_item_id=llm_batch_item_id,
|
letta/agents/voice_agent.py
CHANGED
@@ -38,6 +38,7 @@ from letta.server.rest_api.utils import (
|
|
38
38
|
from letta.services.agent_manager import AgentManager
|
39
39
|
from letta.services.block_manager import BlockManager
|
40
40
|
from letta.services.helpers.agent_manager_helper import compile_system_message
|
41
|
+
from letta.services.job_manager import JobManager
|
41
42
|
from letta.services.message_manager import MessageManager
|
42
43
|
from letta.services.passage_manager import PassageManager
|
43
44
|
from letta.services.summarizer.enums import SummarizationMode
|
@@ -64,6 +65,7 @@ class VoiceAgent(BaseAgent):
|
|
64
65
|
message_manager: MessageManager,
|
65
66
|
agent_manager: AgentManager,
|
66
67
|
block_manager: BlockManager,
|
68
|
+
job_manager: JobManager,
|
67
69
|
passage_manager: PassageManager,
|
68
70
|
actor: User,
|
69
71
|
):
|
@@ -73,6 +75,7 @@ class VoiceAgent(BaseAgent):
|
|
73
75
|
|
74
76
|
# Summarizer settings
|
75
77
|
self.block_manager = block_manager
|
78
|
+
self.job_manager = job_manager
|
76
79
|
self.passage_manager = passage_manager
|
77
80
|
# TODO: This is not guaranteed to exist!
|
78
81
|
self.summary_block_label = "human"
|
@@ -98,6 +101,7 @@ class VoiceAgent(BaseAgent):
|
|
98
101
|
agent_manager=self.agent_manager,
|
99
102
|
actor=self.actor,
|
100
103
|
block_manager=self.block_manager,
|
104
|
+
job_manager=self.job_manager,
|
101
105
|
passage_manager=self.passage_manager,
|
102
106
|
target_block_label=self.summary_block_label,
|
103
107
|
),
|
@@ -146,10 +150,13 @@ class VoiceAgent(BaseAgent):
|
|
146
150
|
system_prompt=agent_state.system,
|
147
151
|
in_context_memory=agent_state.memory,
|
148
152
|
in_context_memory_last_edit=memory_edit_timestamp,
|
153
|
+
timezone=agent_state.timezone,
|
149
154
|
previous_message_count=self.num_messages,
|
150
155
|
archival_memory_size=self.num_archival_memories,
|
151
156
|
)
|
152
|
-
letta_message_db_queue = create_input_messages(
|
157
|
+
letta_message_db_queue = create_input_messages(
|
158
|
+
input_messages=input_messages, agent_id=agent_state.id, timezone=agent_state.timezone, actor=self.actor
|
159
|
+
)
|
153
160
|
in_memory_message_history = self.pre_process_input_message(input_messages)
|
154
161
|
|
155
162
|
# TODO: Define max steps here
|
@@ -208,6 +215,7 @@ class VoiceAgent(BaseAgent):
|
|
208
215
|
agent_id=agent_state.id,
|
209
216
|
model=agent_state.llm_config.model,
|
210
217
|
actor=self.actor,
|
218
|
+
timezone=agent_state.timezone,
|
211
219
|
)
|
212
220
|
letta_message_db_queue.extend(assistant_msgs)
|
213
221
|
|
@@ -268,8 +276,9 @@ class VoiceAgent(BaseAgent):
|
|
268
276
|
function_call_success=success_flag,
|
269
277
|
function_response=tool_result,
|
270
278
|
tool_execution_result=tool_execution_result,
|
279
|
+
timezone=agent_state.timezone,
|
271
280
|
actor=self.actor,
|
272
|
-
|
281
|
+
continue_stepping=True,
|
273
282
|
)
|
274
283
|
letta_message_db_queue.extend(tool_call_messages)
|
275
284
|
|
@@ -439,6 +448,7 @@ class VoiceAgent(BaseAgent):
|
|
439
448
|
message_manager=self.message_manager,
|
440
449
|
agent_manager=self.agent_manager,
|
441
450
|
block_manager=self.block_manager,
|
451
|
+
job_manager=self.job_manager,
|
442
452
|
passage_manager=self.passage_manager,
|
443
453
|
sandbox_env_vars=sandbox_env_vars,
|
444
454
|
actor=self.actor,
|
@@ -15,6 +15,7 @@ from letta.schemas.tool_rule import ChildToolRule, ContinueToolRule, InitToolRul
|
|
15
15
|
from letta.schemas.user import User
|
16
16
|
from letta.services.agent_manager import AgentManager
|
17
17
|
from letta.services.block_manager import BlockManager
|
18
|
+
from letta.services.job_manager import JobManager
|
18
19
|
from letta.services.message_manager import MessageManager
|
19
20
|
from letta.services.passage_manager import PassageManager
|
20
21
|
from letta.services.summarizer.enums import SummarizationMode
|
@@ -34,6 +35,7 @@ class VoiceSleeptimeAgent(LettaAgent):
|
|
34
35
|
message_manager: MessageManager,
|
35
36
|
agent_manager: AgentManager,
|
36
37
|
block_manager: BlockManager,
|
38
|
+
job_manager: JobManager,
|
37
39
|
passage_manager: PassageManager,
|
38
40
|
target_block_label: str,
|
39
41
|
actor: User,
|
@@ -43,6 +45,7 @@ class VoiceSleeptimeAgent(LettaAgent):
|
|
43
45
|
message_manager=message_manager,
|
44
46
|
agent_manager=agent_manager,
|
45
47
|
block_manager=block_manager,
|
48
|
+
job_manager=job_manager,
|
46
49
|
passage_manager=passage_manager,
|
47
50
|
actor=actor,
|
48
51
|
)
|
@@ -64,7 +67,9 @@ class VoiceSleeptimeAgent(LettaAgent):
|
|
64
67
|
self,
|
65
68
|
input_messages: List[MessageCreate],
|
66
69
|
max_steps: int = DEFAULT_MAX_STEPS,
|
70
|
+
run_id: Optional[str] = None,
|
67
71
|
use_assistant_message: bool = True,
|
72
|
+
request_start_timestamp_ns: Optional[int] = None,
|
68
73
|
include_return_message_types: Optional[List[MessageType]] = None,
|
69
74
|
) -> LettaResponse:
|
70
75
|
"""
|
@@ -82,7 +87,7 @@ class VoiceSleeptimeAgent(LettaAgent):
|
|
82
87
|
]
|
83
88
|
|
84
89
|
# Summarize
|
85
|
-
current_in_context_messages, new_in_context_messages,
|
90
|
+
current_in_context_messages, new_in_context_messages, stop_reason, usage = await super()._step(
|
86
91
|
agent_state=agent_state, input_messages=input_messages, max_steps=max_steps
|
87
92
|
)
|
88
93
|
new_in_context_messages, updated = self.summarizer.summarize(
|
@@ -172,7 +177,12 @@ class VoiceSleeptimeAgent(LettaAgent):
|
|
172
177
|
return f"Failed to store memory given start_index {start_index} and end_index {end_index}: {e}", False
|
173
178
|
|
174
179
|
async def step_stream(
|
175
|
-
self,
|
180
|
+
self,
|
181
|
+
input_messages: List[MessageCreate],
|
182
|
+
max_steps: int = DEFAULT_MAX_STEPS,
|
183
|
+
use_assistant_message: bool = True,
|
184
|
+
request_start_timestamp_ns: Optional[int] = None,
|
185
|
+
include_return_message_types: Optional[List[MessageType]] = None,
|
176
186
|
) -> AsyncGenerator[Union[LettaMessage, LegacyLettaMessage, MessageStreamStatus], None]:
|
177
187
|
"""
|
178
188
|
This agent is synchronous-only. If called in an async context, raise an error.
|
letta/constants.py
CHANGED
@@ -6,6 +6,7 @@ LETTA_DIR = os.path.join(os.path.expanduser("~"), ".letta")
|
|
6
6
|
LETTA_TOOL_EXECUTION_DIR = os.path.join(LETTA_DIR, "tool_execution_dir")
|
7
7
|
|
8
8
|
LETTA_MODEL_ENDPOINT = "https://inference.letta.com"
|
9
|
+
DEFAULT_TIMEZONE = "UTC"
|
9
10
|
|
10
11
|
ADMIN_PREFIX = "/v1/admin"
|
11
12
|
API_PREFIX = "/v1"
|
@@ -113,7 +114,7 @@ BASE_VOICE_SLEEPTIME_TOOLS = [
|
|
113
114
|
"finish_rethinking_memory",
|
114
115
|
]
|
115
116
|
# Multi agent tools
|
116
|
-
MULTI_AGENT_TOOLS = ["send_message_to_agent_and_wait_for_reply", "send_message_to_agents_matching_tags"
|
117
|
+
MULTI_AGENT_TOOLS = ["send_message_to_agent_and_wait_for_reply", "send_message_to_agents_matching_tags"]
|
117
118
|
|
118
119
|
# Used to catch if line numbers are pushed in
|
119
120
|
# MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX = re.compile(r"^Line \d+: ", re.MULTILINE)
|
@@ -130,6 +131,11 @@ BUILTIN_TOOLS = ["run_code", "web_search"]
|
|
130
131
|
# Built in tools
|
131
132
|
FILES_TOOLS = ["open_file", "close_file", "grep", "search_files"]
|
132
133
|
|
134
|
+
FILE_MEMORY_EXISTS_MESSAGE = "The following files are currently accessible in memory:"
|
135
|
+
FILE_MEMORY_EMPTY_MESSAGE = (
|
136
|
+
"There are no files currently available in memory. Files will appear here once they are uploaded directly to your system."
|
137
|
+
)
|
138
|
+
|
133
139
|
# Set of all built-in Letta tools
|
134
140
|
LETTA_TOOL_SET = set(
|
135
141
|
BASE_TOOLS
|
@@ -192,10 +198,21 @@ CORE_MEMORY_LINE_NUMBER_WARNING = (
|
|
192
198
|
# Constants to do with summarization / conversation length window
|
193
199
|
# The max amount of tokens supported by the underlying model (eg 8k for gpt-4 and Mistral 7B)
|
194
200
|
LLM_MAX_TOKENS = {
|
195
|
-
"DEFAULT":
|
201
|
+
"DEFAULT": 30000,
|
202
|
+
# deepseek
|
196
203
|
"deepseek-chat": 64000,
|
197
204
|
"deepseek-reasoner": 64000,
|
198
205
|
## OpenAI models: https://platform.openai.com/docs/models/overview
|
206
|
+
# reasoners
|
207
|
+
"o1": 200000,
|
208
|
+
# "o1-pro": 200000, # responses API only
|
209
|
+
"o1-2024-12-17": 200000,
|
210
|
+
"o3": 200000,
|
211
|
+
"o3-2025-04-16": 200000,
|
212
|
+
"o3-mini": 200000,
|
213
|
+
"o3-mini-2025-01-31": 200000,
|
214
|
+
# "o3-pro": 200000, # responses API only
|
215
|
+
# "o3-pro-2025-06-10": 200000,
|
199
216
|
"gpt-4.1": 1047576,
|
200
217
|
"gpt-4.1-2025-04-14": 1047576,
|
201
218
|
"gpt-4.1-mini": 1047576,
|
@@ -209,6 +226,7 @@ LLM_MAX_TOKENS = {
|
|
209
226
|
"chatgpt-4o-latest": 128000,
|
210
227
|
# "o1-preview-2024-09-12
|
211
228
|
"gpt-4o-2024-08-06": 128000,
|
229
|
+
"gpt-4o-2024-11-20": 128000,
|
212
230
|
"gpt-4-turbo-preview": 128000,
|
213
231
|
"gpt-4o": 128000,
|
214
232
|
"gpt-3.5-turbo-instruct": 16385,
|
@@ -218,7 +236,7 @@ LLM_MAX_TOKENS = {
|
|
218
236
|
# "davinci-002": 128000,
|
219
237
|
"gpt-4-turbo-2024-04-09": 128000,
|
220
238
|
# "gpt-4o-realtime-preview-2024-10-01
|
221
|
-
"gpt-4-turbo":
|
239
|
+
"gpt-4-turbo": 128000,
|
222
240
|
"gpt-4o-2024-05-13": 128000,
|
223
241
|
# "o1-mini
|
224
242
|
# "o1-mini-2024-09-12
|
@@ -338,3 +356,6 @@ REDIS_INCLUDE = "include"
|
|
338
356
|
REDIS_EXCLUDE = "exclude"
|
339
357
|
REDIS_SET_DEFAULT_VAL = "None"
|
340
358
|
REDIS_DEFAULT_CACHE_PREFIX = "letta_cache"
|
359
|
+
|
360
|
+
# TODO: This is temporary, eventually use token-based eviction
|
361
|
+
MAX_FILES_OPEN = 5
|
@@ -283,6 +283,12 @@ class NoopAsyncRedisClient(AsyncRedisClient):
|
|
283
283
|
async def scard(self, key: str) -> int:
|
284
284
|
return 0
|
285
285
|
|
286
|
+
async def smembers(self, key: str) -> Set[str]:
|
287
|
+
return set()
|
288
|
+
|
289
|
+
async def srem(self, key: str, *members: Union[str, int, float]) -> int:
|
290
|
+
return 0
|
291
|
+
|
286
292
|
|
287
293
|
async def get_redis_client() -> AsyncRedisClient:
|
288
294
|
global _client_instance
|
letta/errors.py
CHANGED
@@ -17,6 +17,7 @@ class ErrorCode(Enum):
|
|
17
17
|
INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR"
|
18
18
|
CONTEXT_WINDOW_EXCEEDED = "CONTEXT_WINDOW_EXCEEDED"
|
19
19
|
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
|
20
|
+
TIMEOUT = "TIMEOUT"
|
20
21
|
|
21
22
|
|
22
23
|
class LettaError(Exception):
|
@@ -101,6 +102,10 @@ class LLMServerError(LLMError):
|
|
101
102
|
while processing the request."""
|
102
103
|
|
103
104
|
|
105
|
+
class LLMTimeoutError(LLMError):
|
106
|
+
"""Error when LLM request times out"""
|
107
|
+
|
108
|
+
|
104
109
|
class BedrockPermissionError(LettaError):
|
105
110
|
"""Exception raised for errors in the Bedrock permission process."""
|
106
111
|
|
@@ -32,16 +32,23 @@ async def close_file(agent_state: "AgentState", file_name: str) -> str:
|
|
32
32
|
raise NotImplementedError("Tool not implemented. Please contact the Letta team.")
|
33
33
|
|
34
34
|
|
35
|
-
async def grep(
|
35
|
+
async def grep(
|
36
|
+
agent_state: "AgentState",
|
37
|
+
pattern: str,
|
38
|
+
include: Optional[str] = None,
|
39
|
+
context_lines: Optional[int] = 3,
|
40
|
+
) -> str:
|
36
41
|
"""
|
37
|
-
Grep tool to search files across data sources
|
42
|
+
Grep tool to search files across data sources using a keyword or regex pattern.
|
38
43
|
|
39
44
|
Args:
|
40
45
|
pattern (str): Keyword or regex pattern to search within file contents.
|
41
46
|
include (Optional[str]): Optional keyword or regex pattern to filter filenames to include in the search.
|
47
|
+
context_lines (Optional[int]): Number of lines of context to show before and after each match.
|
48
|
+
Equivalent to `-C` in grep. Defaults to 3.
|
42
49
|
|
43
50
|
Returns:
|
44
|
-
str: Matching lines or summary output.
|
51
|
+
str: Matching lines with optional surrounding context or a summary output.
|
45
52
|
"""
|
46
53
|
raise NotImplementedError("Tool not implemented. Please contact the Letta team.")
|
47
54
|
|
@@ -7,7 +7,6 @@ from letta.functions.helpers import (
|
|
7
7
|
_send_message_to_all_agents_in_group_async,
|
8
8
|
execute_send_message_to_agent,
|
9
9
|
extract_send_message_from_steps_messages,
|
10
|
-
fire_and_forget_send_to_agent,
|
11
10
|
)
|
12
11
|
from letta.schemas.enums import MessageRole
|
13
12
|
from letta.schemas.message import MessageCreate
|
@@ -44,37 +43,6 @@ def send_message_to_agent_and_wait_for_reply(self: "Agent", message: str, other_
|
|
44
43
|
)
|
45
44
|
|
46
45
|
|
47
|
-
def send_message_to_agent_async(self: "Agent", message: str, other_agent_id: str) -> str:
|
48
|
-
"""
|
49
|
-
Sends a message to a specific Letta agent within the same organization. The sender's identity is automatically included, so no explicit introduction is required in the message. This function does not expect a response from the target agent, making it suitable for notifications or one-way communication.
|
50
|
-
|
51
|
-
Args:
|
52
|
-
message (str): The content of the message to be sent to the target agent.
|
53
|
-
other_agent_id (str): The unique identifier of the target Letta agent.
|
54
|
-
|
55
|
-
Returns:
|
56
|
-
str: A confirmation message indicating the message was successfully sent.
|
57
|
-
"""
|
58
|
-
message = (
|
59
|
-
f"[Incoming message from agent with ID '{self.agent_state.id}' - to reply to this message, "
|
60
|
-
f"make sure to use the 'send_message_to_agent_async' tool, or the agent will not receive your message] "
|
61
|
-
f"{message}"
|
62
|
-
)
|
63
|
-
messages = [MessageCreate(role=MessageRole.system, content=message, name=self.agent_state.name)]
|
64
|
-
|
65
|
-
# Do the actual fire-and-forget
|
66
|
-
fire_and_forget_send_to_agent(
|
67
|
-
sender_agent=self,
|
68
|
-
messages=messages,
|
69
|
-
other_agent_id=other_agent_id,
|
70
|
-
log_prefix="[send_message_to_agent_async]",
|
71
|
-
use_retries=False, # or True if you want to use _async_send_message_with_retries
|
72
|
-
)
|
73
|
-
|
74
|
-
# Immediately return to caller
|
75
|
-
return "Successfully sent message"
|
76
|
-
|
77
|
-
|
78
46
|
def send_message_to_agents_matching_tags(self: "Agent", message: str, match_all: List[str], match_some: List[str]) -> List[str]:
|
79
47
|
"""
|
80
48
|
Sends a message to all agents within the same organization that match the specified tag criteria. Agents must possess *all* of the tags in `match_all` and *at least one* of the tags in `match_some` to receive the message.
|
@@ -63,6 +63,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
|
|
63
63
|
self,
|
64
64
|
input_messages: List[MessageCreate],
|
65
65
|
max_steps: int = DEFAULT_MAX_STEPS,
|
66
|
+
run_id: Optional[str] = None,
|
66
67
|
use_assistant_message: bool = True,
|
67
68
|
request_start_timestamp_ns: Optional[int] = None,
|
68
69
|
include_return_message_types: Optional[List[MessageType]] = None,
|
@@ -83,6 +84,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
|
|
83
84
|
message_manager=self.message_manager,
|
84
85
|
agent_manager=self.agent_manager,
|
85
86
|
block_manager=self.block_manager,
|
87
|
+
job_manager=self.job_manager,
|
86
88
|
passage_manager=self.passage_manager,
|
87
89
|
actor=self.actor,
|
88
90
|
step_manager=self.step_manager,
|
@@ -92,6 +94,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
|
|
92
94
|
response = await foreground_agent.step(
|
93
95
|
input_messages=new_messages,
|
94
96
|
max_steps=max_steps,
|
97
|
+
run_id=run_id,
|
95
98
|
use_assistant_message=use_assistant_message,
|
96
99
|
include_return_message_types=include_return_message_types,
|
97
100
|
)
|
@@ -170,6 +173,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
|
|
170
173
|
message_manager=self.message_manager,
|
171
174
|
agent_manager=self.agent_manager,
|
172
175
|
block_manager=self.block_manager,
|
176
|
+
job_manager=self.job_manager,
|
173
177
|
passage_manager=self.passage_manager,
|
174
178
|
actor=self.actor,
|
175
179
|
step_manager=self.step_manager,
|
@@ -283,6 +287,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
|
|
283
287
|
message_manager=self.message_manager,
|
284
288
|
agent_manager=self.agent_manager,
|
285
289
|
block_manager=self.block_manager,
|
290
|
+
job_manager=self.job_manager,
|
286
291
|
passage_manager=self.passage_manager,
|
287
292
|
actor=self.actor,
|
288
293
|
step_manager=self.step_manager,
|
@@ -296,6 +301,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
|
|
296
301
|
result = await sleeptime_agent.step(
|
297
302
|
input_messages=sleeptime_agent_messages,
|
298
303
|
use_assistant_message=use_assistant_message,
|
304
|
+
run_id=run_id,
|
299
305
|
)
|
300
306
|
|
301
307
|
# Update job status
|
letta/helpers/converters.py
CHANGED
@@ -8,7 +8,6 @@ from openai.types.chat.chat_completion_message_tool_call import Function as Open
|
|
8
8
|
from sqlalchemy import Dialect
|
9
9
|
|
10
10
|
from letta.functions.mcp_client.types import StdioServerConfig
|
11
|
-
from letta.schemas.agent import AgentStepState
|
12
11
|
from letta.schemas.embedding_config import EmbeddingConfig
|
13
12
|
from letta.schemas.enums import ProviderType, ToolRuleType
|
14
13
|
from letta.schemas.letta_message_content import (
|
@@ -23,6 +22,7 @@ from letta.schemas.letta_message_content import (
|
|
23
22
|
ToolCallContent,
|
24
23
|
ToolReturnContent,
|
25
24
|
)
|
25
|
+
from letta.schemas.llm_batch_job import AgentStepState
|
26
26
|
from letta.schemas.llm_config import LLMConfig
|
27
27
|
from letta.schemas.message import ToolReturn
|
28
28
|
from letta.schemas.response_format import (
|
@@ -39,6 +39,7 @@ from letta.schemas.tool_rule import (
|
|
39
39
|
InitToolRule,
|
40
40
|
MaxCountPerStepToolRule,
|
41
41
|
ParentToolRule,
|
42
|
+
RequiredBeforeExitToolRule,
|
42
43
|
TerminalToolRule,
|
43
44
|
ToolRule,
|
44
45
|
)
|
@@ -131,6 +132,8 @@ def deserialize_tool_rule(
|
|
131
132
|
return MaxCountPerStepToolRule(**data)
|
132
133
|
elif rule_type == ToolRuleType.parent_last_tool:
|
133
134
|
return ParentToolRule(**data)
|
135
|
+
elif rule_type == ToolRuleType.required_before_exit:
|
136
|
+
return RequiredBeforeExitToolRule(**data)
|
134
137
|
raise ValueError(f"Unknown ToolRule type: {rule_type}")
|
135
138
|
|
136
139
|
|
@@ -2,11 +2,12 @@ import re
|
|
2
2
|
import time
|
3
3
|
from datetime import datetime, timedelta
|
4
4
|
from datetime import timezone as dt_timezone
|
5
|
-
from time import strftime
|
6
5
|
from typing import Callable
|
7
6
|
|
8
7
|
import pytz
|
9
8
|
|
9
|
+
from letta.constants import DEFAULT_TIMEZONE
|
10
|
+
|
10
11
|
|
11
12
|
def parse_formatted_time(formatted_time):
|
12
13
|
# parse times returned by letta.utils.get_formatted_time()
|
@@ -18,33 +19,22 @@ def datetime_to_timestamp(dt):
|
|
18
19
|
return int(dt.timestamp())
|
19
20
|
|
20
21
|
|
21
|
-
def
|
22
|
-
# Get
|
22
|
+
def get_local_time_fast(timezone):
|
23
|
+
# Get current UTC time and convert to the specified timezone
|
24
|
+
if not timezone:
|
25
|
+
return datetime.now().strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
|
23
26
|
current_time_utc = datetime.now(pytz.utc)
|
24
|
-
|
25
|
-
|
26
|
-
sf_time_zone = pytz.timezone("America/Los_Angeles")
|
27
|
-
local_time = current_time_utc.astimezone(sf_time_zone)
|
28
|
-
|
29
|
-
# You may format it as you desire
|
30
|
-
formatted_time = local_time.strftime("%Y-%m-%d %H:%M:%S %Z%z")
|
31
|
-
|
32
|
-
return formatted_time
|
33
|
-
|
34
|
-
|
35
|
-
def get_local_time_fast():
|
36
|
-
formatted_time = strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
|
27
|
+
local_time = current_time_utc.astimezone(pytz.timezone(timezone))
|
28
|
+
formatted_time = local_time.strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
|
37
29
|
|
38
30
|
return formatted_time
|
39
31
|
|
40
32
|
|
41
|
-
def get_local_time_timezone(timezone=
|
33
|
+
def get_local_time_timezone(timezone=DEFAULT_TIMEZONE):
|
42
34
|
# Get the current time in UTC
|
43
35
|
current_time_utc = datetime.now(pytz.utc)
|
44
36
|
|
45
|
-
|
46
|
-
sf_time_zone = pytz.timezone(timezone)
|
47
|
-
local_time = current_time_utc.astimezone(sf_time_zone)
|
37
|
+
local_time = current_time_utc.astimezone(pytz.timezone(timezone))
|
48
38
|
|
49
39
|
# You may format it as you desire, including AM/PM
|
50
40
|
formatted_time = local_time.strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
|
@@ -52,7 +42,7 @@ def get_local_time_timezone(timezone="America/Los_Angeles"):
|
|
52
42
|
return formatted_time
|
53
43
|
|
54
44
|
|
55
|
-
def get_local_time(timezone=
|
45
|
+
def get_local_time(timezone=DEFAULT_TIMEZONE):
|
56
46
|
if timezone is not None:
|
57
47
|
time_str = get_local_time_timezone(timezone)
|
58
48
|
else:
|
@@ -89,8 +79,11 @@ def timestamp_to_datetime(timestamp_seconds: int) -> datetime:
|
|
89
79
|
return datetime.fromtimestamp(timestamp_seconds, tz=dt_timezone.utc)
|
90
80
|
|
91
81
|
|
92
|
-
def format_datetime(dt):
|
93
|
-
|
82
|
+
def format_datetime(dt, timezone):
|
83
|
+
if not timezone:
|
84
|
+
# use local timezone
|
85
|
+
return dt.strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
|
86
|
+
return dt.astimezone(pytz.timezone(timezone)).strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
|
94
87
|
|
95
88
|
|
96
89
|
def validate_date_format(date_str):
|
letta/helpers/message_helper.py
CHANGED
@@ -12,6 +12,7 @@ from letta.schemas.message import Message, MessageCreate
|
|
12
12
|
def convert_message_creates_to_messages(
|
13
13
|
message_creates: list[MessageCreate],
|
14
14
|
agent_id: str,
|
15
|
+
timezone: str,
|
15
16
|
wrap_user_message: bool = True,
|
16
17
|
wrap_system_message: bool = True,
|
17
18
|
) -> list[Message]:
|
@@ -19,6 +20,7 @@ def convert_message_creates_to_messages(
|
|
19
20
|
_convert_message_create_to_message(
|
20
21
|
message_create=create,
|
21
22
|
agent_id=agent_id,
|
23
|
+
timezone=timezone,
|
22
24
|
wrap_user_message=wrap_user_message,
|
23
25
|
wrap_system_message=wrap_system_message,
|
24
26
|
)
|
@@ -29,6 +31,7 @@ def convert_message_creates_to_messages(
|
|
29
31
|
def _convert_message_create_to_message(
|
30
32
|
message_create: MessageCreate,
|
31
33
|
agent_id: str,
|
34
|
+
timezone: str,
|
32
35
|
wrap_user_message: bool = True,
|
33
36
|
wrap_system_message: bool = True,
|
34
37
|
) -> Message:
|
@@ -50,9 +53,9 @@ def _convert_message_create_to_message(
|
|
50
53
|
if isinstance(content, TextContent):
|
51
54
|
# Apply wrapping if needed
|
52
55
|
if message_create.role == MessageRole.user and wrap_user_message:
|
53
|
-
content.text = system.package_user_message(user_message=content.text)
|
56
|
+
content.text = system.package_user_message(user_message=content.text, timezone=timezone)
|
54
57
|
elif message_create.role == MessageRole.system and wrap_system_message:
|
55
|
-
content.text = system.package_system_message(system_message=content.text)
|
58
|
+
content.text = system.package_system_message(system_message=content.text, timezone=timezone)
|
56
59
|
elif isinstance(content, ImageContent):
|
57
60
|
if content.source.type == ImageSourceType.url:
|
58
61
|
# Convert URL image to Base64Image if needed
|
@@ -12,6 +12,7 @@ from letta.schemas.tool_rule import (
|
|
12
12
|
InitToolRule,
|
13
13
|
MaxCountPerStepToolRule,
|
14
14
|
ParentToolRule,
|
15
|
+
RequiredBeforeExitToolRule,
|
15
16
|
TerminalToolRule,
|
16
17
|
)
|
17
18
|
|
@@ -41,6 +42,9 @@ class ToolRulesSolver(BaseModel):
|
|
41
42
|
terminal_tool_rules: List[TerminalToolRule] = Field(
|
42
43
|
default_factory=list, description="Terminal tool rules that end the agent loop if called."
|
43
44
|
)
|
45
|
+
required_before_exit_tool_rules: List[RequiredBeforeExitToolRule] = Field(
|
46
|
+
default_factory=list, description="Tool rules that must be called before the agent can exit."
|
47
|
+
)
|
44
48
|
tool_call_history: List[str] = Field(default_factory=list, description="History of tool calls, updated with each tool call.")
|
45
49
|
|
46
50
|
def __init__(
|
@@ -51,6 +55,7 @@ class ToolRulesSolver(BaseModel):
|
|
51
55
|
child_based_tool_rules: Optional[List[Union[ChildToolRule, ConditionalToolRule, MaxCountPerStepToolRule]]] = None,
|
52
56
|
parent_tool_rules: Optional[List[ParentToolRule]] = None,
|
53
57
|
terminal_tool_rules: Optional[List[TerminalToolRule]] = None,
|
58
|
+
required_before_exit_tool_rules: Optional[List[RequiredBeforeExitToolRule]] = None,
|
54
59
|
tool_call_history: Optional[List[str]] = None,
|
55
60
|
**kwargs,
|
56
61
|
):
|
@@ -60,6 +65,7 @@ class ToolRulesSolver(BaseModel):
|
|
60
65
|
child_based_tool_rules=child_based_tool_rules or [],
|
61
66
|
parent_tool_rules=parent_tool_rules or [],
|
62
67
|
terminal_tool_rules=terminal_tool_rules or [],
|
68
|
+
required_before_exit_tool_rules=required_before_exit_tool_rules or [],
|
63
69
|
tool_call_history=tool_call_history or [],
|
64
70
|
**kwargs,
|
65
71
|
)
|
@@ -88,6 +94,9 @@ class ToolRulesSolver(BaseModel):
|
|
88
94
|
elif rule.type == ToolRuleType.parent_last_tool:
|
89
95
|
assert isinstance(rule, ParentToolRule)
|
90
96
|
self.parent_tool_rules.append(rule)
|
97
|
+
elif rule.type == ToolRuleType.required_before_exit:
|
98
|
+
assert isinstance(rule, RequiredBeforeExitToolRule)
|
99
|
+
self.required_before_exit_tool_rules.append(rule)
|
91
100
|
|
92
101
|
def register_tool_call(self, tool_name: str):
|
93
102
|
"""Update the internal state to track tool call history."""
|
@@ -131,7 +140,7 @@ class ToolRulesSolver(BaseModel):
|
|
131
140
|
return list(final_allowed_tools)
|
132
141
|
|
133
142
|
def is_terminal_tool(self, tool_name: str) -> bool:
|
134
|
-
"""Check if the tool is defined as a terminal tool in the terminal tool rules."""
|
143
|
+
"""Check if the tool is defined as a terminal tool in the terminal tool rules or required-before-exit tool rules."""
|
135
144
|
return any(rule.tool_name == tool_name for rule in self.terminal_tool_rules)
|
136
145
|
|
137
146
|
def has_children_tools(self, tool_name):
|
@@ -142,6 +151,24 @@ class ToolRulesSolver(BaseModel):
|
|
142
151
|
"""Check if the tool is defined as a continue tool in the tool rules."""
|
143
152
|
return any(rule.tool_name == tool_name for rule in self.continue_tool_rules)
|
144
153
|
|
154
|
+
def has_required_tools_been_called(self) -> bool:
|
155
|
+
"""Check if all required-before-exit tools have been called."""
|
156
|
+
return len(self.get_uncalled_required_tools()) == 0
|
157
|
+
|
158
|
+
def get_uncalled_required_tools(self) -> List[str]:
|
159
|
+
"""Get the list of required-before-exit tools that have not been called yet."""
|
160
|
+
if not self.required_before_exit_tool_rules:
|
161
|
+
return [] # No required tools means no uncalled tools
|
162
|
+
|
163
|
+
required_tool_names = {rule.tool_name for rule in self.required_before_exit_tool_rules}
|
164
|
+
called_tool_names = set(self.tool_call_history)
|
165
|
+
|
166
|
+
return list(required_tool_names - called_tool_names)
|
167
|
+
|
168
|
+
def get_ending_tool_names(self) -> List[str]:
|
169
|
+
"""Get the names of tools that are required before exit."""
|
170
|
+
return [rule.tool_name for rule in self.required_before_exit_tool_rules]
|
171
|
+
|
145
172
|
def compile_tool_rule_prompts(self) -> Optional[Block]:
|
146
173
|
"""
|
147
174
|
Compile prompt templates from all tool rules into an ephemeral Block.
|
@@ -168,7 +195,7 @@ class ToolRulesSolver(BaseModel):
|
|
168
195
|
return Block(
|
169
196
|
label="tool_usage_rules",
|
170
197
|
value="\n".join(compiled_prompts),
|
171
|
-
description="The following constraints define rules for tool usage and guide desired behavior. These rules must be followed to ensure proper tool execution and workflow.",
|
198
|
+
description="The following constraints define rules for tool usage and guide desired behavior. These rules must be followed to ensure proper tool execution and workflow. A single response may contain multiple tool calls.",
|
172
199
|
)
|
173
200
|
return None
|
174
201
|
|
@@ -6,11 +6,12 @@ from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
|
|
6
6
|
|
7
7
|
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
|
8
8
|
from letta.helpers.datetime_helpers import get_utc_timestamp_ns, ns_to_ms
|
9
|
+
from letta.llm_api.openai_client import is_openai_reasoning_model
|
9
10
|
from letta.log import get_logger
|
10
11
|
from letta.otel.context import get_ctx_attributes
|
11
12
|
from letta.otel.metric_registry import MetricRegistry
|
12
13
|
from letta.schemas.letta_message import AssistantMessage, LettaMessage, ReasoningMessage, ToolCallDelta, ToolCallMessage
|
13
|
-
from letta.schemas.letta_message_content import TextContent
|
14
|
+
from letta.schemas.letta_message_content import OmittedReasoningContent, TextContent
|
14
15
|
from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
|
15
16
|
from letta.schemas.message import Message
|
16
17
|
from letta.schemas.openai.chat_completion_response import FunctionCall, ToolCall
|
@@ -61,7 +62,13 @@ class OpenAIStreamingInterface:
|
|
61
62
|
|
62
63
|
def get_reasoning_content(self) -> List[TextContent]:
|
63
64
|
content = "".join(self.reasoning_messages).strip()
|
64
|
-
|
65
|
+
|
66
|
+
# Right now we assume that all models omit reasoning content for OAI,
|
67
|
+
# if this changes, we should return the reasoning content
|
68
|
+
if is_openai_reasoning_model(self.model):
|
69
|
+
return [OmittedReasoningContent()]
|
70
|
+
else:
|
71
|
+
return [TextContent(text=content)]
|
65
72
|
|
66
73
|
def get_tool_call_object(self) -> ToolCall:
|
67
74
|
"""Useful for agent loop"""
|