letta-nightly 0.6.6.dev20241220190343__py3-none-any.whl → 0.6.6.dev20241221104005__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/server/server.py CHANGED
@@ -40,14 +40,14 @@ from letta.providers import (
40
40
  VLLMChatCompletionsProvider,
41
41
  VLLMCompletionsProvider,
42
42
  )
43
- from letta.schemas.agent import AgentState, AgentType, CreateAgent, UpdateAgent
43
+ from letta.schemas.agent import AgentState, AgentType, CreateAgent
44
44
  from letta.schemas.block import BlockUpdate
45
45
  from letta.schemas.embedding_config import EmbeddingConfig
46
46
 
47
47
  # openai schemas
48
48
  from letta.schemas.enums import JobStatus
49
49
  from letta.schemas.job import Job, JobUpdate
50
- from letta.schemas.letta_message import ToolReturnMessage, LettaMessage
50
+ from letta.schemas.letta_message import LettaMessage, ToolReturnMessage
51
51
  from letta.schemas.llm_config import LLMConfig
52
52
  from letta.schemas.memory import (
53
53
  ArchivalMemorySummary,
@@ -59,7 +59,7 @@ from letta.schemas.message import Message, MessageCreate, MessageRole, MessageUp
59
59
  from letta.schemas.organization import Organization
60
60
  from letta.schemas.passage import Passage
61
61
  from letta.schemas.source import Source
62
- from letta.schemas.tool import Tool, ToolCreate
62
+ from letta.schemas.tool import Tool
63
63
  from letta.schemas.usage import LettaUsageStatistics
64
64
  from letta.schemas.user import User
65
65
  from letta.services.agent_manager import AgentManager
@@ -303,11 +303,6 @@ class SyncServer(Server):
303
303
  self.block_manager.add_default_blocks(actor=self.default_user)
304
304
  self.tool_manager.upsert_base_tools(actor=self.default_user)
305
305
 
306
- # If there is a default org/user
307
- # This logic may have to change in the future
308
- if settings.load_default_external_tools:
309
- self.add_default_external_tools(actor=self.default_user)
310
-
311
306
  # collect providers (always has Letta as a default)
312
307
  self._enabled_providers: List[Provider] = [LettaProvider()]
313
308
  if model_settings.openai_api_key:
@@ -376,25 +371,6 @@ class SyncServer(Server):
376
371
  )
377
372
  )
378
373
 
379
- def initialize_agent(self, agent_id, actor, interface: Union[AgentInterface, None] = None, initial_message_sequence=None) -> Agent:
380
- """Initialize an agent from the database"""
381
- agent_state = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor)
382
-
383
- interface = interface or self.default_interface_factory()
384
- if agent_state.agent_type == AgentType.memgpt_agent:
385
- agent = Agent(agent_state=agent_state, interface=interface, user=actor, initial_message_sequence=initial_message_sequence)
386
- elif agent_state.agent_type == AgentType.offline_memory_agent:
387
- agent = OfflineMemoryAgent(
388
- agent_state=agent_state, interface=interface, user=actor, initial_message_sequence=initial_message_sequence
389
- )
390
- else:
391
- assert initial_message_sequence is None, f"Initial message sequence is not supported for O1Agents"
392
- agent = O1Agent(agent_state=agent_state, interface=interface, user=actor)
393
-
394
- # Persist to agent
395
- save_agent(agent)
396
- return agent
397
-
398
374
  def load_agent(self, agent_id: str, actor: User, interface: Union[AgentInterface, None] = None) -> Agent:
399
375
  """Updated method to load agents from persisted storage"""
400
376
  agent_lock = self.per_agent_lock_manager.get_lock(agent_id)
@@ -413,11 +389,6 @@ class SyncServer(Server):
413
389
  else:
414
390
  raise ValueError(f"Invalid agent type {agent_state.agent_type}")
415
391
 
