letta-nightly 0.5.5.dev20241122170833__py3-none-any.whl → 0.6.0.dev20241204052927__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 (70) hide show
  1. letta/__init__.py +2 -2
  2. letta/agent.py +155 -166
  3. letta/agent_store/chroma.py +2 -0
  4. letta/agent_store/db.py +1 -1
  5. letta/cli/cli.py +12 -8
  6. letta/cli/cli_config.py +1 -1
  7. letta/client/client.py +765 -137
  8. letta/config.py +2 -2
  9. letta/constants.py +10 -14
  10. letta/errors.py +12 -0
  11. letta/functions/function_sets/base.py +38 -1
  12. letta/functions/functions.py +40 -57
  13. letta/functions/helpers.py +0 -4
  14. letta/functions/schema_generator.py +279 -18
  15. letta/helpers/tool_rule_solver.py +6 -5
  16. letta/llm_api/helpers.py +99 -5
  17. letta/llm_api/openai.py +8 -2
  18. letta/local_llm/utils.py +13 -6
  19. letta/log.py +7 -9
  20. letta/main.py +1 -1
  21. letta/metadata.py +53 -38
  22. letta/o1_agent.py +1 -4
  23. letta/orm/__init__.py +2 -0
  24. letta/orm/block.py +7 -3
  25. letta/orm/blocks_agents.py +32 -0
  26. letta/orm/errors.py +8 -0
  27. letta/orm/mixins.py +8 -0
  28. letta/orm/organization.py +8 -1
  29. letta/orm/sandbox_config.py +56 -0
  30. letta/orm/sqlalchemy_base.py +68 -10
  31. letta/persistence_manager.py +1 -0
  32. letta/schemas/agent.py +57 -52
  33. letta/schemas/block.py +85 -26
  34. letta/schemas/blocks_agents.py +32 -0
  35. letta/schemas/enums.py +14 -0
  36. letta/schemas/letta_base.py +10 -1
  37. letta/schemas/letta_request.py +11 -23
  38. letta/schemas/letta_response.py +1 -2
  39. letta/schemas/memory.py +41 -76
  40. letta/schemas/message.py +3 -3
  41. letta/schemas/sandbox_config.py +114 -0
  42. letta/schemas/tool.py +37 -1
  43. letta/schemas/tool_rule.py +13 -5
  44. letta/server/rest_api/app.py +5 -4
  45. letta/server/rest_api/interface.py +12 -19
  46. letta/server/rest_api/routers/openai/assistants/threads.py +2 -3
  47. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -2
  48. letta/server/rest_api/routers/v1/__init__.py +4 -9
  49. letta/server/rest_api/routers/v1/agents.py +145 -61
  50. letta/server/rest_api/routers/v1/blocks.py +50 -5
  51. letta/server/rest_api/routers/v1/sandbox_configs.py +127 -0
  52. letta/server/rest_api/routers/v1/sources.py +8 -1
  53. letta/server/rest_api/routers/v1/tools.py +139 -13
  54. letta/server/rest_api/utils.py +6 -0
  55. letta/server/server.py +397 -340
  56. letta/server/static_files/assets/index-9fa459a2.js +1 -1
  57. letta/services/block_manager.py +23 -2
  58. letta/services/blocks_agents_manager.py +106 -0
  59. letta/services/per_agent_lock_manager.py +18 -0
  60. letta/services/sandbox_config_manager.py +256 -0
  61. letta/services/tool_execution_sandbox.py +352 -0
  62. letta/services/tool_manager.py +16 -22
  63. letta/services/tool_sandbox_env/.gitkeep +0 -0
  64. letta/settings.py +4 -0
  65. letta/utils.py +0 -7
  66. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204052927.dist-info}/METADATA +10 -8
  67. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204052927.dist-info}/RECORD +70 -60
  68. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204052927.dist-info}/LICENSE +0 -0
  69. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204052927.dist-info}/WHEEL +0 -0
  70. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204052927.dist-info}/entry_points.txt +0 -0
letta/server/server.py CHANGED
@@ -1,11 +1,15 @@
1
1
  # inspecting tools
2
+ import json
2
3
  import os
3
4
  import traceback
4
5
  import warnings
5
6
  from abc import abstractmethod
7
+ from asyncio import Lock
6
8
  from datetime import datetime
7
9
  from typing import Callable, Dict, List, Optional, Tuple, Union
8
10
 
11
+ from composio.client import Composio
12
+ from composio.client.collections import ActionModel, AppModel
9
13
  from fastapi import HTTPException
10
14
 
11
15
  import letta.constants as constants
@@ -17,24 +21,10 @@ from letta.agent_store.storage import StorageConnector, TableType
17
21
  from letta.credentials import LettaCredentials
18
22
  from letta.data_sources.connectors import DataConnector, load_data
19
23
 
20
- # from letta.data_types import (
21
- # AgentState,
22
- # EmbeddingConfig,
23
- # LLMConfig,
24
- # Message,
25
- # Preset,
26
- # Source,
27
- # Token,
28
- # User,
29
- # )
30
- from letta.functions.functions import generate_schema, parse_source_code
31
- from letta.functions.schema_generator import generate_schema
32
-
33
24
  # TODO use custom interface
34
25
  from letta.interface import AgentInterface # abstract
35
26
  from letta.interface import CLIInterface # for printing to terminal
36
27
  from letta.log import get_logger
37
- from letta.memory import get_memory_functions
38
28
  from letta.metadata import MetadataStore
39
29
  from letta.o1_agent import O1Agent
40
30
  from letta.orm import Base
@@ -53,14 +43,21 @@ from letta.providers import (
53
43
  VLLMChatCompletionsProvider,
54
44
  VLLMCompletionsProvider,
55
45
  )
56
- from letta.schemas.agent import AgentState, AgentType, CreateAgent, UpdateAgentState
46
+ from letta.schemas.agent import (
47
+ AgentState,
48
+ AgentType,
49
+ CreateAgent,
50
+ PersistedAgentState,
51
+ UpdateAgentState,
52
+ )
57
53
  from letta.schemas.api_key import APIKey, APIKeyCreate
54
+ from letta.schemas.block import Block, BlockUpdate
58
55
  from letta.schemas.embedding_config import EmbeddingConfig
59
56
 
60
57
  # openai schemas
61
58
  from letta.schemas.enums import JobStatus
62
59
  from letta.schemas.job import Job
63
- from letta.schemas.letta_message import LettaMessage
60
+ from letta.schemas.letta_message import FunctionReturn, LettaMessage
64
61
  from letta.schemas.llm_config import LLMConfig
65
62
  from letta.schemas.memory import (
66
63
  ArchivalMemorySummary,
@@ -77,14 +74,15 @@ from letta.schemas.usage import LettaUsageStatistics
77
74
  from letta.schemas.user import User
78
75
  from letta.services.agents_tags_manager import AgentsTagsManager
79
76
  from letta.services.block_manager import BlockManager
77
+ from letta.services.blocks_agents_manager import BlocksAgentsManager
80
78
  from letta.services.organization_manager import OrganizationManager
79
+ from letta.services.per_agent_lock_manager import PerAgentLockManager
80
+ from letta.services.sandbox_config_manager import SandboxConfigManager
81
81
  from letta.services.source_manager import SourceManager
82
+ from letta.services.tool_execution_sandbox import ToolExecutionSandbox
82
83
  from letta.services.tool_manager import ToolManager
83
84
  from letta.services.user_manager import UserManager
84
- from letta.utils import create_random_username, json_dumps, json_loads
85
-
86
- # from letta.llm_api_tools import openai_get_model_list, azure_openai_get_model_list, smart_urljoin
87
-
85
+ from letta.utils import create_random_username, get_utc_time, json_dumps, json_loads
88
86
 
89
87
  logger = get_logger(__name__)
90
88
 
@@ -125,10 +123,11 @@ class Server(object):
125
123
  @abstractmethod
126
124
  def create_agent(
127
125
  self,
128
- user_id: str,
129
- agent_config: Union[dict, AgentState],
130
- interface: Union[AgentInterface, None],
131
- ) -> str:
126
+ request: CreateAgent,
127
+ actor: User,
128
+ # interface
129
+ interface: Union[AgentInterface, None] = None,
130
+ ) -> AgentState:
132
131
  """Create a new agent using a config"""
