letta-nightly 0.6.37.dev20250311104150__py3-none-any.whl → 0.6.39.dev20250313104142__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 (58) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +83 -23
  3. letta/agents/low_latency_agent.py +3 -2
  4. letta/client/client.py +1 -50
  5. letta/constants.py +4 -1
  6. letta/functions/function_sets/base.py +1 -1
  7. letta/functions/function_sets/multi_agent.py +9 -8
  8. letta/functions/helpers.py +47 -6
  9. letta/functions/schema_generator.py +47 -0
  10. letta/helpers/mcp_helpers.py +108 -0
  11. letta/llm_api/cohere.py +1 -1
  12. letta/llm_api/google_ai_client.py +332 -0
  13. letta/llm_api/google_vertex_client.py +214 -0
  14. letta/llm_api/helpers.py +1 -2
  15. letta/llm_api/llm_api_tools.py +0 -1
  16. letta/llm_api/llm_client.py +48 -0
  17. letta/llm_api/llm_client_base.py +129 -0
  18. letta/local_llm/utils.py +30 -20
  19. letta/log.py +1 -1
  20. letta/memory.py +1 -1
  21. letta/orm/__init__.py +1 -0
  22. letta/orm/block.py +8 -0
  23. letta/orm/enums.py +2 -0
  24. letta/orm/identities_blocks.py +13 -0
  25. letta/orm/identity.py +9 -0
  26. letta/orm/sqlalchemy_base.py +4 -4
  27. letta/orm/step.py +1 -0
  28. letta/schemas/block.py +4 -48
  29. letta/schemas/identity.py +3 -0
  30. letta/schemas/letta_message.py +26 -0
  31. letta/schemas/message.py +69 -63
  32. letta/schemas/step.py +1 -0
  33. letta/schemas/tool.py +39 -2
  34. letta/serialize_schemas/agent.py +8 -1
  35. letta/server/rest_api/app.py +15 -0
  36. letta/server/rest_api/chat_completions_interface.py +2 -0
  37. letta/server/rest_api/interface.py +46 -13
  38. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +2 -7
  39. letta/server/rest_api/routers/v1/agents.py +14 -10
  40. letta/server/rest_api/routers/v1/blocks.py +5 -1
  41. letta/server/rest_api/routers/v1/steps.py +2 -0
  42. letta/server/rest_api/routers/v1/tools.py +71 -1
  43. letta/server/rest_api/routers/v1/voice.py +3 -6
  44. letta/server/server.py +102 -5
  45. letta/services/agent_manager.py +58 -3
  46. letta/services/block_manager.py +10 -1
  47. letta/services/helpers/agent_manager_helper.py +12 -1
  48. letta/services/identity_manager.py +61 -15
  49. letta/services/message_manager.py +40 -0
  50. letta/services/step_manager.py +8 -1
  51. letta/services/summarizer/summarizer.py +1 -1
  52. letta/services/tool_manager.py +6 -0
  53. letta/settings.py +11 -12
  54. {letta_nightly-0.6.37.dev20250311104150.dist-info → letta_nightly-0.6.39.dev20250313104142.dist-info}/METADATA +20 -18
  55. {letta_nightly-0.6.37.dev20250311104150.dist-info → letta_nightly-0.6.39.dev20250313104142.dist-info}/RECORD +58 -52
  56. {letta_nightly-0.6.37.dev20250311104150.dist-info → letta_nightly-0.6.39.dev20250313104142.dist-info}/LICENSE +0 -0
  57. {letta_nightly-0.6.37.dev20250311104150.dist-info → letta_nightly-0.6.39.dev20250313104142.dist-info}/WHEEL +0 -0
  58. {letta_nightly-0.6.37.dev20250311104150.dist-info → letta_nightly-0.6.39.dev20250313104142.dist-info}/entry_points.txt +0 -0
@@ -13,13 +13,12 @@ from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
13
13
  from letta.log import get_logger
