letta-nightly 0.6.39.dev20250314104053__py3-none-any.whl → 0.6.40.dev20250314173529__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 (59) hide show
  1. letta/agent.py +13 -3
  2. letta/agents/ephemeral_agent.py +2 -1
  3. letta/agents/low_latency_agent.py +8 -0
  4. letta/dynamic_multi_agent.py +274 -0
  5. letta/functions/function_sets/base.py +1 -0
  6. letta/functions/function_sets/extras.py +2 -1
  7. letta/functions/function_sets/multi_agent.py +17 -0
  8. letta/functions/helpers.py +41 -0
  9. letta/helpers/converters.py +67 -0
  10. letta/helpers/mcp_helpers.py +26 -5
  11. letta/llm_api/openai.py +1 -1
  12. letta/memory.py +2 -1
  13. letta/orm/__init__.py +2 -0
  14. letta/orm/agent.py +69 -20
  15. letta/orm/custom_columns.py +15 -0
  16. letta/orm/group.py +33 -0
  17. letta/orm/groups_agents.py +13 -0
  18. letta/orm/message.py +7 -4
  19. letta/orm/organization.py +1 -0
  20. letta/orm/sqlalchemy_base.py +3 -3
  21. letta/round_robin_multi_agent.py +152 -0
  22. letta/schemas/agent.py +3 -0
  23. letta/schemas/enums.py +0 -4
  24. letta/schemas/group.py +65 -0
  25. letta/schemas/letta_message.py +167 -106
  26. letta/schemas/letta_message_content.py +192 -0
  27. letta/schemas/message.py +28 -36
  28. letta/serialize_schemas/__init__.py +1 -1
  29. letta/serialize_schemas/marshmallow_agent.py +108 -0
  30. letta/serialize_schemas/{agent_environment_variable.py → marshmallow_agent_environment_variable.py} +1 -1
  31. letta/serialize_schemas/marshmallow_base.py +52 -0
  32. letta/serialize_schemas/{block.py → marshmallow_block.py} +1 -1
  33. letta/serialize_schemas/{custom_fields.py → marshmallow_custom_fields.py} +12 -0
  34. letta/serialize_schemas/marshmallow_message.py +42 -0
  35. letta/serialize_schemas/{tag.py → marshmallow_tag.py} +12 -2
  36. letta/serialize_schemas/{tool.py → marshmallow_tool.py} +1 -1
  37. letta/serialize_schemas/pydantic_agent_schema.py +111 -0
  38. letta/server/rest_api/app.py +15 -0
  39. letta/server/rest_api/routers/v1/__init__.py +2 -0
  40. letta/server/rest_api/routers/v1/agents.py +46 -40
  41. letta/server/rest_api/routers/v1/groups.py +233 -0
  42. letta/server/rest_api/routers/v1/tools.py +31 -3
  43. letta/server/rest_api/utils.py +1 -1
  44. letta/server/server.py +267 -12
  45. letta/services/agent_manager.py +65 -28
  46. letta/services/group_manager.py +147 -0
  47. letta/services/helpers/agent_manager_helper.py +151 -1
  48. letta/services/message_manager.py +11 -3
  49. letta/services/passage_manager.py +15 -0
  50. letta/settings.py +5 -0
  51. letta/supervisor_multi_agent.py +103 -0
  52. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/METADATA +1 -2
  53. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/RECORD +56 -46
  54. letta/serialize_schemas/agent.py +0 -80
  55. letta/serialize_schemas/base.py +0 -64
  56. letta/serialize_schemas/message.py +0 -29
  57. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/LICENSE +0 -0
  58. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/WHEEL +0 -0
  59. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/entry_points.txt +0 -0
letta/server/server.py CHANGED
@@ -19,16 +19,18 @@ import letta.system as system
19
19
  from letta.agent import Agent, save_agent
20
20
  from letta.config import LettaConfig
21
21
  from letta.data_sources.connectors import DataConnector, load_data
22
+ from letta.dynamic_multi_agent import DynamicMultiAgent
22
23
  from letta.helpers.datetime_helpers import get_utc_time
23
24
  from letta.helpers.json_helpers import json_dumps, json_loads
24
25
  from letta.helpers.mcp_helpers import (
26
+ MCP_CONFIG_TOPLEVEL_KEY,
25
27
  BaseMCPClient,
26
- LocalMCPClient,
27
- LocalServerConfig,
28
28
  MCPServerType,
29
29
  MCPTool,
30
30
  SSEMCPClient,
31
31
  SSEServerConfig,
32
+ StdioMCPClient,
33
+ StdioServerConfig,
32
34
  )