416
- # Rebuild the system prompt - may be linked to new blocks now
417
- agent.rebuild_system_prompt()
418
-
419
- # Persist to agent
420
- save_agent(agent)
421
392
  return agent
422
393
 
423
394
  def _step(
@@ -455,9 +426,6 @@ class SyncServer(Server):
455
426
  skip_verify=True,
456
427
  )
457
428
 
458
- # save agent after step
459
- save_agent(letta_agent)
460
-
461
429
  except Exception as e:
462
430
  logger.error(f"Error in server._step: {e}")
463
431
  print(traceback.print_exc())
@@ -790,129 +758,23 @@ class SyncServer(Server):
790
758
 
791
759
  """Create a new agent using a config"""
792
760
  # Invoke manager
793
- agent_state = self.agent_manager.create_agent(
761
+ return self.agent_manager.create_agent(
794
762
  agent_create=request,
795
763
  actor=actor,
796
764
  )
797
765
 
798
- # create the agent object
799
- if request.initial_message_sequence is not None:
800
- # init_messages = [Message(user_id=user_id, agent_id=agent_state.id, role=message.role, text=message.text) for message in request.initial_message_sequence]
801
- init_messages = []
802
- for message in request.initial_message_sequence:
803
-
804
- if message.role == MessageRole.user:
805
- packed_message = system.package_user_message(
806
- user_message=message.text,
807
- )
808
- elif message.role == MessageRole.system:
809
- packed_message = system.package_system_message(
810
- system_message=message.text,
811
- )
812
- else:
813
- raise ValueError(f"Invalid message role: {message.role}")
814
-
815
- init_messages.append(Message(role=message.role, text=packed_message, agent_id=agent_state.id))
816
- # init_messages = [Message.dict_to_message(user_id=user_id, agent_id=agent_state.id, openai_message_dict=message.model_dump()) for message in request.initial_message_sequence]
817
- else:
818
- init_messages = None
819
-
820
- # initialize the agent (generates initial message list with system prompt)
821
- if interface is None:
822
- interface = self.default_interface_factory()
823
- self.initialize_agent(agent_id=agent_state.id, interface=interface, initial_message_sequence=init_messages, actor=actor)
824
-
825
- in_memory_agent_state = self.agent_manager.get_agent_by_id(agent_state.id, actor=actor)
826
- return in_memory_agent_state
827
-
828
- # TODO: This is not good!
829
- # TODO: Ideally, this should ALL be handled by the ORM
830
- # TODO: The main blocker here IS the _message updates
831
- def update_agent(
832
- self,
833
- agent_id: str,
834
- request: UpdateAgent,
835
- actor: User,
836
- ) -> AgentState:
837
- """Update the agents core memory block, return the new state"""
838
- # Update agent state in the db first
839
- agent_state = self.agent_manager.update_agent(agent_id=agent_id, agent_update=request, actor=actor)
840
-
841
- # Get the agent object (loaded in memory)
842
- letta_agent = self.load_agent(agent_id=agent_id, actor=actor)
843
-
844
- # TODO: Everything below needs to get removed, no updating anything in memory
845
- # update the system prompt
846
- if request.system:
847
- letta_agent.update_system_prompt(request.system)
848
-
849
- # update in-context messages
850
- if request.message_ids:
851
- # This means the user is trying to change what messages are in the message buffer
852
- # Internally this requires (1) pulling from recall,
853
- # then (2) setting the attributes ._messages and .state.message_ids
854
- letta_agent.set_message_buffer(message_ids=request.message_ids)
855
-
856
- letta_agent.update_state()
857
-
858
- return agent_state
859
-
860
- def get_tools_from_agent(self, agent_id: str, user_id: Optional[str]) -> List[Tool]:
861
- """Get tools from an existing agent"""
862
- # TODO: Thread actor directly through this function, since the top level caller most likely already retrieved the user
863
- actor = self.user_manager.get_user_or_default(user_id=user_id)
864
-
865
- # Get the agent object (loaded in memory)
866
- letta_agent = self.load_agent(agent_id=agent_id, actor=actor)
867
- return letta_agent.agent_state.tools
868
-
869
- def add_tool_to_agent(
870
- self,
871
- agent_id: str,
872
- tool_id: str,
873
- user_id: str,
874
- ):
875
- """Add tools from an existing agent"""
876
- # TODO: Thread actor directly through this function, since the top level caller most likely already retrieved the user
877
- actor = self.user_manager.get_user_or_default(user_id=user_id)
878
-
879
- agent_state = self.agent_manager.attach_tool(agent_id=agent_id, tool_id=tool_id, actor=actor)
880
-
881
- return agent_state
882
-
883
- def remove_tool_from_agent(
884
- self,
885
- agent_id: str,
886
- tool_id: str,
887
- user_id: str,
888
- ):
889
- """Remove tools from an existing agent"""
890
- # TODO: Thread actor directly through this function, since the top level caller most likely already retrieved the user
891
- actor = self.user_manager.get_user_or_default(user_id=user_id)
892
- agent_state = self.agent_manager.detach_tool(agent_id=agent_id, tool_id=tool_id, actor=actor)
893
-
894
- return agent_state
895
-
896
766
  # convert name->id
897
767
 
768
+ # TODO: These can be moved to agent_manager
898
769
  def get_agent_memory(self, agent_id: str, actor: User) -> Memory:
899
770
  """Return the memory of an agent (core memory)"""
900
- agent = self.load_agent(agent_id=agent_id, actor=actor)
901
- return agent.agent_state.memory
771
+ return self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor).memory
902
772
 
