letta-nightly 0.6.38.dev20250312104155__py3-none-any.whl → 0.6.39.dev20250313162623__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 +49 -11
- letta/agents/low_latency_agent.py +3 -2
- letta/constants.py +3 -0
- letta/functions/function_sets/base.py +1 -1
- letta/functions/helpers.py +14 -0
- letta/functions/schema_generator.py +47 -0
- letta/helpers/mcp_helpers.py +108 -0
- letta/llm_api/cohere.py +1 -1
- letta/llm_api/helpers.py +1 -2
- letta/llm_api/llm_api_tools.py +0 -1
- 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/schemas/identity.py +3 -0
- letta/schemas/message.py +68 -62
- letta/schemas/tool.py +39 -2
- 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/v1/agents.py +2 -2
- letta/server/rest_api/routers/v1/blocks.py +5 -1
- letta/server/rest_api/routers/v1/tools.py +71 -1
- letta/server/server.py +102 -5
- letta/services/agent_manager.py +2 -0
- letta/services/block_manager.py +10 -1
- letta/services/identity_manager.py +54 -14
- letta/services/summarizer/summarizer.py +1 -1
- letta/services/tool_manager.py +6 -0
- letta/settings.py +11 -12
- {letta_nightly-0.6.38.dev20250312104155.dist-info → letta_nightly-0.6.39.dev20250313162623.dist-info}/METADATA +4 -3
- {letta_nightly-0.6.38.dev20250312104155.dist-info → letta_nightly-0.6.39.dev20250313162623.dist-info}/RECORD +41 -39
- {letta_nightly-0.6.38.dev20250312104155.dist-info → letta_nightly-0.6.39.dev20250313162623.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.38.dev20250312104155.dist-info → letta_nightly-0.6.39.dev20250313162623.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.38.dev20250312104155.dist-info → letta_nightly-0.6.39.dev20250313162623.dist-info}/entry_points.txt +0 -0
|
@@ -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)
|
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,6 +354,7 @@ 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
|
|
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
|
|
|
@@ -5,6 +5,7 @@ from sqlalchemy.exc import NoResultFound
|
|
|
5
5
|
from sqlalchemy.orm import Session
|
|
6
6
|
|
|
7
7
|
from letta.orm.agent import Agent as AgentModel
|
|
8
|
+
from letta.orm.block import Block as BlockModel
|
|
8
9
|
from letta.orm.identity import Identity as IdentityModel
|
|
9
10
|
from letta.schemas.identity import Identity as PydanticIdentity
|
|
10
11
|
from letta.schemas.identity import IdentityCreate, IdentityType, IdentityUpdate
|
|
@@ -58,9 +59,24 @@ class IdentityManager:
|
|
|
58
59
|
@enforce_types
|
|
59
60
|
def create_identity(self, identity: IdentityCreate, actor: PydanticUser) -> PydanticIdentity:
|
|
60
61
|
with self.session_maker() as session:
|
|
61
|
-
new_identity = IdentityModel(**identity.model_dump(exclude={"agent_ids"}, exclude_unset=True))
|
|
62
|
+
new_identity = IdentityModel(**identity.model_dump(exclude={"agent_ids", "block_ids"}, exclude_unset=True))
|
|
62
63
|
new_identity.organization_id = actor.organization_id
|
|
63
|
-
self.
|
|
64
|
+
self._process_relationship(
|
|
65
|
+
session=session,
|
|
66
|
+
identity=new_identity,
|
|
67
|
+
relationship_name="agents",
|
|
68
|
+
model_class=AgentModel,
|
|
69
|
+
item_ids=identity.agent_ids,
|
|
70
|
+
allow_partial=False,
|
|
71
|
+
)
|
|
72
|
+
self._process_relationship(
|
|
73
|
+
session=session,
|
|
74
|
+
identity=new_identity,
|
|
75
|
+
relationship_name="blocks",
|
|
76
|
+
model_class=BlockModel,
|
|
77
|
+
item_ids=identity.block_ids,
|
|
78
|
+
allow_partial=False,
|
|
79
|
+
)
|
|
64
80
|
new_identity.create(session, actor=actor)
|
|
65
81
|
return new_identity.to_pydantic()
|
|
66
82
|
|
|
@@ -124,9 +140,26 @@ class IdentityManager:
|
|
|
124
140
|
new_properties = existing_identity.properties + [prop.model_dump() for prop in identity.properties]
|
|
125
141
|
existing_identity.properties = new_properties
|
|
126
142
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
143
|
+
if identity.agent_ids is not None:
|
|
144
|
+
self._process_relationship(
|
|
145
|
+
session=session,
|
|
146
|
+
identity=existing_identity,
|
|
147
|
+
relationship_name="agents",
|
|
148
|
+
model_class=AgentModel,
|
|
149
|
+
item_ids=identity.agent_ids,
|
|
150
|
+
allow_partial=False,
|
|
151
|
+
replace=replace,
|
|
152
|
+
)
|
|
153
|
+
if identity.block_ids is not None:
|
|
154
|
+
self._process_relationship(
|
|
155
|
+
session=session,
|
|
156
|
+
identity=existing_identity,
|
|
157
|
+
relationship_name="blocks",
|
|
158
|
+
model_class=BlockModel,
|
|
159
|
+
item_ids=identity.block_ids,
|
|
160
|
+
allow_partial=False,
|
|
161
|
+
replace=replace,
|
|
162
|
+
)
|
|
130
163
|
existing_identity.update(session, actor=actor)
|
|
131
164
|
return existing_identity.to_pydantic()
|
|
132
165
|
|
|
@@ -141,26 +174,33 @@ class IdentityManager:
|
|
|
141
174
|
session.delete(identity)
|
|
142
175
|
session.commit()
|
|
143
176
|
|
|
144
|
-
def
|
|
145
|
-
self,
|
|
177
|
+
def _process_relationship(
|
|
178
|
+
self,
|
|
179
|
+
session: Session,
|
|
180
|
+
identity: PydanticIdentity,
|
|
181
|
+
relationship_name: str,
|
|
182
|
+
model_class,
|
|
183
|
+
item_ids: List[str],
|
|
184
|
+
allow_partial=False,
|
|
185
|
+
replace=True,
|
|
146
186
|
):
|
|
147
|
-
current_relationship = getattr(identity,
|
|
148
|
-
if not
|
|
187
|
+
current_relationship = getattr(identity, relationship_name, [])
|
|
188
|
+
if not item_ids:
|
|
149
189
|
if replace:
|
|
150
|
-
setattr(identity,
|
|
190
|
+
setattr(identity, relationship_name, [])
|
|
151
191
|
return
|
|
152
192
|
|
|
153
193
|
# Retrieve models for the provided IDs
|
|
154
|
-
found_items = session.query(
|
|
194
|
+
found_items = session.query(model_class).filter(model_class.id.in_(item_ids)).all()
|
|
155
195
|
|
|
156
196
|
# Validate all items are found if allow_partial is False
|
|
157
|
-
if not allow_partial and len(found_items) != len(
|
|
158
|
-
missing = set(
|
|
197
|
+
if not allow_partial and len(found_items) != len(item_ids):
|
|
198
|
+
missing = set(item_ids) - {item.id for item in found_items}
|
|
159
199
|
raise NoResultFound(f"Items not found in agents: {missing}")
|
|
160
200
|
|
|
161
201
|
if replace:
|
|
162
202
|
# Replace the relationship
|
|
163
|
-
setattr(identity,
|
|
203
|
+
setattr(identity, relationship_name, found_items)
|
|
164
204
|
else:
|
|
165
205
|
# Extend the relationship (only add new items)
|
|
166
206
|
current_ids = {item.id for item in current_relationship}
|
|
@@ -96,7 +96,7 @@ class Summarizer:
|
|
|
96
96
|
)
|
|
97
97
|
|
|
98
98
|
messages = await self.summarizer_agent.step(UserMessage(content=summary_request_text))
|
|
99
|
-
current_summary = "\n".join([m.text for m in messages])
|
|
99
|
+
current_summary = "\n".join([m.content[0].text for m in messages])
|
|
100
100
|
current_summary = f"{self.summary_prefix}{current_summary}"
|
|
101
101
|
|
|
102
102
|
return updated_in_context_messages, current_summary, True
|
letta/services/tool_manager.py
CHANGED
|
@@ -56,6 +56,12 @@ class ToolManager:
|
|
|
56
56
|
|
|
57
57
|
return tool
|
|
58
58
|
|
|
59
|
+
@enforce_types
|
|
60
|
+
def create_or_update_mcp_tool(self, tool_create: ToolCreate, actor: PydanticUser) -> PydanticTool:
|
|
61
|
+
return self.create_or_update_tool(
|
|
62
|
+
PydanticTool(tool_type=ToolType.EXTERNAL_MCP, name=tool_create.json_schema["name"], **tool_create.model_dump()), actor
|
|
63
|
+
)
|
|
64
|
+
|
|
59
65
|
@enforce_types
|
|
60
66
|
def create_or_update_composio_tool(self, tool_create: ToolCreate, actor: PydanticUser) -> PydanticTool:
|
|
61
67
|
return self.create_or_update_tool(
|
letta/settings.py
CHANGED
|
@@ -119,18 +119,17 @@ class ModelSettings(BaseSettings):
|
|
|
119
119
|
|
|
120
120
|
env_cors_origins = os.getenv("ACCEPTABLE_ORIGINS")
|
|
121
121
|
|
|
122
|
-
cors_origins =
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
)
|
|
122
|
+
cors_origins = [
|
|
123
|
+
"http://letta.localhost",
|
|
124
|
+
"http://localhost:8283",
|
|
125
|
+
"http://localhost:8083",
|
|
126
|
+
"http://localhost:3000",
|
|
127
|
+
"http://localhost:4200",
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
# attach the env_cors_origins to the cors_origins if it exists
|
|
131
|
+
if env_cors_origins:
|
|
132
|
+
cors_origins.extend(env_cors_origins.split(","))
|
|
134
133
|
|
|
135
134
|
# read pg_uri from ~/.letta/pg_uri or set to none, this is to support Letta Desktop
|
|
136
135
|
default_pg_uri = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: letta-nightly
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.39.dev20250313162623
|
|
4
4
|
Summary: Create LLM agents with long-term memory and custom tools
|
|
5
5
|
License: Apache License
|
|
6
6
|
Author: Letta Team
|
|
@@ -32,7 +32,7 @@ Requires-Dist: colorama (>=0.4.6,<0.5.0)
|
|
|
32
32
|
Requires-Dist: composio-core (>=0.7.7,<0.8.0)
|
|
33
33
|
Requires-Dist: composio-langchain (>=0.7.7,<0.8.0)
|
|
34
34
|
Requires-Dist: datamodel-code-generator[http] (>=0.25.0,<0.26.0) ; extra == "desktop" or extra == "all"
|
|
35
|
-
Requires-Dist: datasets (>=2.14.6,<3.0.0)
|
|
35
|
+
Requires-Dist: datasets (>=2.14.6,<3.0.0)
|
|
36
36
|
Requires-Dist: demjson3 (>=3.0.6,<4.0.0)
|
|
37
37
|
Requires-Dist: docker (>=7.1.0,<8.0.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
|
|
38
38
|
Requires-Dist: docstring-parser (>=0.16,<0.17)
|
|
@@ -50,11 +50,12 @@ Requires-Dist: isort (>=5.13.2,<6.0.0) ; extra == "dev" or extra == "all"
|
|
|
50
50
|
Requires-Dist: jinja2 (>=3.1.5,<4.0.0)
|
|
51
51
|
Requires-Dist: langchain (>=0.3.7,<0.4.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
|
|
52
52
|
Requires-Dist: langchain-community (>=0.3.7,<0.4.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
|
|
53
|
-
Requires-Dist: letta_client (>=0.1.
|
|
53
|
+
Requires-Dist: letta_client (>=0.1.65,<0.2.0)
|
|
54
54
|
Requires-Dist: llama-index (>=0.12.2,<0.13.0)
|
|
55
55
|
Requires-Dist: llama-index-embeddings-openai (>=0.3.1,<0.4.0)
|
|
56
56
|
Requires-Dist: locust (>=2.31.5,<3.0.0) ; extra == "dev" or extra == "desktop" or extra == "all"
|
|
57
57
|
Requires-Dist: marshmallow-sqlalchemy (>=1.4.1,<2.0.0)
|
|
58
|
+
Requires-Dist: mcp (>=1.3.0,<2.0.0)
|
|
58
59
|
Requires-Dist: nltk (>=3.8.1,<4.0.0)
|
|
59
60
|
Requires-Dist: numpy (>=1.26.2,<2.0.0)
|
|
60
61
|
Requires-Dist: openai (>=1.60.0,<2.0.0)
|