33
35
 
34
36
  # TODO use custom interface
@@ -37,6 +39,7 @@ from letta.interface import CLIInterface # for printing to terminal
37
39
  from letta.log import get_logger
38
40
  from letta.offline_memory_agent import OfflineMemoryAgent
39
41
  from letta.orm.errors import NoResultFound
42
+ from letta.round_robin_multi_agent import RoundRobinMultiAgent
40
43
  from letta.schemas.agent import AgentState, AgentType, CreateAgent
41
44
  from letta.schemas.block import BlockUpdate
42
45
  from letta.schemas.embedding_config import EmbeddingConfig
@@ -44,12 +47,14 @@ from letta.schemas.embedding_config import EmbeddingConfig
44
47
  # openai schemas
45
48
  from letta.schemas.enums import JobStatus, MessageStreamStatus
46
49
  from letta.schemas.environment_variables import SandboxEnvironmentVariableCreate
50
+ from letta.schemas.group import Group, ManagerType
47
51
  from letta.schemas.job import Job, JobUpdate
48
52
  from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage, ToolReturnMessage
53
+ from letta.schemas.letta_message_content import TextContent
49
54
  from letta.schemas.letta_response import LettaResponse
50
55
  from letta.schemas.llm_config import LLMConfig
51
56
  from letta.schemas.memory import ArchivalMemorySummary, ContextWindowOverview, Memory, RecallMemorySummary
52
- from letta.schemas.message import Message, MessageCreate, MessageRole, MessageUpdate, TextContent
57
+ from letta.schemas.message import Message, MessageCreate, MessageRole, MessageUpdate
53
58
  from letta.schemas.organization import Organization
54
59
  from letta.schemas.passage import Passage, PassageUpdate
55
60
  from letta.schemas.providers import (
@@ -80,6 +85,7 @@ from letta.server.rest_api.interface import StreamingServerInterface
80
85
  from letta.server.rest_api.utils import sse_async_generator
81
86
  from letta.services.agent_manager import AgentManager
82
87
  from letta.services.block_manager import BlockManager
88
+ from letta.services.group_manager import GroupManager
83
89
  from letta.services.identity_manager import IdentityManager
84
90
  from letta.services.job_manager import JobManager
85
91
  from letta.services.message_manager import MessageManager
@@ -94,6 +100,7 @@ from letta.services.tool_execution_sandbox import ToolExecutionSandbox
94
100
  from letta.services.tool_manager import ToolManager
95
101
  from letta.services.user_manager import UserManager
96
102
  from letta.settings import model_settings, settings, tool_settings
103
+ from letta.supervisor_multi_agent import SupervisorMultiAgent
97
104
  from letta.tracing import trace_method
98
105
  from letta.utils import get_friendly_error_msg
99
106
 
@@ -207,6 +214,7 @@ class SyncServer(Server):
207
214
  self.provider_manager = ProviderManager()
208
215
  self.step_manager = StepManager()
209
216
  self.identity_manager = IdentityManager()
217
+ self.group_manager = GroupManager()
210
218
 
211
219
  # Managers that interface with parallelism
212
220
  self.per_agent_lock_manager = PerAgentLockManager()
@@ -331,8 +339,8 @@ class SyncServer(Server):
331
339
  for server_name, server_config in mcp_server_configs.items():
332
340
  if server_config.type == MCPServerType.SSE:
333
341
  self.mcp_clients[server_name] = SSEMCPClient()
334
- elif server_config.type == MCPServerType.LOCAL:
335
- self.mcp_clients[server_name] = LocalMCPClient()
342
+ elif server_config.type == MCPServerType.STDIO:
343
+ self.mcp_clients[server_name] = StdioMCPClient()
336
344
  else:
337
345
  raise ValueError(f"Invalid MCP server config: {server_config}")
338
346
  try:
@@ -353,6 +361,8 @@ class SyncServer(Server):
353
361
  agent_lock = self.per_agent_lock_manager.get_lock(agent_id)
354
362
  with agent_lock:
355
363
  agent_state = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor)
364
+ if agent_state.multi_agent_group:
365
+ return self.load_multi_agent(agent_state.multi_agent_group, actor, interface, agent_state)
356
366
 
357
367
  interface = interface or self.default_interface_factory()