903
773
  def get_archival_memory_summary(self, agent_id: str, actor: User) -> ArchivalMemorySummary:
904
- agent = self.load_agent(agent_id=agent_id, actor=actor)
905
774
  return ArchivalMemorySummary(size=self.agent_manager.passage_size(actor=actor, agent_id=agent_id))
906
775
 
907
776
  def get_recall_memory_summary(self, agent_id: str, actor: User) -> RecallMemorySummary:
908
- agent = self.load_agent(agent_id=agent_id, actor=actor)
909
- return RecallMemorySummary(size=len(agent.message_manager))
910
-
911
- def get_in_context_messages(self, agent_id: str, actor: User) -> List[Message]:
912
- """Get the in-context messages in the agent's memory"""
913
- # Get the agent object (loaded in memory)
914
- agent = self.load_agent(agent_id=agent_id, actor=actor)
915
- return agent._messages
777
+ return RecallMemorySummary(size=self.message_manager.size(actor=actor, agent_id=agent_id))
916
778
 
917
779
  def get_agent_archival(self, user_id: str, agent_id: str, cursor: Optional[str] = None, limit: int = 50) -> List[Passage]:
918
780
  """Paginated query of all messages in agent archival memory"""
@@ -947,24 +809,17 @@ class SyncServer(Server):
947
809
 
948
810
  def insert_archival_memory(self, agent_id: str, memory_contents: str, actor: User) -> List[Passage]:
949
811
  # Get the agent object (loaded in memory)
950
- letta_agent = self.load_agent(agent_id=agent_id, actor=actor)
951
-
812
+ agent_state = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor)
952
813
  # Insert into archival memory
953
- passages = self.passage_manager.insert_passage(
954
- agent_state=letta_agent.agent_state, agent_id=agent_id, text=memory_contents, actor=actor
955
- )
956
-
957
- save_agent(letta_agent)
814
+ # TODO: @mindy look at moving this to agent_manager to avoid above extra call
815
+ passages = self.passage_manager.insert_passage(agent_state=agent_state, agent_id=agent_id, text=memory_contents, actor=actor)
958
816
 
959
817
  return passages
960
818
 
961
- def delete_archival_memory(self, agent_id: str, memory_id: str, actor: User):
962
- # Get the agent object (loaded in memory)
963
- letta_agent = self.load_agent(agent_id=agent_id, actor=actor)
964
-
965
- # Delete by ID
819
+ def delete_archival_memory(self, memory_id: str, actor: User):
966
820
  # TODO check if it exists first, and throw error if not
