letta-nightly 0.6.45.dev20250328104141__py3-none-any.whl → 0.6.46.dev20250330050944__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 (48) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +25 -8
  3. letta/agents/base_agent.py +6 -5
  4. letta/agents/letta_agent.py +323 -0
  5. letta/agents/voice_agent.py +4 -3
  6. letta/client/client.py +2 -0
  7. letta/dynamic_multi_agent.py +5 -5
  8. letta/errors.py +20 -0
  9. letta/helpers/tool_execution_helper.py +1 -1
  10. letta/helpers/tool_rule_solver.py +1 -1
  11. letta/llm_api/anthropic.py +2 -0
  12. letta/llm_api/anthropic_client.py +153 -167
  13. letta/llm_api/google_ai_client.py +112 -29
  14. letta/llm_api/llm_api_tools.py +5 -0
  15. letta/llm_api/llm_client.py +6 -7
  16. letta/llm_api/llm_client_base.py +38 -17
  17. letta/llm_api/openai.py +2 -0
  18. letta/orm/group.py +2 -5
  19. letta/round_robin_multi_agent.py +18 -7
  20. letta/schemas/group.py +6 -0
  21. letta/schemas/message.py +23 -14
  22. letta/schemas/openai/chat_completion_request.py +6 -1
  23. letta/schemas/providers.py +3 -3
  24. letta/serialize_schemas/marshmallow_agent.py +34 -10
  25. letta/serialize_schemas/pydantic_agent_schema.py +23 -3
  26. letta/server/rest_api/app.py +9 -0
  27. letta/server/rest_api/interface.py +25 -2
  28. letta/server/rest_api/optimistic_json_parser.py +1 -1
  29. letta/server/rest_api/routers/v1/agents.py +57 -23
  30. letta/server/rest_api/routers/v1/groups.py +72 -49
  31. letta/server/rest_api/routers/v1/sources.py +1 -0
  32. letta/server/rest_api/utils.py +0 -1
  33. letta/server/server.py +73 -80
  34. letta/server/startup.sh +1 -1
  35. letta/services/agent_manager.py +7 -0
  36. letta/services/group_manager.py +87 -29
  37. letta/services/message_manager.py +5 -0
  38. letta/services/tool_executor/async_tool_execution_sandbox.py +397 -0
  39. letta/services/tool_executor/tool_execution_manager.py +27 -0
  40. letta/services/{tool_execution_sandbox.py → tool_executor/tool_execution_sandbox.py} +40 -12
  41. letta/services/tool_executor/tool_executor.py +23 -6
  42. letta/settings.py +17 -1
  43. letta/supervisor_multi_agent.py +3 -1
  44. {letta_nightly-0.6.45.dev20250328104141.dist-info → letta_nightly-0.6.46.dev20250330050944.dist-info}/METADATA +1 -1
  45. {letta_nightly-0.6.45.dev20250328104141.dist-info → letta_nightly-0.6.46.dev20250330050944.dist-info}/RECORD +48 -46
  46. {letta_nightly-0.6.45.dev20250328104141.dist-info → letta_nightly-0.6.46.dev20250330050944.dist-info}/LICENSE +0 -0
  47. {letta_nightly-0.6.45.dev20250328104141.dist-info → letta_nightly-0.6.46.dev20250330050944.dist-info}/WHEEL +0 -0
  48. {letta_nightly-0.6.45.dev20250328104141.dist-info → letta_nightly-0.6.46.dev20250330050944.dist-info}/entry_points.txt +0 -0
letta/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.6.45"
1
+ __version__ = "0.6.46"
2
2
 
3
3
  # import clients
4
4
  from letta.client.client import LocalClient, RESTClient, create_client
letta/agent.py CHANGED
@@ -58,7 +58,7 @@ from letta.services.message_manager import MessageManager
58
58
  from letta.services.passage_manager import PassageManager
59
59
  from letta.services.provider_manager import ProviderManager
60
60
  from letta.services.step_manager import StepManager
61
- from letta.services.tool_execution_sandbox import ToolExecutionSandbox
61
+ from letta.services.tool_executor.tool_execution_sandbox import ToolExecutionSandbox
62
62
  from letta.services.tool_manager import ToolManager
63
63
  from letta.settings import summarizer_settings
64
64
  from letta.streaming_interface import StreamingRefreshCLIInterface
@@ -220,13 +220,14 @@ class Agent(BaseAgent):
220
220
  messages: List[Message],
221
221
  tool_returns: Optional[List[ToolReturn]] = None,
222
222
  include_function_failed_message: bool = False,
