letta-nightly 0.6.39.dev20250314104053__py3-none-any.whl → 0.6.40.dev20250314222759__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 (67) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +14 -4
  3. letta/agents/ephemeral_agent.py +2 -1
  4. letta/agents/low_latency_agent.py +8 -0
  5. letta/dynamic_multi_agent.py +274 -0
  6. letta/functions/function_sets/base.py +1 -0
  7. letta/functions/function_sets/extras.py +2 -1
  8. letta/functions/function_sets/multi_agent.py +17 -0
  9. letta/functions/helpers.py +41 -0
  10. letta/functions/mcp_client/__init__.py +0 -0
  11. letta/functions/mcp_client/base_client.py +61 -0
  12. letta/functions/mcp_client/sse_client.py +21 -0
  13. letta/functions/mcp_client/stdio_client.py +103 -0
  14. letta/functions/mcp_client/types.py +48 -0
  15. letta/functions/schema_generator.py +1 -1
  16. letta/helpers/converters.py +67 -0
  17. letta/llm_api/openai.py +1 -1
  18. letta/memory.py +2 -1
  19. letta/orm/__init__.py +2 -0
  20. letta/orm/agent.py +69 -20
  21. letta/orm/custom_columns.py +15 -0
  22. letta/orm/group.py +33 -0
  23. letta/orm/groups_agents.py +13 -0
  24. letta/orm/message.py +7 -4
  25. letta/orm/organization.py +1 -0
  26. letta/orm/sqlalchemy_base.py +3 -3
  27. letta/round_robin_multi_agent.py +152 -0
  28. letta/schemas/agent.py +3 -0
  29. letta/schemas/enums.py +0 -4
  30. letta/schemas/group.py +65 -0
  31. letta/schemas/letta_message.py +167 -106
  32. letta/schemas/letta_message_content.py +192 -0
  33. letta/schemas/message.py +28 -36
  34. letta/schemas/tool.py +1 -1
  35. letta/serialize_schemas/__init__.py +1 -1
  36. letta/serialize_schemas/marshmallow_agent.py +108 -0
  37. letta/serialize_schemas/{agent_environment_variable.py → marshmallow_agent_environment_variable.py} +1 -1
  38. letta/serialize_schemas/marshmallow_base.py +52 -0
  39. letta/serialize_schemas/{block.py → marshmallow_block.py} +1 -1
  40. letta/serialize_schemas/{custom_fields.py → marshmallow_custom_fields.py} +12 -0
  41. letta/serialize_schemas/marshmallow_message.py +42 -0
  42. letta/serialize_schemas/{tag.py → marshmallow_tag.py} +12 -2
  43. letta/serialize_schemas/{tool.py → marshmallow_tool.py} +1 -1
  44. letta/serialize_schemas/pydantic_agent_schema.py +111 -0
  45. letta/server/rest_api/app.py +15 -0
  46. letta/server/rest_api/routers/v1/__init__.py +2 -0
  47. letta/server/rest_api/routers/v1/agents.py +46 -40
  48. letta/server/rest_api/routers/v1/groups.py +233 -0
  49. letta/server/rest_api/routers/v1/tools.py +31 -3
  50. letta/server/rest_api/utils.py +1 -1
  51. letta/server/server.py +272 -22
  52. letta/services/agent_manager.py +65 -28
  53. letta/services/group_manager.py +147 -0
  54. letta/services/helpers/agent_manager_helper.py +151 -1
  55. letta/services/message_manager.py +11 -3
  56. letta/services/passage_manager.py +15 -0
  57. letta/settings.py +5 -0
  58. letta/supervisor_multi_agent.py +103 -0
  59. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314222759.dist-info}/METADATA +1 -2
  60. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314222759.dist-info}/RECORD +63 -49
  61. letta/helpers/mcp_helpers.py +0 -108
  62. letta/serialize_schemas/agent.py +0 -80
  63. letta/serialize_schemas/base.py +0 -64
  64. letta/serialize_schemas/message.py +0 -29
  65. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314222759.dist-info}/LICENSE +0 -0
  66. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314222759.dist-info}/WHEEL +0 -0
  67. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314222759.dist-info}/entry_points.txt +0 -0
