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.

Files changed (52) hide show
  1. letta/agent.py +54 -37
  2. letta/agent_store/db.py +1 -77
  3. letta/agent_store/storage.py +0 -5
  4. letta/chat_only_agent.py +103 -0
  5. letta/cli/cli.py +0 -1
  6. letta/client/client.py +3 -7
  7. letta/constants.py +1 -0
  8. letta/functions/function_sets/base.py +37 -9
  9. letta/main.py +2 -2
  10. letta/memory.py +4 -82
  11. letta/metadata.py +0 -35
  12. letta/o1_agent.py +7 -2
  13. letta/offline_memory_agent.py +180 -0
  14. letta/orm/__init__.py +3 -0
  15. letta/orm/file.py +1 -1
  16. letta/orm/message.py +66 -0
  17. letta/orm/mixins.py +16 -0
  18. letta/orm/organization.py +1 -0
  19. letta/orm/sqlalchemy_base.py +118 -26
  20. letta/orm/tool.py +22 -1
  21. letta/orm/tools_agents.py +32 -0
  22. letta/personas/examples/offline_memory_persona.txt +4 -0
  23. letta/prompts/system/memgpt_convo_only.txt +14 -0
  24. letta/prompts/system/memgpt_offline_memory.txt +23 -0
  25. letta/prompts/system/memgpt_offline_memory_chat.txt +35 -0
  26. letta/schemas/agent.py +3 -2
  27. letta/schemas/letta_base.py +7 -6
  28. letta/schemas/message.py +1 -7
  29. letta/schemas/tools_agents.py +32 -0
  30. letta/server/rest_api/app.py +11 -0
  31. letta/server/rest_api/routers/v1/agents.py +2 -2
  32. letta/server/rest_api/routers/v1/blocks.py +2 -2
  33. letta/server/server.py +63 -47
  34. letta/server/static_files/assets/index-43ab4d62.css +1 -0
  35. letta/server/static_files/assets/index-4848e3d7.js +40 -0
  36. letta/server/static_files/index.html +2 -2
  37. letta/services/block_manager.py +1 -1
  38. letta/services/message_manager.py +182 -0
  39. letta/services/organization_manager.py +6 -9
  40. letta/services/source_manager.py +1 -1
  41. letta/services/tool_manager.py +2 -2
  42. letta/services/tools_agents_manager.py +94 -0
  43. letta/services/user_manager.py +1 -1
  44. {letta_nightly-0.6.1.dev20241205211219.dist-info → letta_nightly-0.6.1.dev20241207104149.dist-info}/METADATA +2 -1
  45. {letta_nightly-0.6.1.dev20241205211219.dist-info → letta_nightly-0.6.1.dev20241207104149.dist-info}/RECORD +48 -39
  46. letta/agent_store/lancedb.py +0 -177
  47. letta/persistence_manager.py +0 -149
  48. letta/server/static_files/assets/index-3ab03d5b.css +0 -1
  49. letta/server/static_files/assets/index-9fa459a2.js +0 -271
  50. {letta_nightly-0.6.1.dev20241205211219.dist-info → letta_nightly-0.6.1.dev20241207104149.dist-info}/LICENSE +0 -0
  51. {letta_nightly-0.6.1.dev20241205211219.dist-info → letta_nightly-0.6.1.dev20241207104149.dist-info}/WHEEL +0 -0
  52. {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
- results, total = self.persistence_manager.recall_memory.text_search(query, count=count, start=page * count)
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, total = self.persistence_manager.recall_memory.date_search(start_date, end_date, count=count, start=page * count)
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.persistence_manager.archival_memory.insert(content)
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.persistence_manager.archival_memory.search(query, count=count, start=page * count)
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.persistence_manager.archival_memory.compile()}")
194
- print(f"{letta_agent.persistence_manager.recall_memory.compile()}")
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
- dummy_user_id = agent_state.user_id
70
+ agent_state.user_id
71
71
  dummy_agent_id = agent_state.id
72
72
  message_sequence = []
73
- message_sequence.append(Message(user_id=dummy_user_id, agent_id=dummy_agent_id, role=MessageRole.system, text=summary_prompt))
74
- message_sequence.append(
75
- Message(user_id=dummy_user_id, agent_id=dummy_agent_id, role=MessageRole.assistant, text=MESSAGE_SUMMARY_REQUEST_ACK)
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, msg_obj=self._messages[-1])
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, msg_obj=self._messages[-1])
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
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING, Optional
1
+ from typing import TYPE_CHECKING, Optional, List
2
2
 
3
3
  from sqlalchemy import Integer, String
4
4
  from sqlalchemy.orm import Mapped, mapped_column, relationship
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