223
+ group_id: Optional[str] = None,
223
224
  ) -> List[Message]:
224
225
  """
225
226
  Handle error from function call response
226
227
  """
227
228
  # Update tool rules
228
229
  self.last_function_response = function_response
229
- self.tool_rules_solver.update_tool_usage(function_name)
230
+ self.tool_rules_solver.register_tool_call(function_name)
230
231
 
231
232
  # Extend conversation with function response
232
233
  function_response = package_function_response(False, error_msg)
@@ -240,7 +241,9 @@ class Agent(BaseAgent):
240
241
  "content": function_response,
241
242
  "tool_call_id": tool_call_id,
242
243
  },
244
+ name=self.agent_state.name,
243
245
  tool_returns=tool_returns,
246
+ group_id=group_id,
244
247
  )
245
248
  messages.append(new_message)
246
249
  self.interface.function_message(f"Error: {error_msg}", msg_obj=new_message)
@@ -302,10 +305,8 @@ class Agent(BaseAgent):
302
305
  log_telemetry(self.logger, "_get_ai_reply create start")
303
306
  # New LLM client flow
304
307
  llm_client = LLMClient.create(
305
- agent_id=self.agent_state.id,
306
308
  llm_config=self.agent_state.llm_config,
307
309
  put_inner_thoughts_first=put_inner_thoughts_first,
308
- actor_id=self.agent_state.created_by_id,
309
310
  )
310
311
 
311
312
  if llm_client and not stream:
@@ -331,6 +332,7 @@ class Agent(BaseAgent):
331
332
  stream=stream,
332
333
  stream_interface=self.interface,
333
334
  put_inner_thoughts_first=put_inner_thoughts_first,
335
+ name=self.agent_state.name,
334
336
  )
335
337
  log_telemetry(self.logger, "_get_ai_reply create finish")
336
338
 
@@ -374,6 +376,7 @@ class Agent(BaseAgent):
374
376
  # and now we want to use it in the creation of the Message object
375
377
  # TODO figure out a cleaner way to do this
376
378
  response_message_id: Optional[str] = None,
379
+ group_id: Optional[str] = None,
377
380
  ) -> Tuple[List[Message], bool, bool]:
378
381
  """Handles parsing and function execution"""
379
382
  log_telemetry(self.logger, "_handle_ai_response start")
@@ -419,6 +422,8 @@ class Agent(BaseAgent):
419
422
  user_id=self.agent_state.created_by_id,
420
423
  model=self.model,
421
424
  openai_message_dict=response_message.model_dump(),
425
+ name=self.agent_state.name,
426
+ group_id=group_id,
422
427
  )
423
428
  ) # extend conversation with assistant's reply
424
429
  self.logger.debug(f"Function call message: {messages[-1]}")
@@ -451,7 +456,7 @@ class Agent(BaseAgent):
451
456
  error_msg = f"No function named {function_name}"
452
457
  function_response = "None" # more like "never ran?"
453
458
  messages = self._handle_function_error_response(
454
- error_msg, tool_call_id, function_name, function_args, function_response, messages
459
+ error_msg, tool_call_id, function_name, function_args, function_response, messages, group_id=group_id
455
460
  )
456
461
  return messages, False, True # force a heartbeat to allow agent to handle error
457
462
 
@@ -466,7 +471,7 @@ class Agent(BaseAgent):
466
471
  error_msg = f"Error parsing JSON for function '{function_name}' arguments: {function_call.arguments}"
467
472
  function_response = "None" # more like "never ran?"
468
473
  messages = self._handle_function_error_response(
469
- error_msg, tool_call_id, function_name, function_args, function_response, messages
474
+ error_msg, tool_call_id, function_name, function_args, function_response, messages, group_id=group_id
470
475
  )
471
476
  return messages, False, True # force a heartbeat to allow agent to handle error
472
477
 
@@ -537,6 +542,7 @@ class Agent(BaseAgent):
537
542
  function_response,
538
543
  messages,
539
544
  [tool_return],
545
+ group_id=group_id,
540
546
  )
541
547
  return messages, False, True # force a heartbeat to allow agent to handle error
542
548
 
@@ -573,6 +579,7 @@ class Agent(BaseAgent):
573
579
  messages,
574
580
  [ToolReturn(status="error", stderr=[error_msg_user])],
575
581
  include_function_failed_message=True,
582
+ group_id=group_id,
576
583
  )
577
584
  return messages, False, True # force a heartbeat to allow agent to handle error
578
585
 