14
14
  from letta.orm.errors import NoResultFound
15
15
  from letta.schemas.agent import AgentState, CreateAgent, UpdateAgent
16
- from letta.schemas.block import Block, BlockUpdate, CreateBlock # , BlockLabelUpdate, BlockLimitUpdate
16
+ from letta.schemas.block import Block, BlockUpdate
17
17
  from letta.schemas.job import JobStatus, JobUpdate, LettaRequestConfig
18
- from letta.schemas.letta_message import LettaMessageUnion
18
+ from letta.schemas.letta_message import LettaMessageUnion, LettaMessageUpdateUnion
19
19
  from letta.schemas.letta_request import LettaRequest, LettaStreamingRequest
20
20
  from letta.schemas.letta_response import LettaResponse
21
21
  from letta.schemas.memory import ContextWindowOverview, CreateArchivalMemory, Memory
22
- from letta.schemas.message import Message, MessageUpdate
23
22
  from letta.schemas.passage import Passage, PassageUpdate
24
23
  from letta.schemas.run import Run
25
24
  from letta.schemas.source import Source
@@ -54,7 +53,7 @@ def list_agents(
54
53
  project_id: Optional[str] = Query(None, description="Search agents by project id"),
55
54
  template_id: Optional[str] = Query(None, description="Search agents by template id"),
56
55
  base_template_id: Optional[str] = Query(None, description="Search agents by base template id"),
57
- identifier_id: Optional[str] = Query(None, description="Search agents by identifier id"),
56
+ identity_id: Optional[str] = Query(None, description="Search agents by identifier id"),
58
57
  identifier_keys: Optional[List[str]] = Query(None, description="Search agents by identifier keys"),
59
58
  ):
60
59
  """
@@ -85,7 +84,7 @@ def list_agents(
85
84
  tags=tags,
86
85
  match_all_tags=match_all_tags,
87
86
  identifier_keys=identifier_keys,
88
- identifier_id=identifier_id,
87
+ identity_id=identity_id,
89
88
  **kwargs,
90
89
  )
91
90
  return agents
@@ -119,6 +118,7 @@ async def upload_agent_serialized(
119
118
  True,
120
119
  description="If set to True, existing tools can get their source code overwritten by the uploaded tool definitions. Note that Letta core tools can never be updated externally.",
121
120
  ),
121
+ project_id: Optional[str] = Query(None, description="The project ID to associate the uploaded agent with."),
122
122
  ):
123
123
  """
124
124
  Upload a serialized agent JSON file and recreate the agent in the system.
