letta-nightly 0.6.1.dev20241205211219__py3-none-any.whl → 0.6.1.dev20241207104149__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 +54 -37
- letta/agent_store/db.py +1 -77
- letta/agent_store/storage.py +0 -5
- letta/chat_only_agent.py +103 -0
- letta/cli/cli.py +0 -1
- letta/client/client.py +3 -7
- letta/constants.py +1 -0
- letta/functions/function_sets/base.py +37 -9
- letta/main.py +2 -2
- letta/memory.py +4 -82
- letta/metadata.py +0 -35
- letta/o1_agent.py +7 -2
- letta/offline_memory_agent.py +180 -0
- letta/orm/__init__.py +3 -0
- letta/orm/file.py +1 -1
- letta/orm/message.py +66 -0
- letta/orm/mixins.py +16 -0
- letta/orm/organization.py +1 -0
- letta/orm/sqlalchemy_base.py +118 -26
- letta/orm/tool.py +22 -1
- letta/orm/tools_agents.py +32 -0
- letta/personas/examples/offline_memory_persona.txt +4 -0
- letta/prompts/system/memgpt_convo_only.txt +14 -0
- letta/prompts/system/memgpt_offline_memory.txt +23 -0
- letta/prompts/system/memgpt_offline_memory_chat.txt +35 -0
- letta/schemas/agent.py +3 -2
- letta/schemas/letta_base.py +7 -6
- letta/schemas/message.py +1 -7
- letta/schemas/tools_agents.py +32 -0
- letta/server/rest_api/app.py +11 -0
- letta/server/rest_api/routers/v1/agents.py +2 -2
- letta/server/rest_api/routers/v1/blocks.py +2 -2
- letta/server/server.py +63 -47
- letta/server/static_files/assets/index-43ab4d62.css +1 -0
- letta/server/static_files/assets/index-4848e3d7.js +40 -0
- letta/server/static_files/index.html +2 -2
- letta/services/block_manager.py +1 -1
- letta/services/message_manager.py +182 -0
- letta/services/organization_manager.py +6 -9
- letta/services/source_manager.py +1 -1
- letta/services/tool_manager.py +2 -2
- letta/services/tools_agents_manager.py +94 -0
- letta/services/user_manager.py +1 -1
- {letta_nightly-0.6.1.dev20241205211219.dist-info → letta_nightly-0.6.1.dev20241207104149.dist-info}/METADATA +2 -1
- {letta_nightly-0.6.1.dev20241205211219.dist-info → letta_nightly-0.6.1.dev20241207104149.dist-info}/RECORD +48 -39
- letta/agent_store/lancedb.py +0 -177
- letta/persistence_manager.py +0 -149
- letta/server/static_files/assets/index-3ab03d5b.css +0 -1
- letta/server/static_files/assets/index-9fa459a2.js +0 -271
- {letta_nightly-0.6.1.dev20241205211219.dist-info → letta_nightly-0.6.1.dev20241207104149.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.1.dev20241205211219.dist-info → letta_nightly-0.6.1.dev20241207104149.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.1.dev20241205211219.dist-info → letta_nightly-0.6.1.dev20241207104149.dist-info}/entry_points.txt +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from datetime import datetime
|
|
1
2
|
from typing import Optional
|
|
2
3
|
|
|
3
4
|
from letta.agent import Agent
|
|
@@ -38,7 +39,7 @@ Returns:
|
|
|
38
39
|
"""
|
|
39
40
|
|
|
40
41
|
|
|
41
|
-
def pause_heartbeats(self: Agent, minutes: int) -> Optional[str]:
|
|
42
|
+
def pause_heartbeats(self: "Agent", minutes: int) -> Optional[str]:
|
|
42
43
|
import datetime
|
|
43
44
|
|
|
44
45
|
from letta.constants import MAX_PAUSE_HEARTBEATS
|
|
@@ -56,7 +57,7 @@ def pause_heartbeats(self: Agent, minutes: int) -> Optional[str]:
|
|
|
56
57
|
pause_heartbeats.__doc__ = pause_heartbeats_docstring
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
def conversation_search(self: Agent, query: str, page: Optional[int] = 0) -> Optional[str]:
|
|
60
|
+
def conversation_search(self: "Agent", query: str, page: Optional[int] = 0) -> Optional[str]:
|
|
60
61
|
"""
|
|
61
62
|
Search prior conversation history using case-insensitive string matching.
|
|
62
63
|
|
|
@@ -80,7 +81,15 @@ def conversation_search(self: Agent, query: str, page: Optional[int] = 0) -> Opt
|
|
|
80
81
|
except:
|
|
81
82
|
raise ValueError(f"'page' argument must be an integer")
|
|
82
83
|
count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
|
83
|
-
|
|
84
|
+
# TODO: add paging by page number. currently cursor only works with strings.
|
|
85
|
+
# original: start=page * count
|
|
86
|
+
results = self.message_manager.list_user_messages_for_agent(
|
|
87
|
+
agent_id=self.agent_state.id,
|
|
88
|
+
actor=self.user,
|
|
89
|
+
query_text=query,
|
|
90
|
+
limit=count,
|
|
91
|
+
)
|
|
92
|
+
total = len(results)
|
|
84
93
|
num_pages = math.ceil(total / count) - 1 # 0 index
|
|
85
94
|
if len(results) == 0:
|
|
86
95
|
results_str = f"No results found."
|
|
@@ -91,7 +100,7 @@ def conversation_search(self: Agent, query: str, page: Optional[int] = 0) -> Opt
|
|
|
91
100
|
return results_str
|
|
92
101
|
|
|
93
102
|
|
|
94
|
-
def conversation_search_date(self: Agent, start_date: str, end_date: str, page: Optional[int] = 0) -> Optional[str]:
|
|
103
|
+
def conversation_search_date(self: "Agent", start_date: str, end_date: str, page: Optional[int] = 0) -> Optional[str]:
|
|
95
104
|
"""
|
|
96
105
|
Search prior conversation history using a date range.
|
|
97
106
|
|
|
@@ -112,10 +121,29 @@ def conversation_search_date(self: Agent, start_date: str, end_date: str, page:
|
|
|
112
121
|
page = 0
|
|
113
122
|
try:
|
|
114
123
|
page = int(page)
|
|
124
|
+
if page < 0:
|
|
125
|
+
raise ValueError
|
|
115
126
|
except:
|
|
116
127
|
raise ValueError(f"'page' argument must be an integer")
|
|
128
|
+
|
|
129
|
+
# Convert date strings to datetime objects
|
|
130
|
+
try:
|
|
131
|
+
start_datetime = datetime.strptime(start_date, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0)
|
|
132
|
+
end_datetime = datetime.strptime(end_date, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=999999)
|
|
133
|
+
except ValueError:
|
|
134
|
+
raise ValueError("Dates must be in the format 'YYYY-MM-DD'")
|
|
135
|
+
|
|
117
136
|
count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
|
118
|
-
results
|
|
137
|
+
results = self.message_manager.list_user_messages_for_agent(
|
|
138
|
+
# TODO: add paging by page number. currently cursor only works with strings.
|
|
139
|
+
agent_id=self.agent_state.id,
|
|
140
|
+
actor=self.user,
|
|
141
|
+
start_date=start_datetime,
|
|
142
|
+
end_date=end_datetime,
|
|
143
|
+
limit=count,
|
|
144
|
+
# start_date=start_date, end_date=end_date, limit=count, start=page * count
|
|
145
|
+
)
|
|
146
|
+
total = len(results)
|
|
119
147
|
num_pages = math.ceil(total / count) - 1 # 0 index
|
|
120
148
|
if len(results) == 0:
|
|
121
149
|
results_str = f"No results found."
|
|
@@ -126,7 +154,7 @@ def conversation_search_date(self: Agent, start_date: str, end_date: str, page:
|
|
|
126
154
|
return results_str
|
|
127
155
|
|
|
128
156
|
|
|
129
|
-
def archival_memory_insert(self: Agent, content: str) -> Optional[str]:
|
|
157
|
+
def archival_memory_insert(self: "Agent", content: str) -> Optional[str]:
|
|
130
158
|
"""
|
|
131
159
|
Add to archival memory. Make sure to phrase the memory contents such that it can be easily queried later.
|
|
132
160
|
|
|
@@ -136,11 +164,11 @@ def archival_memory_insert(self: Agent, content: str) -> Optional[str]:
|
|
|
136
164
|
Returns:
|
|
137
165
|
Optional[str]: None is always returned as this function does not produce a response.
|
|
138
166
|
"""
|
|
139
|
-
self.
|
|
167
|
+
self.archival_memory.insert(content)
|
|
140
168
|
return None
|
|
141
169
|
|
|
142
170
|
|
|
143
|
-
def archival_memory_search(self: Agent, query: str, page: Optional[int] = 0) -> Optional[str]:
|
|
171
|
+
def archival_memory_search(self: "Agent", query: str, page: Optional[int] = 0) -> Optional[str]:
|
|
144
172
|
"""
|
|
145
173
|
Search archival memory using semantic (embedding-based) search.
|
|
146
174
|
|
|
@@ -163,7 +191,7 @@ def archival_memory_search(self: Agent, query: str, page: Optional[int] = 0) ->
|
|
|
163
191
|
except:
|
|
164
192
|
raise ValueError(f"'page' argument must be an integer")
|
|
165
193
|
count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
|
166
|
-
results, total = self.
|
|
194
|
+
results, total = self.archival_memory.search(query, count=count, start=page * count)
|
|
167
195
|
num_pages = math.ceil(total / count) - 1 # 0 index
|
|
168
196
|
if len(results) == 0:
|
|
169
197
|
results_str = f"No results found."
|
letta/main.py
CHANGED
|
@@ -190,8 +190,8 @@ def run_agent_loop(
|
|
|
190
190
|
elif user_input.lower() == "/memory":
|
|
191
191
|
print(f"\nDumping memory contents:\n")
|
|
192
192
|
print(f"{letta_agent.agent_state.memory.compile()}")
|
|
193
|
-
print(f"{letta_agent.
|
|
194
|
-
print(f"{letta_agent.
|
|
193
|
+
print(f"{letta_agent.archival_memory.compile()}")
|
|
194
|
+
print(f"{letta_agent.recall_memory.compile()}")
|
|
195
195
|
continue
|
|
196
196
|
|
|
197
197
|
elif user_input.lower() == "/model":
|
letta/memory.py
CHANGED
|
@@ -67,14 +67,12 @@ def summarize_messages(
|
|
|
67
67
|
+ message_sequence_to_summarize[cutoff:]
|
|
68
68
|
)
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
agent_state.user_id
|
|
71
71
|
dummy_agent_id = agent_state.id
|
|
72
72
|
message_sequence = []
|
|
73
|
-
message_sequence.append(Message(
|
|
74
|
-
message_sequence.append(
|
|
75
|
-
|
|
76
|
-
)
|
|
77
|
-
message_sequence.append(Message(user_id=dummy_user_id, agent_id=dummy_agent_id, role=MessageRole.user, text=summary_input))
|
|
73
|
+
message_sequence.append(Message(agent_id=dummy_agent_id, role=MessageRole.system, text=summary_prompt))
|
|
74
|
+
message_sequence.append(Message(agent_id=dummy_agent_id, role=MessageRole.assistant, text=MESSAGE_SUMMARY_REQUEST_ACK))
|
|
75
|
+
message_sequence.append(Message(agent_id=dummy_agent_id, role=MessageRole.user, text=summary_input))
|
|
78
76
|
|
|
79
77
|
# TODO: We need to eventually have a separate LLM config for the summarizer LLM
|
|
80
78
|
llm_config_no_inner_thoughts = agent_state.llm_config.model_copy(deep=True)
|
|
@@ -252,82 +250,6 @@ class DummyRecallMemory(RecallMemory):
|
|
|
252
250
|
return matches, len(matches)
|
|
253
251
|
|
|
254
252
|
|
|
255
|
-
class BaseRecallMemory(RecallMemory):
|
|
256
|
-
"""Recall memory based on base functions implemented by storage connectors"""
|
|
257
|
-
|
|
258
|
-
def __init__(self, agent_state, restrict_search_to_summaries=False):
|
|
259
|
-
# If true, the pool of messages that can be queried are the automated summaries only
|
|
260
|
-
# (generated when the conversation window needs to be shortened)
|
|
261
|
-
self.restrict_search_to_summaries = restrict_search_to_summaries
|
|
262
|
-
from letta.agent_store.storage import StorageConnector
|
|
263
|
-
|
|
264
|
-
self.agent_state = agent_state
|
|
265
|
-
|
|
266
|
-
# create embedding model
|
|
267
|
-
self.embed_model = embedding_model(agent_state.embedding_config)
|
|
268
|
-
self.embedding_chunk_size = agent_state.embedding_config.embedding_chunk_size
|
|
269
|
-
|
|
270
|
-
# create storage backend
|
|
271
|
-
self.storage = StorageConnector.get_recall_storage_connector(user_id=agent_state.user_id, agent_id=agent_state.id)
|
|
272
|
-
# TODO: have some mechanism for cleanup otherwise will lead to OOM
|
|
273
|
-
self.cache = {}
|
|
274
|
-
|
|
275
|
-
def get_all(self, start=0, count=None):
|
|
276
|
-
start = 0 if start is None else int(start)
|
|
277
|
-
count = 0 if count is None else int(count)
|
|
278
|
-
results = self.storage.get_all(start, count)
|
|
279
|
-
results_json = [message.to_openai_dict() for message in results]
|
|
280
|
-
return results_json, len(results)
|
|
281
|
-
|
|
282
|
-
def text_search(self, query_string, count=None, start=None):
|
|
283
|
-
start = 0 if start is None else int(start)
|
|
284
|
-
count = 0 if count is None else int(count)
|
|
285
|
-
results = self.storage.query_text(query_string, count, start)
|
|
286
|
-
results_json = [message.to_openai_dict_search_results() for message in results]
|
|
287
|
-
return results_json, len(results)
|
|
288
|
-
|
|
289
|
-
def date_search(self, start_date, end_date, count=None, start=None):
|
|
290
|
-
start = 0 if start is None else int(start)
|
|
291
|
-
count = 0 if count is None else int(count)
|
|
292
|
-
results = self.storage.query_date(start_date, end_date, count, start)
|
|
293
|
-
results_json = [message.to_openai_dict_search_results() for message in results]
|
|
294
|
-
return results_json, len(results)
|
|
295
|
-
|
|
296
|
-
def compile(self) -> str:
|
|
297
|
-
total = self.storage.size()
|
|
298
|
-
system_count = self.storage.size(filters={"role": "system"})
|
|
299
|
-
user_count = self.storage.size(filters={"role": "user"})
|
|
300
|
-
assistant_count = self.storage.size(filters={"role": "assistant"})
|
|
301
|
-
function_count = self.storage.size(filters={"role": "function"})
|
|
302
|
-
other_count = total - (system_count + user_count + assistant_count + function_count)
|
|
303
|
-
|
|
304
|
-
memory_str = (
|
|
305
|
-
f"Statistics:"
|
|
306
|
-
+ f"\n{total} total messages"
|
|
307
|
-
+ f"\n{system_count} system"
|
|
308
|
-
+ f"\n{user_count} user"
|
|
309
|
-
+ f"\n{assistant_count} assistant"
|
|
310
|
-
+ f"\n{function_count} function"
|
|
311
|
-
+ f"\n{other_count} other"
|
|
312
|
-
)
|
|
313
|
-
return f"\n### RECALL MEMORY ###" + f"\n{memory_str}"
|
|
314
|
-
|
|
315
|
-
def insert(self, message: Message):
|
|
316
|
-
self.storage.insert(message)
|
|
317
|
-
|
|
318
|
-
def insert_many(self, messages: List[Message]):
|
|
319
|
-
self.storage.insert_many(messages)
|
|
320
|
-
|
|
321
|
-
def save(self):
|
|
322
|
-
self.storage.save()
|
|
323
|
-
|
|
324
|
-
def __len__(self):
|
|
325
|
-
return self.storage.size()
|
|
326
|
-
|
|
327
|
-
def count(self) -> int:
|
|
328
|
-
return len(self)
|
|
329
|
-
|
|
330
|
-
|
|
331
253
|
class EmbeddingArchivalMemory(ArchivalMemory):
|
|
332
254
|
"""Archival memory with embedding based search"""
|
|
333
255
|
|
letta/metadata.py
CHANGED
|
@@ -14,7 +14,6 @@ from letta.schemas.api_key import APIKey
|
|
|
14
14
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
15
15
|
from letta.schemas.enums import ToolRuleType
|
|
16
16
|
from letta.schemas.llm_config import LLMConfig
|
|
17
|
-
from letta.schemas.openai.chat_completions import ToolCall, ToolCallFunction
|
|
18
17
|
from letta.schemas.tool_rule import ChildToolRule, InitToolRule, TerminalToolRule
|
|
19
18
|
from letta.schemas.user import User
|
|
20
19
|
from letta.services.per_agent_lock_manager import PerAgentLockManager
|
|
@@ -66,40 +65,6 @@ class EmbeddingConfigColumn(TypeDecorator):
|
|
|
66
65
|
return value
|
|
67
66
|
|
|
68
67
|
|
|
69
|
-
class ToolCallColumn(TypeDecorator):
|
|
70
|
-
|
|
71
|
-
impl = JSON
|
|
72
|
-
cache_ok = True
|
|
73
|
-
|
|
74
|
-
def load_dialect_impl(self, dialect):
|
|
75
|
-
return dialect.type_descriptor(JSON())
|
|
76
|
-
|
|
77
|
-
def process_bind_param(self, value, dialect):
|
|
78
|
-
if value:
|
|
79
|
-
values = []
|
|
80
|
-
for v in value:
|
|
81
|
-
if isinstance(v, ToolCall):
|
|
82
|
-
values.append(v.model_dump())
|
|
83
|
-
else:
|
|
84
|
-
values.append(v)
|
|
85
|
-
return values
|
|
86
|
-
|
|
87
|
-
return value
|
|
88
|
-
|
|
89
|
-
def process_result_value(self, value, dialect):
|
|
90
|
-
if value:
|
|
91
|
-
tools = []
|
|
92
|
-
for tool_value in value:
|
|
93
|
-
if "function" in tool_value:
|
|
94
|
-
tool_call_function = ToolCallFunction(**tool_value["function"])
|
|
95
|
-
del tool_value["function"]
|
|
96
|
-
else:
|
|
97
|
-
tool_call_function = None
|
|
98
|
-
tools.append(ToolCall(function=tool_call_function, **tool_value))
|
|
99
|
-
return tools
|
|
100
|
-
return value
|
|
101
|
-
|
|
102
|
-
|
|
103
68
|
# TODO: eventually store providers?
|
|
104
69
|
# class Provider(Base):
|
|
105
70
|
# __tablename__ = "providers"
|
letta/o1_agent.py
CHANGED
|
@@ -20,7 +20,7 @@ def send_thinking_message(self: "Agent", message: str) -> Optional[str]:
|
|
|
20
20
|
Returns:
|
|
21
21
|
Optional[str]: None is always returned as this function does not produce a response.
|
|
22
22
|
"""
|
|
23
|
-
self.interface.internal_monologue(message
|
|
23
|
+
self.interface.internal_monologue(message)
|
|
24
24
|
return None
|
|
25
25
|
|
|
26
26
|
|
|
@@ -34,7 +34,7 @@ def send_final_message(self: "Agent", message: str) -> Optional[str]:
|
|
|
34
34
|
Returns:
|
|
35
35
|
Optional[str]: None is always returned as this function does not produce a response.
|
|
36
36
|
"""
|
|
37
|
-
self.interface.internal_monologue(message
|
|
37
|
+
self.interface.internal_monologue(message)
|
|
38
38
|
return None
|
|
39
39
|
|
|
40
40
|
|
|
@@ -62,10 +62,15 @@ class O1Agent(Agent):
|
|
|
62
62
|
"""Run Agent.inner_step in a loop, terminate when final thinking message is sent or max_thinking_steps is reached"""
|
|
63
63
|
# assert ms is not None, "MetadataStore is required"
|
|
64
64
|
next_input_message = messages if isinstance(messages, list) else [messages]
|
|
65
|
+
|
|
65
66
|
counter = 0
|
|
66
67
|
total_usage = UsageStatistics()
|
|
67
68
|
step_count = 0
|
|
68
69
|
while step_count < self.max_thinking_steps:
|
|
70
|
+
# This is hacky but we need to do this for now
|
|
71
|
+
for m in next_input_message:
|
|
72
|
+
m.id = m._generate_id()
|
|
73
|
+
|
|
69
74
|
kwargs["ms"] = ms
|
|
70
75
|
kwargs["first_message"] = False
|
|
71
76
|
step_response = self.inner_step(
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from typing import List, Optional, Union
|
|
2
|
+
|
|
3
|
+
from letta.agent import Agent, AgentState, save_agent
|
|
4
|
+
from letta.interface import AgentInterface
|
|
5
|
+
from letta.metadata import MetadataStore
|
|
6
|
+
from letta.orm import User
|
|
7
|
+
from letta.schemas.message import Message
|
|
8
|
+
from letta.schemas.openai.chat_completion_response import UsageStatistics
|
|
9
|
+
from letta.schemas.usage import LettaUsageStatistics
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def trigger_rethink_memory(agent_state: "AgentState", message: Optional[str]) -> Optional[str]: # type: ignore
|
|
13
|
+
"""
|
|
14
|
+
Called if and only when user says the word trigger_rethink_memory". It will trigger the re-evaluation of the memory.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
message (Optional[str]): Description of what aspect of the memory should be re-evaluated.
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
from letta import create_client
|
|
21
|
+
|
|
22
|
+
client = create_client()
|
|
23
|
+
agents = client.list_agents()
|
|
24
|
+
for agent in agents:
|
|
25
|
+
if agent.agent_type == "offline_memory_agent":
|
|
26
|
+
client.user_message(agent_id=agent.id, message=message)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def trigger_rethink_memory_convo(agent_state: "AgentState", message: Optional[str]) -> Optional[str]: # type: ignore
|
|
30
|
+
"""
|
|
31
|
+
Called if and only when user says the word "trigger_rethink_memory". It will trigger the re-evaluation of the memory.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
message (Optional[str]): Description of what aspect of the memory should be re-evaluated.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
from letta import create_client
|
|
38
|
+
|
|
39
|
+
client = create_client()
|
|
40
|
+
recent_convo = "".join([str(message) for message in agent_state.messages])[
|
|
41
|
+
-2000:
|
|
42
|
+
] # TODO: make a better representation of the convo history
|
|
43
|
+
agent_state.memory.update_block_value(label="conversation_block", value=recent_convo)
|
|
44
|
+
|
|
45
|
+
client = create_client()
|
|
46
|
+
agents = client.list_agents()
|
|
47
|
+
for agent in agents:
|
|
48
|
+
if agent.agent_type == "offline_memory_agent":
|
|
49
|
+
client.user_message(agent_id=agent.id, message=message)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def rethink_memory_convo(agent_state: "AgentState", new_memory: str, target_block_label: Optional[str], source_block_label: Optional[str]) -> Optional[str]: # type: ignore
|
|
53
|
+
"""
|
|
54
|
+
Re-evaluate the memory in block_name, integrating new and updated facts. Replace outdated information with the most likely truths, avoiding redundancy with original memories. Ensure consistency with other memory blocks.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
new_memory (str): The new memory with information integrated from the memory block. If there is no new information, then this should be the same as the content in the source block.
|
|
58
|
+
source_block_label (str): The name of the block to integrate information from. None if all the information has been integrated to terminate the loop. This can by any block.
|
|
59
|
+
target_block_label (str): The name of the block to write to. This should be chat_agent_human_new or chat_agent_persona_new.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Optional[str]: None is always returned as this function does not produce a response.
|
|
63
|
+
"""
|
|
64
|
+
if target_block_label is not None:
|
|
65
|
+
if agent_state.memory.get_block(target_block_label) is None:
|
|
66
|
+
agent_state.memory.create_block(label=target_block_label, value=new_memory)
|
|
67
|
+
agent_state.memory.update_block_value(label=target_block_label, value=new_memory)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def rethink_memory(agent_state: "AgentState", new_memory: str, target_block_label: Optional[str], source_block_label: Optional[str]) -> Optional[str]: # type: ignore
|
|
72
|
+
"""
|
|
73
|
+
Re-evaluate the memory in block_name, integrating new and updated facts.
|
|
74
|
+
Replace outdated information with the most likely truths, avoiding redundancy with original memories.
|
|
75
|
+
Ensure consistency with other memory blocks.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
new_memory (str): The new memory with information integrated from the memory block. If there is no new information, then this should be the same as the content in the source block.
|
|
79
|
+
source_block_label (str): The name of the block to integrate information from. None if all the information has been integrated to terminate the loop.
|
|
80
|
+
target_block_label (str): The name of the block to write to.
|
|
81
|
+
Returns:
|
|
82
|
+
Optional[str]: None is always returned as this function does not produce a response.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
if target_block_label is not None:
|
|
86
|
+
if agent_state.memory.get_block(target_block_label) is None:
|
|
87
|
+
agent_state.memory.create_block(label=target_block_label, value=new_memory)
|
|
88
|
+
agent_state.memory.update_block_value(label=target_block_label, value=new_memory)
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def finish_rethinking_memory(agent_state: "AgentState") -> Optional[str]: # type: ignore
|
|
93
|
+
"""
|
|
94
|
+
This function is called when the agent is done rethinking the memory.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Optional[str]: None is always returned as this function does not produce a response.
|
|
98
|
+
"""
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def finish_rethinking_memory_convo(agent_state: "AgentState") -> Optional[str]: # type: ignore
|
|
103
|
+
"""
|
|
104
|
+
This function is called when the agent is done rethinking the memory.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Optional[str]: None is always returned as this function does not produce a response.
|
|
108
|
+
"""
|
|
109
|
+
from letta import create_client
|
|
110
|
+
|
|
111
|
+
client = create_client()
|
|
112
|
+
agents = client.list_agents()
|
|
113
|
+
|
|
114
|
+
agent_state.memory.update_block_value("chat_agent_human", agent_state.memory.get_block("chat_agent_human_new").value)
|
|
115
|
+
agent_state.memory.update_block_value("chat_agent_persona", agent_state.memory.get_block("chat_agent_persona_new").value)
|
|
116
|
+
for agent in agents:
|
|
117
|
+
if agent.name == "conversation_agent":
|
|
118
|
+
agent.memory.update_block_value(label="chat_agent_human", value=agent_state.memory.get_block("chat_agent_human_new").value)
|
|
119
|
+
agent.memory.update_block_value(label="chat_agent_persona", value=agent_state.memory.get_block("chat_agent_persona_new").value)
|
|
120
|
+
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class OfflineMemoryAgent(Agent):
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
interface: AgentInterface,
|
|
128
|
+
agent_state: AgentState,
|
|
129
|
+
user: User = None,
|
|
130
|
+
# extras
|
|
131
|
+
first_message_verify_mono: bool = False,
|
|
132
|
+
max_memory_rethinks: int = 10,
|
|
133
|
+
):
|
|
134
|
+
super().__init__(interface, agent_state, user)
|
|
135
|
+
self.first_message_verify_mono = first_message_verify_mono
|
|
136
|
+
self.max_memory_rethinks = max_memory_rethinks
|
|
137
|
+
|
|
138
|
+
def step(
|
|
139
|
+
self,
|
|
140
|
+
messages: Union[Message, List[Message]],
|
|
141
|
+
chaining: bool = True,
|
|
142
|
+
max_chaining_steps: Optional[int] = None,
|
|
143
|
+
ms: Optional[MetadataStore] = None,
|
|
144
|
+
**kwargs,
|
|
145
|
+
) -> LettaUsageStatistics:
|
|
146
|
+
"""Go through what is currently in memory core memory and integrate information."""
|
|
147
|
+
next_input_message = messages if isinstance(messages, list) else [messages]
|
|
148
|
+
counter = 0
|
|
149
|
+
total_usage = UsageStatistics()
|
|
150
|
+
step_count = 0
|
|
151
|
+
|
|
152
|
+
while counter < self.max_memory_rethinks:
|
|
153
|
+
# This is hacky but we need to do this for now
|
|
154
|
+
# TODO: REMOVE THIS
|
|
155
|
+
for m in next_input_message:
|
|
156
|
+
m.id = m._generate_id()
|
|
157
|
+
|
|
158
|
+
kwargs["ms"] = ms
|
|
159
|
+
kwargs["first_message"] = False
|
|
160
|
+
step_response = self.inner_step(
|
|
161
|
+
messages=next_input_message,
|
|
162
|
+
**kwargs,
|
|
163
|
+
)
|
|
164
|
+
for message in step_response.messages:
|
|
165
|
+
if message.tool_calls:
|
|
166
|
+
for tool_call in message.tool_calls:
|
|
167
|
+
# check if the function name is "finish_rethinking_memory"
|
|
168
|
+
if tool_call.function.name == "finish_rethinking_memory":
|
|
169
|
+
counter = self.max_memory_rethinks
|
|
170
|
+
break
|
|
171
|
+
usage = step_response.usage
|
|
172
|
+
step_count += 1
|
|
173
|
+
total_usage += usage
|
|
174
|
+
counter += 1
|
|
175
|
+
self.interface.step_complete()
|
|
176
|
+
|
|
177
|
+
if ms:
|
|
178
|
+
save_agent(self, ms)
|
|
179
|
+
|
|
180
|
+
return LettaUsageStatistics(**total_usage.model_dump(), step_count=step_count)
|
letta/orm/__init__.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
from letta.orm.agents_tags import AgentsTags
|
|
1
2
|
from letta.orm.base import Base
|
|
2
3
|
from letta.orm.block import Block
|
|
3
4
|
from letta.orm.blocks_agents import BlocksAgents
|
|
4
5
|
from letta.orm.file import FileMetadata
|
|
5
6
|
from letta.orm.job import Job
|
|
7
|
+
from letta.orm.message import Message
|
|
6
8
|
from letta.orm.organization import Organization
|
|
7
9
|
from letta.orm.sandbox_config import SandboxConfig, SandboxEnvironmentVariable
|
|
8
10
|
from letta.orm.source import Source
|
|
9
11
|
from letta.orm.tool import Tool
|
|
12
|
+
from letta.orm.tools_agents import ToolsAgents
|
|
10
13
|
from letta.orm.user import User
|
letta/orm/file.py
CHANGED
letta/orm/message.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import JSON, DateTime, TypeDecorator
|
|
5
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
6
|
+
|
|
7
|
+
from letta.orm.mixins import AgentMixin, OrganizationMixin
|
|
8
|
+
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
|
9
|
+
from letta.schemas.message import Message as PydanticMessage
|
|
10
|
+
from letta.schemas.openai.chat_completions import ToolCall, ToolCallFunction
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ToolCallColumn(TypeDecorator):
|
|
14
|
+
|
|
15
|
+
impl = JSON
|
|
16
|
+
cache_ok = True
|
|
17
|
+
|
|
18
|
+
def load_dialect_impl(self, dialect):
|
|
19
|
+
return dialect.type_descriptor(JSON())
|
|
20
|
+
|
|
21
|
+
def process_bind_param(self, value, dialect):
|
|
22
|
+
if value:
|
|
23
|
+
values = []
|
|
24
|
+
for v in value:
|
|
25
|
+
if isinstance(v, ToolCall):
|
|
26
|
+
values.append(v.model_dump())
|
|
27
|
+
else:
|
|
28
|
+
values.append(v)
|
|
29
|
+
return values
|
|
30
|
+
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
def process_result_value(self, value, dialect):
|
|
34
|
+
if value:
|
|
35
|
+
tools = []
|
|
36
|
+
for tool_value in value:
|
|
37
|
+
if "function" in tool_value:
|
|
38
|
+
tool_call_function = ToolCallFunction(**tool_value["function"])
|
|
39
|
+
del tool_value["function"]
|
|
40
|
+
else:
|
|
41
|
+
tool_call_function = None
|
|
42
|
+
tools.append(ToolCall(function=tool_call_function, **tool_value))
|
|
43
|
+
return tools
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
|
|
48
|
+
"""Defines data model for storing Message objects"""
|
|
49
|
+
|
|
50
|
+
__tablename__ = "messages"
|
|
51
|
+
__table_args__ = {"extend_existing": True}
|
|
52
|
+
__pydantic_model__ = PydanticMessage
|
|
53
|
+
|
|
54
|
+
id: Mapped[str] = mapped_column(primary_key=True, doc="Unique message identifier")
|
|
55
|
+
role: Mapped[str] = mapped_column(doc="Message role (user/assistant/system/tool)")
|
|
56
|
+
text: Mapped[Optional[str]] = mapped_column(nullable=True, doc="Message content")
|
|
57
|
+
model: Mapped[Optional[str]] = mapped_column(nullable=True, doc="LLM model used")
|
|
58
|
+
name: Mapped[Optional[str]] = mapped_column(nullable=True, doc="Name for multi-agent scenarios")
|
|
59
|
+
tool_calls: Mapped[ToolCall] = mapped_column(ToolCallColumn, doc="Tool call information")
|
|
60
|
+
tool_call_id: Mapped[Optional[str]] = mapped_column(nullable=True, doc="ID of the tool call")
|
|
61
|
+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=datetime.utcnow)
|
|
62
|
+
|
|
63
|
+
# Relationships
|
|
64
|
+
# TODO: Add in after Agent ORM is created
|
|
65
|
+
# agent: Mapped["Agent"] = relationship("Agent", back_populates="messages", lazy="selectin")
|
|
66
|
+
organization: Mapped["Organization"] = relationship("Organization", back_populates="messages", lazy="selectin")
|
letta/orm/mixins.py
CHANGED
|
@@ -31,6 +31,22 @@ class UserMixin(Base):
|
|
|
31
31
|
user_id: Mapped[str] = mapped_column(String, ForeignKey("users.id"))
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
class AgentMixin(Base):
|
|
35
|
+
"""Mixin for models that belong to an agent."""
|
|
36
|
+
|
|
37
|
+
__abstract__ = True
|
|
38
|
+
|
|
39
|
+
agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FileMixin(Base):
|
|
43
|
+
"""Mixin for models that belong to a file."""
|
|
44
|
+
|
|
45
|
+
__abstract__ = True
|
|
46
|
+
|
|
47
|
+
file_id: Mapped[str] = mapped_column(String, ForeignKey("files.id"))
|
|
48
|
+
|
|
49
|
+
|
|
34
50
|
class SourceMixin(Base):
|
|
35
51
|
"""Mixin for models (e.g. file) that belong to a source."""
|
|
36
52
|
|
letta/orm/organization.py
CHANGED
|
@@ -33,6 +33,7 @@ class Organization(SqlalchemyBase):
|
|
|
33
33
|
sandbox_environment_variables: Mapped[List["SandboxEnvironmentVariable"]] = relationship(
|
|
34
34
|
"SandboxEnvironmentVariable", back_populates="organization", cascade="all, delete-orphan"
|
|
35
35
|
)
|
|
36
|
+
messages: Mapped[List["Message"]] = relationship("Message", back_populates="organization", cascade="all, delete-orphan")
|
|
36
37
|
|
|
37
38
|
# TODO: Map these relationships later when we actually make these models
|
|
38
39
|
# below is just a suggestion
|