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.
- letta/__init__.py +1 -1
- letta/agent.py +83 -23
- letta/agents/low_latency_agent.py +3 -2
- letta/client/client.py +1 -50
- letta/constants.py +4 -1
- letta/functions/function_sets/base.py +1 -1
- letta/functions/function_sets/multi_agent.py +9 -8
- letta/functions/helpers.py +47 -6
- letta/functions/schema_generator.py +47 -0
- letta/helpers/mcp_helpers.py +108 -0
- letta/llm_api/cohere.py +1 -1
- letta/llm_api/google_ai_client.py +332 -0
- letta/llm_api/google_vertex_client.py +214 -0
- letta/llm_api/helpers.py +1 -2
- letta/llm_api/llm_api_tools.py +0 -1
- letta/llm_api/llm_client.py +48 -0
- letta/llm_api/llm_client_base.py +129 -0
- letta/local_llm/utils.py +30 -20
- letta/log.py +1 -1
- letta/memory.py +1 -1
- letta/orm/__init__.py +1 -0
- letta/orm/block.py +8 -0
- letta/orm/enums.py +2 -0
- letta/orm/identities_blocks.py +13 -0
- letta/orm/identity.py +9 -0
- letta/orm/sqlalchemy_base.py +4 -4
- letta/orm/step.py +1 -0
- letta/schemas/block.py +4 -48
- letta/schemas/identity.py +3 -0
- letta/schemas/letta_message.py +26 -0
- letta/schemas/message.py +69 -63
- letta/schemas/step.py +1 -0
- letta/schemas/tool.py +39 -2
- letta/serialize_schemas/agent.py +8 -1
- letta/server/rest_api/app.py +15 -0
- letta/server/rest_api/chat_completions_interface.py +2 -0
- letta/server/rest_api/interface.py +46 -13
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +2 -7
- letta/server/rest_api/routers/v1/agents.py +14 -10
- letta/server/rest_api/routers/v1/blocks.py +5 -1
- letta/server/rest_api/routers/v1/steps.py +2 -0
- letta/server/rest_api/routers/v1/tools.py +71 -1
- letta/server/rest_api/routers/v1/voice.py +3 -6
- letta/server/server.py +102 -5
- letta/services/agent_manager.py +58 -3
- letta/services/block_manager.py +10 -1
- letta/services/helpers/agent_manager_helper.py +12 -1
- letta/services/identity_manager.py +61 -15
- letta/services/message_manager.py +40 -0
- letta/services/step_manager.py +8 -1
- letta/services/summarizer/summarizer.py +1 -1
- letta/services/tool_manager.py +6 -0
- letta/settings.py +11 -12
- {letta_nightly-0.6.37.dev20250311104150.dist-info → letta_nightly-0.6.39.dev20250313104142.dist-info}/METADATA +20 -18
- {letta_nightly-0.6.37.dev20250311104150.dist-info → letta_nightly-0.6.39.dev20250313104142.dist-info}/RECORD +58 -52
- {letta_nightly-0.6.37.dev20250311104150.dist-info → letta_nightly-0.6.39.dev20250313104142.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.37.dev20250311104150.dist-info → letta_nightly-0.6.39.dev20250313104142.dist-info}/WHEEL +0 -0
- {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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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=
|
|
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:
|
|
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:
|
|
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.
|
|
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(
|
|
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
|
|
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
|
-
|
|
605
|
-
|
|
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
|
|
608
|
-
raise ValueError(f"Invalid input: '{
|
|
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,
|
letta/services/agent_manager.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
#
|
|
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,
|
letta/services/block_manager.py
CHANGED
|
@@ -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(
|
|
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,
|
|
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
|
|