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/server/server.py CHANGED
@@ -90,7 +90,7 @@ from letta.services.provider_manager import ProviderManager
90
90
  from letta.services.sandbox_config_manager import SandboxConfigManager
91
91
  from letta.services.source_manager import SourceManager
92
92
  from letta.services.step_manager import StepManager
93
- from letta.services.tool_execution_sandbox import ToolExecutionSandbox
93
+ from letta.services.tool_executor.tool_execution_sandbox import ToolExecutionSandbox
94
94
  from letta.services.tool_manager import ToolManager
95
95
  from letta.services.user_manager import UserManager
96
96
  from letta.settings import model_settings, settings, tool_settings
@@ -367,6 +367,9 @@ class SyncServer(Server):
367
367
  def load_multi_agent(
368
368
  self, group: Group, actor: User, interface: Union[AgentInterface, None] = None, agent_state: Optional[AgentState] = None
369
369
  ) -> Agent:
370
+ if len(group.agent_ids) == 0:
371
+ raise ValueError("Empty group: group must have at least one agent")
372
+
370
373
  match group.manager_type:
371
374
  case ManagerType.round_robin:
372
375
  agent_state = agent_state or self.agent_manager.get_agent_by_id(agent_id=group.agent_ids[0], actor=actor)
@@ -862,6 +865,7 @@ class SyncServer(Server):
862
865
  after: Optional[str] = None,
863
866
  before: Optional[str] = None,
864
867
  limit: Optional[int] = 100,
868
+ group_id: Optional[str] = None,
865
869
  reverse: Optional[bool] = False,
866
870
  return_message_object: bool = True,
867
871
  use_assistant_message: bool = True,
@@ -879,6 +883,7 @@ class SyncServer(Server):
879
883
  before=before,
880
884
  limit=limit,
881
885
  ascending=not reverse,
886
+ group_id=group_id,
882
887
  )
883
888
 
884
889
  if not return_message_object:
@@ -1591,88 +1596,76 @@ class SyncServer(Server):
1591
1596
  ) -> Union[StreamingResponse, LettaResponse]:
1592
1597
  include_final_message = True
1593
1598
  if not stream_steps and stream_tokens:
1594
- raise HTTPException(status_code=400, detail="stream_steps must be 'true' if stream_tokens is 'true'")
1595
-
1596
- try:
1597
- # fetch the group
1598
- group = self.group_manager.retrieve_group(group_id=group_id, actor=actor)
1599
- letta_multi_agent = self.load_multi_agent(group=group, actor=actor)
1600
-
1601
- llm_config = letta_multi_agent.agent_state.llm_config
1602
- supports_token_streaming = ["openai", "anthropic", "deepseek"]
1603
- if stream_tokens and (
1604
- llm_config.model_endpoint_type not in supports_token_streaming or "inference.memgpt.ai" in llm_config.model_endpoint
1605
- ):
1606
- warnings.warn(
1607
- f"Token streaming is only supported for models with type {' or '.join(supports_token_streaming)} in the model_endpoint: agent has endpoint type {llm_config.model_endpoint_type} and {llm_config.model_endpoint}. Setting stream_tokens to False."
1608
- )
1609
- stream_tokens = False
1610
-
1611
- # Create a new interface per request
1612
- letta_multi_agent.interface = StreamingServerInterface(
1613
- use_assistant_message=use_assistant_message,
1614
- assistant_message_tool_name=assistant_message_tool_name,
1615
- assistant_message_tool_kwarg=assistant_message_tool_kwarg,
1616
- inner_thoughts_in_kwargs=(
1617
- llm_config.put_inner_thoughts_in_kwargs if llm_config.put_inner_thoughts_in_kwargs is not None else False
1618
- ),
1599
+ raise ValueError("stream_steps must be 'true' if stream_tokens is 'true'")
1600
+
1601
+ group = self.group_manager.retrieve_group(group_id=group_id, actor=actor)
1602
+ letta_multi_agent = self.load_multi_agent(group=group, actor=actor)
1603
+
1604
+ llm_config = letta_multi_agent.agent_state.llm_config
1605
+ supports_token_streaming = ["openai", "anthropic", "deepseek"]
1606
+ if stream_tokens and (
1607
+ llm_config.model_endpoint_type not in supports_token_streaming or "inference.memgpt.ai" in llm_config.model_endpoint
1608
+ ):
1609
+ warnings.warn(
1610
+ f"Token streaming is only supported for models with type {' or '.join(supports_token_streaming)} in the model_endpoint: agent has endpoint type {llm_config.model_endpoint_type} and {llm_config.model_endpoint}. Setting stream_tokens to False."
1619
1611
  )
1620
- streaming_interface = letta_multi_agent.interface
1621
- if not isinstance(streaming_interface, StreamingServerInterface):
1622
- raise ValueError(f"Agent has wrong type of interface: {type(streaming_interface)}")
1623
- streaming_interface.streaming_mode = stream_tokens
1624
- streaming_interface.streaming_chat_completion_mode = chat_completion_mode
1625
- if metadata and hasattr(streaming_interface, "metadata"):
1626
- streaming_interface.metadata = metadata
1627
-
1628
- streaming_interface.stream_start()
1629
- task = asyncio.create_task(
1630
- asyncio.to_thread(
1631
- letta_multi_agent.step,
1632
- messages=messages,
1633
- chaining=self.chaining,
1634
- max_chaining_steps=self.max_chaining_steps,
1635
- )
1612
+ stream_tokens = False
1613
+
1614
+ # Create a new interface per request
1615
+ letta_multi_agent.interface = StreamingServerInterface(
1616
+ use_assistant_message=use_assistant_message,
1617
+ assistant_message_tool_name=assistant_message_tool_name,
1618
+ assistant_message_tool_kwarg=assistant_message_tool_kwarg,
1619
+ inner_thoughts_in_kwargs=(
1620
+ llm_config.put_inner_thoughts_in_kwargs if llm_config.put_inner_thoughts_in_kwargs is not None else False
1621
+ ),
1622
+ )
1623
+ streaming_interface = letta_multi_agent.interface
1624
+ if not isinstance(streaming_interface, StreamingServerInterface):
1625
+ raise ValueError(f"Agent has wrong type of interface: {type(streaming_interface)}")
1626
+ streaming_interface.streaming_mode = stream_tokens
1627
+ streaming_interface.streaming_chat_completion_mode = chat_completion_mode
1628
+ if metadata and hasattr(streaming_interface, "metadata"):
1629
+ streaming_interface.metadata = metadata
1630
+
1631
+ streaming_interface.stream_start()
1632
+ task = asyncio.create_task(
1633
+ asyncio.to_thread(
1634
+ letta_multi_agent.step,
1635
+ messages=messages,
1636
+ chaining=self.chaining,
1637
+ max_chaining_steps=self.max_chaining_steps,
1636
1638
  )
1639
+ )
1637
1640
 
1638
- if stream_steps:
1639
- # return a stream
1640
- return StreamingResponse(
1641
- sse_async_generator(
1642
- streaming_interface.get_generator(),
1643
- usage_task=task,
1644
- finish_message=include_final_message,
1645
- ),
1646
- media_type="text/event-stream",
1647
- )
1648
-
1649
- else:
1650
- # buffer the stream, then return the list
1651
- generated_stream = []
1652
- async for message in streaming_interface.get_generator():
1653
- assert (
1654
- isinstance(message, LettaMessage)
1655
- or isinstance(message, LegacyLettaMessage)
1656
- or isinstance(message, MessageStreamStatus)
1657
- ), type(message)
1658
- generated_stream.append(message)
1659
- if message == MessageStreamStatus.done:
1660
- break
1641
+ if stream_steps:
1642
+ # return a stream
1643
+ return StreamingResponse(
1644
+ sse_async_generator(
1645
+ streaming_interface.get_generator(),
1646
+ usage_task=task,
1647
+ finish_message=include_final_message,
1648
+ ),
1649
+ media_type="text/event-stream",
1650
+ )
1661
1651
 
1662
- # Get rid of the stream status messages
1663
- filtered_stream = [d for d in generated_stream if not isinstance(d, MessageStreamStatus)]
1664
- usage = await task
1652
+ else:
1653
+ # buffer the stream, then return the list
1654
+ generated_stream = []
1655
+ async for message in streaming_interface.get_generator():
1656
+ assert (
1657
+ isinstance(message, LettaMessage) or isinstance(message, LegacyLettaMessage) or isinstance(message, MessageStreamStatus)
1658
+ ), type(message)
1659
+ generated_stream.append(message)
1660
+ if message == MessageStreamStatus.done:
1661
+ break
1665
1662
 
1666
- # By default the stream will be messages of type LettaMessage or LettaLegacyMessage
1667
- # If we want to convert these to Message, we can use the attached IDs
1668
- # NOTE: we will need to de-duplicate the Messsage IDs though (since Assistant->Inner+Func_Call)
1669
- # TODO: eventually update the interface to use `Message` and `MessageChunk` (new) inside the deque instead
1670
- return LettaResponse(messages=filtered_stream, usage=usage)
1671
- except HTTPException:
1672
- raise
1673
- except Exception as e:
1674
- print(e)
1675
- import traceback
1663
+ # Get rid of the stream status messages
1664
+ filtered_stream = [d for d in generated_stream if not isinstance(d, MessageStreamStatus)]
1665
+ usage = await task
1676
1666
 
1677
- traceback.print_exc()
1678
- raise HTTPException(status_code=500, detail=f"{e}")
1667
+ # By default the stream will be messages of type LettaMessage or LettaLegacyMessage
1668
+ # If we want to convert these to Message, we can use the attached IDs
1669
+ # NOTE: we will need to de-duplicate the Messsage IDs though (since Assistant->Inner+Func_Call)
1670
+ # TODO: eventually update the interface to use `Message` and `MessageChunk` (new) inside the deque instead
1671
+ return LettaResponse(messages=filtered_stream, usage=usage)
letta/server/startup.sh CHANGED
@@ -73,6 +73,6 @@ cleanup() {
73
73
  }
74
74
  trap cleanup EXIT
75
75
 
76
- echo "Starting Letta server at http://$HOST:$PORT..."
76
+ echo "Starting Letta Server at http://$HOST:$PORT..."
77
77
  echo "Executing: $CMD"
78
78
  exec $CMD
@@ -773,6 +773,13 @@ class AgentManager:
773
773
 
774
774
  return agent_state
775
775
 
776
+ @enforce_types
777
+ def refresh_memory(self, agent_state: PydanticAgentState, actor: PydanticUser) -> PydanticAgentState:
778
+ agent_state.memory.blocks = self.block_manager.get_all_blocks_by_ids(
779
+ block_ids=[b.id for b in agent_state.memory.blocks], actor=actor
780
+ )
781
+ return agent_state
782
+
776
783
  # ======================================================================================================================
777
784
  # Source Management
778
785
  # ======================================================================================================================
@@ -7,7 +7,9 @@ from letta.orm.errors import NoResultFound
7
7
  from letta.orm.group import Group as GroupModel
8
8
  from letta.orm.message import Message as MessageModel
9
9
  from letta.schemas.group import Group as PydanticGroup
10
- from letta.schemas.group import GroupCreate, ManagerType
10
+ from letta.schemas.group import GroupCreate, GroupUpdate, ManagerType
11
+ from letta.schemas.letta_message import LettaMessage
12
+ from letta.schemas.message import Message as PydanticMessage
11
13
  from letta.schemas.user import User as PydanticUser
12
14
  from letta.utils import enforce_types
13
15
 
@@ -22,12 +24,12 @@ class GroupManager:
22
24
  @enforce_types
23
25
  def list_groups(
24
26
  self,
27
+ actor: PydanticUser,
25
28
  project_id: Optional[str] = None,
26
29
  manager_type: Optional[ManagerType] = None,
27
30
  before: Optional[str] = None,
28
31
  after: Optional[str] = None,
29
32
  limit: Optional[int] = 50,
30
- actor: PydanticUser = None,
31
33
  ) -> list[PydanticGroup]:
32
34
  with self.session_maker() as session:
33
35
  filters = {"organization_id": actor.organization_id}
@@ -56,26 +58,65 @@ class GroupManager:
56
58
  new_group = GroupModel()
57
59
  new_group.organization_id = actor.organization_id
58
60
  new_group.description = group.description
61
+
62
+ match group.manager_config.manager_type:
63
+ case ManagerType.round_robin:
64
+ new_group.manager_type = ManagerType.round_robin
65
+ new_group.max_turns = group.manager_config.max_turns
66
+ case ManagerType.dynamic:
67
+ new_group.manager_type = ManagerType.dynamic
68
+ new_group.manager_agent_id = group.manager_config.manager_agent_id
69
+ new_group.max_turns = group.manager_config.max_turns
70
+ new_group.termination_token = group.manager_config.termination_token
71
+ case ManagerType.supervisor:
72
+ new_group.manager_type = ManagerType.supervisor
73
+ new_group.manager_agent_id = group.manager_config.manager_agent_id
74
+ case _:
75
+ raise ValueError(f"Unsupported manager type: {group.manager_config.manager_type}")
76
+
59
77
  self._process_agent_relationship(session=session, group=new_group, agent_ids=group.agent_ids, allow_partial=False)
60
- if group.manager_config is None:
61
- new_group.manager_type = ManagerType.round_robin
62
- else:
63
- match group.manager_config.manager_type:
78
+
79
+ new_group.create(session, actor=actor)
80
+ return new_group.to_pydantic()
81
+
82
+ @enforce_types
83
+ def modify_group(self, group_id: str, group_update: GroupUpdate, actor: PydanticUser) -> PydanticGroup:
84
+ with self.session_maker() as session:
85
+ group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
86
+
87
+ max_turns = None
88
+ termination_token = None
89
+ manager_agent_id = None
90
+ if group_update.manager_config:
91
+ if group_update.manager_config.manager_type != group.manager_type:
92
+ raise ValueError(f"Cannot change group pattern after creation")
93
+ match group_update.manager_config.manager_type:
64
94
  case ManagerType.round_robin:
65
- new_group.manager_type = ManagerType.round_robin
66
- new_group.max_turns = group.manager_config.max_turns
95
+ max_turns = group_update.manager_config.max_turns
67
96
  case ManagerType.dynamic:
68
- new_group.manager_type = ManagerType.dynamic
69
- new_group.manager_agent_id = group.manager_config.manager_agent_id
70
- new_group.max_turns = group.manager_config.max_turns
71
- new_group.termination_token = group.manager_config.termination_token
97
+ manager_agent_id = group_update.manager_config.manager_agent_id
98
+ max_turns = group_update.manager_config.max_turns
99
+ termination_token = group_update.manager_config.termination_token
72
100
  case ManagerType.supervisor:
73
- new_group.manager_type = ManagerType.supervisor
74
- new_group.manager_agent_id = group.manager_config.manager_agent_id
101
+ manager_agent_id = group_update.manager_config.manager_agent_id
75
102
  case _:
76
- raise ValueError(f"Unsupported manager type: {group.manager_config.manager_type}")
77
- new_group.create(session, actor=actor)
78
- return new_group.to_pydantic()
103
+ raise ValueError(f"Unsupported manager type: {group_update.manager_config.manager_type}")
104
+
105
+ if max_turns:
106
+ group.max_turns = max_turns
107
+ if termination_token:
108
+ group.termination_token = termination_token
109
+ if manager_agent_id:
110
+ group.manager_agent_id = manager_agent_id
111
+ if group_update.description:
112
+ group.description = group_update.description
113
+ if group_update.agent_ids:
114
+ self._process_agent_relationship(
115
+ session=session, group=group, agent_ids=group_update.agent_ids, allow_partial=False, replace=True
116
+ )
117
+
118
+ group.update(session, actor=actor)
119
+ return group.to_pydantic()
79
120
 
80
121
  @enforce_types
81
122
  def delete_group(self, group_id: str, actor: PydanticUser) -> None:
@@ -87,23 +128,19 @@ class GroupManager:
87
128
  @enforce_types
88
129
  def list_group_messages(
89
130
  self,
131
+ actor: PydanticUser,
90
132
  group_id: Optional[str] = None,
91
133
  before: Optional[str] = None,
92
134
  after: Optional[str] = None,
93
135
  limit: Optional[int] = 50,
94
- actor: PydanticUser = None,
95
136
  use_assistant_message: bool = True,
96
137
  assistant_message_tool_name: str = "send_message",
97
138
  assistant_message_tool_kwarg: str = "message",
98
- ) -> list[PydanticGroup]:
139
+ ) -> list[LettaMessage]:
99
140
  with self.session_maker() as session:
100
- group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
101
- agent_id = group.manager_agent_id if group.manager_agent_id else group.agent_ids[0]
102
-
103
141
  filters = {
104
142
  "organization_id": actor.organization_id,
105
143
  "group_id": group_id,
106
- "agent_id": agent_id,
107
144
  }
108
145
  messages = MessageModel.list(
109
146
  db_session=session,
@@ -114,21 +151,39 @@ class GroupManager:
114
151
  )
115
152
 
116
153
  messages = PydanticMessage.to_letta_messages_from_list(
117
- messages=messages,
154
+ messages=[msg.to_pydantic() for msg in messages],
118
155
  use_assistant_message=use_assistant_message,
119
156
  assistant_message_tool_name=assistant_message_tool_name,
120
157
  assistant_message_tool_kwarg=assistant_message_tool_kwarg,
121
158
  )
122
159
 
160
+ # TODO: filter messages to return a clean conversation history
161
+
123
162
  return messages
124
163
 
164
+ @enforce_types
165
+ def reset_messages(self, group_id: str, actor: PydanticUser) -> None:
166
+ with self.session_maker() as session:
167
+ # Ensure group is loadable by user
168
+ group = GroupModel.read(db_session=session, identifier=group_id, actor=actor)
169
+
170
+ # Delete all messages in the group
171
+ session.query(MessageModel).filter(
172
+ MessageModel.organization_id == actor.organization_id, MessageModel.group_id == group_id
173
+ ).delete(synchronize_session=False)
174
+
175
+ session.commit()
176
+
125
177
  def _process_agent_relationship(self, session: Session, group: GroupModel, agent_ids: List[str], allow_partial=False, replace=True):
126
- current_relationship = getattr(group, "agents", [])
127
178
  if not agent_ids:
128
179
  if replace:
129
180
  setattr(group, "agents", [])
181
+ setattr(group, "agent_ids", [])
130
182
  return
131
183
 
184
+ if group.manager_type == ManagerType.dynamic and len(agent_ids) != len(set(agent_ids)):
185
+ raise ValueError("Duplicate agent ids found in list")
186
+
132
187
  # Retrieve models for the provided IDs
133
188
  found_items = session.query(AgentModel).filter(AgentModel.id.in_(agent_ids)).all()
134
189
 
@@ -137,11 +192,14 @@ class GroupManager:
137
192
  missing = set(agent_ids) - {item.id for item in found_items}
138
193
  raise NoResultFound(f"Items not found in agents: {missing}")
139
194
 
195
+ if group.manager_type == ManagerType.dynamic:
196
+ names = [item.name for item in found_items]
197
+ if len(names) != len(set(names)):
198
+ raise ValueError("Duplicate agent names found in the provided agent IDs.")
199
+
140
200
  if replace:
141
201
  # Replace the relationship
142
202
  setattr(group, "agents", found_items)
203
+ setattr(group, "agent_ids", agent_ids)
143
204
  else:
144
- # Extend the relationship (only add new items)
145
- current_ids = {item.id for item in current_relationship}
146
- new_items = [item for item in found_items if item.id not in current_ids]
147
- current_relationship.extend(new_items)
205
+ raise ValueError("Extend relationship is not supported for groups.")
@@ -264,6 +264,7 @@ class MessageManager:
264
264
  roles: Optional[Sequence[MessageRole]] = None,
265
265
  limit: Optional[int] = 50,
266
266
  ascending: bool = True,
267
+ group_id: Optional[str] = None,
267
268
  ) -> List[PydanticMessage]:
268
269
  """
269
270
  Most performant query to list messages for an agent by directly querying the Message table.
@@ -296,6 +297,10 @@ class MessageManager:
296
297
  # Build a query that directly filters the Message table by agent_id.
297
298
  query = session.query(MessageModel).filter(MessageModel.agent_id == agent_id)
298
299
 
300
+ # If group_id is provided, filter messages by group_id.
301
+ if group_id:
302
+ query = query.filter(MessageModel.group_id == group_id)
303
+
299
304
  # If query_text is provided, filter messages using subquery.
300
305
  if query_text:
301
306
  content_element = func.json_array_elements(MessageModel.content).alias("content_element")