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