133
132
  raise NotImplementedError
134
133
 
@@ -229,6 +228,14 @@ class SyncServer(Server):
229
228
 
230
229
  self.credentials = LettaCredentials.load()
231
230
 
231
+ # Locks
232
+ self.send_message_lock = Lock()
233
+
234
+ # Composio
235
+ self.composio_client = None
236
+ if tool_settings.composio_api_key:
237
+ self.composio_client = Composio(api_key=tool_settings.composio_api_key)
238
+
232
239
  # Initialize the metadata store
233
240
  config = LettaConfig.load()
234
241
  if settings.letta_pg_uri_no_default:
@@ -247,6 +254,11 @@ class SyncServer(Server):
247
254
  self.block_manager = BlockManager()
248
255
  self.source_manager = SourceManager()
249
256
  self.agents_tags_manager = AgentsTagsManager()
257
+ self.sandbox_config_manager = SandboxConfigManager(tool_settings)
258
+ self.blocks_agents_manager = BlocksAgentsManager()
259
+
260
+ # Managers that interface with parallelism
261
+ self.per_agent_lock_manager = PerAgentLockManager()
250
262
 
251
263
  # Make default user and org
252
264
  if init_with_default_org_and_user:
@@ -360,76 +372,29 @@ class SyncServer(Server):
360
372
  }
361
373
  )
362
374
 
363
- def _load_agent(self, agent_id: str, actor: User, interface: Union[AgentInterface, None] = None) -> Agent:
364
- """Loads a saved agent into memory (if it doesn't exist, throw an error)"""
365
- assert isinstance(agent_id, str), agent_id
366
- user_id = actor.id
367
-
368
- # If an interface isn't specified, use the default
369
- if interface is None:
370
- interface = self.default_interface_factory()
371
-
372
- try:
373
- logger.debug(f"Grabbing agent user_id={user_id} agent_id={agent_id} from database")
374
- agent_state = self.ms.get_agent(agent_id=agent_id, user_id=user_id)
375
- if not agent_state:
376
- logger.exception(f"agent_id {agent_id} does not exist")
377
- raise ValueError(f"agent_id {agent_id} does not exist")
378
-
379
- # Instantiate an agent object using the state retrieved
380
- logger.debug(f"Creating an agent object")
381
- tool_objs = []
382
- for name in agent_state.tools:
383
- # TODO: This should be a hard failure, but for migration reasons, we patch it for now
384
- try:
385
- tool_obj = self.tool_manager.get_tool_by_name(tool_name=name, actor=actor)
386
- tool_objs.append(tool_obj)
387
- except NoResultFound:
388
- warnings.warn(f"Tried to retrieve a tool with name {name} from the agent_state, but does not exist in tool db.")
389
-
390
- # set agent_state tools to only the names of the available tools
391
- agent_state.tools = [t.name for t in tool_objs]
392
-
393
- # Make sure the memory is a memory object
394
- assert isinstance(agent_state.memory, Memory)
375
+ def load_agent(self, agent_id: str, interface: Union[AgentInterface, None] = None) -> Agent:
376
+ """Updated method to load agents from persisted storage"""
377
+ agent_lock = self.per_agent_lock_manager.get_lock(agent_id)
378
+ with agent_lock:
379
+ agent_state = self.get_agent(agent_id=agent_id)
380
+ actor = self.user_manager.get_user_by_id(user_id=agent_state.user_id)
395
381
 
382
+ interface = interface or self.default_interface_factory()
396
383
  if agent_state.agent_type == AgentType.memgpt_agent:
397
- letta_agent = Agent(agent_state=agent_state, interface=interface, tools=tool_objs, user=actor)
398
- elif agent_state.agent_type == AgentType.o1_agent:
399
- letta_agent = O1Agent(agent_state=agent_state, interface=interface, tools=tool_objs, user=actor)
384
+ agent = Agent(agent_state=agent_state, interface=interface, user=actor)
400
385
  else:
401
- raise NotImplementedError("Not a supported agent type")
402
-
403
- # Add the agent to the in-memory store and return its reference
404
- logger.debug(f"Adding agent to the agent cache: user_id={user_id}, agent_id={agent_id}")
405
- self._add_agent(user_id=user_id, agent_id=agent_id, agent_obj=letta_agent)
406
- return letta_agent
386
+ agent = O1Agent(agent_state=agent_state, interface=interface, user=actor)
407
387
 
408
- except Exception as e:
409
- logger.exception(f"Error occurred while trying to get agent {agent_id}:\n{e}")
410
- raise
411
-
412
- def _get_or_load_agent(self, agent_id: str) -> Agent:
413
- """Check if the agent is in-memory, then load"""
414
- agent_state = self.ms.get_agent(agent_id=agent_id)
415
- if not agent_state:
416
- raise ValueError(f"Agent does not exist")
417
- user_id = agent_state.user_id
418
- actor = self.user_manager.get_user_by_id(user_id)
419
-
420
- logger.debug(f"Checking for agent user_id={user_id} agent_id={agent_id}")
421
- # TODO: consider disabling loading cached agents due to potential concurrency issues
422
- letta_agent = self._get_agent(user_id=user_id, agent_id=agent_id)
423
- if not letta_agent:
424
- logger.debug(f"Agent not loaded, loading agent user_id={user_id} agent_id={agent_id}")
425
- letta_agent = self._load_agent(agent_id=agent_id, actor=actor)
426
- return letta_agent
388
+ # Persist to agent
389
+ save_agent(agent, self.ms)
390
+ return agent
427
391
 
428
392
  def _step(
429
393
  self,
430
394
  user_id: str,
431
395
  agent_id: str,
432
396
  input_messages: Union[Message, List[Message]],
397
+ interface: Union[AgentInterface, None] = None, # needed to getting responses
433
398
  # timestamp: Optional[datetime],
434
399
  ) -> LettaUsageStatistics:
435
400
  """Send the input message through the agent"""
@@ -445,7 +410,8 @@ class SyncServer(Server):
445
410
  try:
446
411
 
447
412
  # Get the agent object (loaded in memory)
448
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
413
+ # letta_agent = self._get_or_load_agent(agent_id=agent_id)
414
+ letta_agent = self.load_agent(agent_id=agent_id, interface=interface)
449
415
  if letta_agent is None:
450
416
  raise KeyError(f"Agent (user={user_id}, agent={agent_id}) is not loaded")
451
417
 
@@ -462,6 +428,9 @@ class SyncServer(Server):
462
428
  skip_verify=True,
463
429
  )
464
430
 
431
+ # save agent after step
432
+ save_agent(letta_agent, self.ms)
433
+
465
434
  except Exception as e:
466
435
  logger.error(f"Error in server._step: {e}")
467
436
  print(traceback.print_exc())
@@ -479,7 +448,7 @@ class SyncServer(Server):
479
448
  logger.debug(f"Got command: {command}")
480
449
 
481
450
  # Get the agent object (loaded in memory)
482
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
451
+ letta_agent = self.load_agent(agent_id=agent_id)
483
452
  usage = None
484
453
 
485
454
  if command.lower() == "exit":