967
- letta_agent.passage_manager.delete_passage_by_id(passage_id=memory_id, actor=actor)
821
+ # TODO: @mindy make this return the deleted passage instead
822
+ self.passage_manager.delete_passage_by_id(passage_id=memory_id, actor=actor)
968
823
 
969
824
  # TODO: return archival memory
970
825
 
@@ -1042,9 +897,8 @@ class SyncServer(Server):
1042
897
  # update the block
1043
898
  self.block_manager.update_block(block_id=block.id, block_update=BlockUpdate(value=value), actor=actor)
1044
899
 
1045
- # load agent
1046
- letta_agent = self.load_agent(agent_id=agent_id, actor=actor)
1047
- return letta_agent.agent_state.memory
900
+ # rebuild system prompt for agent, potentially changed
901
+ return self.agent_manager.rebuild_system_prompt(agent_id=agent_id, actor=actor).memory
1048
902
 
1049
903
  def delete_source(self, source_id: str, actor: User):
1050
904
  """Delete a data source"""
@@ -1082,11 +936,10 @@ class SyncServer(Server):
1082
936
  agent_states = self.source_manager.list_attached_agents(source_id=source_id, actor=actor)
1083
937
  for agent_state in agent_states:
1084
938
  agent_id = agent_state.id
1085
- agent = self.load_agent(agent_id=agent_id, actor=actor)
1086
939
 
1087
940
  # Attach source to agent
1088
941
  curr_passage_size = self.agent_manager.passage_size(actor=actor, agent_id=agent_id)
1089
- agent.attach_source(user=actor, source_id=source_id, source_manager=self.source_manager, agent_manager=self.agent_manager)
942
+ self.agent_manager.attach_source(agent_id=agent_state.id, source_id=source_id, actor=actor)
1090
943
  new_passage_size = self.agent_manager.passage_size(actor=actor, agent_id=agent_id)
1091
944
  assert new_passage_size >= curr_passage_size # in case empty files are added
1092
945
 
@@ -1111,56 +964,6 @@ class SyncServer(Server):
1111
964
  passage_count, document_count = load_data(connector, source, self.passage_manager, self.source_manager, actor=user)
1112
965
  return passage_count, document_count
1113
966
 
1114
- def attach_source_to_agent(
1115
- self,
1116
- user_id: str,
1117
- agent_id: str,
1118
- source_id: Optional[str] = None,
1119
- source_name: Optional[str] = None,
1120
- ) -> Source:
1121
- # attach a data source to an agent
1122
- # TODO: Thread actor directly through this function, since the top level caller most likely already retrieved the user
1123
- actor = self.user_manager.get_user_or_default(user_id=user_id)
1124
- if source_id:
1125
- data_source = self.source_manager.get_source_by_id(source_id=source_id, actor=actor)
1126
- elif source_name:
1127
- data_source = self.source_manager.get_source_by_name(source_name=source_name, actor=actor)
1128
- else:
1129
- raise ValueError(f"Need to provide at least source_id or source_name to find the source.")
1130
-
1131
- assert data_source, f"Data source with id={source_id} or name={source_name} does not exist"
1132
-
1133
- # load agent
1134
- agent = self.load_agent(agent_id=agent_id, actor=actor)
1135
-
1136
- # attach source to agent
1137
- agent.attach_source(user=actor, source_id=data_source.id, source_manager=self.source_manager, agent_manager=self.agent_manager)
1138
-
1139
- return data_source
1140
-
1141
- def detach_source_from_agent(
1142
- self,
1143
- user_id: str,
1144
- agent_id: str,
1145
- source_id: Optional[str] = None,
1146
- source_name: Optional[str] = None,
1147
- ) -> Source:
1148
- # TODO: Thread actor directly through this function, since the top level caller most likely already retrieved the user
1149
- actor = self.user_manager.get_user_or_default(user_id=user_id)
1150
- if source_id:
1151
- source = self.source_manager.get_source_by_id(source_id=source_id, actor=actor)
1152
- elif source_name:
1153
- source = self.source_manager.get_source_by_name(source_name=source_name, actor=actor)
1154
- source_id = source.id
1155
- else:
1156
- raise ValueError(f"Need to provide at least source_id or source_name to find the source.")
1157
-
1158
- # delete agent-source mapping
1159
- self.agent_manager.detach_source(agent_id=agent_id, source_id=source_id, actor=actor)
1160
-
1161
- # return back source data
1162
- return source
1163
-
1164
967
  def list_data_source_passages(self, user_id: str, source_id: str) -> List[Passage]:
1165
968
  warnings.warn("list_data_source_passages is not yet implemented, returning empty list.", category=UserWarning)
1166
969
  return []
@@ -1198,52 +1001,11 @@ class SyncServer(Server):
1198
1001
 
1199
1002
  return sources_with_metadata
1200
1003
 
1201
- def add_default_external_tools(self, actor: User) -> bool:
1202
- """Add default langchain tools. Return true if successful, false otherwise."""
1203
- success = True
1204
- tool_creates = ToolCreate.load_default_langchain_tools()
1205
- if tool_settings.composio_api_key:
1206
- tool_creates += ToolCreate.load_default_composio_tools()
1207
- for tool_create in tool_creates:
1208
- try:
1209
- self.tool_manager.create_or_update_tool(Tool(**tool_create.model_dump()), actor=actor)
1210
- except Exception as e:
1211
- warnings.warn(f"An error occurred while creating tool {tool_create}: {e}")
1212
- warnings.warn(traceback.format_exc())
1213
- success = False
1214
-
1215
- return success
1216
-
1217
- def update_agent_message(self, agent_id: str, message_id: str, request: MessageUpdate, actor: User) -> Message:
1004
+ def update_agent_message(self, message_id: str, request: MessageUpdate, actor: User) -> Message:
1218
1005
  """Update the details of a message associated with an agent"""
1219
1006
 
1220
1007
  # Get the current message
1221
- letta_agent = self.load_agent(agent_id=agent_id, actor=actor)
1222
- response = letta_agent.update_message(message_id=message_id, request=request)
1223
- save_agent(letta_agent)
1224
- return response
1225
-
1226
- def rewrite_agent_message(self, agent_id: str, new_text: str, actor: User) -> Message:
1227
-
1228
- # Get the current message
1229
- letta_agent = self.load_agent(agent_id=agent_id, actor=actor)
1230
- response = letta_agent.rewrite_message(new_text=new_text)
1231
- save_agent(letta_agent)
1232
- return response
1233
-
1234
- def rethink_agent_message(self, agent_id: str, new_thought: str, actor: User) -> Message:
1235
- # Get the current message
1236
- letta_agent = self.load_agent(agent_id=agent_id, actor=actor)
1237
- response = letta_agent.rethink_message(new_thought=new_thought)
1238
- save_agent(letta_agent)
1239
- return response
1240
-
1241
- def retry_agent_message(self, agent_id: str, actor: User) -> List[Message]:
1242
- # Get the current message
1243
- letta_agent = self.load_agent(agent_id=agent_id, actor=actor)
1244
- response = letta_agent.retry_message()
1245
- save_agent(letta_agent)
1246
- return response
1008
+ return self.message_manager.update_message_by_id(message_id=message_id, message_update=request, actor=actor)
1247
1009
 
1248
1010
  def get_organization_or_default(self, org_id: Optional[str]) -> Organization:
1249
1011
  """Get the organization object for org_id if it exists, otherwise return the default organization object"""
@@ -1331,15 +1093,7 @@ class SyncServer(Server):
1331
1093
  def add_embedding_model(self, request: EmbeddingConfig) -> EmbeddingConfig:
1332
1094
  """Add a new embedding model"""