358
368
  if agent_state.agent_type == AgentType.memgpt_agent:
@@ -364,6 +374,46 @@ class SyncServer(Server):
364
374
 
365
375
  return agent
366
376
 
377
+ def load_multi_agent(
378
+ self, group: Group, actor: User, interface: Union[AgentInterface, None] = None, agent_state: Optional[AgentState] = None
379
+ ) -> Agent:
380
+ match group.manager_type:
381
+ case ManagerType.round_robin:
382
+ agent_state = agent_state or self.agent_manager.get_agent_by_id(agent_id=group.agent_ids[0], actor=actor)
383
+ return RoundRobinMultiAgent(
384
+ agent_state=agent_state,
385
+ interface=interface,
386
+ user=actor,
387
+ group_id=group.id,
388
+ agent_ids=group.agent_ids,
389
+ description=group.description,
390
+ max_turns=group.max_turns,
391
+ )
392
+ case ManagerType.dynamic:
393
+ agent_state = agent_state or self.agent_manager.get_agent_by_id(agent_id=group.manager_agent_id, actor=actor)
394
+ return DynamicMultiAgent(
395
+ agent_state=agent_state,
396
+ interface=interface,
397
+ user=actor,
398
+ group_id=group.id,
399
+ agent_ids=group.agent_ids,
400
+ description=group.description,
401
+ max_turns=group.max_turns,
402
+ termination_token=group.termination_token,
403
+ )
404
+ case ManagerType.supervisor:
405
+ agent_state = agent_state or self.agent_manager.get_agent_by_id(agent_id=group.manager_agent_id, actor=actor)
406
+ return SupervisorMultiAgent(
407
+ agent_state=agent_state,
408
+ interface=interface,
409
+ user=actor,
410
+ group_id=group.id,
411
+ agent_ids=group.agent_ids,
412
+ description=group.description,
413
+ )
414
+ case _:
415
+ raise ValueError(f"Type {group.manager_type} is not supported.")
416
+
367
417
  def _step(
368
418
  self,
369
419
  actor: User,
@@ -690,7 +740,7 @@ class SyncServer(Server):
690
740
  Message(
691
741
  agent_id=agent_id,
692
742
  role=message.role,
693
- content=[TextContent(text=message.content)],
743
+ content=[TextContent(text=message.content)] if message.content else [],
694
744
  name=message.name,
695
745
  # assigned later?
696
746
  model=None,
@@ -800,6 +850,9 @@ class SyncServer(Server):
800
850
  # TODO: @mindy look at moving this to agent_manager to avoid above extra call
801
851
  passages = self.passage_manager.insert_passage(agent_state=agent_state, agent_id=agent_id, text=memory_contents, actor=actor)
802
852
 
853
+ # rebuild agent system prompt - force since no archival change
854
+ self.agent_manager.rebuild_system_prompt(agent_id=agent_id, actor=actor, force=True)
855
+
803
856
  return passages
804
857
 
805
858
  def modify_archival_memory(self, agent_id: str, memory_id: str, passage: PassageUpdate, actor: User) -> List[Passage]:
@@ -809,10 +862,14 @@ class SyncServer(Server):
809
862
 
810
863
  def delete_archival_memory(self, memory_id: str, actor: User):
811
864
  # TODO check if it exists first, and throw error if not
812
- # TODO: @mindy make this return the deleted passage instead
865
+ # TODO: need to also rebuild the prompt here
866
+ passage = self.passage_manager.get_passage_by_id(passage_id=memory_id, actor=actor)
867
+
868
+ # delete the passage
813
869
  self.passage_manager.delete_passage_by_id(passage_id=memory_id, actor=actor)
814
870
 
815
- # TODO: return archival memory
871
+ # rebuild system prompt and force
872
+ self.agent_manager.rebuild_system_prompt(agent_id=passage.agent_id, actor=actor, force=True)
816
873
 
817
874
  def get_agent_recall(
818
875
  self,
@@ -931,6 +988,9 @@ class SyncServer(Server):
931
988
  new_passage_size = self.agent_manager.passage_size(actor=actor, agent_id=agent_id)
932
989
  assert new_passage_size >= curr_passage_size # in case empty files are added
933
990
 
991
+ # rebuild system prompt and force
992
+ self.agent_manager.rebuild_system_prompt(agent_id=agent_id, actor=actor, force=True)
993
+
934
994
  return job
935
995
 
936
996
  def load_data(
@@ -1209,7 +1269,7 @@ class SyncServer(Server):
1209
1269
 
1210
1270
  # MCP wrappers
1211
1271
  # TODO support both command + SSE servers (via config)
1212
- def get_mcp_servers(self) -> dict[str, Union[SSEServerConfig, LocalServerConfig]]:
1272
+ def get_mcp_servers(self) -> dict[str, Union[SSEServerConfig, StdioServerConfig]]:
1213
1273
  """List the MCP servers in the config (doesn't test that they are actually working)"""
1214
1274
  mcp_server_list = {}
1215
1275
 
@@ -1227,8 +1287,8 @@ class SyncServer(Server):
1227
1287
  # Proper formatting is "mcpServers" key at the top level,
1228
1288
  # then a dict with the MCP server name as the key,
1229
1289
  # with the value being the schema from StdioServerParameters
1230
- if "mcpServers" in mcp_config:
1231
- for server_name, server_params_raw in mcp_config["mcpServers"].items():
1290
+ if MCP_CONFIG_TOPLEVEL_KEY in mcp_config:
1291
+ for server_name, server_params_raw in mcp_config[MCP_CONFIG_TOPLEVEL_KEY].items():
1232
1292
 
1233
1293
  # No support for duplicate server names
1234
1294
  if server_name in mcp_server_list:
@@ -1249,7 +1309,7 @@ class SyncServer(Server):
1249
1309
  else:
1250
1310
  # Attempt to parse the server params as a StdioServerParameters
1251
1311
  try:
1252
- server_params = LocalServerConfig(
1312
+ server_params = StdioServerConfig(
1253
1313
  server_name=server_name,
1254
1314
  command=server_params_raw["command"],
1255
1315
  args=server_params_raw.get("args", []),
@@ -1269,6 +1329,98 @@ class SyncServer(Server):
1269
1329
 
1270
1330
  return self.mcp_clients[mcp_server_name].list_tools()
1271
1331
 
1332
+ def add_mcp_server_to_config(
1333
+ self, server_config: Union[SSEServerConfig, StdioServerConfig], allow_upsert: bool = True
1334
+ ) -> dict[str, Union[SSEServerConfig, StdioServerConfig]]:
1335
+ """Add a new server config to the MCP config file"""
1336
+
1337
+ # If the config file doesn't exist, throw an error.
1338
+ mcp_config_path = os.path.join(constants.LETTA_DIR, constants.MCP_CONFIG_NAME)
1339
+ if not os.path.exists(mcp_config_path):
1340
+ raise FileNotFoundError(f"MCP config file not found: {mcp_config_path}")
1341
+
1342
+ # If the file does exist, attempt to parse it get calling get_mcp_servers
1343
+ try:
1344
+ current_mcp_servers = self.get_mcp_servers()
1345
+ except Exception as e:
1346
+ # Raise an error telling the user to fix the config file
1347
+ logger.error(f"Failed to parse MCP config file at {mcp_config_path}: {e}")
1348
+ raise ValueError(f"Failed to parse MCP config file {mcp_config_path}")
1349
+
1350
+ # Check if the server name is already in the config
1351
+ if server_config.server_name in current_mcp_servers and not allow_upsert:
1352
+ raise ValueError(f"Server name {server_config.server_name} is already in the config file")
1353
+
1354
+ # Attempt to initialize the connection to the server
1355
+ if server_config.type == MCPServerType.SSE:
1356
+ new_mcp_client = SSEMCPClient()
1357
+ elif server_config.type == MCPServerType.STDIO:
1358
+ new_mcp_client = StdioMCPClient()
1359
+ else:
1360
+ raise ValueError(f"Invalid MCP server config: {server_config}")
1361
+ try:
1362
+ new_mcp_client.connect_to_server(server_config)
1363
+ except:
1364
+ logger.exception(f"Failed to connect to MCP server: {server_config.server_name}")
1365
+ raise RuntimeError(f"Failed to connect to MCP server: {server_config.server_name}")
1366
+ # Print out the tools that are connected
1367
+ logger.info(f"Attempting to fetch tools from MCP server: {server_config.server_name}")
1368
+ new_mcp_tools = new_mcp_client.list_tools()
1369
+ logger.info(f"MCP tools connected: {', '.join([t.name for t in new_mcp_tools])}")
1370
+ logger.debug(f"MCP tools: {', '.join([str(t) for t in new_mcp_tools])}")
1371
+
1372
+ # Now that we've confirmed the config is working, let's add it to the client list
1373
+ self.mcp_clients[server_config.server_name] = new_mcp_client
1374
+
1375
+ # Add to the server file
1376
+ current_mcp_servers[server_config.server_name] = server_config
1377
+
1378
+ # Write out the file, and make sure to in include the top-level mcpConfig
1379
+ try:
1380
+ new_mcp_file = {MCP_CONFIG_TOPLEVEL_KEY: {k: v.to_dict() for k, v in current_mcp_servers.items()}}
1381
+ with open(mcp_config_path, "w") as f:
1382
+ json.dump(new_mcp_file, f, indent=4)
1383
+ except Exception as e:
1384
+ logger.error(f"Failed to write MCP config file at {mcp_config_path}: {e}")
1385
+ raise ValueError(f"Failed to write MCP config file {mcp_config_path}")
1386
+
1387
+ return list(current_mcp_servers.values())
1388
+
1389
+ def delete_mcp_server_from_config(self, server_name: str) -> dict[str, Union[SSEServerConfig, StdioServerConfig]]:
1390
+ """Delete a server config from the MCP config file"""
1391
+
1392
+ # If the config file doesn't exist, throw an error.
1393
+ mcp_config_path = os.path.join(constants.LETTA_DIR, constants.MCP_CONFIG_NAME)
1394
+ if not os.path.exists(mcp_config_path):
1395
+ raise FileNotFoundError(f"MCP config file not found: {mcp_config_path}")
1396
+
1397
+ # If the file does exist, attempt to parse it get calling get_mcp_servers
1398
+ try:
1399
+ current_mcp_servers = self.get_mcp_servers()
1400
+ except Exception as e:
1401
+ # Raise an error telling the user to fix the config file
1402
+ logger.error(f"Failed to parse MCP config file at {mcp_config_path}: {e}")
1403
+ raise ValueError(f"Failed to parse MCP config file {mcp_config_path}")
1404
+
1405
+ # Check if the server name is already in the config
1406
+ # If it's not, throw an error
1407
+ if server_name not in current_mcp_servers:
1408
+ raise ValueError(f"Server name {server_name} not found in MCP config file")
1409
+
1410
+ # Remove from the server file
1411
+ del current_mcp_servers[server_name]
1412
+
1413
+ # Write out the file, and make sure to in include the top-level mcpConfig
1414
+ try:
1415
+ new_mcp_file = {MCP_CONFIG_TOPLEVEL_KEY: {k: v.to_dict() for k, v in current_mcp_servers.items()}}
1416
+ with open(mcp_config_path, "w") as f:
1417
+ json.dump(new_mcp_file, f, indent=4)
1418
+ except Exception as e:
1419
+ logger.error(f"Failed to write MCP config file at {mcp_config_path}: {e}")
1420
+ raise ValueError(f"Failed to write MCP config file {mcp_config_path}")
1421
+
1422
+ return list(current_mcp_servers.values())
1423
+
1272
1424
  @trace_method
1273
1425
  async def send_message_to_agent(
1274
1426
  self,
@@ -1403,3 +1555,106 @@ class SyncServer(Server):
1403
1555
 
1404
1556
  traceback.print_exc()
1405
1557
  raise HTTPException(status_code=500, detail=f"{e}")
1558
+
1559
+ @trace_method
1560
+ async def send_group_message_to_agent(
1561
+ self,
1562
+ group_id: str,
1563
+ actor: User,
1564
+ messages: Union[List[Message], List[MessageCreate]],
1565
+ stream_steps: bool,
1566
+ stream_tokens: bool,
1567
+ chat_completion_mode: bool = False,
1568
+ # Support for AssistantMessage
1569
+ use_assistant_message: bool = True,
1570
+ assistant_message_tool_name: str = constants.DEFAULT_MESSAGE_TOOL,
1571
+ assistant_message_tool_kwarg: str = constants.DEFAULT_MESSAGE_TOOL_KWARG,
1572
+ metadata: Optional[dict] = None,
1573
+ ) -> Union[StreamingResponse, LettaResponse]:
1574
+ include_final_message = True
1575
+ if not stream_steps and stream_tokens:
1576
+ raise HTTPException(status_code=400, detail="stream_steps must be 'true' if stream_tokens is 'true'")
1577
+
1578
+ try:
1579
+ # fetch the group
1580
+ group = self.group_manager.retrieve_group(group_id=group_id, actor=actor)
1581
+ letta_multi_agent = self.load_multi_agent(group=group, actor=actor)
1582
+
1583
+ llm_config = letta_multi_agent.agent_state.llm_config
1584
+ supports_token_streaming = ["openai", "anthropic", "deepseek"]
1585
+ if stream_tokens and (
1586
+ llm_config.model_endpoint_type not in supports_token_streaming or "inference.memgpt.ai" in llm_config.model_endpoint
1587
+ ):
1588
+ warnings.warn(
1589
+ 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."
1590
+ )
1591
+ stream_tokens = False
1592
+
1593
+ # Create a new interface per request
1594
+ letta_multi_agent.interface = StreamingServerInterface(
1595
+ use_assistant_message=use_assistant_message,
1596
+ assistant_message_tool_name=assistant_message_tool_name,
1597
+ assistant_message_tool_kwarg=assistant_message_tool_kwarg,
1598
+ inner_thoughts_in_kwargs=(
1599
+ llm_config.put_inner_thoughts_in_kwargs if llm_config.put_inner_thoughts_in_kwargs is not None else False
1600
+ ),
1601
+ )
1602
+ streaming_interface = letta_multi_agent.interface
1603
+ if not isinstance(streaming_interface, StreamingServerInterface):
1604
+ raise ValueError(f"Agent has wrong type of interface: {type(streaming_interface)}")
1605
+ streaming_interface.streaming_mode = stream_tokens
1606
+ streaming_interface.streaming_chat_completion_mode = chat_completion_mode
1607
+ if metadata and hasattr(streaming_interface, "metadata"):
1608
+ streaming_interface.metadata = metadata
1609
+
1610
+ streaming_interface.stream_start()
1611
+ task = asyncio.create_task(
1612
+ asyncio.to_thread(
1613
+ letta_multi_agent.step,
1614
+ messages=messages,
1615
+ chaining=self.chaining,
1616
+ max_chaining_steps=self.max_chaining_steps,
1617
+ )
1618
+ )
1619
+
1620
+ if stream_steps:
1621
+ # return a stream
1622
+ return StreamingResponse(
1623
+ sse_async_generator(
1624
+ streaming_interface.get_generator(),
1625
+ usage_task=task,
1626
+ finish_message=include_final_message,
1627
+ ),
1628
+ media_type="text/event-stream",
1629
+ )
1630
+
1631
+ else:
1632
+ # buffer the stream, then return the list
1633
+ generated_stream = []
1634
+ async for message in streaming_interface.get_generator():
1635
+ assert (
1636
+ isinstance(message, LettaMessage)
1637
+ or isinstance(message, LegacyLettaMessage)
1638
+ or isinstance(message, MessageStreamStatus)
1639
+ ), type(message)
1640
+ generated_stream.append(message)
1641
+ if message == MessageStreamStatus.done:
1642
+ break
1643
+
1644
+ # Get rid of the stream status messages
1645
+ filtered_stream = [d for d in generated_stream if not isinstance(d, MessageStreamStatus)]
1646
+ usage = await task
1647
+
1648
+ # By default the stream will be messages of type LettaMessage or LettaLegacyMessage
1649
+ # If we want to convert these to Message, we can use the attached IDs
1650
+ # NOTE: we will need to de-duplicate the Messsage IDs though (since Assistant->Inner+Func_Call)
1651
+ # TODO: eventually update the interface to use `Message` and `MessageChunk` (new) inside the deque instead
1652
+ return LettaResponse(messages=filtered_stream, usage=usage)
1653
+ except HTTPException:
1654
+ raise
1655
+ except Exception as e:
1656
+ print(e)
1657
+ import traceback
1658
+
1659
+ traceback.print_exc()
1660
+ raise HTTPException(status_code=500, detail=f"{e}")
@@ -18,6 +18,7 @@ from letta.orm import Tool as ToolModel
18
18
  from letta.orm.enums import ToolType
19
19
  from letta.orm.errors import NoResultFound
20
20
  from letta.orm.sandbox_config import AgentEnvironmentVariable as AgentEnvironmentVariableModel
21
+ from letta.orm.sqlalchemy_base import AccessType
21
22
  from letta.orm.sqlite_functions import adapt_array
22
23
  from letta.schemas.agent import AgentState as PydanticAgentState
23
24
  from letta.schemas.agent import AgentType, CreateAgent, UpdateAgent
@@ -35,10 +36,15 @@ from letta.schemas.tool_rule import ContinueToolRule as PydanticContinueToolRule
35
36
  from letta.schemas.tool_rule import TerminalToolRule as PydanticTerminalToolRule
36
37
  from letta.schemas.tool_rule import ToolRule as PydanticToolRule
37
38
  from letta.schemas.user import User as PydanticUser
38
- from letta.serialize_schemas import SerializedAgentSchema
39
- from letta.serialize_schemas.tool import SerializedToolSchema
39
+ from letta.serialize_schemas import MarshmallowAgentSchema
40
+ from letta.serialize_schemas.marshmallow_tool import SerializedToolSchema
41
+ from letta.serialize_schemas.pydantic_agent_schema import AgentSchema
40
42
  from letta.services.block_manager import BlockManager
41
43
  from letta.services.helpers.agent_manager_helper import (
44
+ _apply_filters,
45
+ _apply_identity_filters,
46
+ _apply_pagination,
47
+ _apply_tag_filter,
42
48
  _process_relationship,
43
49
  _process_tags,
44
50
  check_supports_structured_output,
@@ -49,6 +55,7 @@ from letta.services.helpers.agent_manager_helper import (
49
55
  )
50
56
  from letta.services.identity_manager import IdentityManager
51
57
  from letta.services.message_manager import MessageManager
58
+ from letta.services.passage_manager import PassageManager
52
59
  from letta.services.source_manager import SourceManager
53
60
  from letta.services.tool_manager import ToolManager
54
61
  from letta.settings import settings
@@ -70,6 +77,7 @@ class AgentManager:
70
77
  self.tool_manager = ToolManager()
71
78
  self.source_manager = SourceManager()
72
79
  self.message_manager = MessageManager()
80
+ self.passage_manager = PassageManager()
73
81
  self.identity_manager = IdentityManager()
74
82
 
75
83
  # ======================================================================================================================
@@ -326,39 +334,60 @@ class AgentManager:
326
334
  # Convert to PydanticAgentState and return
327
335
  return agent.to_pydantic()
328
336
 
329
- @enforce_types
337
+ # TODO: Make this general and think about how to roll this into sqlalchemybase
330
338
  def list_agents(
331
339
  self,
332
340
  actor: PydanticUser,
341
+ name: Optional[str] = None,
342
+ tags: Optional[List[str]] = None,
343
+ match_all_tags: bool = False,
333
344
  before: Optional[str] = None,
334
345
  after: Optional[str] = None,
335
346
  limit: Optional[int] = 50,
336
- tags: Optional[List[str]] = None,
337
- match_all_tags: bool = False,
338
347
  query_text: Optional[str] = None,
339
- identifier_keys: Optional[List[str]] = None,
348
+ project_id: Optional[str] = None,
349
+ template_id: Optional[str] = None,
350
+ base_template_id: Optional[str] = None,
340
351
  identity_id: Optional[str] = None,
341
- **kwargs,
352
+ identifier_keys: Optional[List[str]] = None,
353
+ include_relationships: Optional[List[str]] = None,
342
354
  ) -> List[PydanticAgentState]:
343
355
  """
344
- List agents that have the specified tags.
356
+ Retrieves agents with optimized filtering and optional field selection.
357
+
358
+ Args:
359
+ actor: The User requesting the list
360
+ name (Optional[str]): Filter by agent name.
361
+ tags (Optional[List[str]]): Filter agents by tags.
362
+ match_all_tags (bool): If True, only return agents that match ALL given tags.
363
+ before (Optional[str]): Cursor for pagination.
364
+ after (Optional[str]): Cursor for pagination.
365
+ limit (Optional[int]): Maximum number of agents to return.
366
+ query_text (Optional[str]): Search agents by name.
367
+ project_id (Optional[str]): Filter by project ID.
368
+ template_id (Optional[str]): Filter by template ID.
369
+ base_template_id (Optional[str]): Filter by base template ID.
370
+ identity_id (Optional[str]): Filter by identifier ID.
371
+ identifier_keys (Optional[List[str]]): Search agents by identifier keys.
372
+ include_relationships (Optional[List[str]]): List of fields to load for performance optimization.
373
+
374
+ Returns:
375
+ List[PydanticAgentState]: The filtered list of matching agents.
345
376
  """
346
377
  with self.session_maker() as session:
347
- agents = AgentModel.list(
348
- db_session=session,
349
- before=before,
350
- after=after,
351
- limit=limit,
352
- tags=tags,
353
- match_all_tags=match_all_tags,
354
- organization_id=actor.organization_id if actor else None,
355
- query_text=query_text,
356
- identifier_keys=identifier_keys,
357
- identity_id=identity_id,
358
- **kwargs,
359
- )
378
+ query = select(AgentModel).distinct(AgentModel.created_at, AgentModel.id)
379
+ query = AgentModel.apply_access_predicate(query, actor, ["read"], AccessType.ORGANIZATION)
380
+
381
+ # Apply filters
382
+ query = _apply_filters(query, name, query_text, project_id, template_id, base_template_id)
383
+ query = _apply_identity_filters(query, identity_id, identifier_keys)
384
+ query = _apply_tag_filter(query, tags, match_all_tags)
385
+ query = _apply_pagination(query, before, after, session)
360
386
 
361
- return [agent.to_pydantic() for agent in agents]
387
+ query = query.limit(limit)
388
+
389
+ agents = session.execute(query).scalars().all()
390
+ return [agent.to_pydantic(include_relationships=include_relationships) for agent in agents]
362
391
 
363
392
  @enforce_types
364
393
  def list_agents_matching_tags(
@@ -399,7 +428,7 @@ class AgentManager:
399
428
  # Ensures agents match at least one tag in match_some
400
429
  query = query.join(AgentsTags).where(AgentsTags.tag.in_(match_some))
401
430
 
402
- query = query.group_by(AgentModel.id).limit(limit)
431
+ query = query.distinct(AgentModel.id).order_by(AgentModel.id).limit(limit)
403
432
 
404
433
  return list(session.execute(query).scalars())
405
434
 
@@ -434,29 +463,32 @@ class AgentManager:
434
463
  with self.session_maker() as session:
435
464
  # Retrieve the agent
436
465
  agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
466
+ # TODO check if it is managing a group
437
467
  agent.hard_delete(session)
438
468
 
439
469
  @enforce_types
440
- def serialize(self, agent_id: str, actor: PydanticUser) -> dict:
470
+ def serialize(self, agent_id: str, actor: PydanticUser) -> AgentSchema:
441
471
  with self.session_maker() as session:
442
472
  # Retrieve the agent
443
473
  agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
444
- schema = SerializedAgentSchema(session=session, actor=actor)
445
- return schema.dump(agent)
474
+ schema = MarshmallowAgentSchema(session=session, actor=actor)
475
+ data = schema.dump(agent)
476
+ return AgentSchema(**data)
446
477
 
447
478
  @enforce_types
448
479
  def deserialize(
449
480
  self,
450
- serialized_agent: dict,
481
+ serialized_agent: AgentSchema,
451
482
  actor: PydanticUser,
452
483
  append_copy_suffix: bool = True,
453
484
  override_existing_tools: bool = True,
454
485
  project_id: Optional[str] = None,
455
486
  ) -> PydanticAgentState:
487
+ serialized_agent = serialized_agent.model_dump()
456
488
  tool_data_list = serialized_agent.pop("tools", [])
457
489
 
458
490
  with self.session_maker() as session:
459
- schema = SerializedAgentSchema(session=session, actor=actor)
491
+ schema = MarshmallowAgentSchema(session=session, actor=actor)
460
492
  agent = schema.load(serialized_agent, session=session)
461
493
  if append_copy_suffix:
462
494
  agent.name += "_copy"
@@ -595,12 +627,17 @@ class AgentManager:
595
627
  # NOTE: a bit of a hack - we pull the timestamp from the message created_by
596
628
  memory_edit_timestamp = curr_system_message.created_at
597
629
 
630
+ num_messages = self.message_manager.size(actor=actor, agent_id=agent_id)
631
+ num_archival_memories = self.passage_manager.size(actor=actor, agent_id=agent_id)
632
+
598
633
  # update memory (TODO: potentially update recall/archival stats separately)
599
634
  new_system_message_str = compile_system_message(
600
635
  system_prompt=agent_state.system,
601
636
  in_context_memory=agent_state.memory,
602
637
  in_context_memory_last_edit=memory_edit_timestamp,
603
638
  recent_passages=self.list_passages(actor=actor, agent_id=agent_id, ascending=False, limit=10),
639
+ previous_message_count=num_messages,
640
+ archival_memory_size=num_archival_memories,
604
641
  )
605
642
 
606
643
  diff = united_diff(curr_system_message_openai["content"], new_system_message_str)