@@ -516,7 +485,7 @@ class SyncServer(Server):
516
485
  elif command.lower() == "memory":
517
486
  ret_str = (
518
487
  f"\nDumping memory contents:\n"
519
- + f"\n{str(letta_agent.memory)}"
488
+ + f"\n{str(letta_agent.agent_state.memory)}"
520
489
  + f"\n{str(letta_agent.persistence_manager.archival_memory)}"
521
490
  + f"\n{str(letta_agent.persistence_manager.recall_memory)}"
522
491
  )
@@ -708,6 +677,7 @@ class SyncServer(Server):
708
677
  # whether or not to wrap user and system message as MemGPT-style stringified JSON
709
678
  wrap_user_message: bool = True,
710
679
  wrap_system_message: bool = True,
680
+ interface: Union[AgentInterface, None] = None, # needed to getting responses
711
681
  ) -> LettaUsageStatistics:
712
682
  """Send a list of messages to the agent
713
683
 
@@ -760,7 +730,7 @@ class SyncServer(Server):
760
730
  raise ValueError(f"All messages must be of type Message or MessageCreate, got {[type(message) for message in messages]}")
761
731
 
762
732
  # Run the agent state forward
763
- return self._step(user_id=user_id, agent_id=agent_id, input_messages=message_objects)
733
+ return self._step(user_id=user_id, agent_id=agent_id, input_messages=message_objects, interface=interface)
764
734
 
765
735
  # @LockingServer.agent_lock_decorator
766
736
  def run_command(self, user_id: str, agent_id: str, command: str) -> LettaUsageStatistics:
@@ -808,129 +778,109 @@ class SyncServer(Server):
808
778
  else:
809
779
  raise ValueError(f"Invalid agent type: {request.agent_type}")
810
780
 
781
+ # create blocks (note: cannot be linked into the agent_id is created)
782
+ blocks = []
783
+ for create_block in request.memory_blocks:
784
+ block = self.block_manager.create_or_update_block(Block(**create_block.model_dump()), actor=actor)
785
+ blocks.append(block)
786
+
787
+ # get tools + only add if they exist
788
+ tool_objs = []
789
+ if request.tools:
790
+ for tool_name in request.tools:
791
+ tool_obj = self.tool_manager.get_tool_by_name(tool_name=tool_name, actor=actor)
792
+ if tool_obj:
793
+ tool_objs.append(tool_obj)
794
+ else:
795
+ warnings.warn(f"Attempted to add a nonexistent tool {tool_name} to agent {request.name}, skipping.")
796
+ # reset the request.tools to only valid tools
797
+ request.tools = [t.name for t in tool_objs]
798
+
799
+ # get the user
811
800
  logger.debug(f"Attempting to find user: {user_id}")
812
801
  user = self.user_manager.get_user_by_id(user_id=user_id)
813
802
  if not user:
814
803
  raise ValueError(f"cannot find user with associated client id: {user_id}")
815
804
 
816
- try:
817
- # model configuration
818
- llm_config = request.llm_config
819
- embedding_config = request.embedding_config
820
-
821
- # get tools + only add if they exist
822
- tool_objs = []
823
- if request.tools:
824
- for tool_name in request.tools:
825
- try:
826
- tool_obj = self.tool_manager.get_tool_by_name(tool_name=tool_name, actor=actor)
827
- tool_objs.append(tool_obj)
828
- except NoResultFound:
829
- warnings.warn(f"Attempted to add a nonexistent tool {tool_name} to agent {request.name}, skipping.")
830
- # reset the request.tools to only valid tools
831
- request.tools = [t.name for t in tool_objs]
832
-
833
- assert request.memory is not None
834
- memory_functions = get_memory_functions(request.memory)
835
- for func_name, func in memory_functions.items():
836
-
837
- if request.tools and func_name in request.tools:
838
- # tool already added
839
- continue
840
- source_code = parse_source_code(func)
841
- # memory functions are not terminal
842
- json_schema = generate_schema(func, name=func_name)
843
- source_type = "python"
844
- tags = ["memory", "memgpt-base"]
845
- tool = self.tool_manager.create_or_update_tool(
846
- Tool(
847
- source_code=source_code,
848
- source_type=source_type,
849
- tags=tags,
850
- json_schema=json_schema,
851
- ),
852
- actor=actor,
853
- )
854
- tool_objs.append(tool)
855
- if not request.tools:
856
- request.tools = []
857
- request.tools.append(tool.name)
858
-
859
- # TODO: save the agent state
860
- agent_state = AgentState(
861
- name=request.name,
862
- user_id=user_id,
863
- tools=request.tools if request.tools else [],
864
- tool_rules=request.tool_rules if request.tool_rules else [],
865
- agent_type=request.agent_type or AgentType.memgpt_agent,
866
- llm_config=llm_config,
867
- embedding_config=embedding_config,
868
- system=request.system,
869
- memory=request.memory,
870
- description=request.description,
871
- metadata_=request.metadata_,
872
- tags=request.tags,
873
- )
874
- if request.agent_type == AgentType.memgpt_agent:
875
- agent = Agent(
876
- interface=interface,
877
- agent_state=agent_state,
878
- tools=tool_objs,
879
- # gpt-3.5-turbo tends to omit inner monologue, relax this requirement for now
880
- first_message_verify_mono=(
881
- True if (llm_config and llm_config.model is not None and "gpt-4" in llm_config.model) else False
882
- ),
883
- user=actor,
884
- initial_message_sequence=request.initial_message_sequence,
885
- )
886
- elif request.agent_type == AgentType.o1_agent:
887
- agent = O1Agent(
888
- interface=interface,
889
- agent_state=agent_state,
890
- tools=tool_objs,
891
- # gpt-3.5-turbo tends to omit inner monologue, relax this requirement for now
892
- first_message_verify_mono=(
893
- True if (llm_config and llm_config.model is not None and "gpt-4" in llm_config.model) else False
894
- ),
895
- user=actor,
896
- )
897
- # rebuilding agent memory on agent create in case shared memory blocks
898
- # were specified in the new agent's memory config. we're doing this for two reasons:
899
- # 1. if only the ID of the shared memory block was specified, we can fetch its most recent value
900
- # 2. if the shared block state changed since this agent initialization started, we can be sure to have the latest value
901
- agent.rebuild_memory(force=True, ms=self.ms)
902
- # FIXME: this is a hacky way to get the system prompts injected into agent into the DB
903
- # self.ms.update_agent(agent.agent_state)
904
- except Exception as e:
905
- logger.exception(e)
906
- try:
907
- if agent:
908
- self.ms.delete_agent(agent_id=agent.agent_state.id)
909
- except Exception as delete_e:
910
- logger.exception(f"Failed to delete_agent:\n{delete_e}")
911
- raise e
805
+ # TODO: create the message objects (NOTE: do this after we migrate to `CreateMessage`)
806
+
807
+ # created and persist the agent state in the DB
808
+ agent_state = PersistedAgentState(
809
+ name=request.name,
810
+ user_id=user_id,
811
+ tool_names=request.tools if request.tools else [],
812
+ tool_rules=request.tool_rules,
813
+ agent_type=request.agent_type or AgentType.memgpt_agent,
814
+ llm_config=request.llm_config,
815
+ embedding_config=request.embedding_config,
816
+ system=request.system,
817
+ # other metadata
818
+ description=request.description,
819
+ metadata_=request.metadata_,
820
+ )
821
+ # TODO: move this to agent ORM
822
+ # this saves the agent ID and state into the DB
823
+ self.ms.create_agent(agent_state)
912
824
 
913
- # save agent
914
- save_agent(agent, self.ms)
915
- logger.debug(f"Created new agent from config: {agent}")
825
+ # Note: mappings (e.g. tags, blocks) are created after the agent is persisted
826
+ # TODO: add source mappings here as well
916
827
 
917
- # TODO: move this into save_agent. save_agent should be moved to server.py
828
+ # create the tags
918
829
  if request.tags:
919
830
  for tag in request.tags:
920
- self.agents_tags_manager.add_tag_to_agent(agent_id=agent.agent_state.id, tag=tag, actor=actor)
831
+ self.agents_tags_manager.add_tag_to_agent(agent_id=agent_state.id, tag=tag, actor=actor)
832
+
833
+ # create block mappins (now that agent is persisted)
834
+ for block in blocks:
835
+ # this links the created block to the agent
836
+ self.blocks_agents_manager.add_block_to_agent(block_id=block.id, agent_id=agent_state.id, block_label=block.label)
837
+
838
+ in_memory_agent_state = self.get_agent(agent_state.id)
839
+ return in_memory_agent_state
840
+
841
+ def get_agent(self, agent_id: str) -> AgentState:
842
+ """
843
+ Retrieve the full agent state from the DB.
844
+ This gathers data accross multiple tables to provide the full state of an agent, which is passed into the `Agent` object for creation.
845
+ """
846
+
847
+ # get data persisted from the DB
848
+ agent_state = self.ms.get_agent(agent_id=agent_id)
849
+ if agent_state is None:
850
+ # agent does not exist
851
+ return None
852
+ user = self.user_manager.get_user_by_id(user_id=agent_state.user_id)
853
+
854
+ # construct the in-memory, full agent state - this gather data stored in different tables but that needs to be passed to `Agent`
855
+ # we also return this data to the user to provide all the state related to an agent
856
+
857
+ # get `Memory` object by getting the linked block IDs and fetching the blocks, then putting that into a `Memory` object
858
+ # this is the "in memory" representation of the in-context memory
859
+ block_ids = self.blocks_agents_manager.list_block_ids_for_agent(agent_id=agent_id)
860
+ blocks = []
861
+ for block_id in block_ids:
862
+ block = self.block_manager.get_block_by_id(block_id=block_id, actor=user)
863
+ assert block, f"Block with ID {block_id} does not exist"
864
+ blocks.append(block)
865
+ memory = Memory(blocks=blocks)
866
+
867
+ # get `Tool` objects
868
+ tools = [self.tool_manager.get_tool_by_name(tool_name=tool_name, actor=user) for tool_name in agent_state.tool_names]
921
869
 
922
- assert isinstance(agent.agent_state.memory, Memory), f"Invalid memory type: {type(agent_state.memory)}"
870
+ # get `Source` objects
871
+ sources = self.list_attached_sources(agent_id=agent_id)
923
872
 
924
- # TODO: remove (hacky)
925
- agent.agent_state.tags = self.agents_tags_manager.get_tags_for_agent(agent_id=agent.agent_state.id, actor=actor)
873
+ # get the tags
874
+ tags = self.agents_tags_manager.get_tags_for_agent(agent_id=agent_id, actor=user)
926
875
 
927
- return agent.agent_state
876
+ # return the full agent state - this contains all data needed to recreate the agent
877
+ return AgentState(**agent_state.model_dump(), memory=memory, tools=tools, sources=sources, tags=tags)
928
878
 
929
879
  def update_agent(
930
880
  self,
931
881
  request: UpdateAgentState,
932
882
  actor: User,
933
- ):
883
+ ) -> AgentState:
934
884
  """Update the agents core memory block, return the new state"""
935
885
  try:
936
886
  self.user_manager.get_user_by_id(user_id=actor.id)
@@ -941,13 +891,7 @@ class SyncServer(Server):
941
891
  raise ValueError(f"Agent agent_id={request.id} does not exist")
942
892
 
943
893
  # Get the agent object (loaded in memory)
944
- letta_agent = self._get_or_load_agent(agent_id=request.id)
945
-
946
- # update the core memory of the agent
947
- if request.memory:
948
- assert isinstance(request.memory, Memory), type(request.memory)
949
- new_memory_contents = request.memory.to_flat_dict()
950
- _ = self.update_agent_core_memory(user_id=actor.id, agent_id=request.id, new_memory_contents=new_memory_contents)
894
+ letta_agent = self.load_agent(agent_id=request.id)
951
895
 
952
896
  # update the system prompt
953
897
  if request.system:
@@ -961,13 +905,13 @@ class SyncServer(Server):
961
905
  letta_agent.set_message_buffer(message_ids=request.message_ids)
962
906
 
963
907
  # tools
964
- if request.tools:
908
+ if request.tool_names:
965
909
  # Replace tools and also re-link
966
910
 
967
911
  # (1) get tools + make sure they exist
968
912
  # Current and target tools as sets of tool names
969
- current_tools = set(letta_agent.agent_state.tools)
970
- target_tools = set(request.tools)
913
+ current_tools = set(letta_agent.agent_state.tool_names)
914
+ target_tools = set(request.tool_names)
971
915
 
972
916
  # Calculate tools to add and remove
973
917
  tools_to_add = target_tools - current_tools
@@ -984,7 +928,7 @@ class SyncServer(Server):
984
928
  self.add_tool_to_agent(agent_id=request.id, tool_id=tool.id, user_id=actor.id)
985
929
 
986
930
  # reload agent
987
- letta_agent = self._get_or_load_agent(agent_id=request.id)
931
+ letta_agent = self.load_agent(agent_id=request.id)
988
932
 
989
933
  # configs
990
934
  if request.llm_config:
@@ -1012,7 +956,6 @@ class SyncServer(Server):
1012
956
  self.agents_tags_manager.delete_tag_from_agent(agent_id=letta_agent.agent_state.id, tag=tag, actor=actor)
1013
957
 
1014
958
  # save the agent
1015
- assert isinstance(letta_agent.memory, Memory)
1016
959
  save_agent(letta_agent, self.ms)
1017
960
  # TODO: probably reload the agent somehow?
1018
961
  return letta_agent.agent_state
@@ -1025,8 +968,8 @@ class SyncServer(Server):
1025
968
  raise ValueError(f"Agent agent_id={agent_id} does not exist")
1026
969
 
1027
970
  # Get the agent object (loaded in memory)
1028
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1029
- return letta_agent.tools
971
+ letta_agent = self.load_agent(agent_id=agent_id)
972
+ return letta_agent.agent_state.tools
1030
973
 
1031
974
  def add_tool_to_agent(
1032
975
  self,
@@ -1044,7 +987,7 @@ class SyncServer(Server):
1044
987
  raise ValueError(f"Agent agent_id={agent_id} does not exist")
1045
988
 
1046
989
  # Get the agent object (loaded in memory)
1047
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
990
+ letta_agent = self.load_agent(agent_id=agent_id)
1048
991
 
1049
992
  # Get all the tool objects from the request
1050
993
  tool_objs = []
@@ -1052,7 +995,7 @@ class SyncServer(Server):
1052
995
  assert tool_obj, f"Tool with id={tool_id} does not exist"
1053
996
  tool_objs.append(tool_obj)
1054
997
 
1055
- for tool in letta_agent.tools:
998
+ for tool in letta_agent.agent_state.tools:
1056
999
  tool_obj = self.tool_manager.get_tool_by_id(tool_id=tool.id, actor=user)
1057
1000
  assert tool_obj, f"Tool with id={tool.id} does not exist"
1058
1001
 
@@ -1061,7 +1004,7 @@ class SyncServer(Server):
1061
1004
  tool_objs.append(tool_obj)
1062
1005
 
1063
1006
  # replace the list of tool names ("ids") inside the agent state
1064
- letta_agent.agent_state.tools = [tool.name for tool in tool_objs]
1007
+ letta_agent.agent_state.tool_names = [tool.name for tool in tool_objs]
1065
1008
 
1066
1009
  # then attempt to link the tools modules
1067
1010
  letta_agent.link_tools(tool_objs)
@@ -1086,11 +1029,11 @@ class SyncServer(Server):
1086
1029
  raise ValueError(f"Agent agent_id={agent_id} does not exist")
1087
1030
 
1088
1031
  # Get the agent object (loaded in memory)
1089
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1032
+ letta_agent = self.load_agent(agent_id=agent_id)
1090
1033
 
1091
1034
  # Get all the tool_objs
1092
1035
  tool_objs = []
1093
- for tool in letta_agent.tools:
1036
+ for tool in letta_agent.agent_state.tools:
1094
1037
  tool_obj = self.tool_manager.get_tool_by_id(tool_id=tool.id, actor=user)
1095
1038
  assert tool_obj, f"Tool with id={tool.id} does not exist"
1096
1039
 
@@ -1099,7 +1042,7 @@ class SyncServer(Server):
1099
1042
  tool_objs.append(tool_obj)
1100
1043
 
1101
1044
  # replace the list of tool names ("ids") inside the agent state
1102
- letta_agent.agent_state.tools = [tool.name for tool in tool_objs]
1045
+ letta_agent.agent_state.tool_names = [tool.name for tool in tool_objs]
1103
1046
 
1104
1047
  # then attempt to link the tools modules
1105
1048
  letta_agent.link_tools(tool_objs)
@@ -1108,18 +1051,9 @@ class SyncServer(Server):
1108
1051
  save_agent(letta_agent, self.ms)
1109
1052
  return letta_agent.agent_state
1110
1053
 
1111
- def _agent_state_to_config(self, agent_state: AgentState) -> dict:
1112
- """Convert AgentState to a dict for a JSON response"""
1113
- assert agent_state is not None
1114
-
1115
- agent_config = {
1116
- "id": agent_state.id,
1117
- "name": agent_state.name,
1118
- "human": agent_state._metadata.get("human", None),
1119
- "persona": agent_state._metadata.get("persona", None),
1120
- "created_at": agent_state.created_at.isoformat(),
1121
- }
1122
- return agent_config
1054
+ def get_agent_state(self, user_id: str, agent_id: str) -> AgentState:
1055
+ # TODO: duplicate, remove
1056
+ return self.get_agent(agent_id=agent_id)
1123
1057
 
1124
1058
  def list_agents(self, user_id: str, tags: Optional[List[str]] = None) -> List[AgentState]:
1125
1059
  """List all available agents to a user"""
@@ -1127,13 +1061,13 @@ class SyncServer(Server):
1127
1061
 
1128
1062
  if tags is None:
1129
1063
  agents_states = self.ms.list_agents(user_id=user_id)
1130
- return agents_states
1064
+ agent_ids = [agent.id for agent in agents_states]
1131
1065
  else:
1132
1066
  agent_ids = []
1133
1067
  for tag in tags:
1134
1068
  agent_ids += self.agents_tags_manager.get_agents_by_tag(tag=tag, actor=user)
1135
1069
 
1136
- return [self.get_agent_state(user_id=user.id, agent_id=agent_id) for agent_id in agent_ids]
1070
+ return [self.get_agent(agent_id=agent_id) for agent_id in agent_ids]
1137
1071
 
1138
1072
  # convert name->id
1139
1073
 
@@ -1157,34 +1091,34 @@ class SyncServer(Server):
1157
1091
 
1158
1092
  def get_agent_memory(self, agent_id: str) -> Memory:
1159
1093
  """Return the memory of an agent (core memory)"""
1160
- agent = self._get_or_load_agent(agent_id=agent_id)
1161
- return agent.memory
1094
+ agent = self.load_agent(agent_id=agent_id)
1095
+ return agent.agent_state.memory
1162
1096
 
1163
1097
  def get_archival_memory_summary(self, agent_id: str) -> ArchivalMemorySummary:
1164
- agent = self._get_or_load_agent(agent_id=agent_id)
1098
+ agent = self.load_agent(agent_id=agent_id)
1165
1099
  return ArchivalMemorySummary(size=len(agent.persistence_manager.archival_memory))
1166
1100
 
1167
1101
  def get_recall_memory_summary(self, agent_id: str) -> RecallMemorySummary:
1168
- agent = self._get_or_load_agent(agent_id=agent_id)
1102
+ agent = self.load_agent(agent_id=agent_id)
1169
1103
  return RecallMemorySummary(size=len(agent.persistence_manager.recall_memory))
1170
1104
 
1171
1105
  def get_in_context_message_ids(self, agent_id: str) -> List[str]:
1172
1106
  """Get the message ids of the in-context messages in the agent's memory"""
