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/agent.py +97 -665
- letta/chat_only_agent.py +2 -2
- letta/client/client.py +15 -8
- letta/client/streaming.py +2 -2
- letta/constants.py +2 -0
- letta/main.py +0 -76
- letta/offline_memory_agent.py +1 -2
- letta/orm/source.py +10 -3
- letta/orm/sources_agents.py +2 -2
- letta/providers.py +20 -3
- letta/schemas/agent.py +1 -0
- letta/schemas/embedding_config.py +1 -0
- letta/schemas/llm_config.py +1 -0
- letta/schemas/usage.py +2 -1
- letta/server/rest_api/interface.py +2 -2
- letta/server/rest_api/routers/v1/agents.py +9 -8
- letta/server/rest_api/routers/v1/sources.py +4 -7
- letta/server/rest_api/routers/v1/tools.py +2 -0
- letta/server/rest_api/utils.py +1 -1
- letta/server/server.py +19 -265
- letta/services/agent_manager.py +152 -2
- letta/services/helpers/agent_manager_helper.py +172 -2
- letta/services/message_manager.py +15 -0
- letta/settings.py +0 -3
- letta/utils.py +2 -1
- {letta_nightly-0.6.6.dev20241220190343.dist-info → letta_nightly-0.6.6.dev20241221104005.dist-info}/METADATA +1 -1
- {letta_nightly-0.6.6.dev20241220190343.dist-info → letta_nightly-0.6.6.dev20241221104005.dist-info}/RECORD +30 -30
- {letta_nightly-0.6.6.dev20241220190343.dist-info → letta_nightly-0.6.6.dev20241221104005.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.6.dev20241220190343.dist-info → letta_nightly-0.6.6.dev20241221104005.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.6.dev20241220190343.dist-info → letta_nightly-0.6.6.dev20241221104005.dist-info}/entry_points.txt +0 -0
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
951
|
-
|
|
812
|
+
agent_state = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor)
|
|
952
813
|
# Insert into archival memory
|
|
953
|
-
|
|
954
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
#
|
|
1046
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
letta/services/agent_manager.py
CHANGED
|
@@ -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
|
-
|
|
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
|
# ======================================================================================================================
|