letta-nightly 0.1.7.dev20240924104148__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 (189) hide show
  1. letta/__init__.py +24 -0
  2. letta/__main__.py +3 -0
  3. letta/agent.py +1427 -0
  4. letta/agent_store/chroma.py +295 -0
  5. letta/agent_store/db.py +546 -0
  6. letta/agent_store/lancedb.py +177 -0
  7. letta/agent_store/milvus.py +198 -0
  8. letta/agent_store/qdrant.py +201 -0
  9. letta/agent_store/storage.py +188 -0
  10. letta/benchmark/benchmark.py +96 -0
  11. letta/benchmark/constants.py +14 -0
  12. letta/cli/cli.py +689 -0
  13. letta/cli/cli_config.py +1282 -0
  14. letta/cli/cli_load.py +166 -0
  15. letta/client/__init__.py +0 -0
  16. letta/client/admin.py +171 -0
  17. letta/client/client.py +2360 -0
  18. letta/client/streaming.py +90 -0
  19. letta/client/utils.py +61 -0
  20. letta/config.py +484 -0
  21. letta/configs/anthropic.json +13 -0
  22. letta/configs/letta_hosted.json +11 -0
  23. letta/configs/openai.json +12 -0
  24. letta/constants.py +134 -0
  25. letta/credentials.py +140 -0
  26. letta/data_sources/connectors.py +247 -0
  27. letta/embeddings.py +218 -0
  28. letta/errors.py +26 -0
  29. letta/functions/__init__.py +0 -0
  30. letta/functions/function_sets/base.py +174 -0
  31. letta/functions/function_sets/extras.py +132 -0
  32. letta/functions/functions.py +105 -0
  33. letta/functions/schema_generator.py +205 -0
  34. letta/humans/__init__.py +0 -0
  35. letta/humans/examples/basic.txt +1 -0
  36. letta/humans/examples/cs_phd.txt +9 -0
  37. letta/interface.py +314 -0
  38. letta/llm_api/__init__.py +0 -0
  39. letta/llm_api/anthropic.py +383 -0
  40. letta/llm_api/azure_openai.py +155 -0
  41. letta/llm_api/cohere.py +396 -0
  42. letta/llm_api/google_ai.py +468 -0
  43. letta/llm_api/llm_api_tools.py +485 -0
  44. letta/llm_api/openai.py +470 -0
  45. letta/local_llm/README.md +3 -0
  46. letta/local_llm/__init__.py +0 -0
  47. letta/local_llm/chat_completion_proxy.py +279 -0
  48. letta/local_llm/constants.py +31 -0
  49. letta/local_llm/function_parser.py +68 -0
  50. letta/local_llm/grammars/__init__.py +0 -0
  51. letta/local_llm/grammars/gbnf_grammar_generator.py +1324 -0
  52. letta/local_llm/grammars/json.gbnf +26 -0
  53. letta/local_llm/grammars/json_func_calls_with_inner_thoughts.gbnf +32 -0
  54. letta/local_llm/groq/api.py +97 -0
  55. letta/local_llm/json_parser.py +202 -0
  56. letta/local_llm/koboldcpp/api.py +62 -0
  57. letta/local_llm/koboldcpp/settings.py +23 -0
  58. letta/local_llm/llamacpp/api.py +58 -0
  59. letta/local_llm/llamacpp/settings.py +22 -0
  60. letta/local_llm/llm_chat_completion_wrappers/__init__.py +0 -0
  61. letta/local_llm/llm_chat_completion_wrappers/airoboros.py +452 -0
  62. letta/local_llm/llm_chat_completion_wrappers/chatml.py +470 -0
  63. letta/local_llm/llm_chat_completion_wrappers/configurable_wrapper.py +387 -0
  64. letta/local_llm/llm_chat_completion_wrappers/dolphin.py +246 -0
  65. letta/local_llm/llm_chat_completion_wrappers/llama3.py +345 -0
  66. letta/local_llm/llm_chat_completion_wrappers/simple_summary_wrapper.py +156 -0
  67. letta/local_llm/llm_chat_completion_wrappers/wrapper_base.py +11 -0
  68. letta/local_llm/llm_chat_completion_wrappers/zephyr.py +345 -0
  69. letta/local_llm/lmstudio/api.py +100 -0
  70. letta/local_llm/lmstudio/settings.py +29 -0
  71. letta/local_llm/ollama/api.py +88 -0
  72. letta/local_llm/ollama/settings.py +32 -0
  73. letta/local_llm/settings/__init__.py +0 -0
  74. letta/local_llm/settings/deterministic_mirostat.py +45 -0
  75. letta/local_llm/settings/settings.py +72 -0
  76. letta/local_llm/settings/simple.py +28 -0
  77. letta/local_llm/utils.py +265 -0
  78. letta/local_llm/vllm/api.py +63 -0
  79. letta/local_llm/webui/api.py +60 -0
  80. letta/local_llm/webui/legacy_api.py +58 -0
  81. letta/local_llm/webui/legacy_settings.py +23 -0
  82. letta/local_llm/webui/settings.py +24 -0
  83. letta/log.py +76 -0
  84. letta/main.py +437 -0
  85. letta/memory.py +440 -0
  86. letta/metadata.py +884 -0
  87. letta/openai_backcompat/__init__.py +0 -0
  88. letta/openai_backcompat/openai_object.py +437 -0
  89. letta/persistence_manager.py +148 -0
  90. letta/personas/__init__.py +0 -0
  91. letta/personas/examples/anna_pa.txt +13 -0
  92. letta/personas/examples/google_search_persona.txt +15 -0
  93. letta/personas/examples/memgpt_doc.txt +6 -0
  94. letta/personas/examples/memgpt_starter.txt +4 -0
  95. letta/personas/examples/sam.txt +14 -0
  96. letta/personas/examples/sam_pov.txt +14 -0
  97. letta/personas/examples/sam_simple_pov_gpt35.txt +13 -0
  98. letta/personas/examples/sqldb/test.db +0 -0
  99. letta/prompts/__init__.py +0 -0
  100. letta/prompts/gpt_summarize.py +14 -0
  101. letta/prompts/gpt_system.py +26 -0
  102. letta/prompts/system/memgpt_base.txt +49 -0
  103. letta/prompts/system/memgpt_chat.txt +58 -0
  104. letta/prompts/system/memgpt_chat_compressed.txt +13 -0
  105. letta/prompts/system/memgpt_chat_fstring.txt +51 -0
  106. letta/prompts/system/memgpt_doc.txt +50 -0
  107. letta/prompts/system/memgpt_gpt35_extralong.txt +53 -0
  108. letta/prompts/system/memgpt_intuitive_knowledge.txt +31 -0
  109. letta/prompts/system/memgpt_modified_chat.txt +23 -0
  110. letta/pytest.ini +0 -0
  111. letta/schemas/agent.py +117 -0
  112. letta/schemas/api_key.py +21 -0
  113. letta/schemas/block.py +135 -0
  114. letta/schemas/document.py +21 -0
  115. letta/schemas/embedding_config.py +54 -0
  116. letta/schemas/enums.py +35 -0
  117. letta/schemas/job.py +38 -0
  118. letta/schemas/letta_base.py +80 -0
  119. letta/schemas/letta_message.py +175 -0
  120. letta/schemas/letta_request.py +23 -0
  121. letta/schemas/letta_response.py +28 -0
  122. letta/schemas/llm_config.py +54 -0
  123. letta/schemas/memory.py +224 -0
  124. letta/schemas/message.py +727 -0
  125. letta/schemas/openai/chat_completion_request.py +123 -0
  126. letta/schemas/openai/chat_completion_response.py +136 -0
  127. letta/schemas/openai/chat_completions.py +123 -0
  128. letta/schemas/openai/embedding_response.py +11 -0
  129. letta/schemas/openai/openai.py +157 -0
  130. letta/schemas/organization.py +20 -0
  131. letta/schemas/passage.py +80 -0
  132. letta/schemas/source.py +62 -0
  133. letta/schemas/tool.py +143 -0
  134. letta/schemas/usage.py +18 -0
  135. letta/schemas/user.py +33 -0
  136. letta/server/__init__.py +0 -0
  137. letta/server/constants.py +6 -0
  138. letta/server/rest_api/__init__.py +0 -0
  139. letta/server/rest_api/admin/__init__.py +0 -0
  140. letta/server/rest_api/admin/agents.py +21 -0
  141. letta/server/rest_api/admin/tools.py +83 -0
  142. letta/server/rest_api/admin/users.py +98 -0
  143. letta/server/rest_api/app.py +193 -0
  144. letta/server/rest_api/auth/__init__.py +0 -0
  145. letta/server/rest_api/auth/index.py +43 -0
  146. letta/server/rest_api/auth_token.py +22 -0
  147. letta/server/rest_api/interface.py +726 -0
  148. letta/server/rest_api/routers/__init__.py +0 -0
  149. letta/server/rest_api/routers/openai/__init__.py +0 -0
  150. letta/server/rest_api/routers/openai/assistants/__init__.py +0 -0
  151. letta/server/rest_api/routers/openai/assistants/assistants.py +115 -0
  152. letta/server/rest_api/routers/openai/assistants/schemas.py +121 -0
  153. letta/server/rest_api/routers/openai/assistants/threads.py +336 -0
  154. letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
  155. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +131 -0
  156. letta/server/rest_api/routers/v1/__init__.py +15 -0
  157. letta/server/rest_api/routers/v1/agents.py +543 -0
  158. letta/server/rest_api/routers/v1/blocks.py +73 -0
  159. letta/server/rest_api/routers/v1/jobs.py +46 -0
  160. letta/server/rest_api/routers/v1/llms.py +28 -0
  161. letta/server/rest_api/routers/v1/organizations.py +61 -0
  162. letta/server/rest_api/routers/v1/sources.py +199 -0
  163. letta/server/rest_api/routers/v1/tools.py +103 -0
  164. letta/server/rest_api/routers/v1/users.py +109 -0
  165. letta/server/rest_api/static_files.py +74 -0
  166. letta/server/rest_api/utils.py +69 -0
  167. letta/server/server.py +1995 -0
  168. letta/server/startup.sh +8 -0
  169. letta/server/static_files/assets/index-0cbf7ad5.js +274 -0
  170. letta/server/static_files/assets/index-156816da.css +1 -0
  171. letta/server/static_files/assets/index-486e3228.js +274 -0
  172. letta/server/static_files/favicon.ico +0 -0
  173. letta/server/static_files/index.html +39 -0
  174. letta/server/static_files/memgpt_logo_transparent.png +0 -0
  175. letta/server/utils.py +46 -0
  176. letta/server/ws_api/__init__.py +0 -0
  177. letta/server/ws_api/example_client.py +104 -0
  178. letta/server/ws_api/interface.py +108 -0
  179. letta/server/ws_api/protocol.py +100 -0
  180. letta/server/ws_api/server.py +145 -0
  181. letta/settings.py +165 -0
  182. letta/streaming_interface.py +396 -0
  183. letta/system.py +207 -0
  184. letta/utils.py +1065 -0
  185. letta_nightly-0.1.7.dev20240924104148.dist-info/LICENSE +190 -0
  186. letta_nightly-0.1.7.dev20240924104148.dist-info/METADATA +98 -0
  187. letta_nightly-0.1.7.dev20240924104148.dist-info/RECORD +189 -0
  188. letta_nightly-0.1.7.dev20240924104148.dist-info/WHEEL +4 -0
  189. letta_nightly-0.1.7.dev20240924104148.dist-info/entry_points.txt +3 -0