1173
1107
  # Get the agent object (loaded in memory)
1174
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1175
- return [m.id for m in letta_agent._messages]
1108
+ agent = self.load_agent(agent_id=agent_id)
1109
+ return [m.id for m in agent._messages]
1176
1110
 
1177
1111
  def get_in_context_messages(self, agent_id: str) -> List[Message]:
1178
1112
  """Get the in-context messages in the agent's memory"""
1179
1113
  # Get the agent object (loaded in memory)
1180
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1181
- return letta_agent._messages
1114
+ agent = self.load_agent(agent_id=agent_id)
1115
+ return agent._messages
1182
1116
 
1183
1117
  def get_agent_message(self, agent_id: str, message_id: str) -> Message:
1184
1118
  """Get a single message from the agent's memory"""
1185
1119
  # Get the agent object (loaded in memory)
1186
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1187
- message = letta_agent.persistence_manager.recall_memory.storage.get(id=message_id)
1120
+ agent = self.load_agent(agent_id=agent_id)
1121
+ message = agent.persistence_manager.recall_memory.storage.get(id=message_id)
1188
1122
  return message
1189
1123
 
1190
1124
  def get_agent_messages(
@@ -1192,11 +1126,10 @@ class SyncServer(Server):
1192
1126
  agent_id: str,
1193
1127
  start: int,
1194
1128
  count: int,
1195
- return_message_object: bool = True,
1196
1129
  ) -> Union[List[Message], List[LettaMessage]]:
1197
1130
  """Paginated query of all messages in agent message queue"""
1198
1131
  # Get the agent object (loaded in memory)
1199
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1132
+ letta_agent = self.load_agent(agent_id=agent_id)
1200
1133
 
1201
1134
  if start < 0 or count < 0:
1202
1135
  raise ValueError("Start and count values should be non-negative")
@@ -1214,10 +1147,6 @@ class SyncServer(Server):
1214
1147
  # Slice the list for pagination
1215
1148
  messages = reversed_messages[start:end_index]
1216
1149
 
1217
- ## Convert to json
1218
- ## Add a tag indicating in-context or not
1219
- # json_messages = [{**record.to_json(), "in_context": True} for record in messages]
1220
-
1221
1150
  else:
1222
1151
  # need to access persistence manager for additional messages
1223
1152
  db_iterator = letta_agent.persistence_manager.recall_memory.storage.get_all_paginated(page_size=count, offset=start)
@@ -1237,9 +1166,6 @@ class SyncServer(Server):
1237
1166
  # for d in json_messages:
1238
1167
  # d["in_context"] = True if str(d["id"]) in in_context_message_ids else False
1239
1168
 
1240
- if not return_message_object:
1241
- messages = [msg for m in messages for msg in m.to_letta_message()]
1242
-
1243
1169
  return messages
1244
1170
 
1245
1171
  def get_agent_archival(self, user_id: str, agent_id: str, start: int, count: int) -> List[Passage]:
@@ -1250,7 +1176,7 @@ class SyncServer(Server):
1250
1176
  raise ValueError(f"Agent agent_id={agent_id} does not exist")
1251
1177
 
1252
1178
  # Get the agent object (loaded in memory)
1253
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1179
+ letta_agent = self.load_agent(agent_id=agent_id)
1254
1180
 
1255
1181
  # iterate over records
1256
1182
  db_iterator = letta_agent.persistence_manager.archival_memory.storage.get_all_paginated(page_size=count, offset=start)
@@ -1275,7 +1201,7 @@ class SyncServer(Server):
1275
1201
  raise ValueError(f"Agent agent_id={agent_id} does not exist")
1276
1202
 
1277
1203
  # Get the agent object (loaded in memory)
1278
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1204
+ letta_agent = self.load_agent(agent_id=agent_id)
1279
1205
 
1280
1206
  # iterate over recorde
1281
1207
  cursor, records = letta_agent.persistence_manager.archival_memory.storage.get_all_cursor(
@@ -1290,11 +1216,15 @@ class SyncServer(Server):
1290
1216
  raise ValueError(f"Agent agent_id={agent_id} does not exist")
1291
1217
 
1292
1218
  # Get the agent object (loaded in memory)
1293
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1219
+ letta_agent = self.load_agent(agent_id=agent_id)
1294
1220
 
1295
1221
  # Insert into archival memory
1296
1222
  passage_ids = letta_agent.persistence_manager.archival_memory.insert(memory_string=memory_contents, return_ids=True)
1297
1223
 
1224
+ # Update the agent
1225
+ # TODO: should this update the system prompt?
1226
+ save_agent(letta_agent, self.ms)
1227
+
1298
1228
  # TODO: this is gross, fix
1299
1229
  return [letta_agent.persistence_manager.archival_memory.storage.get(id=passage_id) for passage_id in passage_ids]
1300
1230
 
@@ -1307,7 +1237,7 @@ class SyncServer(Server):
1307
1237
  # TODO: should return a passage
1308
1238
 
1309
1239
  # Get the agent object (loaded in memory)
1310
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1240
+ letta_agent = self.load_agent(agent_id=agent_id)
1311
1241
 
1312
1242
  # Delete by ID
1313
1243
  # TODO check if it exists first, and throw error if not
@@ -1326,9 +1256,8 @@ class SyncServer(Server):
1326
1256
  order: Optional[str] = "asc",
1327
1257
  reverse: Optional[bool] = False,
1328
1258
  return_message_object: bool = True,
1329
- use_assistant_message: bool = False,
1330
- assistant_message_function_name: str = constants.DEFAULT_MESSAGE_TOOL,
1331
- assistant_message_function_kwarg: str = constants.DEFAULT_MESSAGE_TOOL_KWARG,
1259
+ assistant_message_tool_name: str = constants.DEFAULT_MESSAGE_TOOL,
1260
+ assistant_message_tool_kwarg: str = constants.DEFAULT_MESSAGE_TOOL_KWARG,
1332
1261
  ) -> Union[List[Message], List[LettaMessage]]:
1333
1262
  if self.user_manager.get_user_by_id(user_id=user_id) is None:
1334
1263
  raise ValueError(f"User user_id={user_id} does not exist")
@@ -1336,7 +1265,7 @@ class SyncServer(Server):
1336
1265
  raise ValueError(f"Agent agent_id={agent_id} does not exist")
1337
1266
 
1338
1267
  # Get the agent object (loaded in memory)
1339
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1268
+ letta_agent = self.load_agent(agent_id=agent_id)
1340
1269
 
1341
1270
  # iterate over records