@@ -129,7 +129,11 @@ async def upload_agent_serialized(
129
129
  serialized_data = await file.read()
130
130
  agent_json = json.loads(serialized_data)
131
131
  new_agent = server.agent_manager.deserialize(
132
- serialized_agent=agent_json, actor=actor, append_copy_suffix=append_copy_suffix, override_existing_tools=override_existing_tools
132
+ serialized_agent=agent_json,
133
+ actor=actor,
134
+ append_copy_suffix=append_copy_suffix,
135
+ override_existing_tools=override_existing_tools,
136
+ project_id=project_id,
133
137
  )
134
138
  return new_agent
135
139
 
@@ -526,20 +530,20 @@ def list_messages(
526
530
  )
527
531
 
528
532
 
529
- @router.patch("/{agent_id}/messages/{message_id}", response_model=Message, operation_id="modify_message")
533
+ @router.patch("/{agent_id}/messages/{message_id}", response_model=LettaMessageUpdateUnion, operation_id="modify_message")
530
534
  def modify_message(
531
535
  agent_id: str,
532
536
  message_id: str,
533
- request: MessageUpdate = Body(...),
537
+ request: LettaMessageUpdateUnion = Body(...),
534
538
  server: "SyncServer" = Depends(get_letta_server),
535
539
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
536
540
  ):
537
541
  """
538
542
  Update the details of a message associated with an agent.
539
543
  """
540
- # TODO: Get rid of agent_id here, it's not really relevant
544
+ # TODO: support modifying tool calls/returns
541
545
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
542
- return server.message_manager.update_message_by_id(message_id=message_id, message_update=request, actor=actor)
546
+ return server.message_manager.update_message_by_letta_message(message_id=message_id, letta_message_update=request, actor=actor)
543
547
 
544
548
 
545
549
  @router.post(
@@ -20,11 +20,15 @@ def list_blocks(
20
20
  label: Optional[str] = Query(None, description="Labels to include (e.g. human, persona)"),
21
21
  templates_only: bool = Query(True, description="Whether to include only templates"),
22
22
  name: Optional[str] = Query(None, description="Name of the block"),
23
+ identity_id: Optional[str] = Query(None, description="Search agents by identifier id"),
24
+ identifier_keys: Optional[List[str]] = Query(None, description="Search agents by identifier keys"),
23
25
  server: SyncServer = Depends(get_letta_server),
24
26
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
25
27
  ):
26
28
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
27
- return server.block_manager.get_blocks(actor=actor, label=label, is_template=templates_only, template_name=name)
29
+ return server.block_manager.get_blocks(
30
+ actor=actor, label=label, is_template=templates_only, template_name=name, identity_id=identity_id, identifier_keys=identifier_keys
31
+ )
28
32
 
29
33
 
30
34
  @router.post("/", response_model=Block, operation_id="create_block")
@@ -20,6 +20,7 @@ def list_steps(
20
20
  start_date: Optional[str] = Query(None, description='Return steps after this ISO datetime (e.g. "2025-01-29T15:01:19-08:00")'),
21
21
  end_date: Optional[str] = Query(None, description='Return steps before this ISO datetime (e.g. "2025-01-29T15:01:19-08:00")'),
22
22
  model: Optional[str] = Query(None, description="Filter by the name of the model used for the step"),
23
+ agent_id: Optional[str] = Query(None, description="Filter by the ID of the agent that performed the step"),
23
24
  server: SyncServer = Depends(get_letta_server),
24
25
  actor_id: Optional[str] = Header(None, alias="user_id"),
25
26
  ):
@@ -42,6 +43,7 @@ def list_steps(
42
43
  limit=limit,
43
44
  order=order,
44
45
  model=model,
46
+ agent_id=agent_id,
45
47
  )
46
48
 
47
49
 
@@ -1,4 +1,4 @@
1
- from typing import List, Optional
1
+ from typing import List, Optional, Union
2
2
 
3
3
  from composio.client import ComposioClientError, HTTPError, NoItemsFound
4
4
  from composio.client.collections import ActionModel, AppModel
@@ -13,6 +13,7 @@ from fastapi import APIRouter, Body, Depends, Header, HTTPException
13
13
 
14
14
  from letta.errors import LettaToolCreateError
15
15
  from letta.helpers.composio_helpers import get_composio_api_key
16
+ from letta.helpers.mcp_helpers import LocalServerConfig, MCPTool, SSEServerConfig
16
17
  from letta.log import get_logger
17
18
  from letta.orm.errors import UniqueConstraintViolationError
18
19
  from letta.schemas.letta_message import ToolReturnMessage
@@ -329,3 +330,72 @@ def add_composio_tool(
329
330
  "composio_action_name": composio_action_name,
330
331
  },
331
332
  )
333
+
334
+
335
+ # Specific routes for MCP
336
+ @router.get("/mcp/servers", response_model=dict[str, Union[SSEServerConfig, LocalServerConfig]], operation_id="list_mcp_servers")
337
+ def list_mcp_servers(server: SyncServer = Depends(get_letta_server), user_id: Optional[str] = Header(None, alias="user_id")):
338
+ """
339
+ Get a list of all configured MCP servers
340
+ """
341
+ actor = server.user_manager.get_user_or_default(user_id=user_id)
342
+ return server.get_mcp_servers()
343
+
344
+
345
+ # NOTE: async because the MCP client/session calls are async
346
+ # TODO: should we make the return type MCPTool, not Tool (since we don't have ID)?
347
+ @router.get("/mcp/servers/{mcp_server_name}/tools", response_model=List[MCPTool], operation_id="list_mcp_tools_by_server")
348
+ def list_mcp_tools_by_server(
349
+ mcp_server_name: str,
350
+ server: SyncServer = Depends(get_letta_server),
351
+ actor_id: Optional[str] = Header(None, alias="user_id"),
352
+ ):
353
+ """
354
+ Get a list of all tools for a specific MCP server
355
+ """
356
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
357
+ try:
358
+ return server.get_tools_from_mcp_server(mcp_server_name=mcp_server_name)
359
+ except ValueError as e:
360
+ # ValueError means that the MCP server name doesn't exist
361
+ raise HTTPException(
362
+ status_code=400, # Bad Request
363
+ detail={
364
+ "code": "MCPServerNotFoundError",
365
+ "message": str(e),
366
+ "mcp_server_name": mcp_server_name,
367
+ },
368
+ )
369
+
370
+
371
+ @router.post("/mcp/servers/{mcp_server_name}/{mcp_tool_name}", response_model=Tool, operation_id="add_mcp_tool")
372
+ def add_mcp_tool(
373
+ mcp_server_name: str,
374
+ mcp_tool_name: str,
375
+ server: SyncServer = Depends(get_letta_server),
376
+ actor_id: Optional[str] = Header(None, alias="user_id"),
377
+ ):
378
+ """
379
+ Add a new MCP tool by server + tool name
380
+ """
381
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
382
+
383
+ available_tools = server.get_tools_from_mcp_server(mcp_server_name=mcp_server_name)
384
+ # See if the tool is in the avaialable list
385
+ mcp_tool = None
386
+ for tool in available_tools:
387
+ if tool.name == mcp_tool_name:
388
+ mcp_tool = tool
389
+ break
390
+ if not mcp_tool:
391
+ raise HTTPException(
392
+ status_code=400, # Bad Request
393
+ detail={
394
+ "code": "MCPToolNotFoundError",
395
+ "message": f"Tool {mcp_tool_name} not found in MCP server {mcp_server_name} - available tools: {', '.join([tool.name for tool in available_tools])}",
396
+ "mcp_tool_name": mcp_tool_name,
397
+ },
398
+ )
399
+
400
+ tool_create = ToolCreate.from_mcp(mcp_server_name=mcp_server_name, mcp_tool=mcp_tool)
401
+ return server.tool_manager.create_or_update_mcp_tool(tool_create=tool_create, actor=actor)
@@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Optional
2
2
 
3
3
  import httpx
4
4
  import openai
5
- from fastapi import APIRouter, Body, Depends, Header, HTTPException
5
+ from fastapi import APIRouter, Body, Depends, Header
6
6
  from fastapi.responses import StreamingResponse
7
7
  from openai.types.chat.completion_create_params import CompletionCreateParams
8
8
 
@@ -22,7 +22,7 @@ logger = get_logger(__name__)
22
22
 
23
23
 
24
24
  @router.post(
25
- "/chat/completions",
25
+ "/{agent_id}/chat/completions",
26
26
  response_model=None,
27
27
  operation_id="create_voice_chat_completions",
28
28
  responses={
@@ -35,16 +35,13 @@ logger = get_logger(__name__)
35
35
  },
36
36
  )
37
37
  async def create_voice_chat_completions(
38
+ agent_id: str,
38
39
  completion_request: CompletionCreateParams = Body(...),
39
40
  server: "SyncServer" = Depends(get_letta_server),
40
41
  user_id: Optional[str] = Header(None, alias="user_id"),
41
42
  ):
42
43
  actor = server.user_manager.get_user_or_default(user_id=user_id)
43
44
 
44
- agent_id = str(completion_request.get("user", None))
45
- if agent_id is None:
46
- raise HTTPException(status_code=400, detail="Must pass agent_id in the 'user' field")
47
-
48
45
  # Also parse the user's new input
49
46
  input_message = UserMessage(**get_messages_from_completion_request(completion_request)[-1])
50
47
 
letta/server/server.py CHANGED
@@ -21,6 +21,15 @@ from letta.config import LettaConfig
21
21
  from letta.data_sources.connectors import DataConnector, load_data
22
22
  from letta.helpers.datetime_helpers import get_utc_time
23
23
  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
+ )
24
33
 
