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.

Files changed (40) hide show
  1. letta/agent.py +48 -25
  2. letta/agent_store/db.py +1 -1
  3. letta/client/client.py +361 -7
  4. letta/constants.py +5 -14
  5. letta/functions/helpers.py +5 -42
  6. letta/functions/schema_generator.py +24 -4
  7. letta/local_llm/utils.py +6 -3
  8. letta/log.py +7 -9
  9. letta/metadata.py +17 -4
  10. letta/orm/__init__.py +2 -0
  11. letta/orm/block.py +5 -2
  12. letta/orm/blocks_agents.py +29 -0
  13. letta/orm/mixins.py +8 -0
  14. letta/orm/organization.py +8 -1
  15. letta/orm/sandbox_config.py +56 -0
  16. letta/orm/sqlalchemy_base.py +9 -3
  17. letta/schemas/block.py +15 -1
  18. letta/schemas/blocks_agents.py +32 -0
  19. letta/schemas/letta_base.py +9 -0
  20. letta/schemas/memory.py +42 -8
  21. letta/schemas/sandbox_config.py +114 -0
  22. letta/schemas/tool.py +2 -45
  23. letta/server/rest_api/routers/v1/__init__.py +4 -9
  24. letta/server/rest_api/routers/v1/agents.py +85 -1
  25. letta/server/rest_api/routers/v1/sandbox_configs.py +108 -0
  26. letta/server/rest_api/routers/v1/tools.py +3 -5
  27. letta/server/rest_api/utils.py +6 -0
  28. letta/server/server.py +159 -12
  29. letta/services/block_manager.py +3 -1
  30. letta/services/blocks_agents_manager.py +84 -0
  31. letta/services/sandbox_config_manager.py +256 -0
  32. letta/services/tool_execution_sandbox.py +326 -0
  33. letta/services/tool_manager.py +10 -10
  34. letta/services/tool_sandbox_env/.gitkeep +0 -0
  35. letta/settings.py +4 -0
  36. {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/METADATA +28 -27
  37. {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/RECORD +40 -31
  38. {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/LICENSE +0 -0
  39. {letta_nightly-0.5.4.dev20241121104201.dist-info → letta_nightly-0.5.4.dev20241123104112.dist-info}/WHEEL +0 -0
  40. {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
- try:
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
- except NoResultFound:
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
- # TODO: consider disabling loading cached agents due to potential concurrency issues
413
- letta_agent = self._get_agent(user_id=user_id, agent_id=agent_id)
414
- if not letta_agent:
415
- logger.debug(f"Agent not loaded, loading agent user_id={user_id} agent_id={agent_id}")
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
- try:
817
- tool_obj = self.tool_manager.get_tool_by_name(tool_name=tool_name, actor=actor)
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
- except NoResultFound:
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() + ToolCreate.load_default_crewai_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
@@ -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, organization_id=actor.organization_id)
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