@@ -597,6 +604,7 @@ class Agent(BaseAgent):
597
604
  messages,
598
605
  [tool_return],
599
606
  include_function_failed_message=True,
607
+ group_id=group_id,
600
608
  )
601
609
  return messages, False, True # force a heartbeat to allow agent to handle error
602
610
 
@@ -622,7 +630,9 @@ class Agent(BaseAgent):
622
630
  "content": function_response,
623
631
  "tool_call_id": tool_call_id,
624
632
  },
633
+ name=self.agent_state.name,
625
634
  tool_returns=[tool_return] if sandbox_run_result else None,
635
+ group_id=group_id,
626
636
  )
627
637
  ) # extend conversation with function response
628
638
  self.interface.function_message(f"Ran {function_name}({function_args})", msg_obj=messages[-1])
@@ -638,6 +648,8 @@ class Agent(BaseAgent):
638
648
  user_id=self.agent_state.created_by_id,
639
649
  model=self.model,
640
650
  openai_message_dict=response_message.model_dump(),
651
+ name=self.agent_state.name,
652
+ group_id=group_id,
641
653
  )
642
654
  ) # extend conversation with assistant's reply
643
655
  self.interface.internal_monologue(response_message.content, msg_obj=messages[-1])
@@ -649,7 +661,7 @@ class Agent(BaseAgent):
649
661
  self.agent_state = self.agent_manager.rebuild_system_prompt(agent_id=self.agent_state.id, actor=self.user)
650
662
 
651
663
  # Update ToolRulesSolver state with last called function
652
- self.tool_rules_solver.update_tool_usage(function_name)
664
+ self.tool_rules_solver.register_tool_call(function_name)
653
665
  # Update heartbeat request according to provided tool rules
654
666
  if self.tool_rules_solver.has_children_tools(function_name):
655
667
  heartbeat_request = True
@@ -801,7 +813,11 @@ class Agent(BaseAgent):
801
813
  in_context_messages = self.agent_manager.get_in_context_messages(agent_id=self.agent_state.id, actor=self.user)
802
814
  input_message_sequence = in_context_messages + messages
803
815
 
804
- if len(input_message_sequence) > 1 and input_message_sequence[-1].role != "user":
816
+ if (
817
+ len(input_message_sequence) > 1
818
+ and input_message_sequence[-1].role != "user"
819
+ and input_message_sequence[-1].group_id is None
820
+ ):
805
821
  self.logger.warning(f"{CLI_WARNING_PREFIX}Attempting to run ChatCompletion without user as the last message in the queue")
806
822
 
807
823
  # Step 2: send the conversation and available functions to the LLM
@@ -834,6 +850,7 @@ class Agent(BaseAgent):
834
850
  # TODO this is kind of hacky, find a better way to handle this
835
851
  # the only time we set up message creation ahead of time is when streaming is on
836
852
  response_message_id=response.id if stream else None,
853
+ group_id=input_message_sequence[-1].group_id,
837
854
  )
838
855
 
839
856
  # Step 6: extend the message history
@@ -1,10 +1,10 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Any, AsyncGenerator, List
2
+ from typing import Any, AsyncGenerator, Optional
3
3
 
4
4
  import openai
5
5
 
6
6
  from letta.schemas.letta_message import UserMessage
7
- from letta.schemas.message import Message
7
+ from letta.schemas.letta_response import LettaResponse
8
8
  from letta.schemas.user import User
9
9
  from letta.services.agent_manager import AgentManager
10
10
  from letta.services.message_manager import MessageManager