25
34
  # TODO use custom interface
26
35
  from letta.interface import AgentInterface # abstract
@@ -314,6 +323,31 @@ class SyncServer(Server):
314
323
  if model_settings.xai_api_key:
315
324
  self._enabled_providers.append(xAIProvider(api_key=model_settings.xai_api_key))
316
325
 
326
+ # For MCP
327
+ """Initialize the MCP clients (there may be multiple)"""
328
+ mcp_server_configs = self.get_mcp_servers()
329
+ self.mcp_clients: Dict[str, BaseMCPClient] = {}
330
+
331
+ for server_name, server_config in mcp_server_configs.items():
332
+ if server_config.type == MCPServerType.SSE:
333
+ self.mcp_clients[server_name] = SSEMCPClient()
334
+ elif server_config.type == MCPServerType.LOCAL:
335
+ self.mcp_clients[server_name] = LocalMCPClient()
336
+ else:
337
+ raise ValueError(f"Invalid MCP server config: {server_config}")
338
+ try:
339
+ 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
+
344
+ # Print out the tools that are connected
345
+ for server_name, client in self.mcp_clients.items():
346
+ logger.info(f"Attempting to fetch tools from MCP server: {server_name}")
347
+ mcp_tools = client.list_tools()
348
+ logger.info(f"MCP tools connected: {', '.join([t.name for t in mcp_tools])}")
349
+ logger.debug(f"MCP tools: {', '.join([str(t) for t in mcp_tools])}")
350
+
317
351
  def load_agent(self, agent_id: str, actor: User, interface: Union[AgentInterface, None] = None) -> Agent:
318
352
  """Updated method to load agents from persisted storage"""
319
353
  agent_lock = self.per_agent_lock_manager.get_lock(agent_id)
@@ -322,7 +356,7 @@ class SyncServer(Server):
322
356
 
323
357
  interface = interface or self.default_interface_factory()
324
358
  if agent_state.agent_type == AgentType.memgpt_agent:
325
- agent = Agent(agent_state=agent_state, interface=interface, user=actor)
359
+ agent = Agent(agent_state=agent_state, interface=interface, user=actor, mcp_clients=self.mcp_clients)
326
360
  elif agent_state.agent_type == AgentType.offline_memory_agent:
327
361
  agent = OfflineMemoryAgent(agent_state=agent_state, interface=interface, user=actor)
328
362
  else:
@@ -601,11 +635,12 @@ class SyncServer(Server):
601
635
 
602
636
  if isinstance(message, Message):
603
637
  # Can't have a null text field
604
- if message.text is None or len(message.text) == 0:
605
- raise ValueError(f"Invalid input: '{message.text}'")
638
+ message_text = message.content[0].text
639
+ if message_text is None or len(message_text) == 0:
640
+ raise ValueError(f"Invalid input: '{message_text}'")
606
641
  # If the input begins with a command prefix, reject
607
- elif message.text.startswith("/"):
608
- raise ValueError(f"Invalid input: '{message.text}'")
642
+ elif message_text.startswith("/"):
643
+ raise ValueError(f"Invalid input: '{message_text}'")
609
644
 
610
645
  else:
611
646
  raise TypeError(f"Invalid input: '{message}' - type {type(message)}")
@@ -1172,6 +1207,68 @@ class SyncServer(Server):
1172
1207
  actions = self.get_composio_client(api_key=api_key).actions.get(apps=[composio_app_name])
