letta-nightly 0.5.4.dev20241121104201__py3-none-any.whl → 0.5.4.dev20241123104112__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 +48 -25
- letta/agent_store/db.py +1 -1
- letta/client/client.py +361 -7
- letta/constants.py +5 -14
- letta/functions/helpers.py +5 -42
- letta/functions/schema_generator.py +24 -4
- letta/local_llm/utils.py +6 -3
- letta/log.py +7 -9
- letta/metadata.py +17 -4
- letta/orm/__init__.py +2 -0
- letta/orm/block.py +5 -2
- letta/orm/blocks_agents.py +29 -0
- letta/orm/mixins.py +8 -0
- letta/orm/organization.py +8 -1
- letta/orm/sandbox_config.py +56 -0
- letta/orm/sqlalchemy_base.py +9 -3
- letta/schemas/block.py +15 -1
- letta/schemas/blocks_agents.py +32 -0
- letta/schemas/letta_base.py +9 -0
- letta/schemas/memory.py +42 -8
- letta/schemas/sandbox_config.py +114 -0
- letta/schemas/tool.py +2 -45
- letta/server/rest_api/routers/v1/__init__.py +4 -9
- letta/server/rest_api/routers/v1/agents.py +85 -1
- letta/server/rest_api/routers/v1/sandbox_configs.py +108 -0
- letta/server/rest_api/routers/v1/tools.py +3 -5
- letta/server/rest_api/utils.py +6 -0
- letta/server/server.py +159 -12
- letta/services/block_manager.py +3 -1
- letta/services/blocks_agents_manager.py +84 -0
- letta/services/sandbox_config_manager.py +256 -0
- letta/services/tool_execution_sandbox.py +326 -0
- letta/services/tool_manager.py +10 -10
- letta/services/tool_sandbox_env/.gitkeep +0 -0
- letta/settings.py +4 -0
- {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/METADATA +28 -27
- {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/RECORD +40 -31
- {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/LICENSE +0 -0
- {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/WHEEL +0 -0
- {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/entry_points.txt +0 -0
letta/server/server.py
CHANGED
|
@@ -77,7 +77,9 @@ from letta.schemas.usage import LettaUsageStatistics
|
|
|
77
77
|
from letta.schemas.user import User
|
|
78
78
|
from letta.services.agents_tags_manager import AgentsTagsManager
|
|
79
79
|
from letta.services.block_manager import BlockManager
|
|
80
|
+
from letta.services.blocks_agents_manager import BlocksAgentsManager
|
|
80
81
|
from letta.services.organization_manager import OrganizationManager
|
|
82
|
+
from letta.services.sandbox_config_manager import SandboxConfigManager
|
|
81
83
|
from letta.services.source_manager import SourceManager
|
|
82
84
|
from letta.services.tool_manager import ToolManager
|
|
83
85
|
from letta.services.user_manager import UserManager
|
|
@@ -247,6 +249,8 @@ class SyncServer(Server):
|
|
|
247
249
|
self.block_manager = BlockManager()
|
|
248
250
|
self.source_manager = SourceManager()
|
|
249
251
|
self.agents_tags_manager = AgentsTagsManager()
|
|
252
|
+
self.blocks_agents_manager = BlocksAgentsManager()
|
|
253
|
+
self.sandbox_config_manager = SandboxConfigManager(tool_settings)
|
|
250
254
|
|
|
251
255
|
# Make default user and org
|
|
252
256
|
if init_with_default_org_and_user:
|
|
@@ -328,6 +332,15 @@ class SyncServer(Server):
|
|
|
328
332
|
)
|
|
329
333
|
)
|
|
330
334
|
|
|
335
|
+
def save_agents(self):
|
|
336
|
+
"""Saves all the agents that are in the in-memory object store"""
|
|
337
|
+
for agent_d in self.active_agents:
|
|
338
|
+
try:
|
|
339
|
+
save_agent(agent_d["agent"], self.ms)
|
|
340
|
+
logger.info(f"Saved agent {agent_d['agent_id']}")
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.exception(f"Error occurred while trying to save agent {agent_d['agent_id']}:\n{e}")
|
|
343
|
+
|
|
331
344
|
def _get_agent(self, user_id: str, agent_id: str) -> Union[Agent, None]:
|
|
332
345
|
"""Get the agent object from the in-memory object store"""
|
|
333
346
|
for d in self.active_agents:
|
|
@@ -372,10 +385,11 @@ class SyncServer(Server):
|
|
|
372
385
|
tool_objs = []
|
|
373
386
|
for name in agent_state.tools:
|
|
374
387
|
# TODO: This should be a hard failure, but for migration reasons, we patch it for now
|
|
375
|
-
|
|
388
|
+
tool_obj = self.tool_manager.get_tool_by_name(tool_name=name, actor=actor)
|
|
389
|
+
if tool_obj:
|
|
376
390
|
tool_obj = self.tool_manager.get_tool_by_name(tool_name=name, actor=actor)
|
|
377
391
|
tool_objs.append(tool_obj)
|
|
378
|
-
|
|
392
|
+
else:
|
|
379
393
|
warnings.warn(f"Tried to retrieve a tool with name {name} from the agent_state, but does not exist in tool db.")
|
|
380
394
|
|
|
381
395
|
# set agent_state tools to only the names of the available tools
|
|
@@ -400,8 +414,10 @@ class SyncServer(Server):
|
|
|
400
414
|
logger.exception(f"Error occurred while trying to get agent {agent_id}:\n{e}")
|
|
401
415
|
raise
|
|
402
416
|
|
|
403
|
-
def _get_or_load_agent(self, agent_id: str) -> Agent:
|
|
417
|
+
def _get_or_load_agent(self, agent_id: str, caching: bool = True) -> Agent:
|
|
404
418
|
"""Check if the agent is in-memory, then load"""
|
|
419
|
+
|
|
420
|
+
# Gets the agent state
|
|
405
421
|
agent_state = self.ms.get_agent(agent_id=agent_id)
|
|
406
422
|
if not agent_state:
|
|
407
423
|
raise ValueError(f"Agent does not exist")
|
|
@@ -409,11 +425,24 @@ class SyncServer(Server):
|
|
|
409
425
|
actor = self.user_manager.get_user_by_id(user_id)
|
|
410
426
|
|
|
411
427
|
logger.debug(f"Checking for agent user_id={user_id} agent_id={agent_id}")
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
428
|
+
if caching:
|
|
429
|
+
# TODO: consider disabling loading cached agents due to potential concurrency issues
|
|
430
|
+
letta_agent = self._get_agent(user_id=user_id, agent_id=agent_id)
|
|
431
|
+
if not letta_agent:
|
|
432
|
+
logger.debug(f"Agent not loaded, loading agent user_id={user_id} agent_id={agent_id}")
|
|
433
|
+
letta_agent = self._load_agent(agent_id=agent_id, actor=actor)
|
|
434
|
+
else:
|
|
435
|
+
# This breaks unit tests in test_local_client.py
|
|
416
436
|
letta_agent = self._load_agent(agent_id=agent_id, actor=actor)
|
|
437
|
+
|
|
438
|
+
# letta_agent = self._get_agent(user_id=user_id, agent_id=agent_id)
|
|
439
|
+
# if not letta_agent:
|
|
440
|
+
# logger.debug(f"Agent not loaded, loading agent user_id={user_id} agent_id={agent_id}")
|
|
441
|
+
|
|
442
|
+
# NOTE: no longer caching, always forcing a lot from the database
|
|
443
|
+
# Loads the agent objects
|
|
444
|
+
# letta_agent = self._load_agent(agent_id=agent_id, actor=actor)
|
|
445
|
+
|
|
417
446
|
return letta_agent
|
|
418
447
|
|
|
419
448
|
def _step(
|
|
@@ -813,10 +842,10 @@ class SyncServer(Server):
|
|
|
813
842
|
tool_objs = []
|
|
814
843
|
if request.tools:
|
|
815
844
|
for tool_name in request.tools:
|
|
816
|
-
|
|
817
|
-
|
|
845
|
+
tool_obj = self.tool_manager.get_tool_by_name(tool_name=tool_name, actor=actor)
|
|
846
|
+
if tool_obj:
|
|
818
847
|
tool_objs.append(tool_obj)
|
|
819
|
-
|
|
848
|
+
else:
|
|
820
849
|
warnings.warn(f"Attempted to add a nonexistent tool {tool_name} to agent {request.name}, skipping.")
|
|
821
850
|
# reset the request.tools to only valid tools
|
|
822
851
|
request.tools = [t.name for t in tool_objs]
|
|
@@ -1376,8 +1405,9 @@ class SyncServer(Server):
|
|
|
1376
1405
|
# Get the agent object (loaded in memory)
|
|
1377
1406
|
letta_agent = self._get_or_load_agent(agent_id=agent_id)
|
|
1378
1407
|
assert isinstance(letta_agent.memory, Memory)
|
|
1379
|
-
agent_state = letta_agent.agent_state.model_copy(deep=True)
|
|
1380
1408
|
|
|
1409
|
+
letta_agent.update_memory_blocks_from_db()
|
|
1410
|
+
agent_state = letta_agent.agent_state.model_copy(deep=True)
|
|
1381
1411
|
# Load the tags in for the agent_state
|
|
1382
1412
|
agent_state.tags = self.agents_tags_manager.get_tags_for_agent(agent_id=agent_id, actor=user)
|
|
1383
1413
|
return agent_state
|
|
@@ -1431,6 +1461,7 @@ class SyncServer(Server):
|
|
|
1431
1461
|
# If we modified the memory contents, we need to rebuild the memory block inside the system message
|
|
1432
1462
|
if modified:
|
|
1433
1463
|
letta_agent.rebuild_memory()
|
|
1464
|
+
# letta_agent.rebuild_memory(force=True, ms=self.ms) # This breaks unit tests in test_local_client.py
|
|
1434
1465
|
# save agent
|
|
1435
1466
|
save_agent(letta_agent, self.ms)
|
|
1436
1467
|
|
|
@@ -1723,7 +1754,7 @@ class SyncServer(Server):
|
|
|
1723
1754
|
def add_default_external_tools(self, actor: User) -> bool:
|
|
1724
1755
|
"""Add default langchain tools. Return true if successful, false otherwise."""
|
|
1725
1756
|
success = True
|
|
1726
|
-
tool_creates = ToolCreate.load_default_langchain_tools()
|
|
1757
|
+
tool_creates = ToolCreate.load_default_langchain_tools()
|
|
1727
1758
|
if tool_settings.composio_api_key:
|
|
1728
1759
|
tool_creates += ToolCreate.load_default_composio_tools()
|
|
1729
1760
|
for tool_create in tool_creates:
|
|
@@ -1817,3 +1848,119 @@ class SyncServer(Server):
|
|
|
1817
1848
|
# Get the current message
|
|
1818
1849
|
letta_agent = self._get_or_load_agent(agent_id=agent_id)
|
|
1819
1850
|
return letta_agent.get_context_window()
|
|
1851
|
+
|
|
1852
|
+
def update_agent_memory_label(self, user_id: str, agent_id: str, current_block_label: str, new_block_label: str) -> Memory:
|
|
1853
|
+
"""Update the label of a block in an agent's memory"""
|
|
1854
|
+
|
|
1855
|
+
# Get the user
|
|
1856
|
+
user = self.user_manager.get_user_by_id(user_id=user_id)
|
|
1857
|
+
|
|
1858
|
+
# Link a block to an agent's memory
|
|
1859
|
+
letta_agent = self._get_or_load_agent(agent_id=agent_id)
|
|
1860
|
+
letta_agent.memory.update_block_label(current_label=current_block_label, new_label=new_block_label)
|
|
1861
|
+
assert new_block_label in letta_agent.memory.list_block_labels()
|
|
1862
|
+
self.block_manager.create_or_update_block(block=letta_agent.memory.get_block(new_block_label), actor=user)
|
|
1863
|
+
|
|
1864
|
+
# check that the block was updated
|
|
1865
|
+
updated_block = self.block_manager.get_block_by_id(block_id=letta_agent.memory.get_block(new_block_label).id, actor=user)
|
|
1866
|
+
|
|
1867
|
+
# Recompile the agent memory
|
|
1868
|
+
letta_agent.rebuild_memory(force=True, ms=self.ms)
|
|
1869
|
+
|
|
1870
|
+
# save agent
|
|
1871
|
+
save_agent(letta_agent, self.ms)
|
|
1872
|
+
|
|
1873
|
+
updated_agent = self.ms.get_agent(agent_id=agent_id)
|
|
1874
|
+
if updated_agent is None:
|
|
1875
|
+
raise ValueError(f"Agent with id {agent_id} not found after linking block")
|
|
1876
|
+
assert new_block_label in updated_agent.memory.list_block_labels()
|
|
1877
|
+
assert current_block_label not in updated_agent.memory.list_block_labels()
|
|
1878
|
+
return updated_agent.memory
|
|
1879
|
+
|
|
1880
|
+
def link_block_to_agent_memory(self, user_id: str, agent_id: str, block_id: str) -> Memory:
|
|
1881
|
+
"""Link a block to an agent's memory"""
|
|
1882
|
+
|
|
1883
|
+
# Get the user
|
|
1884
|
+
user = self.user_manager.get_user_by_id(user_id=user_id)
|
|
1885
|
+
|
|
1886
|
+
# Get the block first
|
|
1887
|
+
block = self.block_manager.get_block_by_id(block_id=block_id, actor=user)
|
|
1888
|
+
if block is None:
|
|
1889
|
+
raise ValueError(f"Block with id {block_id} not found")
|
|
1890
|
+
|
|
1891
|
+
# Link a block to an agent's memory
|
|
1892
|
+
letta_agent = self._get_or_load_agent(agent_id=agent_id)
|
|
1893
|
+
letta_agent.memory.link_block(block=block)
|
|
1894
|
+
assert block.label in letta_agent.memory.list_block_labels()
|
|
1895
|
+
|
|
1896
|
+
# Recompile the agent memory
|
|
1897
|
+
letta_agent.rebuild_memory(force=True, ms=self.ms)
|
|
1898
|
+
|
|
1899
|
+
# save agent
|
|
1900
|
+
save_agent(letta_agent, self.ms)
|
|
1901
|
+
|
|
1902
|
+
updated_agent = self.ms.get_agent(agent_id=agent_id)
|
|
1903
|
+
if updated_agent is None:
|
|
1904
|
+
raise ValueError(f"Agent with id {agent_id} not found after linking block")
|
|
1905
|
+
assert block.label in updated_agent.memory.list_block_labels()
|
|
1906
|
+
|
|
1907
|
+
return updated_agent.memory
|
|
1908
|
+
|
|
1909
|
+
def unlink_block_from_agent_memory(self, user_id: str, agent_id: str, block_label: str, delete_if_no_ref: bool = True) -> Memory:
|
|
1910
|
+
"""Unlink a block from an agent's memory. If the block is not linked to any agent, delete it."""
|
|
1911
|
+
|
|
1912
|
+
# Get the user
|
|
1913
|
+
user = self.user_manager.get_user_by_id(user_id=user_id)
|
|
1914
|
+
|
|
1915
|
+
# Link a block to an agent's memory
|
|
1916
|
+
letta_agent = self._get_or_load_agent(agent_id=agent_id)
|
|
1917
|
+
unlinked_block = letta_agent.memory.unlink_block(block_label=block_label)
|
|
1918
|
+
assert unlinked_block.label not in letta_agent.memory.list_block_labels()
|
|
1919
|
+
|
|
1920
|
+
# Check if the block is linked to any other agent
|
|
1921
|
+
# TODO needs reference counting GC to handle loose blocks
|
|
1922
|
+
# block = self.block_manager.get_block_by_id(block_id=unlinked_block.id, actor=user)
|
|
1923
|
+
# if block is None:
|
|
1924
|
+
# raise ValueError(f"Block with id {block_id} not found")
|
|
1925
|
+
|
|
1926
|
+
# Recompile the agent memory
|
|
1927
|
+
letta_agent.rebuild_memory(force=True, ms=self.ms)
|
|
1928
|
+
|
|
1929
|
+
# save agent
|
|
1930
|
+
save_agent(letta_agent, self.ms)
|
|
1931
|
+
|
|
1932
|
+
updated_agent = self.ms.get_agent(agent_id=agent_id)
|
|
1933
|
+
if updated_agent is None:
|
|
1934
|
+
raise ValueError(f"Agent with id {agent_id} not found after linking block")
|
|
1935
|
+
assert unlinked_block.label not in updated_agent.memory.list_block_labels()
|
|
1936
|
+
return updated_agent.memory
|
|
1937
|
+
|
|
1938
|
+
def update_agent_memory_limit(self, user_id: str, agent_id: str, block_label: str, limit: int) -> Memory:
|
|
1939
|
+
"""Update the limit of a block in an agent's memory"""
|
|
1940
|
+
|
|
1941
|
+
# Get the user
|
|
1942
|
+
user = self.user_manager.get_user_by_id(user_id=user_id)
|
|
1943
|
+
|
|
1944
|
+
# Link a block to an agent's memory
|
|
1945
|
+
letta_agent = self._get_or_load_agent(agent_id=agent_id)
|
|
1946
|
+
letta_agent.memory.update_block_limit(label=block_label, limit=limit)
|
|
1947
|
+
assert block_label in letta_agent.memory.list_block_labels()
|
|
1948
|
+
|
|
1949
|
+
# write out the update the database
|
|
1950
|
+
self.block_manager.create_or_update_block(block=letta_agent.memory.get_block(block_label), actor=user)
|
|
1951
|
+
|
|
1952
|
+
# check that the block was updated
|
|
1953
|
+
updated_block = self.block_manager.get_block_by_id(block_id=letta_agent.memory.get_block(block_label).id, actor=user)
|
|
1954
|
+
assert updated_block and updated_block.limit == limit
|
|
1955
|
+
|
|
1956
|
+
# Recompile the agent memory
|
|
1957
|
+
letta_agent.rebuild_memory(force=True, ms=self.ms)
|
|
1958
|
+
|
|
1959
|
+
# save agent
|
|
1960
|
+
save_agent(letta_agent, self.ms)
|
|
1961
|
+
|
|
1962
|
+
updated_agent = self.ms.get_agent(agent_id=agent_id)
|
|
1963
|
+
if updated_agent is None:
|
|
1964
|
+
raise ValueError(f"Agent with id {agent_id} not found after linking block")
|
|
1965
|
+
assert updated_agent.memory.get_block(label=block_label).limit == limit
|
|
1966
|
+
return updated_agent.memory
|
letta/services/block_manager.py
CHANGED
|
@@ -28,8 +28,10 @@ class BlockManager:
|
|
|
28
28
|
self.update_block(block.id, update_data, actor)
|
|
29
29
|
else:
|
|
30
30
|
with self.session_maker() as session:
|
|
31
|
+
# Always write the organization_id
|
|
32
|
+
block.organization_id = actor.organization_id
|
|
31
33
|
data = block.model_dump(exclude_none=True)
|
|
32
|
-
block = BlockModel(**data
|
|
34
|
+
block = BlockModel(**data)
|
|
33
35
|
block.create(session, actor=actor)
|
|
34
36
|
return block.to_pydantic()
|
|
35
37
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from letta.orm.blocks_agents import BlocksAgents as BlocksAgentsModel
|
|
5
|
+
from letta.orm.errors import NoResultFound
|
|
6
|
+
from letta.schemas.blocks_agents import BlocksAgents as PydanticBlocksAgents
|
|
7
|
+
from letta.utils import enforce_types
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# TODO: DELETE THIS ASAP
|
|
11
|
+
# TODO: So we have a patch where we manually specify CRUD operations
|
|
12
|
+
# TODO: This is because Agent is NOT migrated to the ORM yet
|
|
13
|
+
# TODO: Once we migrate Agent to the ORM, we should deprecate any agents relationship table managers
|
|
14
|
+
class BlocksAgentsManager:
|
|
15
|
+
"""Manager class to handle business logic related to Blocks and Agents."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
from letta.server.server import db_context
|
|
19
|
+
|
|
20
|
+
self.session_maker = db_context
|
|
21
|
+
|
|
22
|
+
@enforce_types
|
|
23
|
+
def add_block_to_agent(self, agent_id: str, block_id: str, block_label: str) -> PydanticBlocksAgents:
|
|
24
|
+
"""Add a block to an agent. If the label already exists on that agent, this will error."""
|
|
25
|
+
with self.session_maker() as session:
|
|
26
|
+
try:
|
|
27
|
+
# Check if the block-label combination already exists for this agent
|
|
28
|
+
blocks_agents_record = BlocksAgentsModel.read(db_session=session, agent_id=agent_id, block_label=block_label)
|
|
29
|
+
warnings.warn(f"Block label '{block_label}' already exists for agent '{agent_id}'.")
|
|
30
|
+
except NoResultFound:
|
|
31
|
+
blocks_agents_record = PydanticBlocksAgents(agent_id=agent_id, block_id=block_id, block_label=block_label)
|
|
32
|
+
blocks_agents_record = BlocksAgentsModel(**blocks_agents_record.model_dump(exclude_none=True))
|
|
33
|
+
blocks_agents_record.create(session)
|
|
34
|
+
|
|
35
|
+
return blocks_agents_record.to_pydantic()
|
|
36
|
+
|
|
37
|
+
@enforce_types
|
|
38
|
+
def remove_block_with_label_from_agent(self, agent_id: str, block_label: str) -> PydanticBlocksAgents:
|
|
39
|
+
"""Remove a block with a label from an agent."""
|
|
40
|
+
with self.session_maker() as session:
|
|
41
|
+
try:
|
|
42
|
+
# Find and delete the block-label association for the agent
|
|
43
|
+
blocks_agents_record = BlocksAgentsModel.read(db_session=session, agent_id=agent_id, block_label=block_label)
|
|
44
|
+
blocks_agents_record.hard_delete(session)
|
|
45
|
+
return blocks_agents_record.to_pydantic()
|
|
46
|
+
except NoResultFound:
|
|
47
|
+
raise ValueError(f"Block label '{block_label}' not found for agent '{agent_id}'.")
|
|
48
|
+
|
|
49
|
+
@enforce_types
|
|
50
|
+
def remove_block_with_id_from_agent(self, agent_id: str, block_id: str) -> PydanticBlocksAgents:
|
|
51
|
+
"""Remove a block with a label from an agent."""
|
|
52
|
+
with self.session_maker() as session:
|
|
53
|
+
try:
|
|
54
|
+
# Find and delete the block-label association for the agent
|
|
55
|
+
blocks_agents_record = BlocksAgentsModel.read(db_session=session, agent_id=agent_id, block_id=block_id)
|
|
56
|
+
blocks_agents_record.hard_delete(session)
|
|
57
|
+
return blocks_agents_record.to_pydantic()
|
|
58
|
+
except NoResultFound:
|
|
59
|
+
raise ValueError(f"Block id '{block_id}' not found for agent '{agent_id}'.")
|
|
60
|
+
|
|
61
|
+
@enforce_types
|
|
62
|
+
def update_block_id_for_agent(self, agent_id: str, block_label: str, new_block_id: str) -> PydanticBlocksAgents:
|
|
63
|
+
"""Update the block ID for a specific block label for an agent."""
|
|
64
|
+
with self.session_maker() as session:
|
|
65
|
+
try:
|
|
66
|
+
blocks_agents_record = BlocksAgentsModel.read(db_session=session, agent_id=agent_id, block_label=block_label)
|
|
67
|
+
blocks_agents_record.block_id = new_block_id
|
|
68
|
+
return blocks_agents_record.to_pydantic()
|
|
69
|
+
except NoResultFound:
|
|
70
|
+
raise ValueError(f"Block label '{block_label}' not found for agent '{agent_id}'.")
|
|
71
|
+
|
|
72
|
+
@enforce_types
|
|
73
|
+
def list_block_ids_for_agent(self, agent_id: str) -> List[str]:
|
|
74
|
+
"""List all blocks associated with a specific agent."""
|
|
75
|
+
with self.session_maker() as session:
|
|
76
|
+
blocks_agents_record = BlocksAgentsModel.list(db_session=session, agent_id=agent_id)
|
|
77
|
+
return [record.block_id for record in blocks_agents_record]
|
|
78
|
+
|
|
79
|
+
@enforce_types
|
|
80
|
+
def list_agent_ids_with_block(self, block_id: str) -> List[str]:
|
|
81
|
+
"""List all agents associated with a specific block."""
|
|
82
|
+
with self.session_maker() as session:
|
|
83
|
+
blocks_agents_record = BlocksAgentsModel.list(db_session=session, block_id=block_id)
|
|
84
|
+
return [record.agent_id for record in blocks_agents_record]
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from letta.log import get_logger
|
|
5
|
+
from letta.orm.errors import NoResultFound
|
|
6
|
+
from letta.orm.sandbox_config import SandboxConfig as SandboxConfigModel
|
|
7
|
+
from letta.orm.sandbox_config import SandboxEnvironmentVariable as SandboxEnvVarModel
|
|
8
|
+
from letta.schemas.sandbox_config import E2BSandboxConfig, LocalSandboxConfig
|
|
9
|
+
from letta.schemas.sandbox_config import SandboxConfig as PydanticSandboxConfig
|
|
10
|
+
from letta.schemas.sandbox_config import SandboxConfigCreate, SandboxConfigUpdate
|
|
11
|
+
from letta.schemas.sandbox_config import SandboxEnvironmentVariable as PydanticEnvVar
|
|
12
|
+
from letta.schemas.sandbox_config import (
|
|
13
|
+
SandboxEnvironmentVariableCreate,
|
|
14
|
+
SandboxEnvironmentVariableUpdate,
|
|
15
|
+
SandboxType,
|
|
16
|
+
)
|
|
17
|
+
from letta.schemas.user import User as PydanticUser
|
|
18
|
+
from letta.utils import enforce_types, printd
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SandboxConfigManager:
|
|
24
|
+
"""Manager class to handle business logic related to SandboxConfig and SandboxEnvironmentVariable."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, settings):
|
|
27
|
+
from letta.server.server import db_context
|
|
28
|
+
|
|
29
|
+
self.session_maker = db_context
|
|
30
|
+
self.e2b_template_id = settings.e2b_sandbox_template_id
|
|
31
|
+
|
|
32
|
+
@enforce_types
|
|
33
|
+
def get_or_create_default_sandbox_config(self, sandbox_type: SandboxType, actor: PydanticUser) -> PydanticSandboxConfig:
|
|
34
|
+
sandbox_config = self.get_sandbox_config_by_type(sandbox_type, actor=actor)
|
|
35
|
+
if not sandbox_config:
|
|
36
|
+
logger.info(f"Creating new sandbox config of type {sandbox_type}, none found for organization {actor.organization_id}.")
|
|
37
|
+
|
|
38
|
+
# TODO: Add more sandbox types later
|
|
39
|
+
if sandbox_type == SandboxType.E2B:
|
|
40
|
+
default_config = E2BSandboxConfig(template=self.e2b_template_id).model_dump(exclude_none=True)
|
|
41
|
+
else:
|
|
42
|
+
default_local_sandbox_path = str(Path(__file__).parent / "tool_sandbox_env")
|
|
43
|
+
default_config = LocalSandboxConfig(sandbox_dir=default_local_sandbox_path).model_dump(exclude_none=True)
|
|
44
|
+
|
|
45
|
+
sandbox_config = self.create_or_update_sandbox_config(SandboxConfigCreate(config=default_config), actor=actor)
|
|
46
|
+
return sandbox_config
|
|
47
|
+
|
|
48
|
+
@enforce_types
|
|
49
|
+
def create_or_update_sandbox_config(self, sandbox_config_create: SandboxConfigCreate, actor: PydanticUser) -> PydanticSandboxConfig:
|
|
50
|
+
"""Create or update a sandbox configuration based on the PydanticSandboxConfig schema."""
|
|
51
|
+
config = sandbox_config_create.config
|
|
52
|
+
sandbox_type = config.type
|
|
53
|
+
sandbox_config = PydanticSandboxConfig(
|
|
54
|
+
type=sandbox_type, config=config.model_dump(exclude_none=True), organization_id=actor.organization_id
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Attempt to retrieve the existing sandbox configuration by type within the organization
|
|
58
|
+
db_sandbox = self.get_sandbox_config_by_type(sandbox_config.type, actor=actor)
|
|
59
|
+
if db_sandbox:
|
|
60
|
+
# Prepare the update data, excluding fields that should not be reset
|
|
61
|
+
update_data = sandbox_config.model_dump(exclude_unset=True, exclude_none=True)
|
|
62
|
+
update_data = {key: value for key, value in update_data.items() if getattr(db_sandbox, key) != value}
|
|
63
|
+
|
|
64
|
+
# If there are changes, update the sandbox configuration
|
|
65
|
+
if update_data:
|
|
66
|
+
db_sandbox = self.update_sandbox_config(db_sandbox.id, SandboxConfigUpdate(**update_data), actor)
|
|
67
|
+
else:
|
|
68
|
+
printd(
|
|
69
|
+
f"`create_or_update_sandbox_config` was called with user_id={actor.id}, organization_id={actor.organization_id}, "
|
|
70
|
+
f"type={sandbox_config.type}, but found existing configuration with nothing to update."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return db_sandbox
|
|
74
|
+
else:
|
|
75
|
+
# If the sandbox configuration doesn't exist, create a new one
|
|
76
|
+
with self.session_maker() as session:
|
|
77
|
+
db_sandbox = SandboxConfigModel(**sandbox_config.model_dump(exclude_none=True))
|
|
78
|
+
db_sandbox.create(session, actor=actor)
|
|
79
|
+
return db_sandbox.to_pydantic()
|
|
80
|
+
|
|
81
|
+
@enforce_types
|
|
82
|
+
def update_sandbox_config(
|
|
83
|
+
self, sandbox_config_id: str, sandbox_update: SandboxConfigUpdate, actor: PydanticUser
|
|
84
|
+
) -> PydanticSandboxConfig:
|
|
85
|
+
"""Update an existing sandbox configuration."""
|
|
86
|
+
with self.session_maker() as session:
|
|
87
|
+
sandbox = SandboxConfigModel.read(db_session=session, identifier=sandbox_config_id, actor=actor)
|
|
88
|
+
# We need to check that the sandbox_update provided is the same type as the original sandbox
|
|
89
|
+
if sandbox.type != sandbox_update.config.type:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"Mismatched type for sandbox config update: tried to update sandbox_config of type {sandbox.type} with config of type {sandbox_update.config.type}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
update_data = sandbox_update.model_dump(exclude_unset=True, exclude_none=True)
|
|
95
|
+
update_data = {key: value for key, value in update_data.items() if getattr(sandbox, key) != value}
|
|
96
|
+
|
|
97
|
+
if update_data:
|
|
98
|
+
for key, value in update_data.items():
|
|
99
|
+
setattr(sandbox, key, value)
|
|
100
|
+
sandbox.update(db_session=session, actor=actor)
|
|
101
|
+
else:
|
|
102
|
+
printd(
|
|
103
|
+
f"`update_sandbox_config` called with user_id={actor.id}, organization_id={actor.organization_id}, "
|
|
104
|
+
f"name={sandbox.type}, but nothing to update."
|
|
105
|
+
)
|
|
106
|
+
return sandbox.to_pydantic()
|
|
107
|
+
|
|
108
|
+
@enforce_types
|
|
109
|
+
def delete_sandbox_config(self, sandbox_config_id: str, actor: PydanticUser) -> PydanticSandboxConfig:
|
|
110
|
+
"""Delete a sandbox configuration by its ID."""
|
|
111
|
+
with self.session_maker() as session:
|
|
112
|
+
sandbox = SandboxConfigModel.read(db_session=session, identifier=sandbox_config_id, actor=actor)
|
|
113
|
+
sandbox.hard_delete(db_session=session, actor=actor)
|
|
114
|
+
return sandbox.to_pydantic()
|
|
115
|
+
|
|
116
|
+
@enforce_types
|
|
117
|
+
def list_sandbox_configs(
|
|
118
|
+
self, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50
|
|
119
|
+
) -> List[PydanticSandboxConfig]:
|
|
120
|
+
"""List all sandbox configurations with optional pagination."""
|
|
121
|
+
with self.session_maker() as session:
|
|
122
|
+
sandboxes = SandboxConfigModel.list(
|
|
123
|
+
db_session=session,
|
|
124
|
+
cursor=cursor,
|
|
125
|
+
limit=limit,
|
|
126
|
+
organization_id=actor.organization_id,
|
|
127
|
+
)
|
|
128
|
+
return [sandbox.to_pydantic() for sandbox in sandboxes]
|
|
129
|
+
|
|
130
|
+
@enforce_types
|
|
131
|
+
def get_sandbox_config_by_id(self, sandbox_config_id: str, actor: Optional[PydanticUser] = None) -> Optional[PydanticSandboxConfig]:
|
|
132
|
+
"""Retrieve a sandbox configuration by its ID."""
|
|
133
|
+
with self.session_maker() as session:
|
|
134
|
+
try:
|
|
135
|
+
sandbox = SandboxConfigModel.read(db_session=session, identifier=sandbox_config_id, actor=actor)
|
|
136
|
+
return sandbox.to_pydantic()
|
|
137
|
+
except NoResultFound:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
@enforce_types
|
|
141
|
+
def get_sandbox_config_by_type(self, type: SandboxType, actor: Optional[PydanticUser] = None) -> Optional[PydanticSandboxConfig]:
|
|
142
|
+
"""Retrieve a sandbox config by its type."""
|
|
143
|
+
with self.session_maker() as session:
|
|
144
|
+
try:
|
|
145
|
+
sandboxes = SandboxConfigModel.list(
|
|
146
|
+
db_session=session,
|
|
147
|
+
type=type,
|
|
148
|
+
organization_id=actor.organization_id,
|
|
149
|
+
limit=1,
|
|
150
|
+
)
|
|
151
|
+
if sandboxes:
|
|
152
|
+
return sandboxes[0].to_pydantic()
|
|
153
|
+
return None
|
|
154
|
+
except NoResultFound:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
@enforce_types
|
|
158
|
+
def create_sandbox_env_var(
|
|
159
|
+
self, env_var_create: SandboxEnvironmentVariableCreate, sandbox_config_id: str, actor: PydanticUser
|
|
160
|
+
) -> PydanticEnvVar:
|
|
161
|
+
"""Create a new sandbox environment variable."""
|
|
162
|
+
env_var = PydanticEnvVar(**env_var_create.model_dump(), sandbox_config_id=sandbox_config_id, organization_id=actor.organization_id)
|
|
163
|
+
|
|
164
|
+
db_env_var = self.get_sandbox_env_var_by_key_and_sandbox_config_id(env_var.key, env_var.sandbox_config_id, actor=actor)
|
|
165
|
+
if db_env_var:
|
|
166
|
+
update_data = env_var.model_dump(exclude_unset=True, exclude_none=True)
|
|
167
|
+
update_data = {key: value for key, value in update_data.items() if getattr(db_env_var, key) != value}
|
|
168
|
+
# If there are changes, update the environment variable
|
|
169
|
+
if update_data:
|
|
170
|
+
db_env_var = self.update_sandbox_env_var(db_env_var.id, SandboxEnvironmentVariableUpdate(**update_data), actor)
|
|
171
|
+
else:
|
|
172
|
+
printd(
|
|
173
|
+
f"`create_or_update_sandbox_env_var` was called with user_id={actor.id}, organization_id={actor.organization_id}, "
|
|
174
|
+
f"key={env_var.key}, but found existing variable with nothing to update."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return db_env_var
|
|
178
|
+
else:
|
|
179
|
+
with self.session_maker() as session:
|
|
180
|
+
env_var = SandboxEnvVarModel(**env_var.model_dump(exclude_none=True))
|
|
181
|
+
env_var.create(session, actor=actor)
|
|
182
|
+
return env_var.to_pydantic()
|
|
183
|
+
|
|
184
|
+
@enforce_types
|
|
185
|
+
def update_sandbox_env_var(
|
|
186
|
+
self, env_var_id: str, env_var_update: SandboxEnvironmentVariableUpdate, actor: PydanticUser
|
|
187
|
+
) -> PydanticEnvVar:
|
|
188
|
+
"""Update an existing sandbox environment variable."""
|
|
189
|
+
with self.session_maker() as session:
|
|
190
|
+
env_var = SandboxEnvVarModel.read(db_session=session, identifier=env_var_id, actor=actor)
|
|
191
|
+
update_data = env_var_update.model_dump(exclude_unset=True, exclude_none=True)
|
|
192
|
+
update_data = {key: value for key, value in update_data.items() if getattr(env_var, key) != value}
|
|
193
|
+
|
|
194
|
+
if update_data:
|
|
195
|
+
for key, value in update_data.items():
|
|
196
|
+
setattr(env_var, key, value)
|
|
197
|
+
env_var.update(db_session=session, actor=actor)
|
|
198
|
+
else:
|
|
199
|
+
printd(
|
|
200
|
+
f"`update_sandbox_env_var` called with user_id={actor.id}, organization_id={actor.organization_id}, "
|
|
201
|
+
f"key={env_var.key}, but nothing to update."
|
|
202
|
+
)
|
|
203
|
+
return env_var.to_pydantic()
|
|
204
|
+
|
|
205
|
+
@enforce_types
|
|
206
|
+
def delete_sandbox_env_var(self, env_var_id: str, actor: PydanticUser) -> PydanticEnvVar:
|
|
207
|
+
"""Delete a sandbox environment variable by its ID."""
|
|
208
|
+
with self.session_maker() as session:
|
|
209
|
+
env_var = SandboxEnvVarModel.read(db_session=session, identifier=env_var_id, actor=actor)
|
|
210
|
+
env_var.hard_delete(db_session=session, actor=actor)
|
|
211
|
+
return env_var.to_pydantic()
|
|
212
|
+
|
|
213
|
+
@enforce_types
|
|
214
|
+
def list_sandbox_env_vars(
|
|
215
|
+
self, sandbox_config_id: str, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50
|
|
216
|
+
) -> List[PydanticEnvVar]:
|
|
217
|
+
"""List all sandbox environment variables with optional pagination."""
|
|
218
|
+
with self.session_maker() as session:
|
|
219
|
+
env_vars = SandboxEnvVarModel.list(
|
|
220
|
+
db_session=session,
|
|
221
|
+
cursor=cursor,
|
|
222
|
+
limit=limit,
|
|
223
|
+
organization_id=actor.organization_id,
|
|
224
|
+
sandbox_config_id=sandbox_config_id,
|
|
225
|
+
)
|
|
226
|
+
return [env_var.to_pydantic() for env_var in env_vars]
|
|
227
|
+
|
|
228
|
+
@enforce_types
|
|
229
|
+
def get_sandbox_env_vars_as_dict(
|
|
230
|
+
self, sandbox_config_id: str, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50
|
|
231
|
+
) -> Dict[str, str]:
|
|
232
|
+
env_vars = self.list_sandbox_env_vars(sandbox_config_id, actor, cursor, limit)
|
|
233
|
+
result = {}
|
|
234
|
+
for env_var in env_vars:
|
|
235
|
+
result[env_var.key] = env_var.value
|
|
236
|
+
return result
|
|
237
|
+
|
|
238
|
+
@enforce_types
|
|
239
|
+
def get_sandbox_env_var_by_key_and_sandbox_config_id(
|
|
240
|
+
self, key: str, sandbox_config_id: str, actor: Optional[PydanticUser] = None
|
|
241
|
+
) -> Optional[PydanticEnvVar]:
|
|
242
|
+
"""Retrieve a sandbox environment variable by its key and sandbox_config_id."""
|
|
243
|
+
with self.session_maker() as session:
|
|
244
|
+
try:
|
|
245
|
+
env_var = SandboxEnvVarModel.list(
|
|
246
|
+
db_session=session,
|
|
247
|
+
key=key,
|
|
248
|
+
sandbox_config_id=sandbox_config_id,
|
|
249
|
+
organization_id=actor.organization_id,
|
|
250
|
+
limit=1,
|
|
251
|
+
)
|
|
252
|
+
if env_var:
|
|
253
|
+
return env_var[0].to_pydantic()
|
|
254
|
+
return None
|
|
255
|
+
except NoResultFound:
|
|
256
|
+
return None
|