1342
1271
  cursor, records = letta_agent.persistence_manager.recall_memory.storage.get_all_cursor(
@@ -1347,50 +1276,19 @@ class SyncServer(Server):
1347
1276
 
1348
1277
  if not return_message_object:
1349
1278
  # If we're GETing messages in reverse, we need to reverse the inner list (generated by to_letta_message)
1350
- if reverse:
1351
- records = [
1352
- msg
1353
- for m in records
1354
- for msg in m.to_letta_message(
1355
- assistant_message=use_assistant_message,
1356
- assistant_message_function_name=assistant_message_function_name,
1357
- assistant_message_function_kwarg=assistant_message_function_kwarg,
1358
- )[::-1]
1359
- ]
1360
- else:
1361
- records = [
1362
- msg
1363
- for m in records
1364
- for msg in m.to_letta_message(
1365
- assistant_message=use_assistant_message,
1366
- assistant_message_function_name=assistant_message_function_name,
1367
- assistant_message_function_kwarg=assistant_message_function_kwarg,
1368
- )
1369
- ]
1370
-
1371
- return records
1372
-
1373
- def get_agent_state(self, user_id: str, agent_id: Optional[str], agent_name: Optional[str] = None) -> Optional[AgentState]:
1374
- """Return the config of an agent"""
1375
- user = self.user_manager.get_user_by_id(user_id=user_id)
1376
- if agent_id:
1377
- if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
1378
- return None
1379
- else:
1380
- agent_state = self.ms.get_agent(agent_name=agent_name, user_id=user_id)
1381
- if agent_state is None:
1382
- raise ValueError(f"Agent agent_name={agent_name} does not exist")
1383
- agent_id = agent_state.id
1279
+ records = [
1280
+ msg
1281
+ for m in records
1282
+ for msg in m.to_letta_message(
1283
+ assistant_message_tool_name=assistant_message_tool_name,
1284
+ assistant_message_tool_kwarg=assistant_message_tool_kwarg,
1285
+ )
1286
+ ]
1384
1287
 
1385
- # Get the agent object (loaded in memory)
1386
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1387
- assert isinstance(letta_agent.memory, Memory)
1288
+ if reverse:
1289
+ records = records[::-1]
1388
1290
 
1389
- letta_agent.update_memory_blocks_from_db()
1390
- agent_state = letta_agent.agent_state.model_copy(deep=True)
1391
- # Load the tags in for the agent_state
1392
- agent_state.tags = self.agents_tags_manager.get_tags_for_agent(agent_id=agent_id, actor=user)
1393
- return agent_state
1291
+ return records
1394
1292
 
1395
1293
  def get_server_config(self, include_defaults: bool = False) -> dict:
1396
1294
  """Return the base config"""
@@ -1415,38 +1313,23 @@ class SyncServer(Server):
1415
1313
 
1416
1314
  return response
1417
1315
 
1418
- def update_agent_core_memory(self, user_id: str, agent_id: str, new_memory_contents: dict) -> Memory:
1419
- """Update the agents core memory block, return the new state"""
1420
- if self.user_manager.get_user_by_id(user_id=user_id) is None:
1421
- raise ValueError(f"User user_id={user_id} does not exist")
1422
- if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
1423
- raise ValueError(f"Agent agent_id={agent_id} does not exist")
1316
+ def update_agent_core_memory(self, user_id: str, agent_id: str, label: str, value: str) -> Memory:
1317
+ """Update the value of a block in the agent's memory"""
1424
1318
 
1425
- # Get the agent object (loaded in memory)
1426
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1427
-
1428
- # old_core_memory = self.get_agent_memory(agent_id=agent_id)
1429
-
1430
- modified = False
1431
- for key, value in new_memory_contents.items():
1432
- if letta_agent.memory.get_block(key) is None:
1433
- # raise ValueError(f"Key {key} not found in agent memory {list(letta_agent.memory.list_block_names())}")
1434
- raise ValueError(f"Key {key} not found in agent memory {str(letta_agent.memory.memory)}")
1435
- if value is None:
1436
- continue
1437
- if letta_agent.memory.get_block(key) != value:
1438
- letta_agent.memory.update_block_value(label=key, value=value) # update agent memory
1439
- modified = True
1440
-
1441
- # If we modified the memory contents, we need to rebuild the memory block inside the system message
1442
- if modified:
1443
- letta_agent.rebuild_memory()
1444
- # save agent
1445
- save_agent(letta_agent, self.ms)
1319
+ # get the block id
1320
+ block = self.get_agent_block_by_label(user_id=user_id, agent_id=agent_id, label=label)
1321
+ block_id = block.id
1322
+
1323
+ # update the block
1324
+ self.block_manager.update_block(
1325
+ block_id=block_id, block_update=BlockUpdate(value=value), actor=self.user_manager.get_user_by_id(user_id=user_id)
1326
+ )
1446
1327
 
1447
- return self.ms.get_agent(agent_id=agent_id).memory
1328
+ # load agent
1329
+ letta_agent = self.load_agent(agent_id=agent_id)
1330
+ return letta_agent.agent_state.memory
1448
1331
 
1449
- def rename_agent(self, user_id: str, agent_id: str, new_agent_name: str) -> AgentState:
1332
+ def rename_agent(self, user_id: str, agent_id: str, new_agent_name: str) -> PersistedAgentState:
1450
1333
  """Update the name of the agent in the database"""
1451
1334
  if self.user_manager.get_user_by_id(user_id=user_id) is None:
1452
1335
  raise ValueError(f"User user_id={user_id} does not exist")
@@ -1454,7 +1337,7 @@ class SyncServer(Server):
1454
1337
  raise ValueError(f"Agent agent_id={agent_id} does not exist")
1455
1338
 
1456
1339
  # Get the agent object (loaded in memory)
1457
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1340
+ letta_agent = self.load_agent(agent_id=agent_id)
1458
1341
 
1459
1342
  current_name = letta_agent.agent_state.name
1460
1343
  if current_name == new_agent_name:
@@ -1476,6 +1359,7 @@ class SyncServer(Server):
1476
1359
  # TODO: REMOVE THIS ONCE WE MIGRATE AGENTMODEL TO ORM MODEL
1477
1360
  # TODO: EVENTUALLY WE GET AUTO-DELETES WHEN WE SPECIFY RELATIONSHIPS IN THE ORM
1478
1361
  self.agents_tags_manager.delete_all_tags_from_agent(agent_id=agent_id, actor=actor)
1362
+ self.blocks_agents_manager.remove_all_agent_blocks(agent_id=agent_id)
1479
1363
 
1480
1364
  if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
1481
1365
  raise ValueError(f"Agent agent_id={agent_id} does not exist")
@@ -1501,7 +1385,7 @@ class SyncServer(Server):
1501
1385
 
1502
1386
  # Next, attempt to delete it from the actual database
1503
1387
  try:
1504
- self.ms.delete_agent(agent_id=agent_id)
1388
+ self.ms.delete_agent(agent_id=agent_id, per_agent_lock_manager=self.per_agent_lock_manager)
1505
1389
  except Exception as e:
1506
1390
  logger.exception(f"Failed to delete agent {agent_id} via ID with:\n{str(e)}")
1507
1391
  raise ValueError(f"Failed to delete agent {agent_id} in database")
@@ -1643,9 +1527,10 @@ class SyncServer(Server):
1643
1527
  raise ValueError(f"Need to provide at least source_id or source_name to find the source.")
1644
1528
  # get connection to data source storage
1645
1529
  source_connector = StorageConnector.get_storage_connector(TableType.PASSAGES, self.config, user_id=user_id)
1530
+ assert data_source, f"Data source with id={source_id} or name={source_name} does not exist"
1646
1531
 
1647
1532
  # load agent
1648
- agent = self._get_or_load_agent(agent_id=agent_id)
1533
+ agent = self.load_agent(agent_id=agent_id)
1649
1534
 
1650
1535
  # attach source to agent
1651
1536
  agent.attach_source(data_source.id, source_connector, self.ms)
@@ -1670,7 +1555,7 @@ class SyncServer(Server):
1670
1555
  source_id = source.id
1671
1556
 
1672
1557
  # delete all Passage objects with source_id==source_id from agent's archival memory
1673
- agent = self._get_or_load_agent(agent_id=agent_id)
1558
+ agent = self.load_agent(agent_id=agent_id)
1674
1559
  archival_memory = agent.persistence_manager.archival_memory
1675
1560
  archival_memory.storage.delete({"source_id": source_id})
1676
1561
 
@@ -1749,34 +1634,43 @@ class SyncServer(Server):
1749
1634
  def get_agent_message(self, agent_id: str, message_id: str) -> Optional[Message]:
1750
1635
  """Get a single message from the agent's memory"""
1751
1636
  # Get the agent object (loaded in memory)
1752
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1637
+ letta_agent = self.load_agent(agent_id=agent_id)
1753
1638
  message = letta_agent.persistence_manager.recall_memory.storage.get(id=message_id)
1639
+ save_agent(letta_agent, self.ms)
1754
1640
  return message
1755
1641
 
1756
1642
  def update_agent_message(self, agent_id: str, request: UpdateMessage) -> Message:
1757
1643
  """Update the details of a message associated with an agent"""
1758
1644
 
1759
1645
  # Get the current message
1760
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1761
- return letta_agent.update_message(request=request)
1646
+ letta_agent = self.load_agent(agent_id=agent_id)
1647
+ response = letta_agent.update_message(request=request)
1648
+ save_agent(letta_agent, self.ms)
1649
+ return response
1762
1650
 
1763
1651
  def rewrite_agent_message(self, agent_id: str, new_text: str) -> Message:
1764
1652
 
1765
1653
  # Get the current message
1766
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1767
- return letta_agent.rewrite_message(new_text=new_text)
1654
+ letta_agent = self.load_agent(agent_id=agent_id)
1655
+ response = letta_agent.rewrite_message(new_text=new_text)
1656
+ save_agent(letta_agent, self.ms)
1657
+ return response
1768
1658
 
1769
1659
  def rethink_agent_message(self, agent_id: str, new_thought: str) -> Message:
1770
1660
 
1771
1661
  # Get the current message
1772
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1773
- return letta_agent.rethink_message(new_thought=new_thought)
1662
+ letta_agent = self.load_agent(agent_id=agent_id)
1663
+ response = letta_agent.rethink_message(new_thought=new_thought)
1664
+ save_agent(letta_agent, self.ms)
1665
+ return response
1774
1666
 
1775
1667
  def retry_agent_message(self, agent_id: str) -> List[Message]:
1776
1668
 
1777
1669
  # Get the current message
1778
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1779
- return letta_agent.retry_message()
1670
+ letta_agent = self.load_agent(agent_id=agent_id)
1671
+ response = letta_agent.retry_message()
1672
+ save_agent(letta_agent, self.ms)
1673
+ return response
1780
1674
 
1781
1675
  def get_user_or_default(self, user_id: Optional[str]) -> User:
1782
1676
  """Get the user object for user_id if it exists, otherwise return the default user object"""
@@ -1825,5 +1719,168 @@ class SyncServer(Server):
1825
1719
  agent_id: str,
1826
1720
  ) -> ContextWindowOverview:
1827
1721
  # Get the current message
1828
- letta_agent = self._get_or_load_agent(agent_id=agent_id)
1722
+ letta_agent = self.load_agent(agent_id=agent_id)
1829
1723
  return letta_agent.get_context_window()
