letta-nightly 0.6.43.dev20250323104014__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.

Files changed (42) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +106 -104
  3. letta/agents/voice_agent.py +1 -1
  4. letta/client/streaming.py +3 -1
  5. letta/functions/function_sets/base.py +2 -1
  6. letta/functions/function_sets/multi_agent.py +51 -40
  7. letta/functions/helpers.py +26 -22
  8. letta/helpers/message_helper.py +41 -0
  9. letta/llm_api/anthropic.py +150 -44
  10. letta/llm_api/aws_bedrock.py +5 -3
  11. letta/llm_api/azure_openai.py +0 -1
  12. letta/llm_api/llm_api_tools.py +4 -0
  13. letta/orm/organization.py +1 -0
  14. letta/orm/sqlalchemy_base.py +2 -4
  15. letta/schemas/agent.py +8 -0
  16. letta/schemas/letta_message.py +8 -4
  17. letta/schemas/llm_config.py +6 -0
  18. letta/schemas/message.py +143 -24
  19. letta/schemas/openai/chat_completion_response.py +5 -0
  20. letta/schemas/organization.py +7 -0
  21. letta/schemas/providers.py +17 -0
  22. letta/schemas/tool.py +5 -1
  23. letta/schemas/usage.py +5 -1
  24. letta/serialize_schemas/pydantic_agent_schema.py +1 -1
  25. letta/server/rest_api/interface.py +44 -7
  26. letta/server/rest_api/routers/v1/agents.py +13 -2
  27. letta/server/rest_api/routers/v1/organizations.py +19 -1
  28. letta/server/rest_api/utils.py +1 -1
  29. letta/server/server.py +49 -70
  30. letta/services/agent_manager.py +6 -2
  31. letta/services/helpers/agent_manager_helper.py +24 -38
  32. letta/services/message_manager.py +7 -6
  33. letta/services/organization_manager.py +13 -0
  34. letta/services/tool_execution_sandbox.py +5 -1
  35. letta/services/tool_executor/__init__.py +0 -0
  36. letta/services/tool_executor/tool_execution_manager.py +74 -0
  37. letta/services/tool_executor/tool_executor.py +380 -0
  38. {letta_nightly-0.6.43.dev20250323104014.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/METADATA +2 -3
  39. {letta_nightly-0.6.43.dev20250323104014.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/RECORD +42 -38
  40. {letta_nightly-0.6.43.dev20250323104014.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/LICENSE +0 -0
  41. {letta_nightly-0.6.43.dev20250323104014.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/WHEEL +0 -0
  42. {letta_nightly-0.6.43.dev20250323104014.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/entry_points.txt +0 -0
letta/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.6.43"
1
+ __version__ = "0.6.44"
2
2
 
3
3
  # import clients
4
4
  from letta.client.client import LocalClient, RESTClient, create_client
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"""
@@ -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.info(f"Rebuilding system with new memory...\nDiff:\n{diff}")
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.assistant_message(message) # , msg_obj=self._messages[-1])
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.utils import log_telemetry
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 agent with ID '{self.agent_state.id}' - to reply to this message, "
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
- # Retrieve up to 100 matching agents
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
- log_telemetry(
116
- self.logger,
117
- "_send_message_to_agents_matching_tags_async listing agents finish",
118
- message=message,
119
- match_all=match_all,
120
- match_some=match_some,
121
- )
122
-
123
- # Create a system message
124
- messages = [MessageCreate(role=MessageRole.system, content=augmented_message, name=self.agent_state.name)]
125
-
126
- result = asyncio.run(_send_message_to_agents_matching_tags_async(self, server, messages, matching_agents))
127
- log_telemetry(
128
- self.logger,
129
- "_send_message_to_agents_matching_tags_async finish",
130
- messages=message,
131
- match_all=match_all,
132
- match_some=match_some,
133
- )
134
- return result
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]:
@@ -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
+ )