letta/agent.py ADDED
@@ -0,0 +1,1427 @@
1
+ import datetime
2
+ import inspect
3
+ import traceback
4
+ import warnings
5
+ from abc import ABC, abstractmethod
6
+ from typing import List, Literal, Optional, Tuple, Union
7
+
8
+ from tqdm import tqdm
9
+
10
+ from letta.agent_store.storage import StorageConnector
11
+ from letta.constants import (
12
+ CLI_WARNING_PREFIX,
13
+ FIRST_MESSAGE_ATTEMPTS,
14
+ IN_CONTEXT_MEMORY_KEYWORD,
15
+ LLM_MAX_TOKENS,
16
+ MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST,
17
+ MESSAGE_SUMMARY_TRUNC_TOKEN_FRAC,
18
+ MESSAGE_SUMMARY_WARNING_FRAC,
19
+ )
20
+ from letta.interface import AgentInterface
21
+ from letta.llm_api.llm_api_tools import create, is_context_overflow_error
22
+ from letta.memory import ArchivalMemory, RecallMemory, summarize_messages
23
+ from letta.metadata import MetadataStore
24
+ from letta.persistence_manager import LocalStateManager
25
+ from letta.schemas.agent import AgentState, AgentStepResponse
26
+ from letta.schemas.block import Block
27
+ from letta.schemas.embedding_config import EmbeddingConfig
28
+ from letta.schemas.enums import MessageRole, OptionState
29
+ from letta.schemas.memory import Memory
30
+ from letta.schemas.message import Message, UpdateMessage
31
+ from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
32
+ from letta.schemas.openai.chat_completion_response import (
33
+ Message as ChatCompletionMessage,
34
+ )
35
+ from letta.schemas.passage import Passage
36
+ from letta.schemas.tool import Tool
37
+ from letta.system import (
38
+ get_initial_boot_messages,
39
+ get_login_event,
40
+ package_function_response,
41
+ package_summarize_message,
42
+ )
43
+ from letta.utils import (
44
+ count_tokens,
45
+ get_local_time,
46
+ get_tool_call_id,
47
+ get_utc_time,
48
+ is_utc_datetime,
49
+ json_dumps,
50
+ json_loads,
51
+ parse_json,
52
+ printd,
53
+ united_diff,
54
+ validate_function_response,
55
+ verify_first_message_correctness,
56
+ )
57
+
58
+ from .errors import LLMError
59
+
60
+
61
+ def compile_memory_metadata_block(
62
+ memory_edit_timestamp: datetime.datetime,
63
+ archival_memory: Optional[ArchivalMemory] = None,
64
+ recall_memory: Optional[RecallMemory] = None,
65
+ ) -> str:
66
+ # Put the timestamp in the local timezone (mimicking get_local_time())
67
+ timestamp_str = memory_edit_timestamp.astimezone().strftime("%Y-%m-%d %I:%M:%S %p %Z%z").strip()
68
+
69
+ # Create a metadata block of info so the agent knows about the metadata of out-of-context memories
70
+ memory_metadata_block = "\n".join(
71
+ [
72
+ f"### Memory [last modified: {timestamp_str}]",
73
+ f"{recall_memory.count() if recall_memory else 0} previous messages between you and the user are stored in recall memory (use functions to access them)",
74
+ f"{archival_memory.count() if archival_memory else 0} total memories you created are stored in archival memory (use functions to access them)",
75
+ "\nCore memory shown below (limited in size, additional information stored in archival / recall memory):",
76
+ ]
77
+ )
78
+ return memory_metadata_block
79
+
80
+
81
+ def compile_system_message(
82
+ system_prompt: str,
83
+ in_context_memory: Memory,
84
+ in_context_memory_last_edit: datetime.datetime, # TODO move this inside of BaseMemory?
85
+ archival_memory: Optional[ArchivalMemory] = None,
86
+ recall_memory: Optional[RecallMemory] = None,
87
+ user_defined_variables: Optional[dict] = None,
88
+ append_icm_if_missing: bool = True,
89
+ template_format: Literal["f-string", "mustache", "jinja2"] = "f-string",
90
+ ) -> str:
91
+ """Prepare the final/full system message that will be fed into the LLM API
92
+
93
+ The base system message may be templated, in which case we need to render the variables.
94
+
95
+ The following are reserved variables:
96
+ - CORE_MEMORY: the in-context memory of the LLM
97
+ """
98
+
99
+ if user_defined_variables is not None:
100
+ # TODO eventually support the user defining their own variables to inject
101
+ raise NotImplementedError
102
+ else:
103
+ variables = {}
104
+
105
+ # Add the protected memory variable
106
+ if IN_CONTEXT_MEMORY_KEYWORD in variables:
107
+ raise ValueError(f"Found protected variable '{IN_CONTEXT_MEMORY_KEYWORD}' in user-defined vars: {str(user_defined_variables)}")
108
+ else:
109
+ # TODO should this all put into the memory.__repr__ function?
110
+ memory_metadata_string = compile_memory_metadata_block(
111
+ memory_edit_timestamp=in_context_memory_last_edit,
112
+ archival_memory=archival_memory,
113
+ recall_memory=recall_memory,
114
+ )
115
+ full_memory_string = memory_metadata_string + "\n" + in_context_memory.compile()
116
+
117
+ # Add to the variables list to inject
118
+ variables[IN_CONTEXT_MEMORY_KEYWORD] = full_memory_string
119
+
120
+ if template_format == "f-string":
121
+
122
+ # Catch the special case where the system prompt is unformatted
123
+ if append_icm_if_missing:
124
+ memory_variable_string = "{" + IN_CONTEXT_MEMORY_KEYWORD + "}"
125
+ if memory_variable_string not in system_prompt:
126
+ # In this case, append it to the end to make sure memory is still injected
127
+ # warnings.warn(f"{IN_CONTEXT_MEMORY_KEYWORD} variable was missing from system prompt, appending instead")
128
+ system_prompt += "\n" + memory_variable_string
129
+
130
+ # render the variables using the built-in templater
131
+ try:
132
+ formatted_prompt = system_prompt.format_map(variables)
133
+ except Exception as e:
134
+ raise ValueError(f"Failed to format system prompt - {str(e)}. System prompt value:\n{system_prompt}")
135
+
136
+ else:
137
+ # TODO support for mustache and jinja2
138
+ raise NotImplementedError(template_format)
139
+
140
+ return formatted_prompt
141
+
142
+
143
+ def initialize_message_sequence(
144
+ model: str,
145
+ system: str,
146
+ memory: Memory,
147
+ archival_memory: Optional[ArchivalMemory] = None,
148
+ recall_memory: Optional[RecallMemory] = None,
149
+ memory_edit_timestamp: Optional[datetime.datetime] = None,
150
+ include_initial_boot_message: bool = True,
151
+ ) -> List[dict]:
152
+ if memory_edit_timestamp is None:
153
+ memory_edit_timestamp = get_local_time()
154
+
155
+ # full_system_message = construct_system_with_memory(
156
+ # system, memory, memory_edit_timestamp, archival_memory=archival_memory, recall_memory=recall_memory
157
+ # )
158
+ full_system_message = compile_system_message(
159
+ system_prompt=system,
160
+ in_context_memory=memory,
161
+ in_context_memory_last_edit=memory_edit_timestamp,
162
+ archival_memory=archival_memory,
163
+ recall_memory=recall_memory,
164
+ user_defined_variables=None,
165
+ append_icm_if_missing=True,
166
+ )
167
+ first_user_message = get_login_event() # event letting Letta know the user just logged in
168
+
169
+ if include_initial_boot_message:
170
+ if model is not None and "gpt-3.5" in model:
171
+ initial_boot_messages = get_initial_boot_messages("startup_with_send_message_gpt35")
172
+ else:
173
+ initial_boot_messages = get_initial_boot_messages("startup_with_send_message")
174
+ messages = (
175
+ [
176
+ {"role": "system", "content": full_system_message},
177
+ ]
178
+ + initial_boot_messages
179
+ + [
180
+ {"role": "user", "content": first_user_message},
181
+ ]
182
+ )
183
+
184
+ else:
185
+ messages = [
186
+ {"role": "system", "content": full_system_message},
187
+ {"role": "user", "content": first_user_message},
188
+ ]
189
+
190
+ return messages
191
+
192
+
193
+ class BaseAgent(ABC):
194
+ """
195
+ Abstract class for all agents.
196
+ Only two interfaces are required: step and update_state.
197
+ """
198
+
199
+ @abstractmethod
200
+ def step(
201
+ self,
202
+ messages: Union[Message, List[Message], str], # TODO deprecate str inputs
203
+ first_message: bool = False,
204
+ first_message_retry_limit: int = FIRST_MESSAGE_ATTEMPTS,
205
+ skip_verify: bool = False,
206
+ return_dicts: bool = True, # if True, return dicts, if False, return Message objects
207
+ recreate_message_timestamp: bool = True, # if True, when input is a Message type, recreated the 'created_at' field
208
+ stream: bool = False, # TODO move to config?
209
+ timestamp: Optional[datetime.datetime] = None,
210
+ inner_thoughts_in_kwargs: OptionState = OptionState.DEFAULT,
211
+ ms: Optional[MetadataStore] = None,
212
+ ) -> AgentStepResponse:
213
+ """
214
+ Top-level event message handler for the agent.
215
+ """
216
+ raise NotImplementedError
217
+
218
+ @abstractmethod
219
+ def update_state(self) -> AgentState:
220
+ raise NotImplementedError
221
+
222
+
223
+ class Agent(BaseAgent):
224
+ def __init__(
225
+ self,
226
+ interface: AgentInterface,
227
+ # agents can be created from providing agent_state
228
+ agent_state: AgentState,
229
+ tools: List[Tool],
230
+ # memory: Memory,
231
+ # extras
232
+ messages_total: Optional[int] = None, # TODO remove?
233
+ first_message_verify_mono: bool = True, # TODO move to config?
234
+ ):
235
+ assert isinstance(agent_state.memory, Memory), f"Memory object is not of type Memory: {type(agent_state.memory)}"
236
+ # Hold a copy of the state that was used to init the agent
237
+ self.agent_state = agent_state
238
+ assert isinstance(self.agent_state.memory, Memory), f"Memory object is not of type Memory: {type(self.agent_state.memory)}"
239
+
240
+ try:
241
+ self.link_tools(tools)
242
+ except Exception as e:
243
+ raise ValueError(f"Encountered an error while trying to link agent tools during initialization:\n{str(e)}")
244
+
245
+ # gpt-4, gpt-3.5-turbo, ...
246
+ self.model = self.agent_state.llm_config.model
247
+
248
+ # Store the system instructions (used to rebuild memory)
249
+ self.system = self.agent_state.system
250
+
251
+ # Initialize the memory object
252
+ self.memory = self.agent_state.memory
253
+ assert isinstance(self.memory, Memory), f"Memory object is not of type Memory: {type(self.memory)}"
254
+ printd("Initialized memory object", self.memory.compile())
255
+
256
+ # Interface must implement:
257
+ # - internal_monologue
258
+ # - assistant_message
259
+ # - function_message
260
+ # ...
261
+ # Different interfaces can handle events differently
262
+ # e.g., print in CLI vs send a discord message with a discord bot
263
+ self.interface = interface
264
+
265
+ # Create the persistence manager object based on the AgentState info
266
+ self.persistence_manager = LocalStateManager(agent_state=self.agent_state)
267
+
268
+ # State needed for heartbeat pausing
269
+ self.pause_heartbeats_start = None
270
+ self.pause_heartbeats_minutes = 0
271
+
272
+ self.first_message_verify_mono = first_message_verify_mono
273
+
274
+ # Controls if the convo memory pressure warning is triggered
275
+ # When an alert is sent in the message queue, set this to True (to avoid repeat alerts)
276
+ # When the summarizer is run, set this back to False (to reset)
277
+ self.agent_alerted_about_memory_pressure = False
278
+
279
+ self._messages: List[Message] = []
280
+
281
+ # Once the memory object is initialized, use it to "bake" the system message
282
+ if self.agent_state.message_ids is not None:
283
+ self.set_message_buffer(message_ids=self.agent_state.message_ids)
284
+
285
+ else:
286
+ printd(f"Agent.__init__ :: creating, state={agent_state.message_ids}")
287
+
288
+ # Generate a sequence of initial messages to put in the buffer
289
+ init_messages = initialize_message_sequence(
290
+ model=self.model,
291
+ system=self.system,
292
+ memory=self.memory,
293
+ archival_memory=None,
294
+ recall_memory=None,
295
+ memory_edit_timestamp=get_utc_time(),
296
+ include_initial_boot_message=True,
297
+ )
298
+
299
+ # Cast the messages to actual Message objects to be synced to the DB
300
+ init_messages_objs = []
301
+ for msg in init_messages:
302
+ init_messages_objs.append(
303
+ Message.dict_to_message(
304
+ agent_id=self.agent_state.id, user_id=self.agent_state.user_id, model=self.model, openai_message_dict=msg
305
+ )
306
+ )
307
+ assert all([isinstance(msg, Message) for msg in init_messages_objs]), (init_messages_objs, init_messages)
308
+
309
+ # Put the messages inside the message buffer
310
+ self.messages_total = 0
311
+ # self._append_to_messages(added_messages=[cast(Message, msg) for msg in init_messages_objs if msg is not None])
312
+ self._append_to_messages(added_messages=init_messages_objs)
313
+ self._validate_message_buffer_is_utc()
314
+
315
+ # Keep track of the total number of messages throughout all time
316
+ self.messages_total = messages_total if messages_total is not None else (len(self._messages) - 1) # (-system)
317
+ self.messages_total_init = len(self._messages) - 1
318
+ printd(f"Agent initialized, self.messages_total={self.messages_total}")
319
+
320
+ # Create the agent in the DB
321
+ self.update_state()
322
+
323
+ @property
324
+ def messages(self) -> List[dict]:
325
+ """Getter method that converts the internal Message list into OpenAI-style dicts"""
326
+ return [msg.to_openai_dict() for msg in self._messages]
327
+
328
+ @messages.setter
329
+ def messages(self, value):
330
+ raise Exception("Modifying message list directly not allowed")
331
+
332
+ def link_tools(self, tools: List[Tool]):
333
+ """Bind a tool object (schema + python function) to the agent object"""
334
+
335
+ # tools
336
+ for tool in tools:
337
+ assert tool, f"Tool is None - must be error in querying tool from DB"
338
+ assert tool.name in self.agent_state.tools, f"Tool {tool} not found in agent_state.tools"
339
+ for tool_name in self.agent_state.tools:
340
+ assert tool_name in [tool.name for tool in tools], f"Tool name {tool_name} not included in agent tool list"
341
+
342
+ # Store the functions schemas (this is passed as an argument to ChatCompletion)
343
+ self.functions = []
344
+ self.functions_python = {}
345
+ env = {}
346
+ env.update(globals())
347
+ for tool in tools:
348
+ # WARNING: name may not be consistent?
349
+ if tool.module: # execute the whole module
350
+ exec(tool.module, env)
351
+ else:
352
+ exec(tool.source_code, env)
353
+ self.functions_python[tool.name] = env[tool.name]
354
+ self.functions.append(tool.json_schema)
355
+ assert all([callable(f) for k, f in self.functions_python.items()]), self.functions_python
356
+
357
+ def _load_messages_from_recall(self, message_ids: List[str]) -> List[Message]:
358
+ """Load a list of messages from recall storage"""
359
+
360
+ # Pull the message objects from the database
361
+ message_objs = []
362
+ for msg_id in message_ids:
363
+ msg_obj = self.persistence_manager.recall_memory.storage.get(msg_id)
364
+ if msg_obj:
365
+ if isinstance(msg_obj, Message):
366
+ message_objs.append(msg_obj)
367
+ else:
368
+ printd(f"Warning - message ID {msg_id} is not a Message object")
369
+ warnings.warn(f"Warning - message ID {msg_id} is not a Message object")
370
+ else:
371
+ printd(f"Warning - message ID {msg_id} not found in recall storage")
372
+ warnings.warn(f"Warning - message ID {msg_id} not found in recall storage")
373
+
374
+ return message_objs
375
+
376
+ def _validate_message_buffer_is_utc(self):
377
+ """Iterate over the message buffer and force all messages to be UTC stamped"""
378
+
379
+ for m in self._messages:
380
+ # assert is_utc_datetime(m.created_at), f"created_at on message for agent {self.agent_state.name} isn't UTC:\n{vars(m)}"
381
+ # TODO eventually do casting via an edit_message function
382
+ if not is_utc_datetime(m.created_at):
383
+ printd(f"Warning - created_at on message for agent {self.agent_state.name} isn't UTC (text='{m.text}')")
384
+ m.created_at = m.created_at.replace(tzinfo=datetime.timezone.utc)
385
+
386
+ def set_message_buffer(self, message_ids: List[str], force_utc: bool = True):
387
+ """Set the messages in the buffer to the message IDs list"""
388
+
389
+ message_objs = self._load_messages_from_recall(message_ids=message_ids)
390
+
391
+ # set the objects in the buffer
392
+ self._messages = message_objs
393
+
394
+ # bugfix for old agents that may not have had UTC specified in their timestamps
395
+ if force_utc:
396
+ self._validate_message_buffer_is_utc()
397
+
398
+ # also sync the message IDs attribute
399
+ self.agent_state.message_ids = message_ids
400
+
401
+ def refresh_message_buffer(self):
402
+ """Refresh the message buffer from the database"""
403
+
404
+ messages_to_sync = self.agent_state.message_ids
405
+ assert messages_to_sync and all([isinstance(msg_id, str) for msg_id in messages_to_sync])
406
+
407
+ self.set_message_buffer(message_ids=messages_to_sync)
408
+
409
+ def _trim_messages(self, num):
410
+ """Trim messages from the front, not including the system message"""
411
+ self.persistence_manager.trim_messages(num)
412
+
413
+ new_messages = [self._messages[0]] + self._messages[num:]
414
+ self._messages = new_messages
415
+
416
+ def _prepend_to_messages(self, added_messages: List[Message]):
417
+ """Wrapper around self.messages.prepend to allow additional calls to a state/persistence manager"""
418
+ assert all([isinstance(msg, Message) for msg in added_messages])
419
+
420
+ self.persistence_manager.prepend_to_messages(added_messages)
421
+
422
+ new_messages = [self._messages[0]] + added_messages + self._messages[1:] # prepend (no system)
423
+ self._messages = new_messages
424
+ self.messages_total += len(added_messages) # still should increment the message counter (summaries are additions too)
425
+
426
+ def _append_to_messages(self, added_messages: List[Message]):
427
+ """Wrapper around self.messages.append to allow additional calls to a state/persistence manager"""
428
+ assert all([isinstance(msg, Message) for msg in added_messages])
429
+
430
+ self.persistence_manager.append_to_messages(added_messages)
431
+
432
+ # strip extra metadata if it exists
433
+ # for msg in added_messages:
434
+ # msg.pop("api_response", None)
435
+ # msg.pop("api_args", None)
436
+ new_messages = self._messages + added_messages # append
437
+
438
+ self._messages = new_messages
439
+ self.messages_total += len(added_messages)
440
+
441
+ def append_to_messages(self, added_messages: List[dict]):
442
+ """An external-facing message append, where dict-like messages are first converted to Message objects"""
443
+ added_messages_objs = [
444
+ Message.dict_to_message(
445
+ agent_id=self.agent_state.id,
446
+ user_id=self.agent_state.user_id,
447
+ model=self.model,
448
+ openai_message_dict=msg,
449
+ )
450
+ for msg in added_messages
451
+ ]
452
+ self._append_to_messages(added_messages_objs)
453
+
454
+ def _get_ai_reply(
455
+ self,
456
+ message_sequence: List[Message],
457
+ function_call: str = "auto",
458
+ first_message: bool = False, # hint
459
+ stream: bool = False, # TODO move to config?
460
+ inner_thoughts_in_kwargs: OptionState = OptionState.DEFAULT,
461
+ ) -> ChatCompletionResponse:
462
+ """Get response from LLM API"""
463
+ try:
464
+ response = create(
465
+ # agent_state=self.agent_state,
466
+ llm_config=self.agent_state.llm_config,
467
+ user_id=self.agent_state.user_id,
468
+ messages=message_sequence,
469
+ functions=self.functions,
470
+ functions_python=self.functions_python,
471
+ function_call=function_call,
472
+ # hint
473
+ first_message=first_message,
474
+ # streaming
475
+ stream=stream,
476
+ stream_inferface=self.interface,
477
+ # putting inner thoughts in func args or not
478
+ inner_thoughts_in_kwargs=inner_thoughts_in_kwargs,
479
+ )
480
+
481
+ if len(response.choices) == 0:
482
+ raise Exception(f"API call didn't return a message: {response}")
483
+
484
+ # special case for 'length'
485
+ if response.choices[0].finish_reason == "length":
486
+ raise Exception("Finish reason was length (maximum context length)")
487
+
488
+ # catches for soft errors
489
+ if response.choices[0].finish_reason not in ["stop", "function_call", "tool_calls"]:
490
+ raise Exception(f"API call finish with bad finish reason: {response}")
491
+
492
+ # unpack with response.choices[0].message.content
493
+ return response
494
+ except Exception as e:
495
+ raise e
496
+
497
+ def _handle_ai_response(
498
+ self,
499
+ response_message: ChatCompletionMessage, # TODO should we eventually move the Message creation outside of this function?
500
+ override_tool_call_id: bool = True,
501
+ # If we are streaming, we needed to create a Message ID ahead of time,
502
+ # and now we want to use it in the creation of the Message object
503
+ # TODO figure out a cleaner way to do this
504
+ response_message_id: Optional[str] = None,
505
+ ) -> Tuple[List[Message], bool, bool]:
506
+ """Handles parsing and function execution"""
507
+
508
+ # Hacky failsafe for now to make sure we didn't implement the streaming Message ID creation incorrectly
509
+ if response_message_id is not None:
510
+ assert response_message_id.startswith("message-"), response_message_id
511
+
512
+ messages = [] # append these to the history when done
513
+
514
+ # Step 2: check if LLM wanted to call a function
515
+ if response_message.function_call or (response_message.tool_calls is not None and len(response_message.tool_calls) > 0):
516
+ if response_message.function_call:
517
+ raise DeprecationWarning(response_message)
518
+ if response_message.tool_calls is not None and len(response_message.tool_calls) > 1:
519
+ # raise NotImplementedError(f">1 tool call not supported")
520
+ # TODO eventually support sequential tool calling
521
+ printd(f">1 tool call not supported, using index=0 only\n{response_message.tool_calls}")
522
+ response_message.tool_calls = [response_message.tool_calls[0]]
523
+ assert response_message.tool_calls is not None and len(response_message.tool_calls) > 0
524
+
525
+ # generate UUID for tool call
526
+ if override_tool_call_id or response_message.function_call:
527
+ tool_call_id = get_tool_call_id() # needs to be a string for JSON
528
+ response_message.tool_calls[0].id = tool_call_id
529
+ else:
530
+ tool_call_id = response_message.tool_calls[0].id
531
+ assert tool_call_id is not None # should be defined
532
+
533
+ # only necessary to add the tool_cal_id to a function call (antipattern)
534
+ # response_message_dict = response_message.model_dump()
535
+ # response_message_dict["tool_call_id"] = tool_call_id
536
+
537
+ # role: assistant (requesting tool call, set tool call ID)
538
+ messages.append(
539
+ # NOTE: we're recreating the message here
540
+ # TODO should probably just overwrite the fields?
541
+ Message.dict_to_message(
542
+ id=response_message_id,
543
+ agent_id=self.agent_state.id,
544
+ user_id=self.agent_state.user_id,
545
+ model=self.model,
546
+ openai_message_dict=response_message.model_dump(),
547
+ )
548
+ ) # extend conversation with assistant's reply
549
+ printd(f"Function call message: {messages[-1]}")
550
+
551
+ # The content if then internal monologue, not chat
552
+ self.interface.internal_monologue(response_message.content, msg_obj=messages[-1])
553
+
554
+ # Step 3: call the function
555
+ # Note: the JSON response may not always be valid; be sure to handle errors
556
+
557
+ # Failure case 1: function name is wrong
558
+ function_call = (
559
+ response_message.function_call if response_message.function_call is not None else response_message.tool_calls[0].function
560
+ )
561
+ function_name = function_call.name
562
+ printd(f"Request to call function {function_name} with tool_call_id: {tool_call_id}")
563
+ try:
564
+ function_to_call = self.functions_python[function_name]
565
+ except KeyError:
566
+ error_msg = f"No function named {function_name}"
567
+ function_response = package_function_response(False, error_msg)
568
+ messages.append(
569
+ Message.dict_to_message(
570
+ agent_id=self.agent_state.id,
571
+ user_id=self.agent_state.user_id,
572
+ model=self.model,
573
+ openai_message_dict={
574
+ "role": "tool",
575
+ "name": function_name,
576
+ "content": function_response,
577
+ "tool_call_id": tool_call_id,
578
+ },
579
+ )
580
+ ) # extend conversation with function response
581
+ self.interface.function_message(f"Error: {error_msg}", msg_obj=messages[-1])
582
+ return messages, False, True # force a heartbeat to allow agent to handle error
583
+
584
+ # Failure case 2: function name is OK, but function args are bad JSON
585
+ try:
586
+ raw_function_args = function_call.arguments
587
+ function_args = parse_json(raw_function_args)
588
+ except Exception:
589
+ error_msg = f"Error parsing JSON for function '{function_name}' arguments: {function_call.arguments}"
590
+ function_response = package_function_response(False, error_msg)
591
+ messages.append(
592
+ Message.dict_to_message(
593
+ agent_id=self.agent_state.id,
594
+ user_id=self.agent_state.user_id,
595
+ model=self.model,
596
+ openai_message_dict={
597
+ "role": "tool",
598
+ "name": function_name,
599
+ "content": function_response,
600
+ "tool_call_id": tool_call_id,
601
+ },
602
+ )
603
+ ) # extend conversation with function response
604
+ self.interface.function_message(f"Error: {error_msg}", msg_obj=messages[-1])
605
+ return messages, False, True # force a heartbeat to allow agent to handle error
606
+
607
+ # (Still parsing function args)
608
+ # Handle requests for immediate heartbeat
609
+ heartbeat_request = function_args.pop("request_heartbeat", None)
610
+ if not isinstance(heartbeat_request, bool) or heartbeat_request is None:
611
+ printd(
612
+ f"{CLI_WARNING_PREFIX}'request_heartbeat' arg parsed was not a bool or None, type={type(heartbeat_request)}, value={heartbeat_request}"
613
+ )
614
+ heartbeat_request = False
615
+
616
+ # Failure case 3: function failed during execution
617
+ # NOTE: the msg_obj associated with the "Running " message is the prior assistant message, not the function/tool role message
618
+ # this is because the function/tool role message is only created once the function/tool has executed/returned
619
+ self.interface.function_message(f"Running {function_name}({function_args})", msg_obj=messages[-1])
620
+ try:
621
+ spec = inspect.getfullargspec(function_to_call).annotations
622
+
623
+ for name, arg in function_args.items():
624
+ if isinstance(function_args[name], dict):
625
+ function_args[name] = spec[name](**function_args[name])
626
+
627
+ function_args["self"] = self # need to attach self to arg since it's dynamically linked
628
+
629
+ function_response = function_to_call(**function_args)
630
+ if function_name in ["conversation_search", "conversation_search_date", "archival_memory_search"]:
631
+ # with certain functions we rely on the paging mechanism to handle overflow
632
+ truncate = False
633
+ else:
634
+ # but by default, we add a truncation safeguard to prevent bad functions from
635
+ # overflow the agent context window
636
+ truncate = True
637
+ function_response_string = validate_function_response(function_response, truncate=truncate)
638
+ function_args.pop("self", None)
639
+ function_response = package_function_response(True, function_response_string)
640
+ function_failed = False
641
+ except Exception as e:
642
+ function_args.pop("self", None)
643
+ # error_msg = f"Error calling function {function_name} with args {function_args}: {str(e)}"
644
+ # Less detailed - don't provide full args, idea is that it should be in recent context so no need (just adds noise)
645
+ error_msg = f"Error calling function {function_name}: {str(e)}"
646
+ error_msg_user = f"{error_msg}\n{traceback.format_exc()}"
647
+ printd(error_msg_user)
648
+ function_response = package_function_response(False, error_msg)
649
+ messages.append(
650
+ Message.dict_to_message(
651
+ agent_id=self.agent_state.id,
652
+ user_id=self.agent_state.user_id,
653
+ model=self.model,
654
+ openai_message_dict={
655
+ "role": "tool",
656
+ "name": function_name,
657
+ "content": function_response,
658
+ "tool_call_id": tool_call_id,
659
+ },
660
+ )
661
+ ) # extend conversation with function response
662
+ self.interface.function_message(f"Ran {function_name}({function_args})", msg_obj=messages[-1])
663
+ self.interface.function_message(f"Error: {error_msg}", msg_obj=messages[-1])
664
+ return messages, False, True # force a heartbeat to allow agent to handle error
665
+
666
+ # If no failures happened along the way: ...
667
+ # Step 4: send the info on the function call and function response to GPT
668
+ messages.append(
669
+ Message.dict_to_message(
670
+ agent_id=self.agent_state.id,
671
+ user_id=self.agent_state.user_id,
672
+ model=self.model,
673
+ openai_message_dict={
674
+ "role": "tool",
675
+ "name": function_name,
676
+ "content": function_response,
677
+ "tool_call_id": tool_call_id,
678
+ },
679
+ )
680
+ ) # extend conversation with function response
681
+ self.interface.function_message(f"Ran {function_name}({function_args})", msg_obj=messages[-1])
682
+ self.interface.function_message(f"Success: {function_response_string}", msg_obj=messages[-1])
683
+
684
+ else:
685
+ # Standard non-function reply
686
+ messages.append(
687
+ Message.dict_to_message(
688
+ id=response_message_id,
689
+ agent_id=self.agent_state.id,
690
+ user_id=self.agent_state.user_id,
691
+ model=self.model,
692
+ openai_message_dict=response_message.model_dump(),
693
+ )
694
+ ) # extend conversation with assistant's reply
695
+ self.interface.internal_monologue(response_message.content, msg_obj=messages[-1])
696
+ heartbeat_request = False
697
+ function_failed = False
698
+
699
+ # rebuild memory
700
+ # TODO: @charles please check this
701
+ self.rebuild_memory()
702
+
703
+ return messages, heartbeat_request, function_failed
704
+
705
+ def step(
706
+ self,
707
+ user_message: Union[Message, None, str], # NOTE: should be json.dump(dict)
708
+ first_message: bool = False,
709
+ first_message_retry_limit: int = FIRST_MESSAGE_ATTEMPTS,
710
+ skip_verify: bool = False,
711
+ return_dicts: bool = True,
712
+ recreate_message_timestamp: bool = True, # if True, when input is a Message type, recreated the 'created_at' field
713
+ stream: bool = False, # TODO move to config?
714
+ timestamp: Optional[datetime.datetime] = None,
715
+ inner_thoughts_in_kwargs: OptionState = OptionState.DEFAULT,
716
+ ms: Optional[MetadataStore] = None,
717
+ ) -> AgentStepResponse:
718
+ """Top-level event message handler for the Letta agent"""
719
+
720
+ try:
721
+
722
+ # Step 0: update core memory
723
+ # only pulling latest block data if shared memory is being used
724
+ # TODO: ensure we're passing in metadata store from all surfaces
725
+ if ms is not None:
726
+ should_update = False
727
+ for block in self.agent_state.memory.to_dict()["memory"].values():
728
+ if not block.get("template", False):
729
+ should_update = True
730
+ if should_update:
731
+ # TODO: the force=True can be optimized away
732
+ # once we ensure we're correctly comparing whether in-memory core
733
+ # data is different than persisted core data.
734
+ self.rebuild_memory(force=True, ms=ms)
735
+
736
+ # Step 1: add user message
737
+ if user_message is not None:
738
+ if isinstance(user_message, Message):
739
+ assert user_message.text is not None
740
+
741
+ # Validate JSON via save/load
742
+ user_message_text = validate_json(user_message.text)
743
+ cleaned_user_message_text, name = strip_name_field_from_user_message(user_message_text)
744
+
745
+ if name is not None:
746
+ # Update Message object
747
+ user_message.text = cleaned_user_message_text
748
+ user_message.name = name
749
+
750
+ # Recreate timestamp
751
+ if recreate_message_timestamp:
752
+ user_message.created_at = get_utc_time()
753
+
754
+ elif isinstance(user_message, str):
755
+ # Validate JSON via save/load
756
+ user_message = validate_json(user_message)
757
+ cleaned_user_message_text, name = strip_name_field_from_user_message(user_message)
758
+
759
+ # If user_message['name'] is not None, it will be handled properly by dict_to_message
760
+ # So no need to run strip_name_field_from_user_message
761
+
762
+ # Create the associated Message object (in the database)
763
+ user_message = Message.dict_to_message(
764
+ agent_id=self.agent_state.id,
765
+ user_id=self.agent_state.user_id,
766
+ model=self.model,
767
+ openai_message_dict={"role": "user", "content": cleaned_user_message_text, "name": name},
768
+ created_at=timestamp,
769
+ )
770
+
771
+ else:
772
+ raise ValueError(f"Bad type for user_message: {type(user_message)}")
773
+
774
+ self.interface.user_message(user_message.text, msg_obj=user_message)
775
+
776
+ input_message_sequence = self._messages + [user_message]
777
+
778
+ # Alternatively, the requestor can send an empty user message
779
+ else:
780
+ input_message_sequence = self._messages
781
+
782
+ if len(input_message_sequence) > 1 and input_message_sequence[-1].role != "user":
783
+ printd(f"{CLI_WARNING_PREFIX}Attempting to run ChatCompletion without user as the last message in the queue")
784
+
785
+ # Step 2: send the conversation and available functions to the LLM
786
+ if not skip_verify and (first_message or self.messages_total == self.messages_total_init):
787
+ printd(f"This is the first message. Running extra verifier on AI response.")
788
+ counter = 0
789
+ while True:
790
+ response = self._get_ai_reply(
791
+ message_sequence=input_message_sequence,
792
+ first_message=True, # passed through to the prompt formatter
793
+ stream=stream,
794
+ inner_thoughts_in_kwargs=inner_thoughts_in_kwargs,
795
+ )
796
+ if verify_first_message_correctness(response, require_monologue=self.first_message_verify_mono):
797
+ break
798
+
799
+ counter += 1
800
+ if counter > first_message_retry_limit:
801
+ raise Exception(f"Hit first message retry limit ({first_message_retry_limit})")
802
+
803
+ else:
804
+ response = self._get_ai_reply(
805
+ message_sequence=input_message_sequence,
806
+ stream=stream,
807
+ inner_thoughts_in_kwargs=inner_thoughts_in_kwargs,
808
+ )
809
+
810
+ # Step 3: check if LLM wanted to call a function
811
+ # (if yes) Step 4: call the function
812
+ # (if yes) Step 5: send the info on the function call and function response to LLM
813
+ response_message = response.choices[0].message
814
+ response_message.model_copy() # TODO why are we copying here?
815
+ all_response_messages, heartbeat_request, function_failed = self._handle_ai_response(
816
+ response_message,
817
+ # TODO this is kind of hacky, find a better way to handle this
818
+ # the only time we set up message creation ahead of time is when streaming is on
819
+ response_message_id=response.id if stream else None,
820
+ )
821
+
822
+ # Step 6: extend the message history
823
+ if user_message is not None:
824
+ if isinstance(user_message, Message):
825
+ all_new_messages = [user_message] + all_response_messages
826
+ else:
827
+ raise ValueError(type(user_message))
828
+ else:
829
+ all_new_messages = all_response_messages
830
+
831
+ # Check the memory pressure and potentially issue a memory pressure warning
832
+ current_total_tokens = response.usage.total_tokens
833
+ active_memory_warning = False
834
+
835
+ # We can't do summarize logic properly if context_window is undefined
836
+ if self.agent_state.llm_config.context_window is None:
837
+ # Fallback if for some reason context_window is missing, just set to the default
838
+ print(f"{CLI_WARNING_PREFIX}could not find context_window in config, setting to default {LLM_MAX_TOKENS['DEFAULT']}")
839
+ print(f"{self.agent_state}")
840
+ self.agent_state.llm_config.context_window = (
841
+ LLM_MAX_TOKENS[self.model] if (self.model is not None and self.model in LLM_MAX_TOKENS) else LLM_MAX_TOKENS["DEFAULT"]
842
+ )
843
+
844
+ if current_total_tokens > MESSAGE_SUMMARY_WARNING_FRAC * int(self.agent_state.llm_config.context_window):
845
+ printd(
846
+ f"{CLI_WARNING_PREFIX}last response total_tokens ({current_total_tokens}) > {MESSAGE_SUMMARY_WARNING_FRAC * int(self.agent_state.llm_config.context_window)}"
847
+ )
848
+
849
+ # Only deliver the alert if we haven't already (this period)
850
+ if not self.agent_alerted_about_memory_pressure:
851
+ active_memory_warning = True
852
+ self.agent_alerted_about_memory_pressure = True # it's up to the outer loop to handle this
853
+
854
+ else:
855
+ printd(
856
+ f"last response total_tokens ({current_total_tokens}) < {MESSAGE_SUMMARY_WARNING_FRAC * int(self.agent_state.llm_config.context_window)}"
857
+ )
858
+
859
+ self._append_to_messages(all_new_messages)
860
+ messages_to_return = [msg.to_openai_dict() for msg in all_new_messages] if return_dicts else all_new_messages
861
+
862
+ # update state after each step
863
+ self.update_state()
864
+
865
+ return AgentStepResponse(
866
+ messages=messages_to_return,
867
+ heartbeat_request=heartbeat_request,
868
+ function_failed=function_failed,
869
+ in_context_memory_warning=active_memory_warning,
870
+ usage=response.usage,
871
+ )
872
+
873
+ except Exception as e:
874
+ printd(f"step() failed\nuser_message = {user_message}\nerror = {e}")
875
+
876
+ # If we got a context alert, try trimming the messages length, then try again
877
+ if is_context_overflow_error(e):
878
+ # A separate API call to run a summarizer
879
+ self.summarize_messages_inplace()
880
+
881
+ # Try step again
882
+ return self.step(
883
+ user_message,
884
+ first_message=first_message,
885
+ first_message_retry_limit=first_message_retry_limit,
886
+ skip_verify=skip_verify,
887
+ return_dicts=return_dicts,
888
+ recreate_message_timestamp=recreate_message_timestamp,
889
+ stream=stream,
890
+ timestamp=timestamp,
891
+ inner_thoughts_in_kwargs=inner_thoughts_in_kwargs,
892
+ ms=ms,
893
+ )
894
+
895
+ else:
896
+ printd(f"step() failed with an unrecognized exception: '{str(e)}'")
897
+ raise e
898
+
899
+ def summarize_messages_inplace(self, cutoff=None, preserve_last_N_messages=True, disallow_tool_as_first=True):
900
+ assert self.messages[0]["role"] == "system", f"self.messages[0] should be system (instead got {self.messages[0]})"
901
+
902
+ # Start at index 1 (past the system message),
903
+ # and collect messages for summarization until we reach the desired truncation token fraction (eg 50%)
904
+ # Do not allow truncation of the last N messages, since these are needed for in-context examples of function calling
905
+ token_counts = [count_tokens(str(msg)) for msg in self.messages]
906
+ message_buffer_token_count = sum(token_counts[1:]) # no system message
907
+ desired_token_count_to_summarize = int(message_buffer_token_count * MESSAGE_SUMMARY_TRUNC_TOKEN_FRAC)
908
+ candidate_messages_to_summarize = self.messages[1:]
909
+ token_counts = token_counts[1:]
910
+
911
+ if preserve_last_N_messages:
912
+ candidate_messages_to_summarize = candidate_messages_to_summarize[:-MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST]
913
+ token_counts = token_counts[:-MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST]
914
+
915
+ printd(f"MESSAGE_SUMMARY_TRUNC_TOKEN_FRAC={MESSAGE_SUMMARY_TRUNC_TOKEN_FRAC}")
916
+ printd(f"MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST={MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST}")
917
+ printd(f"token_counts={token_counts}")
918
+ printd(f"message_buffer_token_count={message_buffer_token_count}")
919
+ printd(f"desired_token_count_to_summarize={desired_token_count_to_summarize}")
920
+ printd(f"len(candidate_messages_to_summarize)={len(candidate_messages_to_summarize)}")
921
+
922
+ # If at this point there's nothing to summarize, throw an error
923
+ if len(candidate_messages_to_summarize) == 0:
924
+ raise LLMError(
925
+ f"Summarize error: tried to run summarize, but couldn't find enough messages to compress [len={len(self.messages)}, preserve_N={MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST}]"
926
+ )
927
+
928
+ # Walk down the message buffer (front-to-back) until we hit the target token count
929
+ tokens_so_far = 0
930
+ cutoff = 0
931
+ for i, msg in enumerate(candidate_messages_to_summarize):
932
+ cutoff = i
933
+ tokens_so_far += token_counts[i]
934
+ if tokens_so_far > desired_token_count_to_summarize:
935
+ break
936
+ # Account for system message
937
+ cutoff += 1
938
+
939
+ # Try to make an assistant message come after the cutoff
940
+ try:
941
+ printd(f"Selected cutoff {cutoff} was a 'user', shifting one...")
942
+ if self.messages[cutoff]["role"] == "user":
943
+ new_cutoff = cutoff + 1
944
+ if self.messages[new_cutoff]["role"] == "user":
945
+ printd(f"Shifted cutoff {new_cutoff} is still a 'user', ignoring...")
946
+ cutoff = new_cutoff
947
+ except IndexError:
948
+ pass
949
+
950
+ # Make sure the cutoff isn't on a 'tool' or 'function'
951
+ if disallow_tool_as_first:
952
+ while self.messages[cutoff]["role"] in ["tool", "function"] and cutoff < len(self.messages):
953
+ printd(f"Selected cutoff {cutoff} was a 'tool', shifting one...")
954
+ cutoff += 1
955
+
956
+ message_sequence_to_summarize = self._messages[1:cutoff] # do NOT get rid of the system message
957
+ if len(message_sequence_to_summarize) <= 1:
958
+ # This prevents a potential infinite loop of summarizing the same message over and over
959
+ raise LLMError(
960
+ f"Summarize error: tried to run summarize, but couldn't find enough messages to compress [len={len(message_sequence_to_summarize)} <= 1]"
961
+ )
962
+ else:
963
+ printd(f"Attempting to summarize {len(message_sequence_to_summarize)} messages [1:{cutoff}] of {len(self._messages)}")
964
+
965
+ # We can't do summarize logic properly if context_window is undefined
966
+ if self.agent_state.llm_config.context_window is None:
967
+ # Fallback if for some reason context_window is missing, just set to the default
968
+ print(f"{CLI_WARNING_PREFIX}could not find context_window in config, setting to default {LLM_MAX_TOKENS['DEFAULT']}")
969
+ print(f"{self.agent_state}")
970
+ self.agent_state.llm_config.context_window = (
971
+ LLM_MAX_TOKENS[self.model] if (self.model is not None and self.model in LLM_MAX_TOKENS) else LLM_MAX_TOKENS["DEFAULT"]
972
+ )
973
+ summary = summarize_messages(agent_state=self.agent_state, message_sequence_to_summarize=message_sequence_to_summarize)
974
+ printd(f"Got summary: {summary}")
975
+
976
+ # Metadata that's useful for the agent to see
977
+ all_time_message_count = self.messages_total
978
+ remaining_message_count = len(self.messages[cutoff:])
979
+ hidden_message_count = all_time_message_count - remaining_message_count
980
+ summary_message_count = len(message_sequence_to_summarize)
981
+ summary_message = package_summarize_message(summary, summary_message_count, hidden_message_count, all_time_message_count)
982
+ printd(f"Packaged into message: {summary_message}")
983
+
984
+ prior_len = len(self.messages)
985
+ self._trim_messages(cutoff)
986
+ packed_summary_message = {"role": "user", "content": summary_message}
987
+ self._prepend_to_messages(
988
+ [
989
+ Message.dict_to_message(
990
+ agent_id=self.agent_state.id,
991
+ user_id=self.agent_state.user_id,
992
+ model=self.model,
993
+ openai_message_dict=packed_summary_message,
994
+ )
995
+ ]
996
+ )
997
+
998
+ # reset alert
999
+ self.agent_alerted_about_memory_pressure = False
1000
+
1001
+ printd(f"Ran summarizer, messages length {prior_len} -> {len(self.messages)}")
1002
+
1003
+ def heartbeat_is_paused(self):
1004
+ """Check if there's a requested pause on timed heartbeats"""
1005
+
1006
+ # Check if the pause has been initiated
1007
+ if self.pause_heartbeats_start is None:
1008
+ return False
1009
+
1010
+ # Check if it's been more than pause_heartbeats_minutes since pause_heartbeats_start
1011
+ elapsed_time = get_utc_time() - self.pause_heartbeats_start
1012
+ return elapsed_time.total_seconds() < self.pause_heartbeats_minutes * 60
1013
+
1014
+ def _swap_system_message_in_buffer(self, new_system_message: str):
1015
+ """Update the system message (NOT prompt) of the Agent (requires updating the internal buffer)"""
1016
+ assert isinstance(new_system_message, str)
1017
+ new_system_message_obj = Message.dict_to_message(
1018
+ agent_id=self.agent_state.id,
1019
+ user_id=self.agent_state.user_id,
1020
+ model=self.model,
1021
+ openai_message_dict={"role": "system", "content": new_system_message},
1022
+ )
1023
+
1024
+ assert new_system_message_obj.role == "system", new_system_message_obj
1025
+ assert self._messages[0].role == "system", self._messages
1026
+
1027
+ self.persistence_manager.swap_system_message(new_system_message_obj)
1028
+
1029
+ new_messages = [new_system_message_obj] + self._messages[1:] # swap index 0 (system)
1030
+ self._messages = new_messages
1031
+
1032
+ def rebuild_memory(self, force=False, update_timestamp=True, ms: Optional[MetadataStore] = None):
1033
+ """Rebuilds the system message with the latest memory object and any shared memory block updates"""
1034
+ curr_system_message = self.messages[0] # this is the system + memory bank, not just the system prompt
1035
+
1036
+ # NOTE: This is a hacky way to check if the memory has changed
1037
+ memory_repr = self.memory.compile()
1038
+ if not force and memory_repr == curr_system_message["content"][-(len(memory_repr)) :]:
1039
+ printd(f"Memory has not changed, not rebuilding system")
1040
+ return
1041
+
1042
+ if ms:
1043
+ for block in self.memory.to_dict()["memory"].values():
1044
+ if block.get("templates", False):
1045
+ # we don't expect to update shared memory blocks that
1046
+ # are templates. this is something we could update in the
1047
+ # future if we expect templates to change often.
1048
+ continue
1049
+ block_id = block.get("id")
1050
+ db_block = ms.get_block(block_id=block_id)
1051
+ if db_block is None:
1052
+ # this case covers if someone has deleted a shared block by interacting
1053
+ # with some other agent.
1054
+ # in that case we should remove this shared block from the agent currently being
1055
+ # evaluated.
1056
+ printd(f"removing block: {block_id=}")
1057
+ continue
1058
+ if not isinstance(db_block.value, str):
1059
+ printd(f"skipping block update, unexpected value: {block_id=}")
1060
+ continue
1061
+ # TODO: we may want to update which columns we're updating from shared memory e.g. the limit
1062
+ self.memory.update_block_value(name=block.get("label", ""), value=db_block.value)
1063
+
1064
+ # If the memory didn't update, we probably don't want to update the timestamp inside
1065
+ # For example, if we're doing a system prompt swap, this should probably be False
1066
+ if update_timestamp:
1067
+ memory_edit_timestamp = get_utc_time()
1068
+ else:
1069
+ # NOTE: a bit of a hack - we pull the timestamp from the message created_by
1070
+ memory_edit_timestamp = self._messages[0].created_at
1071
+
1072
+ # update memory (TODO: potentially update recall/archival stats seperately)
1073
+ new_system_message_str = compile_system_message(
1074
+ system_prompt=self.system,
1075
+ in_context_memory=self.memory,
1076
+ in_context_memory_last_edit=memory_edit_timestamp,
1077
+ archival_memory=self.persistence_manager.archival_memory,
1078
+ recall_memory=self.persistence_manager.recall_memory,
1079
+ user_defined_variables=None,
1080
+ append_icm_if_missing=True,
1081
+ )
1082
+ new_system_message = {
1083
+ "role": "system",
1084
+ "content": new_system_message_str,
1085
+ }
1086
+
1087
+ diff = united_diff(curr_system_message["content"], new_system_message["content"])
1088
+ if len(diff) > 0: # there was a diff
1089
+ printd(f"Rebuilding system with new memory...\nDiff:\n{diff}")
1090
+
1091
+ # Swap the system message out (only if there is a diff)
1092
+ self._swap_system_message_in_buffer(new_system_message=new_system_message_str)
1093
+ assert self.messages[0]["content"] == new_system_message["content"], (
1094
+ self.messages[0]["content"],
1095
+ new_system_message["content"],
1096
+ )
1097
+
1098
+ def update_system_prompt(self, new_system_prompt: str):
1099
+ """Update the system prompt of the agent (requires rebuilding the memory block if there's a difference)"""
1100
+ assert isinstance(new_system_prompt, str)
1101
+
1102
+ if new_system_prompt == self.system:
1103
+ input("same???")
1104
+ return
1105
+
1106
+ self.system = new_system_prompt
1107
+
1108
+ # updating the system prompt requires rebuilding the memory block inside the compiled system message
1109
+ self.rebuild_memory(force=True, update_timestamp=False)
1110
+
1111
+ # make sure to persist the change
1112
+ _ = self.update_state()
1113
+
1114
+ def add_function(self, function_name: str) -> str:
1115
+ # TODO: refactor
1116
+ raise NotImplementedError
1117
+ # if function_name in self.functions_python.keys():
1118
+ # msg = f"Function {function_name} already loaded"
1119
+ # printd(msg)
1120
+ # return msg
1121
+
1122
+ # available_functions = load_all_function_sets()
1123
+ # if function_name not in available_functions.keys():
1124
+ # raise ValueError(f"Function {function_name} not found in function library")
1125
+
1126
+ # self.functions.append(available_functions[function_name]["json_schema"])
1127
+ # self.functions_python[function_name] = available_functions[function_name]["python_function"]
1128
+
1129
+ # msg = f"Added function {function_name}"
1130
+ ## self.save()
1131
+ # self.update_state()
1132
+ # printd(msg)
1133
+ # return msg
1134
+
1135
+ def remove_function(self, function_name: str) -> str:
1136
+ # TODO: refactor
1137
+ raise NotImplementedError
1138
+ # if function_name not in self.functions_python.keys():
1139
+ # msg = f"Function {function_name} not loaded, ignoring"
1140
+ # printd(msg)
1141
+ # return msg
1142
+
1143
+ ## only allow removal of user defined functions
1144
+ # user_func_path = Path(USER_FUNCTIONS_DIR)
1145
+ # func_path = Path(inspect.getfile(self.functions_python[function_name]))
1146
+ # is_subpath = func_path.resolve().parts[: len(user_func_path.resolve().parts)] == user_func_path.resolve().parts
1147
+
1148
+ # if not is_subpath:
1149
+ # raise ValueError(f"Function {function_name} is not user defined and cannot be removed")
1150
+
1151
+ # self.functions = [f_schema for f_schema in self.functions if f_schema["name"] != function_name]
1152
+ # self.functions_python.pop(function_name)
1153
+
1154
+ # msg = f"Removed function {function_name}"
1155
+ ## self.save()
1156
+ # self.update_state()
1157
+ # printd(msg)
1158
+ # return msg
1159
+
1160
+ def update_state(self) -> AgentState:
1161
+ message_ids = [msg.id for msg in self._messages]
1162
+ assert isinstance(self.memory, Memory), f"Memory is not a Memory object: {type(self.memory)}"
1163
+
1164
+ # override any fields that may have been updated
1165
+ self.agent_state.message_ids = message_ids
1166
+ self.agent_state.memory = self.memory
1167
+ self.agent_state.system = self.system
1168
+
1169
+ return self.agent_state
1170
+
1171
+ def migrate_embedding(self, embedding_config: EmbeddingConfig):
1172
+ """Migrate the agent to a new embedding"""
1173
+ # TODO: archival memory
1174
+
1175
+ # TODO: recall memory
1176
+ raise NotImplementedError()
1177
+
1178
+ def attach_source(self, source_id: str, source_connector: StorageConnector, ms: MetadataStore):
1179
+ """Attach data with name `source_name` to the agent from source_connector."""
1180
+ # TODO: eventually, adding a data source should just give access to the retriever the source table, rather than modifying archival memory
1181
+
1182
+ filters = {"user_id": self.agent_state.user_id, "source_id": source_id}
1183
+ size = source_connector.size(filters)
1184
+ page_size = 100
1185
+ generator = source_connector.get_all_paginated(filters=filters, page_size=page_size) # yields List[Passage]
1186
+ all_passages = []
1187
+ for i in tqdm(range(0, size, page_size)):
1188
+ passages = next(generator)
1189
+
1190
+ # need to associated passage with agent (for filtering)
1191
+ for passage in passages:
1192
+ assert isinstance(passage, Passage), f"Generate yielded bad non-Passage type: {type(passage)}"
1193
+ passage.agent_id = self.agent_state.id
1194
+
1195
+ # regenerate passage ID (avoid duplicates)
1196
+ # TODO: need to find another solution to the text duplication issue
1197
+ # passage.id = create_uuid_from_string(f"{source_id}_{str(passage.agent_id)}_{passage.text}")
1198
+
1199
+ # insert into agent archival memory
1200
+ self.persistence_manager.archival_memory.storage.insert_many(passages)
1201
+ all_passages += passages
1202
+
1203
+ assert size == len(all_passages), f"Expected {size} passages, but only got {len(all_passages)}"
1204
+
1205
+ # save destination storage
1206
+ self.persistence_manager.archival_memory.storage.save()
1207
+
1208
+ # attach to agent
1209
+ source = ms.get_source(source_id=source_id)
1210
+ assert source is not None, f"Source {source_id} not found in metadata store"
1211
+ ms.attach_source(agent_id=self.agent_state.id, source_id=source_id, user_id=self.agent_state.user_id)
1212
+
1213
+ total_agent_passages = self.persistence_manager.archival_memory.storage.size()
1214
+
1215
+ printd(
1216
+ f"Attached data source {source.name} to agent {self.agent_state.name}, consisting of {len(all_passages)}. Agent now has {total_agent_passages} embeddings in archival memory.",
1217
+ )
1218
+
1219
+ def update_message(self, request: UpdateMessage) -> Message:
1220
+ """Update the details of a message associated with an agent"""
1221
+
1222
+ message = self.persistence_manager.recall_memory.storage.get(id=request.id)
1223
+ if message is None:
1224
+ raise ValueError(f"Message with id {request.id} not found")
1225
+ assert isinstance(message, Message), f"Message is not a Message object: {type(message)}"
1226
+
1227
+ # Override fields
1228
+ # NOTE: we try to do some sanity checking here (see asserts), but it's not foolproof
1229
+ if request.role:
1230
+ message.role = request.role
1231
+ if request.text:
1232
+ message.text = request.text
1233
+ if request.name:
1234
+ message.name = request.name
1235
+ if request.tool_calls:
1236
+ assert message.role == MessageRole.assistant, "Tool calls can only be added to assistant messages"
1237
+ message.tool_calls = request.tool_calls
1238
+ if request.tool_call_id:
1239
+ assert message.role == MessageRole.tool, "tool_call_id can only be added to tool messages"
1240
+ message.tool_call_id = request.tool_call_id
1241
+
1242
+ # Save the updated message
1243
+ self.persistence_manager.recall_memory.storage.update(record=message)
1244
+
1245
+ # Return the updated message
1246
+ updated_message = self.persistence_manager.recall_memory.storage.get(id=message.id)
1247
+ if updated_message is None:
1248
+ raise ValueError(f"Error persisting message - message with id {request.id} not found")
1249
+ return updated_message
1250
+
1251
+ # TODO(sarah): should we be creating a new message here, or just editing a message?
1252
+ def rethink_message(self, new_thought: str) -> Message:
1253
+ """Rethink / update the last message"""
1254
+ for x in range(len(self.messages) - 1, 0, -1):
1255
+ msg_obj = self._messages[x]
1256
+ if msg_obj.role == MessageRole.assistant:
1257
+ updated_message = self.update_message(
1258
+ request=UpdateMessage(
1259
+ id=msg_obj.id,
1260
+ text=new_thought,
1261
+ )
1262
+ )
1263
+ self.refresh_message_buffer()
1264
+ return updated_message
1265
+ raise ValueError(f"No assistant message found to update")
1266
+
1267
+ # TODO(sarah): should we be creating a new message here, or just editing a message?
1268
+ def rewrite_message(self, new_text: str) -> Message:
1269
+ """Rewrite / update the send_message text on the last message"""
1270
+
1271
+ # Walk backwards through the messages until we find an assistant message
1272
+ for x in range(len(self._messages) - 1, 0, -1):
1273
+ if self._messages[x].role == MessageRole.assistant:
1274
+ # Get the current message content
1275
+ message_obj = self._messages[x]
1276
+
1277
+ # The rewrite target is the output of send_message
1278
+ if message_obj.tool_calls is not None and len(message_obj.tool_calls) > 0:
1279
+
1280
+ # Check that we hit an assistant send_message call
1281
+ name_string = message_obj.tool_calls[0].function.name
1282
+ if name_string is None or name_string != "send_message":
1283
+ raise ValueError("Assistant missing send_message function call")
1284
+
1285
+ args_string = message_obj.tool_calls[0].function.arguments
1286
+ if args_string is None:
1287
+ raise ValueError("Assistant missing send_message function arguments")
1288
+
1289
+ args_json = json_loads(args_string)
1290
+ if "message" not in args_json:
1291
+ raise ValueError("Assistant missing send_message message argument")
1292
+
1293
+ # Once we found our target, rewrite it
1294
+ args_json["message"] = new_text
1295
+ new_args_string = json_dumps(args_json)
1296
+ message_obj.tool_calls[0].function.arguments = new_args_string
1297
+
1298
+ # Write the update to the DB
1299
+ updated_message = self.update_message(
1300
+ request=UpdateMessage(
1301
+ id=message_obj.id,
1302
+ tool_calls=message_obj.tool_calls,
1303
+ )
1304
+ )
1305
+ self.refresh_message_buffer()
1306
+ return updated_message
1307
+
1308
+ raise ValueError("No assistant message found to update")
1309
+
1310
+ def pop_message(self, count: int = 1) -> List[Message]:
1311
+ """Pop the last N messages from the agent's memory"""
1312
+ n_messages = len(self._messages)
1313
+ popped_messages = []
1314
+ MIN_MESSAGES = 2
1315
+ if n_messages <= MIN_MESSAGES:
1316
+ raise ValueError(f"Agent only has {n_messages} messages in stack, none left to pop")
1317
+ elif n_messages - count < MIN_MESSAGES:
1318
+ raise ValueError(f"Agent only has {n_messages} messages in stack, cannot pop more than {n_messages - MIN_MESSAGES}")
1319
+ else:
1320
+ # print(f"Popping last {count} messages from stack")
1321
+ for _ in range(min(count, len(self._messages))):
1322
+ # remove the message from the internal state of the agent
1323
+ deleted_message = self._messages.pop()
1324
+ # then also remove it from recall storage
1325
+ try:
1326
+ self.persistence_manager.recall_memory.storage.delete(filters={"id": deleted_message.id})
1327
+ popped_messages.append(deleted_message)
1328
+ except Exception as e:
1329
+ warnings.warn(f"Error deleting message {deleted_message.id} from recall memory: {e}")
1330
+ self._messages.append(deleted_message)
1331
+ break
1332
+
1333
+ return popped_messages
1334
+
1335
+ def pop_until_user(self) -> List[Message]:
1336
+ """Pop all messages until the last user message"""
1337
+ if MessageRole.user not in [msg.role for msg in self._messages]:
1338
+ raise ValueError("No user message found in buffer")
1339
+
1340
+ popped_messages = []
1341
+ while len(self._messages) > 0:
1342
+ if self._messages[-1].role == MessageRole.user:
1343
+ # we want to pop up to the last user message
1344
+ return popped_messages
1345
+ else:
1346
+ popped_messages.append(self.pop_message(count=1))
1347
+
1348
+ raise ValueError("No user message found in buffer")
1349
+
1350
+ def retry_message(self) -> List[Message]:
1351
+ """Retry / regenerate the last message"""
1352
+
1353
+ self.pop_until_user()
1354
+ user_message = self.pop_message(count=1)[0]
1355
+ step_response = self.step(user_message=user_message.text, return_dicts=False)
1356
+ messages = step_response.messages
1357
+
1358
+ assert messages is not None
1359
+ assert all(isinstance(msg, Message) for msg in messages), "step() returned non-Message objects"
1360
+ return messages
1361
+
1362
+
1363
+ def save_agent(agent: Agent, ms: MetadataStore):
1364
+ """Save agent to metadata store"""
1365
+
1366
+ agent.update_state()
1367
+ agent_state = agent.agent_state
1368
+ agent_id = agent_state.id
1369
+ assert isinstance(agent_state.memory, Memory), f"Memory is not a Memory object: {type(agent_state.memory)}"
1370
+
1371
+ # NOTE: we're saving agent memory before persisting the agent to ensure
1372
+ # that allocated block_ids for each memory block are present in the agent model
1373
+ save_agent_memory(agent=agent, ms=ms)
1374
+
1375
+ if ms.get_agent(agent_id=agent.agent_state.id):
1376
+ ms.update_agent(agent_state)
1377
+ else:
1378
+ ms.create_agent(agent_state)
1379
+
1380
+ agent.agent_state = ms.get_agent(agent_id=agent_id)
1381
+ assert isinstance(agent.agent_state.memory, Memory), f"Memory is not a Memory object: {type(agent_state.memory)}"
1382
+
1383
+
1384
+ def save_agent_memory(agent: Agent, ms: MetadataStore):
1385
+ """
1386
+ Save agent memory to metadata store. Memory is a collection of blocks and each block is persisted to the block table.
1387
+
1388
+ NOTE: we are assuming agent.update_state has already been called.
1389
+ """
1390
+
1391
+ for block_dict in agent.memory.to_dict()["memory"].values():
1392
+ # TODO: block creation should happen in one place to enforce these sort of constraints consistently.
1393
+ if block_dict.get("user_id", None) is None:
1394
+ block_dict["user_id"] = agent.agent_state.user_id
1395
+ block = Block(**block_dict)
1396
+ # FIXME: should we expect for block values to be None? If not, we need to figure out why that is
1397
+ # the case in some tests, if so we should relax the DB constraint.
1398
+ if block.value is None:
1399
+ block.value = ""
1400
+ ms.update_or_create_block(block)
1401
+
1402
+
1403
+ def strip_name_field_from_user_message(user_message_text: str) -> Tuple[str, Optional[str]]:
1404
+ """If 'name' exists in the JSON string, remove it and return the cleaned text + name value"""
1405
+ try:
1406
+ user_message_json = dict(json_loads(user_message_text))
1407
+ # Special handling for AutoGen messages with 'name' field
1408
+ # Treat 'name' as a special field
1409
+ # If it exists in the input message, elevate it to the 'message' level
1410
+ name = user_message_json.pop("name", None)
1411
+ clean_message = json_dumps(user_message_json)
1412
+ return clean_message, name
1413
+
1414
+ except Exception as e:
1415
+ print(f"{CLI_WARNING_PREFIX}handling of 'name' field failed with: {e}")
1416
+ raise e
1417
+
1418
+
1419
+ def validate_json(user_message_text: str) -> str:
1420
+ """Make sure that the user input message is valid JSON"""
1421
+ try:
1422
+ user_message_json = dict(json_loads(user_message_text))
1423
+ user_message_json_val = json_dumps(user_message_json)
1424
+ return user_message_json_val
1425
+ except Exception as e:
1426
+ print(f"{CLI_WARNING_PREFIX}couldn't parse user input message as JSON: {e}")
1427
+ raise e