1333
1095
 
1334
- def get_agent_context_window(
1335
- self,
1336
- user_id: str,
1337
- agent_id: str,
1338
- ) -> ContextWindowOverview:
1339
- # TODO: Thread actor directly through this function, since the top level caller most likely already retrieved the user
1340
- actor = self.user_manager.get_user_or_default(user_id=user_id)
1341
-
1342
- # Get the current message
1096
+ def get_agent_context_window(self, agent_id: str, actor: User) -> ContextWindowOverview:
1343
1097
  letta_agent = self.load_agent(agent_id=agent_id, actor=actor)
1344
1098
  return letta_agent.get_context_window()
1345
1099
 
@@ -20,6 +20,7 @@ from letta.schemas.agent import AgentType, CreateAgent, UpdateAgent
20
20
  from letta.schemas.block import Block as PydanticBlock
21
21
  from letta.schemas.embedding_config import EmbeddingConfig
22
22
  from letta.schemas.llm_config import LLMConfig
23
+ from letta.schemas.message import Message as PydanticMessage
23
24
  from letta.schemas.passage import Passage as PydanticPassage
24
25
  from letta.schemas.source import Source as PydanticSource
25
26
  from letta.schemas.tool_rule import ToolRule as PydanticToolRule
@@ -28,12 +29,17 @@ from letta.services.block_manager import BlockManager
28
29
  from letta.services.helpers.agent_manager_helper import (
29
30
  _process_relationship,
30
31
  _process_tags,
32
+ check_supports_structured_output,
33
+ compile_system_message,
31
34
  derive_system_message,
35
+ initialize_message_sequence,
36
+ package_initial_message_sequence,
32
37
  )
38
+ from letta.services.message_manager import MessageManager
33
39
  from letta.services.source_manager import SourceManager
34
40
  from letta.services.tool_manager import ToolManager
35
41
  from letta.settings import settings
36
- from letta.utils import enforce_types
42
+ from letta.utils import enforce_types, get_utc_time, united_diff
37
43
 
38
44
  logger = get_logger(__name__)
39
45
 
@@ -49,6 +55,7 @@ class AgentManager:
49
55
  self.block_manager = BlockManager()
50
56
  self.tool_manager = ToolManager()
51
57
  self.source_manager = SourceManager()
58
+ self.message_manager = MessageManager()
52
59
 
53
60
  # ======================================================================================================================
54
61
  # Basic CRUD operations
@@ -64,6 +71,10 @@ class AgentManager:
64
71
  if not agent_create.llm_config or not agent_create.embedding_config:
65
72
  raise ValueError("llm_config and embedding_config are required")
66
73
 
74
+ # Check tool rules are valid
75
+ if agent_create.tool_rules:
76
+ check_supports_structured_output(model=agent_create.llm_config.model, tool_rules=agent_create.tool_rules)
77
+
67
78
  # create blocks (note: cannot be linked into the agent_id is created)
68
79
  block_ids = list(agent_create.block_ids or []) # Create a local copy to avoid modifying the original
69
80
  for create_block in agent_create.memory_blocks:
@@ -88,7 +99,8 @@ class AgentManager:
88
99
  # Remove duplicates
89
100
  tool_ids = list(set(tool_ids))
90
101
 
