letta-nightly 0.6.43.dev20250324104208__py3-none-any.whl → 0.6.44.dev20250325050316__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.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +1 -1
- letta/agent.py +106 -104
- letta/agents/voice_agent.py +1 -1
- letta/client/streaming.py +3 -1
- letta/functions/function_sets/base.py +2 -1
- letta/functions/function_sets/multi_agent.py +51 -40
- letta/functions/helpers.py +26 -22
- letta/helpers/message_helper.py +41 -0
- letta/llm_api/anthropic.py +150 -44
- letta/llm_api/aws_bedrock.py +5 -3
- letta/llm_api/azure_openai.py +0 -1
- letta/llm_api/llm_api_tools.py +4 -0
- letta/orm/organization.py +1 -0
- letta/orm/sqlalchemy_base.py +2 -4
- letta/schemas/agent.py +8 -0
- letta/schemas/letta_message.py +8 -4
- letta/schemas/llm_config.py +6 -0
- letta/schemas/message.py +143 -24
- letta/schemas/openai/chat_completion_response.py +5 -0
- letta/schemas/organization.py +7 -0
- letta/schemas/providers.py +17 -0
- letta/schemas/tool.py +5 -1
- letta/schemas/usage.py +5 -1
- letta/serialize_schemas/pydantic_agent_schema.py +1 -1
- letta/server/rest_api/interface.py +44 -7
- letta/server/rest_api/routers/v1/agents.py +13 -2
- letta/server/rest_api/routers/v1/organizations.py +19 -1
- letta/server/rest_api/utils.py +1 -1
- letta/server/server.py +49 -70
- letta/services/agent_manager.py +6 -2
- letta/services/helpers/agent_manager_helper.py +24 -38
- letta/services/message_manager.py +7 -6
- letta/services/organization_manager.py +13 -0
- letta/services/tool_execution_sandbox.py +5 -1
- letta/services/tool_executor/__init__.py +0 -0
- letta/services/tool_executor/tool_execution_manager.py +74 -0
- letta/services/tool_executor/tool_executor.py +380 -0
- {letta_nightly-0.6.43.dev20250324104208.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/METADATA +2 -3
- {letta_nightly-0.6.43.dev20250324104208.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/RECORD +42 -38
- {letta_nightly-0.6.43.dev20250324104208.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.43.dev20250324104208.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.43.dev20250324104208.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/entry_points.txt +0 -0
letta/__init__.py
CHANGED
letta/agent.py
CHANGED
|
@@ -210,107 +210,6 @@ class Agent(BaseAgent):
|
|
|
210
210
|
return True
|
|
211
211
|
return False
|
|
212
212
|
|
|
213
|
-
# TODO: Refactor into separate class v.s. large if/elses here
|
|
214
|
-
def execute_tool_and_persist_state(
|
|
215
|
-
self, function_name: str, function_args: dict, target_letta_tool: Tool
|
|
216
|
-
) -> tuple[Any, Optional[SandboxRunResult]]:
|
|
217
|
-
"""
|
|
218
|
-
Execute tool modifications and persist the state of the agent.
|
|
219
|
-
Note: only some agent state modifications will be persisted, such as data in the AgentState ORM and block data
|
|
220
|
-
"""
|
|
221
|
-
# TODO: add agent manager here
|
|
222
|
-
orig_memory_str = self.agent_state.memory.compile()
|
|
223
|
-
|
|
224
|
-
# TODO: need to have an AgentState object that actually has full access to the block data
|
|
225
|
-
# this is because the sandbox tools need to be able to access block.value to edit this data
|
|
226
|
-
try:
|
|
227
|
-
if target_letta_tool.tool_type == ToolType.LETTA_CORE:
|
|
228
|
-
# base tools are allowed to access the `Agent` object and run on the database
|
|
229
|
-
callable_func = get_function_from_module(LETTA_CORE_TOOL_MODULE_NAME, function_name)
|
|
230
|
-
function_args["self"] = self # need to attach self to arg since it's dynamically linked
|
|
231
|
-
function_response = callable_func(**function_args)
|
|
232
|
-
elif target_letta_tool.tool_type == ToolType.LETTA_MULTI_AGENT_CORE:
|
|
233
|
-
callable_func = get_function_from_module(LETTA_MULTI_AGENT_TOOL_MODULE_NAME, function_name)
|
|
234
|
-
function_args["self"] = self # need to attach self to arg since it's dynamically linked
|
|
235
|
-
function_response = callable_func(**function_args)
|
|
236
|
-
elif target_letta_tool.tool_type == ToolType.LETTA_MEMORY_CORE:
|
|
237
|
-
callable_func = get_function_from_module(LETTA_CORE_TOOL_MODULE_NAME, function_name)
|
|
238
|
-
agent_state_copy = self.agent_state.__deepcopy__()
|
|
239
|
-
function_args["agent_state"] = agent_state_copy # need to attach self to arg since it's dynamically linked
|
|
240
|
-
function_response = callable_func(**function_args)
|
|
241
|
-
self.update_memory_if_changed(agent_state_copy.memory)
|
|
242
|
-
elif target_letta_tool.tool_type == ToolType.EXTERNAL_COMPOSIO:
|
|
243
|
-
action_name = generate_composio_action_from_func_name(target_letta_tool.name)
|
|
244
|
-
# Get entity ID from the agent_state
|
|
245
|
-
entity_id = None
|
|
246
|
-
for env_var in self.agent_state.tool_exec_environment_variables:
|
|
247
|
-
if env_var.key == COMPOSIO_ENTITY_ENV_VAR_KEY:
|
|
248
|
-
entity_id = env_var.value
|
|
249
|
-
# Get composio_api_key
|
|
250
|
-
composio_api_key = get_composio_api_key(actor=self.user, logger=self.logger)
|
|
251
|
-
function_response = execute_composio_action(
|
|
252
|
-
action_name=action_name, args=function_args, api_key=composio_api_key, entity_id=entity_id
|
|
253
|
-
)
|
|
254
|
-
elif target_letta_tool.tool_type == ToolType.EXTERNAL_MCP:
|
|
255
|
-
# Get the server name from the tool tag
|
|
256
|
-
# TODO make a property instead?
|
|
257
|
-
server_name = target_letta_tool.tags[0].split(":")[1]
|
|
258
|
-
|
|
259
|
-
# Get the MCPClient from the server's handle
|
|
260
|
-
# TODO these don't get raised properly
|
|
261
|
-
if not self.mcp_clients:
|
|
262
|
-
raise ValueError(f"No MCP client available to use")
|
|
263
|
-
if server_name not in self.mcp_clients:
|
|
264
|
-
raise ValueError(f"Unknown MCP server name: {server_name}")
|
|
265
|
-
mcp_client = self.mcp_clients[server_name]
|
|
266
|
-
if not isinstance(mcp_client, BaseMCPClient):
|
|
267
|
-
raise RuntimeError(f"Expected an MCPClient, but got: {type(mcp_client)}")
|
|
268
|
-
|
|
269
|
-
# Check that tool exists
|
|
270
|
-
available_tools = mcp_client.list_tools()
|
|
271
|
-
available_tool_names = [t.name for t in available_tools]
|
|
272
|
-
if function_name not in available_tool_names:
|
|
273
|
-
raise ValueError(
|
|
274
|
-
f"{function_name} is not available in MCP server {server_name}. Please check your `~/.letta/mcp_config.json` file."
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
function_response, is_error = mcp_client.execute_tool(tool_name=function_name, tool_args=function_args)
|
|
278
|
-
sandbox_run_result = SandboxRunResult(status="error" if is_error else "success")
|
|
279
|
-
return function_response, sandbox_run_result
|
|
280
|
-
else:
|
|
281
|
-
try:
|
|
282
|
-
# Parse the source code to extract function annotations
|
|
283
|
-
annotations = get_function_annotations_from_source(target_letta_tool.source_code, function_name)
|
|
284
|
-
# Coerce the function arguments to the correct types based on the annotations
|
|
285
|
-
function_args = coerce_dict_args_by_annotations(function_args, annotations)
|
|
286
|
-
except ValueError as e:
|
|
287
|
-
self.logger.debug(f"Error coercing function arguments: {e}")
|
|
288
|
-
|
|
289
|
-
# execute tool in a sandbox
|
|
290
|
-
# TODO: allow agent_state to specify which sandbox to execute tools in
|
|
291
|
-
# TODO: This is only temporary, can remove after we publish a pip package with this object
|
|
292
|
-
agent_state_copy = self.agent_state.__deepcopy__()
|
|
293
|
-
agent_state_copy.tools = []
|
|
294
|
-
agent_state_copy.tool_rules = []
|
|
295
|
-
|
|
296
|
-
sandbox_run_result = ToolExecutionSandbox(function_name, function_args, self.user, tool_object=target_letta_tool).run(
|
|
297
|
-
agent_state=agent_state_copy
|
|
298
|
-
)
|
|
299
|
-
function_response, updated_agent_state = sandbox_run_result.func_return, sandbox_run_result.agent_state
|
|
300
|
-
assert orig_memory_str == self.agent_state.memory.compile(), "Memory should not be modified in a sandbox tool"
|
|
301
|
-
if updated_agent_state is not None:
|
|
302
|
-
self.update_memory_if_changed(updated_agent_state.memory)
|
|
303
|
-
return function_response, sandbox_run_result
|
|
304
|
-
except Exception as e:
|
|
305
|
-
# Need to catch error here, or else trunction wont happen
|
|
306
|
-
# TODO: modify to function execution error
|
|
307
|
-
function_response = get_friendly_error_msg(
|
|
308
|
-
function_name=function_name, exception_name=type(e).__name__, exception_message=str(e)
|
|
309
|
-
)
|
|
310
|
-
return function_response, SandboxRunResult(status="error")
|
|
311
|
-
|
|
312
|
-
return function_response, None
|
|
313
|
-
|
|
314
213
|
def _handle_function_error_response(
|
|
315
214
|
self,
|
|
316
215
|
error_msg: str,
|
|
@@ -525,7 +424,7 @@ class Agent(BaseAgent):
|
|
|
525
424
|
self.logger.debug(f"Function call message: {messages[-1]}")
|
|
526
425
|
|
|
527
426
|
nonnull_content = False
|
|
528
|
-
if response_message.content:
|
|
427
|
+
if response_message.content or response_message.reasoning_content or response_message.redacted_reasoning_content:
|
|
529
428
|
# The content if then internal monologue, not chat
|
|
530
429
|
self.interface.internal_monologue(response_message.content, msg_obj=messages[-1])
|
|
531
430
|
# Flag to avoid printing a duplicate if inner thoughts get popped from the function call
|
|
@@ -786,6 +685,7 @@ class Agent(BaseAgent):
|
|
|
786
685
|
total_usage = UsageStatistics()
|
|
787
686
|
step_count = 0
|
|
788
687
|
function_failed = False
|
|
688
|
+
steps_messages = []
|
|
789
689
|
while True:
|
|
790
690
|
kwargs["first_message"] = False
|
|
791
691
|
kwargs["step_count"] = step_count
|
|
@@ -800,6 +700,7 @@ class Agent(BaseAgent):
|
|
|
800
700
|
function_failed = step_response.function_failed
|
|
801
701
|
token_warning = step_response.in_context_memory_warning
|
|
802
702
|
usage = step_response.usage
|
|
703
|
+
steps_messages.append(step_response.messages)
|
|
803
704
|
|
|
804
705
|
step_count += 1
|
|
805
706
|
total_usage += usage
|
|
@@ -859,9 +760,9 @@ class Agent(BaseAgent):
|
|
|
859
760
|
break
|
|
860
761
|
|
|
861
762
|
if self.agent_state.message_buffer_autoclear:
|
|
862
|
-
self.agent_manager.trim_all_in_context_messages_except_system(self.agent_state.id, actor=self.user)
|
|
763
|
+
self.agent_state = self.agent_manager.trim_all_in_context_messages_except_system(self.agent_state.id, actor=self.user)
|
|
863
764
|
|
|
864
|
-
return LettaUsageStatistics(**total_usage.model_dump(), step_count=step_count)
|
|
765
|
+
return LettaUsageStatistics(**total_usage.model_dump(), step_count=step_count, steps_messages=steps_messages)
|
|
865
766
|
|
|
866
767
|
def inner_step(
|
|
867
768
|
self,
|
|
@@ -1303,6 +1204,107 @@ class Agent(BaseAgent):
|
|
|
1303
1204
|
context_window_breakdown = self.get_context_window()
|
|
1304
1205
|
return context_window_breakdown.context_window_size_current
|
|
1305
1206
|
|
|
1207
|
+
# TODO: Refactor into separate class v.s. large if/elses here
|
|
1208
|
+
def execute_tool_and_persist_state(
|
|
1209
|
+
self, function_name: str, function_args: dict, target_letta_tool: Tool
|
|
1210
|
+
) -> tuple[Any, Optional[SandboxRunResult]]:
|
|
1211
|
+
"""
|
|
1212
|
+
Execute tool modifications and persist the state of the agent.
|
|
1213
|
+
Note: only some agent state modifications will be persisted, such as data in the AgentState ORM and block data
|
|
1214
|
+
"""
|
|
1215
|
+
# TODO: add agent manager here
|
|
1216
|
+
orig_memory_str = self.agent_state.memory.compile()
|
|
1217
|
+
|
|
1218
|
+
# TODO: need to have an AgentState object that actually has full access to the block data
|
|
1219
|
+
# this is because the sandbox tools need to be able to access block.value to edit this data
|
|
1220
|
+
try:
|
|
1221
|
+
if target_letta_tool.tool_type == ToolType.LETTA_CORE:
|
|
1222
|
+
# base tools are allowed to access the `Agent` object and run on the database
|
|
1223
|
+
callable_func = get_function_from_module(LETTA_CORE_TOOL_MODULE_NAME, function_name)
|
|
1224
|
+
function_args["self"] = self # need to attach self to arg since it's dynamically linked
|
|
1225
|
+
function_response = callable_func(**function_args)
|
|
1226
|
+
elif target_letta_tool.tool_type == ToolType.LETTA_MULTI_AGENT_CORE:
|
|
1227
|
+
callable_func = get_function_from_module(LETTA_MULTI_AGENT_TOOL_MODULE_NAME, function_name)
|
|
1228
|
+
function_args["self"] = self # need to attach self to arg since it's dynamically linked
|
|
1229
|
+
function_response = callable_func(**function_args)
|
|
1230
|
+
elif target_letta_tool.tool_type == ToolType.LETTA_MEMORY_CORE:
|
|
1231
|
+
callable_func = get_function_from_module(LETTA_CORE_TOOL_MODULE_NAME, function_name)
|
|
1232
|
+
agent_state_copy = self.agent_state.__deepcopy__()
|
|
1233
|
+
function_args["agent_state"] = agent_state_copy # need to attach self to arg since it's dynamically linked
|
|
1234
|
+
function_response = callable_func(**function_args)
|
|
1235
|
+
self.update_memory_if_changed(agent_state_copy.memory)
|
|
1236
|
+
elif target_letta_tool.tool_type == ToolType.EXTERNAL_COMPOSIO:
|
|
1237
|
+
action_name = generate_composio_action_from_func_name(target_letta_tool.name)
|
|
1238
|
+
# Get entity ID from the agent_state
|
|
1239
|
+
entity_id = None
|
|
1240
|
+
for env_var in self.agent_state.tool_exec_environment_variables:
|
|
1241
|
+
if env_var.key == COMPOSIO_ENTITY_ENV_VAR_KEY:
|
|
1242
|
+
entity_id = env_var.value
|
|
1243
|
+
# Get composio_api_key
|
|
1244
|
+
composio_api_key = get_composio_api_key(actor=self.user, logger=self.logger)
|
|
1245
|
+
function_response = execute_composio_action(
|
|
1246
|
+
action_name=action_name, args=function_args, api_key=composio_api_key, entity_id=entity_id
|
|
1247
|
+
)
|
|
1248
|
+
elif target_letta_tool.tool_type == ToolType.EXTERNAL_MCP:
|
|
1249
|
+
# Get the server name from the tool tag
|
|
1250
|
+
# TODO make a property instead?
|
|
1251
|
+
server_name = target_letta_tool.tags[0].split(":")[1]
|
|
1252
|
+
|
|
1253
|
+
# Get the MCPClient from the server's handle
|
|
1254
|
+
# TODO these don't get raised properly
|
|
1255
|
+
if not self.mcp_clients:
|
|
1256
|
+
raise ValueError(f"No MCP client available to use")
|
|
1257
|
+
if server_name not in self.mcp_clients:
|
|
1258
|
+
raise ValueError(f"Unknown MCP server name: {server_name}")
|
|
1259
|
+
mcp_client = self.mcp_clients[server_name]
|
|
1260
|
+
if not isinstance(mcp_client, BaseMCPClient):
|
|
1261
|
+
raise RuntimeError(f"Expected an MCPClient, but got: {type(mcp_client)}")
|
|
1262
|
+
|
|
1263
|
+
# Check that tool exists
|
|
1264
|
+
available_tools = mcp_client.list_tools()
|
|
1265
|
+
available_tool_names = [t.name for t in available_tools]
|
|
1266
|
+
if function_name not in available_tool_names:
|
|
1267
|
+
raise ValueError(
|
|
1268
|
+
f"{function_name} is not available in MCP server {server_name}. Please check your `~/.letta/mcp_config.json` file."
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
function_response, is_error = mcp_client.execute_tool(tool_name=function_name, tool_args=function_args)
|
|
1272
|
+
sandbox_run_result = SandboxRunResult(status="error" if is_error else "success")
|
|
1273
|
+
return function_response, sandbox_run_result
|
|
1274
|
+
else:
|
|
1275
|
+
try:
|
|
1276
|
+
# Parse the source code to extract function annotations
|
|
1277
|
+
annotations = get_function_annotations_from_source(target_letta_tool.source_code, function_name)
|
|
1278
|
+
# Coerce the function arguments to the correct types based on the annotations
|
|
1279
|
+
function_args = coerce_dict_args_by_annotations(function_args, annotations)
|
|
1280
|
+
except ValueError as e:
|
|
1281
|
+
self.logger.debug(f"Error coercing function arguments: {e}")
|
|
1282
|
+
|
|
1283
|
+
# execute tool in a sandbox
|
|
1284
|
+
# TODO: allow agent_state to specify which sandbox to execute tools in
|
|
1285
|
+
# TODO: This is only temporary, can remove after we publish a pip package with this object
|
|
1286
|
+
agent_state_copy = self.agent_state.__deepcopy__()
|
|
1287
|
+
agent_state_copy.tools = []
|
|
1288
|
+
agent_state_copy.tool_rules = []
|
|
1289
|
+
|
|
1290
|
+
sandbox_run_result = ToolExecutionSandbox(function_name, function_args, self.user, tool_object=target_letta_tool).run(
|
|
1291
|
+
agent_state=agent_state_copy
|
|
1292
|
+
)
|
|
1293
|
+
function_response, updated_agent_state = sandbox_run_result.func_return, sandbox_run_result.agent_state
|
|
1294
|
+
assert orig_memory_str == self.agent_state.memory.compile(), "Memory should not be modified in a sandbox tool"
|
|
1295
|
+
if updated_agent_state is not None:
|
|
1296
|
+
self.update_memory_if_changed(updated_agent_state.memory)
|
|
1297
|
+
return function_response, sandbox_run_result
|
|
1298
|
+
except Exception as e:
|
|
1299
|
+
# Need to catch error here, or else trunction wont happen
|
|
1300
|
+
# TODO: modify to function execution error
|
|
1301
|
+
function_response = get_friendly_error_msg(
|
|
1302
|
+
function_name=function_name, exception_name=type(e).__name__, exception_message=str(e)
|
|
1303
|
+
)
|
|
1304
|
+
return function_response, SandboxRunResult(status="error")
|
|
1305
|
+
|
|
1306
|
+
return function_response, None
|
|
1307
|
+
|
|
1306
1308
|
|
|
1307
1309
|
def save_agent(agent: Agent):
|
|
1308
1310
|
"""Save agent to metadata store"""
|
letta/agents/voice_agent.py
CHANGED
|
@@ -274,7 +274,7 @@ class VoiceAgent(BaseAgent):
|
|
|
274
274
|
|
|
275
275
|
diff = united_diff(curr_system_message_text, new_system_message_str)
|
|
276
276
|
if len(diff) > 0:
|
|
277
|
-
logger.
|
|
277
|
+
logger.debug(f"Rebuilding system with new memory...\nDiff:\n{diff}")
|
|
278
278
|
|
|
279
279
|
new_system_message = self.message_manager.update_message_by_id(
|
|
280
280
|
curr_system_message.id, message_update=MessageUpdate(content=new_system_message_str), actor=self.actor
|
letta/client/streaming.py
CHANGED
|
@@ -9,7 +9,7 @@ from letta.constants import OPENAI_CONTEXT_WINDOW_ERROR_SUBSTRING
|
|
|
9
9
|
from letta.errors import LLMError
|
|
10
10
|
from letta.log import get_logger
|
|
11
11
|
from letta.schemas.enums import MessageStreamStatus
|
|
12
|
-
from letta.schemas.letta_message import AssistantMessage, ReasoningMessage, ToolCallMessage, ToolReturnMessage
|
|
12
|
+
from letta.schemas.letta_message import AssistantMessage, HiddenReasoningMessage, ReasoningMessage, ToolCallMessage, ToolReturnMessage
|
|
13
13
|
from letta.schemas.letta_response import LettaStreamingResponse
|
|
14
14
|
from letta.schemas.usage import LettaUsageStatistics
|
|
15
15
|
|
|
@@ -57,6 +57,8 @@ def _sse_post(url: str, data: dict, headers: dict) -> Generator[Union[LettaStrea
|
|
|
57
57
|
yield ReasoningMessage(**chunk_data)
|
|
58
58
|
elif chunk_data.get("message_type") == "assistant_message":
|
|
59
59
|
yield AssistantMessage(**chunk_data)
|
|
60
|
+
elif "hidden_reasoning" in chunk_data:
|
|
61
|
+
yield HiddenReasoningMessage(**chunk_data)
|
|
60
62
|
elif "tool_call" in chunk_data:
|
|
61
63
|
yield ToolCallMessage(**chunk_data)
|
|
62
64
|
elif "tool_return" in chunk_data:
|
|
@@ -14,7 +14,8 @@ def send_message(self: "Agent", message: str) -> Optional[str]:
|
|
|
14
14
|
Optional[str]: None is always returned as this function does not produce a response.
|
|
15
15
|
"""
|
|
16
16
|
# FIXME passing of msg_obj here is a hack, unclear if guaranteed to be the correct reference
|
|
17
|
-
self.interface
|
|
17
|
+
if self.interface:
|
|
18
|
+
self.interface.assistant_message(message) # , msg_obj=self._messages[-1])
|
|
18
19
|
return None
|
|
19
20
|
|
|
20
21
|
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
2
4
|
from typing import TYPE_CHECKING, List
|
|
3
5
|
|
|
4
6
|
from letta.functions.helpers import (
|
|
5
|
-
_send_message_to_agents_matching_tags_async,
|
|
6
7
|
_send_message_to_all_agents_in_group_async,
|
|
7
8
|
execute_send_message_to_agent,
|
|
9
|
+
extract_send_message_from_steps_messages,
|
|
8
10
|
fire_and_forget_send_to_agent,
|
|
9
11
|
)
|
|
12
|
+
from letta.helpers.message_helper import prepare_input_message_create
|
|
10
13
|
from letta.schemas.enums import MessageRole
|
|
11
14
|
from letta.schemas.message import MessageCreate
|
|
12
15
|
from letta.server.rest_api.utils import get_letta_server
|
|
13
|
-
from letta.
|
|
16
|
+
from letta.settings import settings
|
|
14
17
|
|
|
15
18
|
if TYPE_CHECKING:
|
|
16
19
|
from letta.agent import Agent
|
|
@@ -87,51 +90,59 @@ def send_message_to_agents_matching_tags(self: "Agent", message: str, match_all:
|
|
|
87
90
|
response corresponds to a single agent. Agents that do not respond will not have an entry
|
|
88
91
|
in the returned list.
|
|
89
92
|
"""
|
|
90
|
-
log_telemetry(
|
|
91
|
-
self.logger,
|
|
92
|
-
"_send_message_to_agents_matching_tags_async start",
|
|
93
|
-
message=message,
|
|
94
|
-
match_all=match_all,
|
|
95
|
-
match_some=match_some,
|
|
96
|
-
)
|
|
97
93
|
server = get_letta_server()
|
|
98
|
-
|
|
99
94
|
augmented_message = (
|
|
100
|
-
f"[Incoming message from
|
|
95
|
+
f"[Incoming message from external Letta agent - to reply to this message, "
|
|
101
96
|
f"make sure to use the 'send_message' at the end, and the system will notify the sender of your response] "
|
|
102
97
|
f"{message}"
|
|
103
98
|
)
|
|
104
99
|
|
|
105
|
-
#
|
|
106
|
-
log_telemetry(
|
|
107
|
-
self.logger,
|
|
108
|
-
"_send_message_to_agents_matching_tags_async listing agents start",
|
|
109
|
-
message=message,
|
|
110
|
-
match_all=match_all,
|
|
111
|
-
match_some=match_some,
|
|
112
|
-
)
|
|
100
|
+
# Find matching agents
|
|
113
101
|
matching_agents = server.agent_manager.list_agents_matching_tags(actor=self.user, match_all=match_all, match_some=match_some)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
message
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
102
|
+
if not matching_agents:
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
def process_agent(agent_id: str) -> str:
|
|
106
|
+
"""Loads an agent, formats the message, and executes .step()"""
|
|
107
|
+
actor = self.user # Ensure correct actor context
|
|
108
|
+
agent = server.load_agent(agent_id=agent_id, interface=None, actor=actor)
|
|
109
|
+
|
|
110
|
+
# Prepare the message
|
|
111
|
+
messages = [MessageCreate(role=MessageRole.system, content=augmented_message, name=self.agent_state.name)]
|
|
112
|
+
input_messages = [prepare_input_message_create(m, agent_id) for m in messages]
|
|
113
|
+
|
|
114
|
+
# Run .step() and return the response
|
|
115
|
+
usage_stats = agent.step(
|
|
116
|
+
messages=input_messages,
|
|
117
|
+
chaining=True,
|
|
118
|
+
max_chaining_steps=None,
|
|
119
|
+
stream=False,
|
|
120
|
+
skip_verify=True,
|
|
121
|
+
metadata=None,
|
|
122
|
+
put_inner_thoughts_first=True,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
send_messages = extract_send_message_from_steps_messages(usage_stats.steps_messages, logger=agent.logger)
|
|
126
|
+
response_data = {
|
|
127
|
+
"agent_id": agent_id,
|
|
128
|
+
"response_messages": send_messages if send_messages else ["<no response>"],
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return json.dumps(response_data, indent=2)
|
|
132
|
+
|
|
133
|
+
# Use ThreadPoolExecutor for parallel execution
|
|
134
|
+
results = []
|
|
135
|
+
with ThreadPoolExecutor(max_workers=settings.multi_agent_concurrent_sends) as executor:
|
|
136
|
+
future_to_agent = {executor.submit(process_agent, agent_state.id): agent_state for agent_state in matching_agents}
|
|
137
|
+
|
|
138
|
+
for future in as_completed(future_to_agent):
|
|
139
|
+
try:
|
|
140
|
+
results.append(future.result()) # Collect results
|
|
141
|
+
except Exception as e:
|
|
142
|
+
# Log or handle failure for specific agents if needed
|
|
143
|
+
self.logger.exception(f"Error processing agent {future_to_agent[future]}: {e}")
|
|
144
|
+
|
|
145
|
+
return results
|
|
135
146
|
|
|
136
147
|
|
|
137
148
|
def send_message_to_all_agents_in_group(self: "Agent", message: str) -> List[str]:
|
letta/functions/helpers.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
2
4
|
import threading
|
|
3
5
|
from random import uniform
|
|
4
6
|
from typing import Any, Dict, List, Optional, Type, Union
|
|
@@ -17,7 +19,6 @@ from letta.schemas.message import Message, MessageCreate
|
|
|
17
19
|
from letta.schemas.user import User
|
|
18
20
|
from letta.server.rest_api.utils import get_letta_server
|
|
19
21
|
from letta.settings import settings
|
|
20
|
-
from letta.utils import log_telemetry
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
# TODO: This is kind of hacky, as this is used to search up the action later on composio's side
|
|
@@ -386,15 +387,9 @@ async def async_send_message_with_retries(
|
|
|
386
387
|
logging_prefix: Optional[str] = None,
|
|
387
388
|
) -> str:
|
|
388
389
|
logging_prefix = logging_prefix or "[async_send_message_with_retries]"
|
|
389
|
-
log_telemetry(sender_agent.logger, f"async_send_message_with_retries start", target_agent_id=target_agent_id)
|
|
390
390
|
|
|
391
391
|
for attempt in range(1, max_retries + 1):
|
|
392
392
|
try:
|
|
393
|
-
log_telemetry(
|
|
394
|
-
sender_agent.logger,
|
|
395
|
-
f"async_send_message_with_retries -> asyncio wait for send_message_to_agent_no_stream start",
|
|
396
|
-
target_agent_id=target_agent_id,
|
|
397
|
-
)
|
|
398
393
|
response = await asyncio.wait_for(
|
|
399
394
|
send_message_to_agent_no_stream(
|
|
400
395
|
server=server,
|
|
@@ -404,24 +399,15 @@ async def async_send_message_with_retries(
|
|
|
404
399
|
),
|
|
405
400
|
timeout=timeout,
|
|
406
401
|
)
|
|
407
|
-
log_telemetry(
|
|
408
|
-
sender_agent.logger,
|
|
409
|
-
f"async_send_message_with_retries -> asyncio wait for send_message_to_agent_no_stream finish",
|
|
410
|
-
target_agent_id=target_agent_id,
|
|
411
|
-
)
|
|
412
402
|
|
|
413
403
|
# Then parse out the assistant message
|
|
414
404
|
assistant_message = parse_letta_response_for_assistant_message(target_agent_id, response)
|
|
415
405
|
if assistant_message:
|
|
416
406
|
sender_agent.logger.info(f"{logging_prefix} - {assistant_message}")
|
|
417
|
-
log_telemetry(
|
|
418
|
-
sender_agent.logger, f"async_send_message_with_retries finish with assistant message", target_agent_id=target_agent_id
|
|
419
|
-
)
|
|
420
407
|
return assistant_message
|
|
421
408
|
else:
|
|
422
409
|
msg = f"(No response from agent {target_agent_id})"
|
|
423
410
|
sender_agent.logger.info(f"{logging_prefix} - {msg}")
|
|
424
|
-
log_telemetry(sender_agent.logger, f"async_send_message_with_retries finish no response", target_agent_id=target_agent_id)
|
|
425
411
|
return msg
|
|
426
412
|
|
|
427
413
|
except asyncio.TimeoutError:
|
|
@@ -439,12 +425,6 @@ async def async_send_message_with_retries(
|
|
|
439
425
|
await asyncio.sleep(backoff)
|
|
440
426
|
else:
|
|
441
427
|
sender_agent.logger.error(f"{logging_prefix} - Fatal error: {error_msg}")
|
|
442
|
-
log_telemetry(
|
|
443
|
-
sender_agent.logger,
|
|
444
|
-
f"async_send_message_with_retries finish fatal error",
|
|
445
|
-
target_agent_id=target_agent_id,
|
|
446
|
-
error_msg=error_msg,
|
|
447
|
-
)
|
|
448
428
|
raise Exception(error_msg)
|
|
449
429
|
|
|
450
430
|
|
|
@@ -673,3 +653,27 @@ def _get_field_type(field_schema: Dict[str, Any], nested_models: Dict[str, Type[
|
|
|
673
653
|
else:
|
|
674
654
|
return Union[tuple(types)]
|
|
675
655
|
raise ValueError(f"Unable to convert pydantic field schema to type: {field_schema}")
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def extract_send_message_from_steps_messages(
|
|
659
|
+
steps_messages: List[List[Message]],
|
|
660
|
+
agent_send_message_tool_name: str = DEFAULT_MESSAGE_TOOL,
|
|
661
|
+
agent_send_message_tool_kwarg: str = DEFAULT_MESSAGE_TOOL_KWARG,
|
|
662
|
+
logger: Optional[logging.Logger] = None,
|
|
663
|
+
) -> List[str]:
|
|
664
|
+
extracted_messages = []
|
|
665
|
+
|
|
666
|
+
for step in steps_messages:
|
|
667
|
+
for message in step:
|
|
668
|
+
if message.tool_calls:
|
|
669
|
+
for tool_call in message.tool_calls:
|
|
670
|
+
if tool_call.function.name == agent_send_message_tool_name:
|
|
671
|
+
try:
|
|
672
|
+
# Parse arguments to extract the "message" field
|
|
673
|
+
arguments = json.loads(tool_call.function.arguments)
|
|
674
|
+
if agent_send_message_tool_kwarg in arguments:
|
|
675
|
+
extracted_messages.append(arguments[agent_send_message_tool_kwarg])
|
|
676
|
+
except json.JSONDecodeError:
|
|
677
|
+
logger.error(f"Failed to parse arguments for tool call: {tool_call.id}")
|
|
678
|
+
|
|
679
|
+
return extracted_messages
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from letta import system
|
|
2
|
+
from letta.schemas.enums import MessageRole
|
|
3
|
+
from letta.schemas.letta_message_content import TextContent
|
|
4
|
+
from letta.schemas.message import Message, MessageCreate
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def prepare_input_message_create(
|
|
8
|
+
message: MessageCreate,
|
|
9
|
+
agent_id: str,
|
|
10
|
+
wrap_user_message: bool = True,
|
|
11
|
+
wrap_system_message: bool = True,
|
|
12
|
+
) -> Message:
|
|
13
|
+
"""Converts a MessageCreate object into a Message object, applying wrapping if needed."""
|
|
14
|
+
# TODO: This seems like extra boilerplate with little benefit
|
|
15
|
+
assert isinstance(message, MessageCreate)
|
|
16
|
+
|
|
17
|
+
# Extract message content
|
|
18
|
+
if isinstance(message.content, str):
|
|
19
|
+
message_content = message.content
|
|
20
|
+
elif message.content and len(message.content) > 0 and isinstance(message.content[0], TextContent):
|
|
21
|
+
message_content = message.content[0].text
|
|
22
|
+
else:
|
|
23
|
+
raise ValueError("Message content is empty or invalid")
|
|
24
|
+
|
|
25
|
+
# Apply wrapping if needed
|
|
26
|
+
if message.role == MessageRole.user and wrap_user_message:
|
|
27
|
+
message_content = system.package_user_message(user_message=message_content)
|
|
28
|
+
elif message.role == MessageRole.system and wrap_system_message:
|
|
29
|
+
message_content = system.package_system_message(system_message=message_content)
|
|
30
|
+
elif message.role not in {MessageRole.user, MessageRole.system}:
|
|
31
|
+
raise ValueError(f"Invalid message role: {message.role}")
|
|
32
|
+
|
|
33
|
+
return Message(
|
|
34
|
+
agent_id=agent_id,
|
|
35
|
+
role=message.role,
|
|
36
|
+
content=[TextContent(text=message_content)] if message_content else [],
|
|
37
|
+
name=message.name,
|
|
38
|
+
model=None, # assigned later?
|
|
39
|
+
tool_calls=None, # irrelevant
|
|
40
|
+
tool_call_id=None,
|
|
41
|
+
)
|