letta/server/server.py CHANGED
@@ -19,17 +19,13 @@ 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
23
+ from letta.functions.mcp_client.base_client import BaseMCPClient
24
+ from letta.functions.mcp_client.sse_client import MCP_CONFIG_TOPLEVEL_KEY, SSEMCPClient
25
+ from letta.functions.mcp_client.stdio_client import StdioMCPClient
26
+ from letta.functions.mcp_client.types import MCPServerType, MCPTool, SSEServerConfig, StdioServerConfig
22
27
  from letta.helpers.datetime_helpers import get_utc_time
23
28
  from letta.helpers.json_helpers import json_dumps, json_loads
24
- from letta.helpers.mcp_helpers import (
25
- BaseMCPClient,
26
- LocalMCPClient,
27
- LocalServerConfig,
28
- MCPServerType,
29
- MCPTool,
30
- SSEMCPClient,
31
- SSEServerConfig,
32
- )
33
29
 
34
30
  # TODO use custom interface
35
31
  from letta.interface import AgentInterface # abstract
@@ -37,6 +33,7 @@ from letta.interface import CLIInterface # for printing to terminal
37
33
  from letta.log import get_logger
38
34
  from letta.offline_memory_agent import OfflineMemoryAgent
39
35
  from letta.orm.errors import NoResultFound
36
+ from letta.round_robin_multi_agent import RoundRobinMultiAgent
40
37
  from letta.schemas.agent import AgentState, AgentType, CreateAgent
41
38
  from letta.schemas.block import BlockUpdate
42
39
  from letta.schemas.embedding_config import EmbeddingConfig
@@ -44,12 +41,14 @@ from letta.schemas.embedding_config import EmbeddingConfig
44
41
  # openai schemas
45
42
  from letta.schemas.enums import JobStatus, MessageStreamStatus
46
43
  from letta.schemas.environment_variables import SandboxEnvironmentVariableCreate
44
+ from letta.schemas.group import Group, ManagerType
47
45
  from letta.schemas.job import Job, JobUpdate
48
46
  from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage, ToolReturnMessage
47
+ from letta.schemas.letta_message_content import TextContent
49
48
  from letta.schemas.letta_response import LettaResponse
50
49
  from letta.schemas.llm_config import LLMConfig
51
50
  from letta.schemas.memory import ArchivalMemorySummary, ContextWindowOverview, Memory, RecallMemorySummary
52
- from letta.schemas.message import Message, MessageCreate, MessageRole, MessageUpdate, TextContent
51
+ from letta.schemas.message import Message, MessageCreate, MessageRole, MessageUpdate
53
52
  from letta.schemas.organization import Organization
54
53
  from letta.schemas.passage import Passage, PassageUpdate
55
54
  from letta.schemas.providers import (
@@ -80,6 +79,7 @@ from letta.server.rest_api.interface import StreamingServerInterface
80
79
  from letta.server.rest_api.utils import sse_async_generator
81
80
  from letta.services.agent_manager import AgentManager
82
81
  from letta.services.block_manager import BlockManager
82
+ from letta.services.group_manager import GroupManager
83
83
  from letta.services.identity_manager import IdentityManager
84
84
  from letta.services.job_manager import JobManager
85
85
  from letta.services.message_manager import MessageManager
@@ -94,6 +94,7 @@ from letta.services.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
97
+ from letta.supervisor_multi_agent import SupervisorMultiAgent
97
98
  from letta.tracing import trace_method
98
99
  from letta.utils import get_friendly_error_msg
99
100
 
@@ -207,6 +208,7 @@ class SyncServer(Server):
207
208
  self.provider_manager = ProviderManager()
208
209
  self.step_manager = StepManager()
209
210
  self.identity_manager = IdentityManager()
211
+ self.group_manager = GroupManager()
210
212
 
211
213
  # Managers that interface with parallelism
212
214
  self.per_agent_lock_manager = PerAgentLockManager()
@@ -331,15 +333,16 @@ class SyncServer(Server):
331
333
  for server_name, server_config in mcp_server_configs.items():
332
334
  if server_config.type == MCPServerType.SSE:
333
335
  self.mcp_clients[server_name] = SSEMCPClient()
334
- elif server_config.type == MCPServerType.LOCAL:
335
- self.mcp_clients[server_name] = LocalMCPClient()
336
+ elif server_config.type == MCPServerType.STDIO:
337
+ self.mcp_clients[server_name] = StdioMCPClient()
336
338
  else:
337
339
  raise ValueError(f"Invalid MCP server config: {server_config}")
340
+
338
341
  try:
339
342
  self.mcp_clients[server_name].connect_to_server(server_config)
340
- except:
341
- logger.exception(f"Failed to connect to MCP server: {server_name}")
342
- raise
343
+ except Exception as e:
344
+ logger.error(e)
345
+ self.mcp_clients.pop(server_name)
343
346
 
344
347
  # Print out the tools that are connected
345
348
  for server_name, client in self.mcp_clients.items():
@@ -353,6 +356,8 @@ class SyncServer(Server):
353
356
  agent_lock = self.per_agent_lock_manager.get_lock(agent_id)
354
357
  with agent_lock:
355
358
  agent_state = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor)