1724
+
1725
+ def link_block_to_agent_memory(self, user_id: str, agent_id: str, block_id: str) -> Memory:
1726
+ """Link a block to an agent's memory"""
1727
+ block = self.block_manager.get_block_by_id(block_id=block_id, actor=self.user_manager.get_user_by_id(user_id=user_id))
1728
+ if block is None:
1729
+ raise ValueError(f"Block with id {block_id} not found")
1730
+ self.blocks_agents_manager.add_block_to_agent(agent_id, block_id, block_label=block.label)
1731
+
1732
+ # get agent memory
1733
+ memory = self.get_agent(agent_id=agent_id).memory
1734
+ return memory
1735
+
1736
+ def unlink_block_from_agent_memory(self, user_id: str, agent_id: str, block_label: str, delete_if_no_ref: bool = True) -> Memory:
1737
+ """Unlink a block from an agent's memory. If the block is not linked to any agent, delete it."""
1738
+ self.blocks_agents_manager.remove_block_with_label_from_agent(agent_id=agent_id, block_label=block_label)
1739
+
1740
+ # get agent memory
1741
+ memory = self.get_agent(agent_id=agent_id).memory
1742
+ return memory
1743
+
1744
+ def update_agent_memory_limit(self, user_id: str, agent_id: str, block_label: str, limit: int) -> Memory:
1745
+ """Update the limit of a block in an agent's memory"""
1746
+ block = self.get_agent_block_by_label(user_id=user_id, agent_id=agent_id, label=block_label)
1747
+ self.block_manager.update_block(
1748
+ block_id=block.id, block_update=BlockUpdate(limit=limit), actor=self.user_manager.get_user_by_id(user_id=user_id)
1749
+ )
1750
+ # get agent memory
1751
+ memory = self.get_agent(agent_id=agent_id).memory
1752
+ return memory
1753
+
1754
+ def upate_block(self, user_id: str, block_id: str, block_update: BlockUpdate) -> Block:
1755
+ """Update a block"""
1756
+ return self.block_manager.update_block(
1757
+ block_id=block_id, block_update=block_update, actor=self.user_manager.get_user_by_id(user_id=user_id)
1758
+ )
1759
+
1760
+ def get_agent_block_by_label(self, user_id: str, agent_id: str, label: str) -> Block:
1761
+ """Get a block by label"""
1762
+ # TODO: implement at ORM?
1763
+ for block_id in self.blocks_agents_manager.list_block_ids_for_agent(agent_id=agent_id):
1764
+ block = self.block_manager.get_block_by_id(block_id=block_id, actor=self.user_manager.get_user_by_id(user_id=user_id))
1765
+ if block.label == label:
1766
+ return block
1767
+ return None
1768
+
1769
+ # def run_tool(self, tool_id: str, tool_args: str, user_id: str) -> FunctionReturn:
1770
+ # """Run a tool using the sandbox and return the result"""
1771
+
1772
+ # try:
1773
+ # tool_args_dict = json.loads(tool_args)
1774
+ # except json.JSONDecodeError:
1775
+ # raise ValueError("Invalid JSON string for tool_args")
1776
+
1777
+ # # Get the tool by ID
1778
+ # user = self.user_manager.get_user_by_id(user_id=user_id)
1779
+ # tool = self.tool_manager.get_tool_by_id(tool_id=tool_id, actor=user)
1780
+ # if tool.name is None:
1781
+ # raise ValueError(f"Tool with id {tool_id} does not have a name")
1782
+
1783
+ # # TODO eventually allow using agent state in tools
1784
+ # agent_state = None
1785
+
1786
+ # try:
1787
+ # sandbox_run_result = ToolExecutionSandbox(tool.name, tool_args_dict, user_id).run(agent_state=agent_state)
1788
+ # if sandbox_run_result is None:
1789
+ # raise ValueError(f"Tool with id {tool_id} returned execution with None")
1790
+ # function_response = str(sandbox_run_result.func_return)
1791
+
1792
+ # return FunctionReturn(
1793
+ # id="null",
1794
+ # function_call_id="null",
1795
+ # date=get_utc_time(),
1796
+ # status="success",
1797
+ # function_return=function_response,
1798
+ # )
1799
+ # except Exception as e:
1800
+ # # same as agent.py
1801
+ # from letta.constants import MAX_ERROR_MESSAGE_CHAR_LIMIT
1802
+
1803
+ # error_msg = f"Error executing tool {tool.name}: {e}"
1804
+ # if len(error_msg) > MAX_ERROR_MESSAGE_CHAR_LIMIT:
1805
+ # error_msg = error_msg[:MAX_ERROR_MESSAGE_CHAR_LIMIT]
1806
+
1807
+ # return FunctionReturn(
1808
+ # id="null",
1809
+ # function_call_id="null",
1810
+ # date=get_utc_time(),
1811
+ # status="error",
1812
+ # function_return=error_msg,
1813
+ # )
1814
+
1815
+ def run_tool_from_source(
1816
+ self,
1817
+ user_id: str,
1818
+ tool_args: str,
1819
+ tool_source: str,
1820
+ tool_source_type: Optional[str] = None,
1821
+ tool_name: Optional[str] = None,
1822
+ ) -> FunctionReturn:
1823
+ """Run a tool from source code"""
1824
+
1825
+ try:
1826
+ tool_args_dict = json.loads(tool_args)
1827
+ except json.JSONDecodeError:
1828
+ raise ValueError("Invalid JSON string for tool_args")
1829
+
1830
+ if tool_source_type is not None and tool_source_type != "python":
1831
+ raise ValueError("Only Python source code is supported at this time")
1832
+
1833
+ # NOTE: we're creating a floating Tool object and NOT persiting to DB
1834
+ tool = Tool(
1835
+ name=tool_name,
1836
+ source_code=tool_source,
1837
+ )
1838
+ assert tool.name is not None, "Failed to create tool object"
1839
+
1840
+ # TODO eventually allow using agent state in tools
1841
+ agent_state = None
1842
+
1843
+ # Next, attempt to run the tool with the sandbox
1844
+ try:
1845
+ sandbox_run_result = ToolExecutionSandbox(tool.name, tool_args_dict, user_id, tool_object=tool).run(agent_state=agent_state)
1846
+ if sandbox_run_result is None:
1847
+ raise ValueError(f"Tool with id {tool.id} returned execution with None")
1848
+ function_response = str(sandbox_run_result.func_return)
1849
+
1850
+ return FunctionReturn(
1851
+ id="null",
1852
+ function_call_id="null",
1853
+ date=get_utc_time(),
1854
+ status="success",
1855
+ function_return=function_response,
1856
+ )
1857
+ except Exception as e:
1858
+ # same as agent.py
1859
+ from letta.constants import MAX_ERROR_MESSAGE_CHAR_LIMIT
1860
+
1861
+ error_msg = f"Error executing tool {tool.name}: {e}"
1862
+ if len(error_msg) > MAX_ERROR_MESSAGE_CHAR_LIMIT:
1863
+ error_msg = error_msg[:MAX_ERROR_MESSAGE_CHAR_LIMIT]
1864
+
1865
+ return FunctionReturn(
1866
+ id="null",
1867
+ function_call_id="null",
1868
+ date=get_utc_time(),
1869
+ status="error",
1870
+ function_return=error_msg,
1871
+ )
1872
+
1873
+ # Composio wrappers
1874
+ def get_composio_apps(self) -> List["AppModel"]:
1875
+ """Get a list of all Composio apps with actions"""
1876
+ apps = self.composio_client.apps.get()
1877
+ apps_with_actions = []
1878
+ for app in apps:
1879
+ if app.meta["actionsCount"] > 0:
1880
+ apps_with_actions.append(app)
1881
+
1882
+ return apps_with_actions
1883
+
1884
+ def get_composio_actions_from_app_name(self, composio_app_name: str) -> List["ActionModel"]:
1885
+ actions = self.composio_client.actions.get(apps=[composio_app_name])
1886
+ return actions