@@ -19,7 +19,8 @@ class BaseAgent(ABC):
19
19
  def __init__(
20
20
  self,
21
21
  agent_id: str,
22
- openai_client: openai.AsyncClient,
22
+ # TODO: Make required once client refactor hits
23
+ openai_client: Optional[openai.AsyncClient],
23
24
  message_manager: MessageManager,
24
25
  agent_manager: AgentManager,
25
26
  actor: User,
@@ -31,14 +32,14 @@ class BaseAgent(ABC):
31
32
  self.actor = actor
32
33
 
33
34
  @abstractmethod
34
- async def step(self, input_message: UserMessage) -> List[Message]:
35
+ async def step(self, input_message: UserMessage, max_steps: int = 10) -> LettaResponse:
35
36
  """
36
37
  Main execution loop for the agent.
37
38
  """
38
39
  raise NotImplementedError
39
40
 
40
41
  @abstractmethod
41
- async def step_stream(self, input_message: UserMessage) -> AsyncGenerator[str, None]:
42
+ async def step_stream(self, input_message: UserMessage, max_steps: int = 10) -> AsyncGenerator[str, None]:
42
43
  """
43
44
  Main async execution loop for the agent. Implementations must yield messages as SSE events.
44
45
  """
@@ -0,0 +1,323 @@
1
+ import asyncio
2
+ import json
3
+ import uuid
4
+ from typing import Any, AsyncGenerator, Dict, List, Tuple
5
+
6
+ from openai import AsyncStream
7
+ from openai.types.chat import ChatCompletion, ChatCompletionChunk
8
+
9
+ from letta.agents.base_agent import BaseAgent
10
+ from letta.constants import DEFAULT_MESSAGE_TOOL
11
+ from letta.helpers import ToolRulesSolver
12
+ from letta.helpers.datetime_helpers import get_utc_time
13
+ from letta.helpers.tool_execution_helper import enable_strict_mode
14
+ from letta.llm_api.llm_client import LLMClient
15
+ from letta.log import get_logger
16
+ from letta.orm.enums import ToolType
17
+ from letta.schemas.agent import AgentState
18
+ from letta.schemas.letta_message import AssistantMessage
19
+ from letta.schemas.letta_response import LettaResponse
20
+ from letta.schemas.message import Message, MessageUpdate
21
+ from letta.schemas.openai.chat_completion_request import UserMessage
22
+ from letta.schemas.usage import LettaUsageStatistics
23
+ from letta.schemas.user import User
24
+ from letta.server.rest_api.utils import create_tool_call_messages_from_openai_response, create_user_message
25
+ from letta.services.agent_manager import AgentManager
26
+ from letta.services.block_manager import BlockManager
27
+ from letta.services.helpers.agent_manager_helper import compile_system_message
28
+ from letta.services.message_manager import MessageManager
29
+ from letta.services.passage_manager import PassageManager
30
+ from letta.services.tool_executor.tool_execution_manager import ToolExecutionManager
31
+ from letta.tracing import log_event, trace_method
32
+ from letta.utils import united_diff
33
+
34
+ logger = get_logger(__name__)
35
+
36
+
37
+ class LettaAgent(BaseAgent):
38
+
39
+ def __init__(
40
+ self,
41
+ agent_id: str,
42
+ message_manager: MessageManager,
43
+ agent_manager: AgentManager,
44
+ block_manager: BlockManager,
45
+ passage_manager: PassageManager,
46
+ actor: User,
47
+ use_assistant_message: bool = True,
48
+ ):
49
+ super().__init__(agent_id=agent_id, openai_client=None, message_manager=message_manager, agent_manager=agent_manager, actor=actor)
50
+
51
+ # TODO: Make this more general, factorable
52
+ # Summarizer settings
53
+ self.block_manager = block_manager
54
+ self.passage_manager = passage_manager
55
+ self.use_assistant_message = use_assistant_message
56
+
57
+ @trace_method
58
+ async def step(self, input_message: UserMessage, max_steps: int = 10) -> LettaResponse:
59
+ input_message = self.pre_process_input_message(input_message)
60
+ agent_state = self.agent_manager.get_agent_by_id(self.agent_id, actor=self.actor)
61
+ # TODO: Extend to beyond just system message
62
+ system_message = [self.message_manager.get_messages_by_ids(message_ids=agent_state.message_ids, actor=self.actor)[0]]
63
+ persisted_letta_messages = self.message_manager.create_many_messages(
64
+ [create_user_message(input_message=input_message, agent_id=agent_state.id, actor=self.actor)], actor=self.actor
65
+ )
66
+ tool_rules_solver = ToolRulesSolver(agent_state.tool_rules)
67
+
68
+ # TODO: Note that we do absolutely 0 pulling in of in-context messages here
69
+ # TODO: This is specific to B, and needs to be changed
70
+ for step in range(max_steps):
71
+ response = await self._get_ai_reply(
72
+ in_context_messages=system_message + persisted_letta_messages,
73
+ agent_state=agent_state,
74
+ tool_rules_solver=tool_rules_solver,
75
+ )
76
+ persisted_messages, should_continue = await self._handle_ai_response(response, agent_state, tool_rules_solver)
77
+ persisted_letta_messages.extend(persisted_messages)
78
+
79
+ if not should_continue:
80
+ break
81
+
82
+ # Persist messages
83
+ # Translate to letta response messages
84
+ response_messages = []
85
+ for message in persisted_letta_messages:
86
+ response_messages += message.to_letta_message(use_assistant_message=self.use_assistant_message)
87
+
88
+ return LettaResponse(
89
+ messages=response_messages,
90
+ # TODO: Actually populate this
91
+ usage=LettaUsageStatistics(),
92
+ )
93
+
94
+ async def step_stream(self, input_message: UserMessage, max_steps: int = 10) -> AsyncGenerator[str, None]:
95
+ """
96
+ Main streaming loop that yields partial tokens.
97
+ Whenever we detect a tool call, we yield from _handle_ai_response as well.
98
+ """
99
+ raise NotImplementedError("Not implemented for letta agent")
100
+
101
+ @trace_method
102
+ async def _get_ai_reply(
103
+ self,
104
+ in_context_messages: List[Message],
105
+ agent_state: AgentState,
106
+ tool_rules_solver: ToolRulesSolver,
107
+ ) -> ChatCompletion | AsyncStream[ChatCompletionChunk]:
108
+ in_context_messages = self._rebuild_memory(in_context_messages, agent_state)
109
+
110
+ tools = [
111
+ t
112
+ for t in agent_state.tools
113
+ if t.tool_type in {ToolType.CUSTOM}
114
+ or (t.tool_type == ToolType.LETTA_CORE and t.name == DEFAULT_MESSAGE_TOOL)
115
+ or (t.tool_type == ToolType.LETTA_MULTI_AGENT_CORE and t.name == "send_message_to_agents_matching_tags")
116
+ ]
117
+
118
+ valid_tool_names = set(tool_rules_solver.get_allowed_tool_names(available_tools=set([t.name for t in tools])))
119
+ allowed_tools = [enable_strict_mode(t.json_schema) for t in tools if t.name in valid_tool_names]
120
+
121
+ llm_client = LLMClient.create(
122
+ llm_config=agent_state.llm_config,
123
+ put_inner_thoughts_first=True,
124
+ )
125
+
126
+ response = await llm_client.send_llm_request_async(
127
+ messages=in_context_messages,
128
+ tools=allowed_tools,
129
+ tool_call=None,
130
+ stream=False,
131
+ )
132
+
133
+ return response
134
+
135
+ @trace_method
136
+ async def _handle_ai_response(
137
+ self,
138
+ chat_completion_response: ChatCompletion,
139
+ agent_state: AgentState,
140
+ tool_rules_solver: ToolRulesSolver,
141
+ ) -> Tuple[List[Message], bool]:
142
+ """
143
+ Now that streaming is done, handle the final AI response.
144
+ This might yield additional SSE tokens if we do stalling.
145
+ At the end, set self._continue_execution accordingly.
146
+ """
147
+ # TODO: Some key assumptions here.
148
+ # TODO: Assume every call has a tool call, i.e. tool_choice is REQUIRED
149
+ tool_call = chat_completion_response.choices[0].message.tool_calls[0]
150
+
151
+ tool_call_name = tool_call.function.name
152
+ tool_call_args_str = tool_call.function.arguments
153
+
154
+ try:
155
+ tool_args = json.loads(tool_call_args_str)
156
+ except json.JSONDecodeError:
157
+ tool_args = {}
158
+
159
+ # Get request heartbeats and coerce to bool
160
+ request_heartbeat = tool_args.pop("request_heartbeat", False)
161
+
162
+ # So this is necessary, because sometimes non-structured outputs makes mistakes
163
+ if not isinstance(request_heartbeat, bool):
164
+ if isinstance(request_heartbeat, str):
165
+ request_heartbeat = request_heartbeat.lower() == "true"
166
+ else:
167
+ request_heartbeat = bool(request_heartbeat)
168
+
169
+ tool_call_id = tool_call.id or f"call_{uuid.uuid4().hex[:8]}"
170
+
171
+ tool_result, success_flag = await self._execute_tool(
172
+ tool_name=tool_call_name,
173
+ tool_args=tool_args,
174
+ agent_state=agent_state,
175
+ )
176
+
177
+ # 4. Register tool call with tool rule solver
178
+ # Resolve whether or not to continue stepping
179
+ continue_stepping = request_heartbeat
180
+ tool_rules_solver.register_tool_call(tool_name=tool_call_name)
181
+ if tool_rules_solver.is_terminal_tool(tool_name=tool_call_name):
182
+ continue_stepping = False
183
+ elif tool_rules_solver.has_children_tools(tool_name=tool_call_name):
184
+ continue_stepping = True
185
+ elif tool_rules_solver.is_continue_tool(tool_name=tool_call_name):
186
+ continue_stepping = True
187
+
188
+ # 5. Persist to DB
189
+ tool_call_messages = create_tool_call_messages_from_openai_response(
190
+ agent_id=agent_state.id,
191
+ model=agent_state.llm_config.model,
192
+ function_name=tool_call_name,
193
+ function_arguments=tool_args,
194
+ tool_call_id=tool_call_id,
195
+ function_call_success=success_flag,
196
+ function_response=tool_result,
197
+ actor=self.actor,
198
+ add_heartbeat_request_system_message=continue_stepping,
199
+ )
200
+ persisted_messages = self.message_manager.create_many_messages(tool_call_messages, actor=self.actor)
201
+
202
+ return persisted_messages, continue_stepping
203
+
204
+ def _rebuild_memory(self, in_context_messages: List[Message], agent_state: AgentState) -> List[Message]:
205
+ self.agent_manager.refresh_memory(agent_state=agent_state, actor=self.actor)
206
+
207
+ # TODO: This is a pretty brittle pattern established all over our code, need to get rid of this
208
+ curr_system_message = in_context_messages[0]
209
+ curr_memory_str = agent_state.memory.compile()
210
+ curr_system_message_text = curr_system_message.content[0].text
211
+ if curr_memory_str in curr_system_message_text:
212
+ # NOTE: could this cause issues if a block is removed? (substring match would still work)
213
+ logger.debug(
214
+ f"Memory hasn't changed for agent id={agent_state.id} and actor=({self.actor.id}, {self.actor.name}), skipping system prompt rebuild"
215
+ )
216
+ return in_context_messages
217
+
218
+ memory_edit_timestamp = get_utc_time()
219
+
220
+ num_messages = self.message_manager.size(actor=self.actor, agent_id=agent_state.id)
221
+ num_archival_memories = self.passage_manager.size(actor=self.actor, agent_id=agent_state.id)
222
+
223
+ new_system_message_str = compile_system_message(
224
+ system_prompt=agent_state.system,
225
+ in_context_memory=agent_state.memory,
226
+ in_context_memory_last_edit=memory_edit_timestamp,
227
+ previous_message_count=num_messages,
228
+ archival_memory_size=num_archival_memories,
229
+ )
230
+
231
+ diff = united_diff(curr_system_message_text, new_system_message_str)
232
+ if len(diff) > 0:
233
+ logger.debug(f"Rebuilding system with new memory...\nDiff:\n{diff}")
234
+
235
+ new_system_message = self.message_manager.update_message_by_id(
236
+ curr_system_message.id, message_update=MessageUpdate(content=new_system_message_str), actor=self.actor
237
+ )
238
+
239
+ # Skip pulling down the agent's memory again to save on a db call
240
+ return [new_system_message] + in_context_messages[1:]
241
+
242
+ else:
243
+ return in_context_messages
244
+
245
+ @trace_method
246
+ async def _execute_tool(self, tool_name: str, tool_args: dict, agent_state: AgentState) -> Tuple[str, bool]:
247
+ """
248
+ Executes a tool and returns (result, success_flag).
249
+ """
250
+ # Special memory case
251
+ target_tool = next((x for x in agent_state.tools if x.name == tool_name), None)
252
+ if not target_tool:
253
+ return f"Tool not found: {tool_name}", False
254
+
255
+ # TODO: This temp. Move this logic and code to executors
256
+ try:
257
+ if target_tool.name == "send_message_to_agents_matching_tags" and target_tool.tool_type == ToolType.LETTA_MULTI_AGENT_CORE:
258
+ log_event(name="start_send_message_to_agents_matching_tags", attributes=tool_args)
259
+ results = await self._send_message_to_agents_matching_tags(**tool_args)
260
+ log_event(name="finish_send_message_to_agents_matching_tags", attributes=tool_args)
261
+ return json.dumps(results), True
262
+ else:
263
+ tool_execution_manager = ToolExecutionManager(agent_state=agent_state, actor=self.actor)
264
+ # TODO: Integrate sandbox result
265
+ log_event(name=f"start_{tool_name}_execution", attributes=tool_args)
266
+ function_response, _ = await tool_execution_manager.execute_tool_async(
267
+ function_name=tool_name, function_args=tool_args, tool=target_tool
268
+ )
269
+ log_event(name=f"finish_{tool_name}_execution", attributes=tool_args)
270
+ return function_response, True
271
+ except Exception as e:
272
+ return f"Failed to call tool. Error: {e}", False
273
+
274
+ @trace_method
275
+ async def _send_message_to_agents_matching_tags(
276
+ self, message: str, match_all: List[str], match_some: List[str]
277
+ ) -> List[Dict[str, Any]]:
278
+ # Find matching agents
279
+ matching_agents = self.agent_manager.list_agents_matching_tags(actor=self.actor, match_all=match_all, match_some=match_some)
280
+ if not matching_agents:
281
+ return []
282
+
283
+ async def process_agent(agent_state: AgentState, message: str) -> Dict[str, Any]:
284
+ try:
285
+ letta_agent = LettaAgent(
286
+ agent_id=agent_state.id,
287
+ message_manager=self.message_manager,
288
+ agent_manager=self.agent_manager,
289
+ block_manager=self.block_manager,
290
+ passage_manager=self.passage_manager,
291
+ actor=self.actor,
292
+ use_assistant_message=True,
293
+ )
294
+
295
+ augmented_message = (
296
+ "[Incoming message from external Letta agent - to reply to this message, "
297
+ "make sure to use the 'send_message' at the end, and the system will notify "
298
+ "the sender of your response] "
299
+ f"{message}"
300
+ )
301
+
302
+ letta_response = await letta_agent.step(UserMessage(content=augmented_message))
303
+ messages = letta_response.messages
304
+
305
+ send_message_content = [message.content for message in messages if isinstance(message, AssistantMessage)]
306
+
307
+ return {
308
+ "agent_id": agent_state.id,
309
+ "agent_name": agent_state.name,
310
+ "response": send_message_content if send_message_content else ["<no response>"],
311
+ }
312
+
313
+ except Exception as e:
314
+ return {
315
+ "agent_id": agent_state.id,
316
+ "agent_name": agent_state.name,
317
+ "error": str(e),
318
+ "type": type(e).__name__,
319
+ }
320
+
321
+ tasks = [asyncio.create_task(process_agent(agent_state=agent_state, message=message)) for agent_state in matching_agents]
322
+ results = await asyncio.gather(*tasks)
323
+ return results
@@ -19,6 +19,7 @@ from letta.log import get_logger
19
19
  from letta.orm.enums import ToolType
20
20
  from letta.schemas.agent import AgentState
21
21
  from letta.schemas.block import BlockUpdate
22
+ from letta.schemas.letta_response import LettaResponse
22
23
  from letta.schemas.message import Message, MessageUpdate
23
24
  from letta.schemas.openai.chat_completion_request import (
24
25
  AssistantMessage,
@@ -92,10 +93,10 @@ class VoiceAgent(BaseAgent):
92
93
  agent_id=agent_id, openai_client=openai_client, message_manager=message_manager, agent_manager=agent_manager, actor=actor
93
94
  )
94
95
 
95
- async def step(self, input_message: UserMessage) -> List[Message]:
96
+ async def step(self, input_message: UserMessage, max_steps: int = 10) -> LettaResponse:
96
97
  raise NotImplementedError("LowLatencyAgent does not have a synchronous step implemented currently.")
97
98
 
98
- async def step_stream(self, input_message: UserMessage) -> AsyncGenerator[str, None]:
99
+ async def step_stream(self, input_message: UserMessage, max_steps: int = 10) -> AsyncGenerator[str, None]:
99
100
  """
100
101
  Main streaming loop that yields partial tokens.
101
102
  Whenever we detect a tool call, we yield from _handle_ai_response as well.
@@ -107,7 +108,7 @@ class VoiceAgent(BaseAgent):
107
108
  in_memory_message_history = [input_message]
108
109
 
109
110
  # TODO: Define max steps here
110
- while True:
111
+ for _ in range(max_steps):
111
112
  # Rebuild memory each loop
112
113
  in_context_messages = self._rebuild_memory(in_context_messages, agent_state)
113
114
  openai_messages = convert_letta_messages_to_openai(in_context_messages)
letta/client/client.py CHANGED
@@ -546,6 +546,7 @@ class RESTClient(AbstractClient):
546
546
  tool_ids: Optional[List[str]] = None,
547
547
  tool_rules: Optional[List[BaseToolRule]] = None,
548
548
  include_base_tools: Optional[bool] = True,
549
+ include_multi_agent_tools: Optional[bool] = False,
549
550
  # metadata
550
551
  metadata: Optional[Dict] = {"human:": DEFAULT_HUMAN, "persona": DEFAULT_PERSONA},
551
552
  description: Optional[str] = None,
@@ -613,6 +614,7 @@ class RESTClient(AbstractClient):
613
614
  "tags": tags,
614
615
  "include_base_tools": include_base_tools,
615
616
  "message_buffer_autoclear": message_buffer_autoclear,
617
+ "include_multi_agent_tools": include_multi_agent_tools,
616
618
  }
617
619
 
618
620
  # Only add name if it's not None
@@ -16,7 +16,7 @@ class DynamicMultiAgent(Agent):
16
16
  self,
17
17
  interface: AgentInterface,
18
18
  agent_state: AgentState,
19
- user: User = None,
19
+ user: User,
20
20
  # custom
21
21
  group_id: str = "",
22
22
  agent_ids: List[str] = [],
@@ -128,7 +128,7 @@ class DynamicMultiAgent(Agent):
128
128
  )
129
129
  for message in assistant_messages
130
130
  ]
131
- message_index[agent_id] = len(chat_history) + len(new_messages)
131
+ message_index[speaker_id] = len(chat_history) + len(new_messages)
132
132
 
133
133
  # sum usage
134
134
  total_usage.prompt_tokens += usage_stats.prompt_tokens
@@ -251,10 +251,10 @@ class DynamicMultiAgent(Agent):
251
251
  chat_history: List[Message],
252
252
  agent_id_options: List[str],
253
253
  ) -> Message:
254
- chat_history = [f"{message.name or 'user'}: {message.content[0].text}" for message in chat_history]
254
+ text_chat_history = [f"{message.name or 'user'}: {message.content[0].text}" for message in chat_history]
255
255
  for message in new_messages:
256
- chat_history.append(f"{message.name or 'user'}: {message.content}")
257
- context_messages = "\n".join(chat_history)
256
+ text_chat_history.append(f"{message.name or 'user'}: {message.content}")
257
+ context_messages = "\n".join(text_chat_history)
258
258
 
259
259
  message_text = (
260
260
  "Choose the most suitable agent to reply to the latest message in the "
letta/errors.py CHANGED
@@ -62,6 +62,26 @@ class LLMError(LettaError):
62
62
  pass
63
63
 
64
64
 
65
+ class LLMConnectionError(LLMError):
66
+ """Error when unable to connect to LLM service"""
67
+
68
+
69
+ class LLMRateLimitError(LLMError):
70
+ """Error when rate limited by LLM service"""
71
+
72
+
73
+ class LLMPermissionDeniedError(LLMError):
74
+ """Error when permission is denied by LLM service"""
75
+
76
+
77
+ class LLMNotFoundError(LLMError):
78
+ """Error when requested resource is not found"""
79
+
80
+
81
+ class LLMUnprocessableEntityError(LLMError):
82
+ """Error when request is well-formed but semantically invalid"""
83
+
84
+
65
85
  class BedrockPermissionError(LettaError):
66
86
  """Exception raised for errors in the Bedrock permission process."""
67
87
 
@@ -10,7 +10,7 @@ from letta.schemas.agent import AgentState
10
10
  from letta.schemas.sandbox_config import SandboxRunResult
11
11
  from letta.schemas.tool import Tool
12
12
  from letta.schemas.user import User
13
- from letta.services.tool_execution_sandbox import ToolExecutionSandbox
13
+ from letta.services.tool_executor.tool_execution_sandbox import ToolExecutionSandbox
14
14
  from letta.utils import get_friendly_error_msg
15
15
 
16
16
 
@@ -62,7 +62,7 @@ class ToolRulesSolver(BaseModel):
62
62
  assert isinstance(rule, MaxCountPerStepToolRule)
63
63
  self.child_based_tool_rules.append(rule)
64
64
 
65
- def update_tool_usage(self, tool_name: str):
65
+ def register_tool_call(self, tool_name: str):
66
66
  """Update the internal state to track tool call history."""
67
67
  self.tool_call_history.append(tool_name)
68
68
 
@@ -859,6 +859,7 @@ def anthropic_chat_completions_process_stream(
859
859
  create_message_id: bool = True,
860
860
  create_message_datetime: bool = True,
861
861
  betas: List[str] = ["tools-2024-04-04"],
862
+ name: Optional[str] = None,
862
863
  ) -> ChatCompletionResponse:
863
864
  """Process a streaming completion response from Anthropic, similar to OpenAI's streaming.
864
865
 
@@ -951,6 +952,7 @@ def anthropic_chat_completions_process_stream(
951
952
  # if extended_thinking is on, then reasoning_content will be flowing as chunks
952
953
  # TODO handle emitting redacted reasoning content (e.g. as concat?)
953
954
  expect_reasoning_content=extended_thinking,
955
+ name=name,
954
956
  )
955
957
  elif isinstance(stream_interface, AgentRefreshStreamingInterface):
956
958
  stream_interface.process_refresh(chat_completion_response)