359
+ if agent_state.multi_agent_group:
360
+ return self.load_multi_agent(agent_state.multi_agent_group, actor, interface, agent_state)
356
361
 
357
362
  interface = interface or self.default_interface_factory()
358
363
  if agent_state.agent_type == AgentType.memgpt_agent:
@@ -364,6 +369,46 @@ class SyncServer(Server):
364
369
 
365
370
  return agent
366
371
 
372
+ def load_multi_agent(
373
+ self, group: Group, actor: User, interface: Union[AgentInterface, None] = None, agent_state: Optional[AgentState] = None
374
+ ) -> Agent:
375
+ match group.manager_type:
376
+ case ManagerType.round_robin:
377
+ agent_state = agent_state or self.agent_manager.get_agent_by_id(agent_id=group.agent_ids[0], actor=actor)
378
+ return RoundRobinMultiAgent(
379
+ agent_state=agent_state,
380
+ interface=interface,
381
+ user=actor,
382
+ group_id=group.id,
383
+ agent_ids=group.agent_ids,
384
+ description=group.description,
385
+ max_turns=group.max_turns,
386
+ )
387
+ case ManagerType.dynamic:
388
+ agent_state = agent_state or self.agent_manager.get_agent_by_id(agent_id=group.manager_agent_id, actor=actor)
389
+ return DynamicMultiAgent(
390
+ agent_state=agent_state,
391
+ interface=interface,
392
+ user=actor,
393
+ group_id=group.id,
394
+ agent_ids=group.agent_ids,
395
+ description=group.description,
396
+ max_turns=group.max_turns,
397
+ termination_token=group.termination_token,
398
+ )
399
+ case ManagerType.supervisor:
400
+ agent_state = agent_state or self.agent_manager.get_agent_by_id(agent_id=group.manager_agent_id, actor=actor)
401
+ return SupervisorMultiAgent(
402
+ agent_state=agent_state,
403
+ interface=interface,
404
+ user=actor,
405
+ group_id=group.id,
406
+ agent_ids=group.agent_ids,
407
+ description=group.description,
408
+ )
409
+ case _:
410
+ raise ValueError(f"Type {group.manager_type} is not supported.")
411
+
367
412
  def _step(
368
413
  self,
369
414
  actor: User,
@@ -690,7 +735,7 @@ class SyncServer(Server):
690
735
  Message(
691
736
  agent_id=agent_id,
692
737
  role=message.role,
693
- content=[TextContent(text=message.content)],
738
+ content=[TextContent(text=message.content)] if message.content else [],
694
739
  name=message.name,
695
740
  # assigned later?
696
741
  model=None,
@@ -800,6 +845,9 @@ class SyncServer(Server):
800
845
  # TODO: @mindy look at moving this to agent_manager to avoid above extra call
801
846
  passages = self.passage_manager.insert_passage(agent_state=agent_state, agent_id=agent_id, text=memory_contents, actor=actor)
802
847
 
848
+ # rebuild agent system prompt - force since no archival change
849
+ self.agent_manager.rebuild_system_prompt(agent_id=agent_id, actor=actor, force=True)
850
+
803
851
  return passages
804
852
 
805
853
  def modify_archival_memory(self, agent_id: str, memory_id: str, passage: PassageUpdate, actor: User) -> List[Passage]:
@@ -809,10 +857,14 @@ class SyncServer(Server):
809
857
 
810
858
  def delete_archival_memory(self, memory_id: str, actor: User):
811
859
  # TODO check if it exists first, and throw error if not
812
- # TODO: @mindy make this return the deleted passage instead
860
+ # TODO: need to also rebuild the prompt here
861
+ passage = self.passage_manager.get_passage_by_id(passage_id=memory_id, actor=actor)
862
+
863
+ # delete the passage
813
864
  self.passage_manager.delete_passage_by_id(passage_id=memory_id, actor=actor)
814
865
 
815
- # TODO: return archival memory
866
+ # rebuild system prompt and force
867
+ self.agent_manager.rebuild_system_prompt(agent_id=passage.agent_id, actor=actor, force=True)
816
868
 
817
869
  def get_agent_recall(
818
870
  self,
@@ -931,6 +983,9 @@ class SyncServer(Server):
931
983
  new_passage_size = self.agent_manager.passage_size(actor=actor, agent_id=agent_id)
932
984
  assert new_passage_size >= curr_passage_size # in case empty files are added
933
985
 
986
+ # rebuild system prompt and force
987
+ self.agent_manager.rebuild_system_prompt(agent_id=agent_id, actor=actor, force=True)
988
+
934
989
  return job
935
990
 
936
991
  def load_data(
@@ -1209,7 +1264,7 @@ class SyncServer(Server):
1209
1264
 
1210
1265
  # MCP wrappers
1211
1266
  # TODO support both command + SSE servers (via config)
1212
- def get_mcp_servers(self) -> dict[str, Union[SSEServerConfig, LocalServerConfig]]:
1267
+ def get_mcp_servers(self) -> dict[str, Union[SSEServerConfig, StdioServerConfig]]:
1213
1268
  """List the MCP servers in the config (doesn't test that they are actually working)"""
1214
1269
  mcp_server_list = {}
1215
1270
 
@@ -1227,8 +1282,8 @@ class SyncServer(Server):
1227
1282
  # Proper formatting is "mcpServers" key at the top level,
1228
1283
  # then a dict with the MCP server name as the key,
1229
1284
  # 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():
1285
+ if MCP_CONFIG_TOPLEVEL_KEY in mcp_config:
1286
+ for server_name, server_params_raw in mcp_config[MCP_CONFIG_TOPLEVEL_KEY].items():
1232
1287
 
1233
1288
  # No support for duplicate server names
1234
1289
  if server_name in mcp_server_list:
@@ -1249,7 +1304,7 @@ class SyncServer(Server):
1249
1304
  else:
1250
1305
  # Attempt to parse the server params as a StdioServerParameters
1251
1306
  try:
1252
- server_params = LocalServerConfig(
1307
+ server_params = StdioServerConfig(
1253
1308
  server_name=server_name,
1254
1309
  command=server_params_raw["command"],
1255
1310
  args=server_params_raw.get("args", []),
@@ -1269,6 +1324,98 @@ class SyncServer(Server):
1269
1324
 
1270
1325
  return self.mcp_clients[mcp_server_name].list_tools()
1271
1326
 
1327
+ def add_mcp_server_to_config(
1328
+ self, server_config: Union[SSEServerConfig, StdioServerConfig], allow_upsert: bool = True
1329
+ ) -> dict[str, Union[SSEServerConfig, StdioServerConfig]]:
1330
+ """Add a new server config to the MCP config file"""
1331
+
1332
+ # If the config file doesn't exist, throw an error.
1333
+ mcp_config_path = os.path.join(constants.LETTA_DIR, constants.MCP_CONFIG_NAME)
1334
+ if not os.path.exists(mcp_config_path):
1335
+ raise FileNotFoundError(f"MCP config file not found: {mcp_config_path}")
1336
+
1337
+ # If the file does exist, attempt to parse it get calling get_mcp_servers
1338
+ try:
1339
+ current_mcp_servers = self.get_mcp_servers()
1340
+ except Exception as e:
1341
+ # Raise an error telling the user to fix the config file
1342
+ logger.error(f"Failed to parse MCP config file at {mcp_config_path}: {e}")
1343
+ raise ValueError(f"Failed to parse MCP config file {mcp_config_path}")
1344
+
1345
+ # Check if the server name is already in the config
1346
+ if server_config.server_name in current_mcp_servers and not allow_upsert:
1347
+ raise ValueError(f"Server name {server_config.server_name} is already in the config file")
1348
+
1349
+ # Attempt to initialize the connection to the server
1350
+ if server_config.type == MCPServerType.SSE:
1351
+ new_mcp_client = SSEMCPClient()
1352
+ elif server_config.type == MCPServerType.STDIO:
1353
+ new_mcp_client = StdioMCPClient()
1354
+ else:
1355
+ raise ValueError(f"Invalid MCP server config: {server_config}")
1356
+ try:
1357
+ new_mcp_client.connect_to_server(server_config)
1358
+ except:
1359
+ logger.exception(f"Failed to connect to MCP server: {server_config.server_name}")
1360
+ raise RuntimeError(f"Failed to connect to MCP server: {server_config.server_name}")
1361
+ # Print out the tools that are connected
1362
+ logger.info(f"Attempting to fetch tools from MCP server: {server_config.server_name}")
1363
+ new_mcp_tools = new_mcp_client.list_tools()
1364
+ logger.info(f"MCP tools connected: {', '.join([t.name for t in new_mcp_tools])}")
1365
+ logger.debug(f"MCP tools: {', '.join([str(t) for t in new_mcp_tools])}")
1366
+
1367
+ # Now that we've confirmed the config is working, let's add it to the client list
1368
+ self.mcp_clients[server_config.server_name] = new_mcp_client
1369
+
1370
+ # Add to the server file
1371
+ current_mcp_servers[server_config.server_name] = server_config
1372
+
1373
+ # Write out the file, and make sure to in include the top-level mcpConfig
1374
+ try:
1375
+ new_mcp_file = {MCP_CONFIG_TOPLEVEL_KEY: {k: v.to_dict() for k, v in current_mcp_servers.items()}}
1376
+ with open(mcp_config_path, "w") as f:
1377
+ json.dump(new_mcp_file, f, indent=4)
1378
+ except Exception as e:
1379
+ logger.error(f"Failed to write MCP config file at {mcp_config_path}: {e}")
1380
+ raise ValueError(f"Failed to write MCP config file {mcp_config_path}")
1381
+
1382
+ return list(current_mcp_servers.values())
1383
+
1384
+ def delete_mcp_server_from_config(self, server_name: str) -> dict[str, Union[SSEServerConfig, StdioServerConfig]]:
1385
+ """Delete a server config from the MCP config file"""
1386
+
1387
+ # If the config file doesn't exist, throw an error.
1388
+ mcp_config_path = os.path.join(constants.LETTA_DIR, constants.MCP_CONFIG_NAME)
1389
+ if not os.path.exists(mcp_config_path):
1390
+ raise FileNotFoundError(f"MCP config file not found: {mcp_config_path}")
1391
+
1392
+ # If the file does exist, attempt to parse it get calling get_mcp_servers
1393
+ try:
1394
+ current_mcp_servers = self.get_mcp_servers()
1395
+ except Exception as e:
1396
+ # Raise an error telling the user to fix the config file
1397
+ logger.error(f"Failed to parse MCP config file at {mcp_config_path}: {e}")
1398
+ raise ValueError(f"Failed to parse MCP config file {mcp_config_path}")
1399
+
1400
+ # Check if the server name is already in the config
1401
+ # If it's not, throw an error
1402
+ if server_name not in current_mcp_servers:
1403
+ raise ValueError(f"Server name {server_name} not found in MCP config file")
1404
+
1405
+ # Remove from the server file
1406
+ del current_mcp_servers[server_name]
1407
+
1408
+ # Write out the file, and make sure to in include the top-level mcpConfig
1409
+ try:
1410
+ new_mcp_file = {MCP_CONFIG_TOPLEVEL_KEY: {k: v.to_dict() for k, v in current_mcp_servers.items()}}
1411
+ with open(mcp_config_path, "w") as f:
1412
+ json.dump(new_mcp_file, f, indent=4)
1413
+ except Exception as e:
1414
+ logger.error(f"Failed to write MCP config file at {mcp_config_path}: {e}")
1415
+ raise ValueError(f"Failed to write MCP config file {mcp_config_path}")
1416
+
1417
+ return list(current_mcp_servers.values())
1418
+
1272
1419
  @trace_method
1273
1420
  async def send_message_to_agent(
1274
1421
  self,
@@ -1403,3 +1550,106 @@ class SyncServer(Server):
1403
1550
 
1404
1551
  traceback.print_exc()
1405
1552
  raise HTTPException(status_code=500, detail=f"{e}")
1553
+
1554
+ @trace_method
1555
+ async def send_group_message_to_agent(
1556
+ self,
1557
+ group_id: str,
1558
+ actor: User,
1559
+ messages: Union[List[Message], List[MessageCreate]],
1560
+ stream_steps: bool,
1561
+ stream_tokens: bool,
1562
+ chat_completion_mode: bool = False,
1563
+ # Support for AssistantMessage
1564
+ use_assistant_message: bool = True,
1565
+ assistant_message_tool_name: str = constants.DEFAULT_MESSAGE_TOOL,
1566
+ assistant_message_tool_kwarg: str = constants.DEFAULT_MESSAGE_TOOL_KWARG,
1567
+ metadata: Optional[dict] = None,
1568
+ ) -> Union[StreamingResponse, LettaResponse]:
1569
+ include_final_message = True
1570
+ if not stream_steps and stream_tokens:
1571
+ raise HTTPException(status_code=400, detail="stream_steps must be 'true' if stream_tokens is 'true'")
1572
+
1573
+ try:
1574
+ # fetch the group
1575
+ group = self.group_manager.retrieve_group(group_id=group_id, actor=actor)
1576
+ letta_multi_agent = self.load_multi_agent(group=group, actor=actor)
1577
+
1578
+ llm_config = letta_multi_agent.agent_state.llm_config
1579
+ supports_token_streaming = ["openai", "anthropic", "deepseek"]
1580
+ if stream_tokens and (
1581
+ llm_config.model_endpoint_type not in supports_token_streaming or "inference.memgpt.ai" in llm_config.model_endpoint
1582
+ ):
1583
+ warnings.warn(
1584
+ 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."
1585
+ )
1586
+ stream_tokens = False
1587
+
1588
+ # Create a new interface per request
1589
+ letta_multi_agent.interface = StreamingServerInterface(
1590
+ use_assistant_message=use_assistant_message,
1591
+ assistant_message_tool_name=assistant_message_tool_name,
1592
+ assistant_message_tool_kwarg=assistant_message_tool_kwarg,
1593
+ inner_thoughts_in_kwargs=(
1594
+ llm_config.put_inner_thoughts_in_kwargs if llm_config.put_inner_thoughts_in_kwargs is not None else False
1595
+ ),
1596
+ )
1597
+ streaming_interface = letta_multi_agent.interface
1598
+ if not isinstance(streaming_interface, StreamingServerInterface):
1599
+ raise ValueError(f"Agent has wrong type of interface: {type(streaming_interface)}")
1600
+ streaming_interface.streaming_mode = stream_tokens
1601
+ streaming_interface.streaming_chat_completion_mode = chat_completion_mode
1602
+ if metadata and hasattr(streaming_interface, "metadata"):
1603
+ streaming_interface.metadata = metadata
1604
+
1605
+ streaming_interface.stream_start()
1606
+ task = asyncio.create_task(
1607
+ asyncio.to_thread(
1608
+ letta_multi_agent.step,
1609
+ messages=messages,
1610
+ chaining=self.chaining,
1611
+ max_chaining_steps=self.max_chaining_steps,
1612
+ )
1613
+ )
1614
+
1615
+ if stream_steps:
1616
+ # return a stream
1617
+ return StreamingResponse(
1618
+ sse_async_generator(
1619
+ streaming_interface.get_generator(),
1620
+ usage_task=task,
1621
+ finish_message=include_final_message,
1622
+ ),
1623
+ media_type="text/event-stream",
1624
+ )
1625
+
1626
+ else:
1627
+ # buffer the stream, then return the list
1628
+ generated_stream = []
1629
+ async for message in streaming_interface.get_generator():
1630
+ assert (
1631
+ isinstance(message, LettaMessage)
1632
+ or isinstance(message, LegacyLettaMessage)
1633
+ or isinstance(message, MessageStreamStatus)
1634
+ ), type(message)
1635
+ generated_stream.append(message)
1636
+ if message == MessageStreamStatus.done:
1637
+ break
1638
+
1639
+ # Get rid of the stream status messages
1640
+ filtered_stream = [d for d in generated_stream if not isinstance(d, MessageStreamStatus)]
1641
+ usage = await task
1642
+
1643
+ # By default the stream will be messages of type LettaMessage or LettaLegacyMessage
1644
+ # If we want to convert these to Message, we can use the attached IDs
1645
+ # NOTE: we will need to de-duplicate the Messsage IDs though (since Assistant->Inner+Func_Call)
1646
+ # TODO: eventually update the interface to use `Message` and `MessageChunk` (new) inside the deque instead
1647
+ return LettaResponse(messages=filtered_stream, usage=usage)
1648
+ except HTTPException:
1649
+ raise
1650
+ except Exception as e:
1651
+ print(e)
1652
+ import traceback
1653
+
1654
+ traceback.print_exc()
1655
+ 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)