1173
1208
  return actions
1174
1209
 
1210
+ # MCP wrappers
1211
+ # TODO support both command + SSE servers (via config)
1212
+ def get_mcp_servers(self) -> dict[str, Union[SSEServerConfig, LocalServerConfig]]:
1213
+ """List the MCP servers in the config (doesn't test that they are actually working)"""
1214
+ mcp_server_list = {}
1215
+
1216
+ # Attempt to read from ~/.letta/mcp_config.json
1217
+ mcp_config_path = os.path.join(constants.LETTA_DIR, constants.MCP_CONFIG_NAME)
1218
+ if os.path.exists(mcp_config_path):
1219
+ with open(mcp_config_path, "r") as f:
1220
+
1221
+ try:
1222
+ mcp_config = json.load(f)
1223
+ except Exception as e:
1224
+ logger.error(f"Failed to parse MCP config file ({mcp_config_path}) as json: {e}")
1225
+ return mcp_server_list
1226
+
1227
+ # Proper formatting is "mcpServers" key at the top level,
1228
+ # then a dict with the MCP server name as the key,
1229
+ # 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():
1232
+
1233
+ # No support for duplicate server names
1234
+ if server_name in mcp_server_list:
1235
+ logger.error(f"Duplicate MCP server name found (skipping): {server_name}")
1236
+ continue
1237
+
1238
+ if "url" in server_params_raw:
1239
+ # Attempt to parse the server params as an SSE server
1240
+ try:
1241
+ server_params = SSEServerConfig(
1242
+ server_name=server_name,
1243
+ server_url=server_params_raw["url"],
1244
+ )
1245
+ mcp_server_list[server_name] = server_params
1246
+ except Exception as e:
1247
+ logger.error(f"Failed to parse server params for MCP server {server_name} (skipping): {e}")
1248
+ continue
1249
+ else:
1250
+ # Attempt to parse the server params as a StdioServerParameters
1251
+ try:
1252
+ server_params = LocalServerConfig(
1253
+ server_name=server_name,
1254
+ command=server_params_raw["command"],
1255
+ args=server_params_raw.get("args", []),
1256
+ )
1257
+ mcp_server_list[server_name] = server_params
1258
+ except Exception as e:
1259
+ logger.error(f"Failed to parse server params for MCP server {server_name} (skipping): {e}")
1260
+ continue
1261
+
1262
+ # If the file doesn't exist, return empty dictionary
1263
+ return mcp_server_list
1264
+
1265
+ def get_tools_from_mcp_server(self, mcp_server_name: str) -> List[MCPTool]:
1266
+ """List the tools in an MCP server. Requires a client to be created."""
1267
+ if mcp_server_name not in self.mcp_clients:
1268
+ raise ValueError(f"No client was created for MCP server: {mcp_server_name}")
1269
+
1270
+ return self.mcp_clients[mcp_server_name].list_tools()
1271
+
1175
1272
  @trace_method
1176
1273
  async def send_message_to_agent(
1177
1274
  self,
@@ -337,6 +337,7 @@ class AgentManager:
337
337
  match_all_tags: bool = False,
338
338
  query_text: Optional[str] = None,
339
339
  identifier_keys: Optional[List[str]] = None,
340
+ identity_id: Optional[str] = None,
340
341
  **kwargs,
341
342
  ) -> List[PydanticAgentState]:
342
343
  """
@@ -353,11 +354,55 @@ class AgentManager:
353
354
  organization_id=actor.organization_id if actor else None,
354
355
  query_text=query_text,
355
356
  identifier_keys=identifier_keys,
357
+ identity_id=identity_id,
356
358
  **kwargs,
357
359
  )
358
360
 
359
361
  return [agent.to_pydantic() for agent in agents]
360
362
 
363
+ @enforce_types
364
+ def list_agents_matching_tags(
365
+ self,
366
+ actor: PydanticUser,
367
+ match_all: List[str],
368
+ match_some: List[str],
369
+ limit: Optional[int] = 50,
370
+ ) -> List[PydanticAgentState]:
371
+ """
372
+ Retrieves agents in the same organization that match all specified `match_all` tags
373
+ and at least one tag from `match_some`. The query is optimized for efficiency by
374
+ leveraging indexed filtering and aggregation.
375
+
376
+ Args:
377
+ actor (PydanticUser): The user requesting the agent list.
378
+ match_all (List[str]): Agents must have all these tags.
379
+ match_some (List[str]): Agents must have at least one of these tags.
380
+ limit (Optional[int]): Maximum number of agents to return.
381
+
382
+ Returns:
383
+ List[PydanticAgentState: The filtered list of matching agents.
384
+ """
385
+ with self.session_maker() as session:
386
+ query = select(AgentModel).where(AgentModel.organization_id == actor.organization_id)
387
+
388
+ if match_all:
389
+ # Subquery to find agent IDs that contain all match_all tags
390
+ subquery = (
391
+ select(AgentsTags.agent_id)
392
+ .where(AgentsTags.tag.in_(match_all))
393
+ .group_by(AgentsTags.agent_id)
394
+ .having(func.count(AgentsTags.tag) == literal(len(match_all)))
395
+ )
396
+ query = query.where(AgentModel.id.in_(subquery))
397
+
398
+ if match_some:
399
+ # Ensures agents match at least one tag in match_some
400
+ query = query.join(AgentsTags).where(AgentsTags.tag.in_(match_some))
401
+
402
+ query = query.group_by(AgentModel.id).limit(limit)
403
+
404
+ return list(session.execute(query).scalars())
405
+
361
406
  @enforce_types
362
407
  def get_agent_by_id(self, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
363
408
  """Fetch an agent by its ID."""
@@ -401,7 +446,12 @@ class AgentManager:
401
446
 
402
447
  @enforce_types
403
448
  def deserialize(
404
- self, serialized_agent: dict, actor: PydanticUser, append_copy_suffix: bool = True, override_existing_tools: bool = True
449
+ self,
450
+ serialized_agent: dict,
451
+ actor: PydanticUser,
452
+ append_copy_suffix: bool = True,
453
+ override_existing_tools: bool = True,
454
+ project_id: Optional[str] = None,
405
455
  ) -> PydanticAgentState:
406
456
  tool_data_list = serialized_agent.pop("tools", [])
407
457
 
@@ -410,7 +460,9 @@ class AgentManager:
410
460
  agent = schema.load(serialized_agent, session=session)
411
461
  if append_copy_suffix:
412
462
  agent.name += "_copy"
413
- agent.create(session, actor=actor)
463
+ if project_id:
464
+ agent.project_id = project_id
465
+ agent = agent.create(session, actor=actor)
414
466
  pydantic_agent = agent.to_pydantic()
415
467
 
416
468
  # Need to do this separately as there's some fancy upsert logic that SqlAlchemy cannot handle
@@ -548,6 +600,7 @@ class AgentManager:
548
600
  system_prompt=agent_state.system,
549
601
  in_context_memory=agent_state.memory,
550
602
  in_context_memory_last_edit=memory_edit_timestamp,
603
+ recent_passages=self.list_passages(actor=actor, agent_id=agent_id, ascending=False, limit=10),
551
604
  )
552
605
 
553
606
  diff = united_diff(curr_system_message_openai["content"], new_system_message_str)
@@ -718,7 +771,9 @@ class AgentManager:
718
771
  # Commit the changes
719
772
  agent.update(session, actor=actor)
720
773
 
721
- # Add system messsage alert to agent
774
+ # Force rebuild of system prompt so that the agent is updated with passage count
775
+ # and recent passages and add system message alert to agent
776
+ self.rebuild_system_prompt(agent_id=agent_id, actor=actor, force=True)
722
777
  self.append_system_message(
723
778
  agent_id=agent_id,
724
779
  content=DATA_SOURCE_ATTACH_ALERT,
@@ -64,6 +64,8 @@ class BlockManager:
64
64
  label: Optional[str] = None,
65
65
  is_template: Optional[bool] = None,
66
66
  template_name: Optional[str] = None,
67
+ identifier_keys: Optional[List[str]] = None,
68
+ identity_id: Optional[str] = None,
67
69
  id: Optional[str] = None,
68
70
  after: Optional[str] = None,
69
71
  limit: Optional[int] = 50,
@@ -81,7 +83,14 @@ class BlockManager:
81
83
  if id:
82
84
  filters["id"] = id
83
85
 
84
- blocks = BlockModel.list(db_session=session, after=after, limit=limit, **filters)
86
+ blocks = BlockModel.list(
87
+ db_session=session,
88
+ after=after,
89
+ limit=limit,
90
+ identifier_keys=identifier_keys,
91
+ identity_id=identity_id,
92
+ **filters,
93
+ )
85
94
 
86
95
  return [block.to_pydantic() for block in blocks]
87
96
 
@@ -13,6 +13,7 @@ from letta.schemas.agent import AgentState, AgentType
13
13
  from letta.schemas.enums import MessageRole
14
14
  from letta.schemas.memory import Memory
15
15
  from letta.schemas.message import Message, MessageCreate, TextContent
16
+ from letta.schemas.passage import Passage as PydanticPassage
16
17
  from letta.schemas.tool_rule import ToolRule
17
18
  from letta.schemas.user import User
18
19
  from letta.system import get_initial_boot_messages, get_login_event
@@ -99,7 +100,10 @@ def derive_system_message(agent_type: AgentType, system: Optional[str] = None):
99
100
 
100
101
  # TODO: This code is kind of wonky and deserves a rewrite
101
102
  def compile_memory_metadata_block(
102
- memory_edit_timestamp: datetime.datetime, previous_message_count: int = 0, archival_memory_size: int = 0
103
+ memory_edit_timestamp: datetime.datetime,
104
+ previous_message_count: int = 0,
105
+ archival_memory_size: int = 0,
106
+ recent_passages: List[PydanticPassage] = None,
103
107
  ) -> str:
104
108
  # Put the timestamp in the local timezone (mimicking get_local_time())
105
109
  timestamp_str = memory_edit_timestamp.astimezone().strftime("%Y-%m-%d %I:%M:%S %p %Z%z").strip()
@@ -110,6 +114,11 @@ def compile_memory_metadata_block(
110
114
  f"### Memory [last modified: {timestamp_str}]",
111
115
  f"{previous_message_count} previous messages between you and the user are stored in recall memory (use functions to access them)",
112
116
  f"{archival_memory_size} total memories you created are stored in archival memory (use functions to access them)",
117
+ (
118
+ f"Most recent archival passages {len(recent_passages)} recent passages: {[passage.text for passage in recent_passages]}"
119
+ if recent_passages is not None
120
+ else ""
121
+ ),
113
122
  "\nCore memory shown below (limited in size, additional information stored in archival / recall memory):",
114
123
  ]
115
124
  )
@@ -146,6 +155,7 @@ def compile_system_message(
146
155
  template_format: Literal["f-string", "mustache", "jinja2"] = "f-string",
147
156
  previous_message_count: int = 0,
148
157
  archival_memory_size: int = 0,
158
+ recent_passages: Optional[List[PydanticPassage]] = None,
149
159
  ) -> str:
150
160
  """Prepare the final/full system message that will be fed into the LLM API
151
161
 
@@ -170,6 +180,7 @@ def compile_system_message(
170
180
  memory_edit_timestamp=in_context_memory_last_edit,
171
181
  previous_message_count=previous_message_count,
172
182
  archival_memory_size=archival_memory_size,
183
+ recent_passages=recent_passages,
173
184
  )
174
185
  full_memory_string = memory_metadata_string + "\n" + in_context_memory.compile()
175
186