91
- return self._create_agent(
102
+ # Create the agent
103
+ agent_state = self._create_agent(
92
104
  name=agent_create.name,
93
105
  system=system,
94
106
  agent_type=agent_create.agent_type,
@@ -104,6 +116,35 @@ class AgentManager:
104
116
  actor=actor,
105
117
  )
106
118
 
119
+ # TODO: See if we can merge this into the above SQL create call for performance reasons
120
+ # Generate a sequence of initial messages to put in the buffer
121
+ init_messages = initialize_message_sequence(
122
+ agent_state=agent_state, memory_edit_timestamp=get_utc_time(), include_initial_boot_message=True
123
+ )
124
+
125
+ if agent_create.initial_message_sequence is not None:
126
+ # We always need the system prompt up front
127
+ system_message_obj = PydanticMessage.dict_to_message(
128
+ agent_id=agent_state.id,
129
+ user_id=agent_state.created_by_id,
130
+ model=agent_state.llm_config.model,
131
+ openai_message_dict=init_messages[0],
132
+ )
133
+ # Don't use anything else in the pregen sequence, instead use the provided sequence
134
+ init_messages = [system_message_obj]
135
+ init_messages.extend(
136
+ package_initial_message_sequence(agent_state.id, agent_create.initial_message_sequence, agent_state.llm_config.model, actor)
137
+ )
138
+ else:
139
+ init_messages = [
140
+ PydanticMessage.dict_to_message(
141
+ agent_id=agent_state.id, user_id=agent_state.created_by_id, model=agent_state.llm_config.model, openai_message_dict=msg
142
+ )
143
+ for msg in init_messages
144
+ ]
145
+
146
+ return self.append_to_in_context_messages(init_messages, agent_id=agent_state.id, actor=actor)
147
+
107
148
  @enforce_types
108
149
  def _create_agent(
109
150
  self,
@@ -149,6 +190,16 @@ class AgentManager:
149
190
 
150
191
  @enforce_types
151
192
  def update_agent(self, agent_id: str, agent_update: UpdateAgent, actor: PydanticUser) -> PydanticAgentState:
193
+ agent_state = self._update_agent(agent_id=agent_id, agent_update=agent_update, actor=actor)
194
+
195
+ # Rebuild the system prompt if it's different
196
+ if agent_update.system and agent_update.system != agent_state.system:
197
+ agent_state = self.rebuild_system_prompt(agent_id=agent_state.id, actor=actor, force=True, update_timestamp=False)
198
+
199
+ return agent_state
200
+
201
+ @enforce_types
202
+ def _update_agent(self, agent_id: str, agent_update: UpdateAgent, actor: PydanticUser) -> PydanticAgentState:
152
203
  """
153
204
  Update an existing agent.
154
205
 
@@ -247,6 +298,105 @@ class AgentManager:
247
298
  agent.hard_delete(session)
248
299
  return agent_state
249
300
 
301
+ # ======================================================================================================================
302
+ # In Context Messages Management
303
+ # ======================================================================================================================
304
+ # TODO: There are several assumptions here that are not explicitly checked
305
+ # TODO: 1) These message ids are valid
306
+ # TODO: 2) These messages are ordered from oldest to newest
307
+ # TODO: This can be fixed by having an actual relationship in the ORM for message_ids
308
+ # TODO: This can also be made more efficient, instead of getting, setting, we can do it all in one db session for one query.
309
+ @enforce_types
310
+ def get_in_context_messages(self, agent_id: str, actor: PydanticUser) -> List[PydanticMessage]:
311
+ message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
312
+ return self.message_manager.get_messages_by_ids(message_ids=message_ids, actor=actor)
313
+
314
+ @enforce_types
315
+ def get_system_message(self, agent_id: str, actor: PydanticUser) -> PydanticMessage:
316
+ message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
317
+ return self.message_manager.get_message_by_id(message_id=message_ids[0], actor=actor)
318
+
319
+ @enforce_types
320
+ def rebuild_system_prompt(self, agent_id: str, actor: PydanticUser, force=False, update_timestamp=True) -> PydanticAgentState:
321
+ """Rebuilds the system message with the latest memory object and any shared memory block updates
322
+
323
+ Updates to core memory blocks should trigger a "rebuild", which itself will create a new message object
324
+
325
+ Updates to the memory header should *not* trigger a rebuild, since that will simply flood recall storage with excess messages
326
+ """
327
+ agent_state = self.get_agent_by_id(agent_id=agent_id, actor=actor)
328
+
329
+ curr_system_message = self.get_system_message(
330
+ agent_id=agent_id, actor=actor
331
+ ) # this is the system + memory bank, not just the system prompt
332
+ curr_system_message_openai = curr_system_message.to_openai_dict()
333
+
334
+ # note: we only update the system prompt if the core memory is changed
335
+ # this means that the archival/recall memory statistics may be someout out of date
336
+ curr_memory_str = agent_state.memory.compile()
337
+ if curr_memory_str in curr_system_message_openai["content"] and not force:
338
+ # NOTE: could this cause issues if a block is removed? (substring match would still work)
339
+ logger.info(
340
+ f"Memory hasn't changed for agent id={agent_id} and actor=({actor.id}, {actor.name}), skipping system prompt rebuild"
341
+ )
342
+ return agent_state
343
+
344
+ # If the memory didn't update, we probably don't want to update the timestamp inside
345
+ # For example, if we're doing a system prompt swap, this should probably be False
346
+ if update_timestamp:
347
+ memory_edit_timestamp = get_utc_time()
348
+ else:
349
+ # NOTE: a bit of a hack - we pull the timestamp from the message created_by
350
+ memory_edit_timestamp = curr_system_message.created_at
351
+
352
+ # update memory (TODO: potentially update recall/archival stats separately)
353
+ new_system_message_str = compile_system_message(
354
+ system_prompt=agent_state.system,
355
+ in_context_memory=agent_state.memory,
356
+ in_context_memory_last_edit=memory_edit_timestamp,
357
+ )
358
+
359
+ diff = united_diff(curr_system_message_openai["content"], new_system_message_str)
360
+ if len(diff) > 0: # there was a diff
361
+ logger.info(f"Rebuilding system with new memory...\nDiff:\n{diff}")
362
+
363
+ # Swap the system message out (only if there is a diff)
364
+ message = PydanticMessage.dict_to_message(
365
+ agent_id=agent_id,
366
+ user_id=actor.id,
367
+ model=agent_state.llm_config.model,
368
+ openai_message_dict={"role": "system", "content": new_system_message_str},
369
+ )
370
+ message = self.message_manager.create_message(message, actor=actor)
371
+ message_ids = [message.id] + agent_state.message_ids[1:] # swap index 0 (system)
372
+ return self.set_in_context_messages(agent_id=agent_id, message_ids=message_ids, actor=actor)
373
+ else:
374
+ return agent_state
375
+
376
+ @enforce_types
377
+ def set_in_context_messages(self, agent_id: str, message_ids: List[str], actor: PydanticUser) -> PydanticAgentState:
378
+ return self.update_agent(agent_id=agent_id, agent_update=UpdateAgent(message_ids=message_ids), actor=actor)
379
+
380
+ @enforce_types
381
+ def trim_older_in_context_messages(self, num: int, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
382
+ message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
383
+ new_messages = [message_ids[0]] + message_ids[num:] # 0 is system message
384
+ return self.set_in_context_messages(agent_id=agent_id, message_ids=new_messages, actor=actor)
385
+
386
+ @enforce_types
387
+ def prepend_to_in_context_messages(self, messages: List[PydanticMessage], agent_id: str, actor: PydanticUser) -> PydanticAgentState:
388
+ message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
389
+ new_messages = self.message_manager.create_many_messages(messages, actor=actor)
390
+ message_ids = [message_ids[0]] + [m.id for m in new_messages] + message_ids[1:]
391
+ return self.set_in_context_messages(agent_id=agent_id, message_ids=message_ids, actor=actor)
392
+
393
+ @enforce_types
394
+ def append_to_in_context_messages(self, messages: List[PydanticMessage], agent_id: str, actor: PydanticUser) -> PydanticAgentState:
395
+ messages = self.message_manager.create_many_messages(messages, actor=actor)
396
+ message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids or []
397
+ message_ids += [m.id for m in messages]
398
+ return self.set_in_context_messages(agent_id=agent_id, message_ids=message_ids, actor=actor)
399
+
250
400
  # ======================================================================================================================
251
401
  # Source Management
252
402
  # ======================================================================================================================