letta-nightly 0.6.6.dev20241220190343__py3-none-any.whl → 0.6.6.dev20241221104005__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/agent.py +97 -665
- letta/chat_only_agent.py +2 -2
- letta/client/client.py +15 -8
- letta/client/streaming.py +2 -2
- letta/constants.py +2 -0
- letta/main.py +0 -76
- letta/offline_memory_agent.py +1 -2
- letta/orm/source.py +10 -3
- letta/orm/sources_agents.py +2 -2
- letta/providers.py +20 -3
- letta/schemas/agent.py +1 -0
- letta/schemas/embedding_config.py +1 -0
- letta/schemas/llm_config.py +1 -0
- letta/schemas/usage.py +2 -1
- letta/server/rest_api/interface.py +2 -2
- letta/server/rest_api/routers/v1/agents.py +9 -8
- letta/server/rest_api/routers/v1/sources.py +4 -7
- letta/server/rest_api/routers/v1/tools.py +2 -0
- letta/server/rest_api/utils.py +1 -1
- letta/server/server.py +19 -265
- letta/services/agent_manager.py +152 -2
- letta/services/helpers/agent_manager_helper.py +172 -2
- letta/services/message_manager.py +15 -0
- letta/settings.py +0 -3
- letta/utils.py +2 -1
- {letta_nightly-0.6.6.dev20241220190343.dist-info → letta_nightly-0.6.6.dev20241221104005.dist-info}/METADATA +1 -1
- {letta_nightly-0.6.6.dev20241220190343.dist-info → letta_nightly-0.6.6.dev20241221104005.dist-info}/RECORD +30 -30
- {letta_nightly-0.6.6.dev20241220190343.dist-info → letta_nightly-0.6.6.dev20241221104005.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.6.dev20241220190343.dist-info → letta_nightly-0.6.6.dev20241221104005.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.6.dev20241220190343.dist-info → letta_nightly-0.6.6.dev20241221104005.dist-info}/entry_points.txt +0 -0
letta/agent.py
CHANGED
|
@@ -1,25 +1,23 @@
|
|
|
1
|
-
import datetime
|
|
2
1
|
import inspect
|
|
3
2
|
import json
|
|
4
3
|
import time
|
|
5
4
|
import traceback
|
|
6
5
|
import warnings
|
|
7
6
|
from abc import ABC, abstractmethod
|
|
8
|
-
from typing import List,
|
|
7
|
+
from typing import List, Optional, Tuple, Union
|
|
9
8
|
|
|
10
9
|
from letta.constants import (
|
|
11
10
|
BASE_TOOLS,
|
|
12
11
|
CLI_WARNING_PREFIX,
|
|
12
|
+
ERROR_MESSAGE_PREFIX,
|
|
13
13
|
FIRST_MESSAGE_ATTEMPTS,
|
|
14
14
|
FUNC_FAILED_HEARTBEAT_MESSAGE,
|
|
15
|
-
IN_CONTEXT_MEMORY_KEYWORD,
|
|
16
15
|
LLM_MAX_TOKENS,
|
|
17
16
|
MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST,
|
|
18
17
|
MESSAGE_SUMMARY_TRUNC_TOKEN_FRAC,
|
|
19
18
|
MESSAGE_SUMMARY_WARNING_FRAC,
|
|
20
19
|
O1_BASE_TOOLS,
|
|
21
20
|
REQ_HEARTBEAT_MESSAGE,
|
|
22
|
-
STRUCTURED_OUTPUT_MODELS,
|
|
23
21
|
)
|
|
24
22
|
from letta.errors import ContextWindowExceededError
|
|
25
23
|
from letta.helpers import ToolRulesSolver
|
|
@@ -34,7 +32,7 @@ from letta.schemas.block import BlockUpdate
|
|
|
34
32
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
35
33
|
from letta.schemas.enums import MessageRole
|
|
36
34
|
from letta.schemas.memory import ContextWindowOverview, Memory
|
|
37
|
-
from letta.schemas.message import Message
|
|
35
|
+
from letta.schemas.message import Message
|
|
38
36
|
from letta.schemas.openai.chat_completion_request import (
|
|
39
37
|
Tool as ChatCompletionRequestTool,
|
|
40
38
|
)
|
|
@@ -46,18 +44,18 @@ from letta.schemas.openai.chat_completion_response import UsageStatistics
|
|
|
46
44
|
from letta.schemas.tool import Tool
|
|
47
45
|
from letta.schemas.tool_rule import TerminalToolRule
|
|
48
46
|
from letta.schemas.usage import LettaUsageStatistics
|
|
49
|
-
from letta.schemas.user import User as PydanticUser
|
|
50
47
|
from letta.services.agent_manager import AgentManager
|
|
51
48
|
from letta.services.block_manager import BlockManager
|
|
49
|
+
from letta.services.helpers.agent_manager_helper import (
|
|
50
|
+
check_supports_structured_output,
|
|
51
|
+
compile_memory_metadata_block,
|
|
52
|
+
)
|
|
52
53
|
from letta.services.message_manager import MessageManager
|
|
53
54
|
from letta.services.passage_manager import PassageManager
|
|
54
|
-
from letta.services.source_manager import SourceManager
|
|
55
55
|
from letta.services.tool_execution_sandbox import ToolExecutionSandbox
|
|
56
56
|
from letta.streaming_interface import StreamingRefreshCLIInterface
|
|
57
57
|
from letta.system import (
|
|
58
58
|
get_heartbeat,
|
|
59
|
-
get_initial_boot_messages,
|
|
60
|
-
get_login_event,
|
|
61
59
|
get_token_limit_warning,
|
|
62
60
|
package_function_response,
|
|
63
61
|
package_summarize_message,
|
|
@@ -66,166 +64,20 @@ from letta.system import (
|
|
|
66
64
|
from letta.utils import (
|
|
67
65
|
count_tokens,
|
|
68
66
|
get_friendly_error_msg,
|
|
69
|
-
get_local_time,
|
|
70
67
|
get_tool_call_id,
|
|
71
68
|
get_utc_time,
|
|
72
|
-
is_utc_datetime,
|
|
73
69
|
json_dumps,
|
|
74
70
|
json_loads,
|
|
75
71
|
parse_json,
|
|
76
72
|
printd,
|
|
77
|
-
united_diff,
|
|
78
73
|
validate_function_response,
|
|
79
|
-
verify_first_message_correctness,
|
|
80
74
|
)
|
|
81
75
|
|
|
82
76
|
|
|
83
|
-
def compile_memory_metadata_block(
|
|
84
|
-
actor: PydanticUser,
|
|
85
|
-
agent_id: str,
|
|
86
|
-
memory_edit_timestamp: datetime.datetime,
|
|
87
|
-
agent_manager: Optional[AgentManager] = None,
|
|
88
|
-
message_manager: Optional[MessageManager] = None,
|
|
89
|
-
) -> str:
|
|
90
|
-
# Put the timestamp in the local timezone (mimicking get_local_time())
|
|
91
|
-
timestamp_str = memory_edit_timestamp.astimezone().strftime("%Y-%m-%d %I:%M:%S %p %Z%z").strip()
|
|
92
|
-
|
|
93
|
-
# Create a metadata block of info so the agent knows about the metadata of out-of-context memories
|
|
94
|
-
memory_metadata_block = "\n".join(
|
|
95
|
-
[
|
|
96
|
-
f"### Memory [last modified: {timestamp_str}]",
|
|
97
|
-
f"{message_manager.size(actor=actor, agent_id=agent_id) if message_manager else 0} previous messages between you and the user are stored in recall memory (use functions to access them)",
|
|
98
|
-
f"{agent_manager.passage_size(actor=actor, agent_id=agent_id) if agent_manager else 0} total memories you created are stored in archival memory (use functions to access them)",
|
|
99
|
-
"\nCore memory shown below (limited in size, additional information stored in archival / recall memory):",
|
|
100
|
-
]
|
|
101
|
-
)
|
|
102
|
-
return memory_metadata_block
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def compile_system_message(
|
|
106
|
-
system_prompt: str,
|
|
107
|
-
agent_id: str,
|
|
108
|
-
in_context_memory: Memory,
|
|
109
|
-
in_context_memory_last_edit: datetime.datetime, # TODO move this inside of BaseMemory?
|
|
110
|
-
actor: PydanticUser,
|
|
111
|
-
agent_manager: Optional[AgentManager] = None,
|
|
112
|
-
message_manager: Optional[MessageManager] = None,
|
|
113
|
-
user_defined_variables: Optional[dict] = None,
|
|
114
|
-
append_icm_if_missing: bool = True,
|
|
115
|
-
template_format: Literal["f-string", "mustache", "jinja2"] = "f-string",
|
|
116
|
-
) -> str:
|
|
117
|
-
"""Prepare the final/full system message that will be fed into the LLM API
|
|
118
|
-
|
|
119
|
-
The base system message may be templated, in which case we need to render the variables.
|
|
120
|
-
|
|
121
|
-
The following are reserved variables:
|
|
122
|
-
- CORE_MEMORY: the in-context memory of the LLM
|
|
123
|
-
"""
|
|
124
|
-
|
|
125
|
-
if user_defined_variables is not None:
|
|
126
|
-
# TODO eventually support the user defining their own variables to inject
|
|
127
|
-
raise NotImplementedError
|
|
128
|
-
else:
|
|
129
|
-
variables = {}
|
|
130
|
-
|
|
131
|
-
# Add the protected memory variable
|
|
132
|
-
if IN_CONTEXT_MEMORY_KEYWORD in variables:
|
|
133
|
-
raise ValueError(f"Found protected variable '{IN_CONTEXT_MEMORY_KEYWORD}' in user-defined vars: {str(user_defined_variables)}")
|
|
134
|
-
else:
|
|
135
|
-
# TODO should this all put into the memory.__repr__ function?
|
|
136
|
-
memory_metadata_string = compile_memory_metadata_block(
|
|
137
|
-
actor=actor,
|
|
138
|
-
agent_id=agent_id,
|
|
139
|
-
memory_edit_timestamp=in_context_memory_last_edit,
|
|
140
|
-
agent_manager=agent_manager,
|
|
141
|
-
message_manager=message_manager,
|
|
142
|
-
)
|
|
143
|
-
full_memory_string = memory_metadata_string + "\n" + in_context_memory.compile()
|
|
144
|
-
|
|
145
|
-
# Add to the variables list to inject
|
|
146
|
-
variables[IN_CONTEXT_MEMORY_KEYWORD] = full_memory_string
|
|
147
|
-
|
|
148
|
-
if template_format == "f-string":
|
|
149
|
-
|
|
150
|
-
# Catch the special case where the system prompt is unformatted
|
|
151
|
-
if append_icm_if_missing:
|
|
152
|
-
memory_variable_string = "{" + IN_CONTEXT_MEMORY_KEYWORD + "}"
|
|
153
|
-
if memory_variable_string not in system_prompt:
|
|
154
|
-
# In this case, append it to the end to make sure memory is still injected
|
|
155
|
-
# warnings.warn(f"{IN_CONTEXT_MEMORY_KEYWORD} variable was missing from system prompt, appending instead")
|
|
156
|
-
system_prompt += "\n" + memory_variable_string
|
|
157
|
-
|
|
158
|
-
# render the variables using the built-in templater
|
|
159
|
-
try:
|
|
160
|
-
formatted_prompt = system_prompt.format_map(variables)
|
|
161
|
-
except Exception as e:
|
|
162
|
-
raise ValueError(f"Failed to format system prompt - {str(e)}. System prompt value:\n{system_prompt}")
|
|
163
|
-
|
|
164
|
-
else:
|
|
165
|
-
# TODO support for mustache and jinja2
|
|
166
|
-
raise NotImplementedError(template_format)
|
|
167
|
-
|
|
168
|
-
return formatted_prompt
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def initialize_message_sequence(
|
|
172
|
-
model: str,
|
|
173
|
-
system: str,
|
|
174
|
-
agent_id: str,
|
|
175
|
-
memory: Memory,
|
|
176
|
-
actor: PydanticUser,
|
|
177
|
-
agent_manager: Optional[AgentManager] = None,
|
|
178
|
-
message_manager: Optional[MessageManager] = None,
|
|
179
|
-
memory_edit_timestamp: Optional[datetime.datetime] = None,
|
|
180
|
-
include_initial_boot_message: bool = True,
|
|
181
|
-
) -> List[dict]:
|
|
182
|
-
if memory_edit_timestamp is None:
|
|
183
|
-
memory_edit_timestamp = get_local_time()
|
|
184
|
-
|
|
185
|
-
# full_system_message = construct_system_with_memory(
|
|
186
|
-
# system, memory, memory_edit_timestamp, agent_manager=agent_manager, recall_memory=recall_memory
|
|
187
|
-
# )
|
|
188
|
-
full_system_message = compile_system_message(
|
|
189
|
-
agent_id=agent_id,
|
|
190
|
-
system_prompt=system,
|
|
191
|
-
in_context_memory=memory,
|
|
192
|
-
in_context_memory_last_edit=memory_edit_timestamp,
|
|
193
|
-
actor=actor,
|
|
194
|
-
agent_manager=agent_manager,
|
|
195
|
-
message_manager=message_manager,
|
|
196
|
-
user_defined_variables=None,
|
|
197
|
-
append_icm_if_missing=True,
|
|
198
|
-
)
|
|
199
|
-
first_user_message = get_login_event() # event letting Letta know the user just logged in
|
|
200
|
-
|
|
201
|
-
if include_initial_boot_message:
|
|
202
|
-
if model is not None and "gpt-3.5" in model:
|
|
203
|
-
initial_boot_messages = get_initial_boot_messages("startup_with_send_message_gpt35")
|
|
204
|
-
else:
|
|
205
|
-
initial_boot_messages = get_initial_boot_messages("startup_with_send_message")
|
|
206
|
-
messages = (
|
|
207
|
-
[
|
|
208
|
-
{"role": "system", "content": full_system_message},
|
|
209
|
-
]
|
|
210
|
-
+ initial_boot_messages
|
|
211
|
-
+ [
|
|
212
|
-
{"role": "user", "content": first_user_message},
|
|
213
|
-
]
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
else:
|
|
217
|
-
messages = [
|
|
218
|
-
{"role": "system", "content": full_system_message},
|
|
219
|
-
{"role": "user", "content": first_user_message},
|
|
220
|
-
]
|
|
221
|
-
|
|
222
|
-
return messages
|
|
223
|
-
|
|
224
|
-
|
|
225
77
|
class BaseAgent(ABC):
|
|
226
78
|
"""
|
|
227
79
|
Abstract class for all agents.
|
|
228
|
-
Only
|
|
80
|
+
Only one interface is required: step.
|
|
229
81
|
"""
|
|
230
82
|
|
|
231
83
|
@abstractmethod
|
|
@@ -238,10 +90,6 @@ class BaseAgent(ABC):
|
|
|
238
90
|
"""
|
|
239
91
|
raise NotImplementedError
|
|
240
92
|
|
|
241
|
-
@abstractmethod
|
|
242
|
-
def update_state(self) -> AgentState:
|
|
243
|
-
raise NotImplementedError
|
|
244
|
-
|
|
245
93
|
|
|
246
94
|
class Agent(BaseAgent):
|
|
247
95
|
def __init__(
|
|
@@ -250,9 +98,7 @@ class Agent(BaseAgent):
|
|
|
250
98
|
agent_state: AgentState, # in-memory representation of the agent state (read from multiple tables)
|
|
251
99
|
user: User,
|
|
252
100
|
# extras
|
|
253
|
-
messages_total: Optional[int] = None, # TODO remove?
|
|
254
101
|
first_message_verify_mono: bool = True, # TODO move to config?
|
|
255
|
-
initial_message_sequence: Optional[List[Message]] = None,
|
|
256
102
|
):
|
|
257
103
|
assert isinstance(agent_state.memory, Memory), f"Memory object is not of type Memory: {type(agent_state.memory)}"
|
|
258
104
|
# Hold a copy of the state that was used to init the agent
|
|
@@ -276,7 +122,7 @@ class Agent(BaseAgent):
|
|
|
276
122
|
|
|
277
123
|
# gpt-4, gpt-3.5-turbo, ...
|
|
278
124
|
self.model = self.agent_state.llm_config.model
|
|
279
|
-
self.
|
|
125
|
+
self.supports_structured_output = check_supports_structured_output(model=self.model, tool_rules=agent_state.tool_rules)
|
|
280
126
|
|
|
281
127
|
# state managers
|
|
282
128
|
self.block_manager = BlockManager()
|
|
@@ -304,99 +150,14 @@ class Agent(BaseAgent):
|
|
|
304
150
|
# When the summarizer is run, set this back to False (to reset)
|
|
305
151
|
self.agent_alerted_about_memory_pressure = False
|
|
306
152
|
|
|
307
|
-
self._messages: List[Message] = []
|
|
308
|
-
|
|
309
|
-
# Once the memory object is initialized, use it to "bake" the system message
|
|
310
|
-
if self.agent_state.message_ids is not None:
|
|
311
|
-
self.set_message_buffer(message_ids=self.agent_state.message_ids)
|
|
312
|
-
|
|
313
|
-
else:
|
|
314
|
-
printd(f"Agent.__init__ :: creating, state={agent_state.message_ids}")
|
|
315
|
-
assert self.agent_state.id is not None and self.agent_state.created_by_id is not None
|
|
316
|
-
|
|
317
|
-
# Generate a sequence of initial messages to put in the buffer
|
|
318
|
-
init_messages = initialize_message_sequence(
|
|
319
|
-
model=self.model,
|
|
320
|
-
system=self.agent_state.system,
|
|
321
|
-
agent_id=self.agent_state.id,
|
|
322
|
-
memory=self.agent_state.memory,
|
|
323
|
-
actor=self.user,
|
|
324
|
-
agent_manager=None,
|
|
325
|
-
message_manager=None,
|
|
326
|
-
memory_edit_timestamp=get_utc_time(),
|
|
327
|
-
include_initial_boot_message=True,
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
if initial_message_sequence is not None:
|
|
331
|
-
# We always need the system prompt up front
|
|
332
|
-
system_message_obj = Message.dict_to_message(
|
|
333
|
-
agent_id=self.agent_state.id,
|
|
334
|
-
user_id=self.agent_state.created_by_id,
|
|
335
|
-
model=self.model,
|
|
336
|
-
openai_message_dict=init_messages[0],
|
|
337
|
-
)
|
|
338
|
-
# Don't use anything else in the pregen sequence, instead use the provided sequence
|
|
339
|
-
init_messages = [system_message_obj] + initial_message_sequence
|
|
340
|
-
|
|
341
|
-
else:
|
|
342
|
-
# Basic "more human than human" initial message sequence
|
|
343
|
-
init_messages = initialize_message_sequence(
|
|
344
|
-
model=self.model,
|
|
345
|
-
system=self.agent_state.system,
|
|
346
|
-
memory=self.agent_state.memory,
|
|
347
|
-
agent_id=self.agent_state.id,
|
|
348
|
-
actor=self.user,
|
|
349
|
-
agent_manager=None,
|
|
350
|
-
message_manager=None,
|
|
351
|
-
memory_edit_timestamp=get_utc_time(),
|
|
352
|
-
include_initial_boot_message=True,
|
|
353
|
-
)
|
|
354
|
-
# Cast to Message objects
|
|
355
|
-
init_messages = [
|
|
356
|
-
Message.dict_to_message(
|
|
357
|
-
agent_id=self.agent_state.id, user_id=self.agent_state.created_by_id, model=self.model, openai_message_dict=msg
|
|
358
|
-
)
|
|
359
|
-
for msg in init_messages
|
|
360
|
-
]
|
|
361
|
-
|
|
362
|
-
# Cast the messages to actual Message objects to be synced to the DB
|
|
363
|
-
init_messages_objs = []
|
|
364
|
-
for msg in init_messages:
|
|
365
|
-
init_messages_objs.append(msg)
|
|
366
|
-
for msg in init_messages_objs:
|
|
367
|
-
assert isinstance(msg, Message), f"Message object is not of type Message: {type(msg)}"
|
|
368
|
-
assert all([isinstance(msg, Message) for msg in init_messages_objs]), (init_messages_objs, init_messages)
|
|
369
|
-
|
|
370
|
-
# Put the messages inside the message buffer
|
|
371
|
-
self.messages_total = 0
|
|
372
|
-
self._append_to_messages(added_messages=init_messages_objs)
|
|
373
|
-
self._validate_message_buffer_is_utc()
|
|
374
|
-
|
|
375
153
|
# Load last function response from message history
|
|
376
154
|
self.last_function_response = self.load_last_function_response()
|
|
377
155
|
|
|
378
|
-
# Keep track of the total number of messages throughout all time
|
|
379
|
-
self.messages_total = messages_total if messages_total is not None else (len(self._messages) - 1) # (-system)
|
|
380
|
-
self.messages_total_init = len(self._messages) - 1
|
|
381
|
-
printd(f"Agent initialized, self.messages_total={self.messages_total}")
|
|
382
|
-
|
|
383
|
-
# Create the agent in the DB
|
|
384
|
-
self.update_state()
|
|
385
|
-
|
|
386
|
-
def check_tool_rules(self):
|
|
387
|
-
if self.model not in STRUCTURED_OUTPUT_MODELS:
|
|
388
|
-
if len(self.tool_rules_solver.init_tool_rules) > 1:
|
|
389
|
-
raise ValueError(
|
|
390
|
-
"Multiple initial tools are not supported for non-structured models. Please use only one initial tool rule."
|
|
391
|
-
)
|
|
392
|
-
self.supports_structured_output = False
|
|
393
|
-
else:
|
|
394
|
-
self.supports_structured_output = True
|
|
395
|
-
|
|
396
156
|
def load_last_function_response(self):
|
|
397
157
|
"""Load the last function response from message history"""
|
|
398
|
-
|
|
399
|
-
|
|
158
|
+
in_context_messages = self.agent_manager.get_in_context_messages(agent_id=self.agent_state.id, actor=self.user)
|
|
159
|
+
for i in range(len(in_context_messages) - 1, -1, -1):
|
|
160
|
+
msg = in_context_messages[i]
|
|
400
161
|
if msg.role == MessageRole.tool and msg.text:
|
|
401
162
|
try:
|
|
402
163
|
response_json = json.loads(msg.text)
|
|
@@ -435,7 +196,7 @@ class Agent(BaseAgent):
|
|
|
435
196
|
# NOTE: don't do this since re-buildin the memory is handled at the start of the step
|
|
436
197
|
# rebuild memory - this records the last edited timestamp of the memory
|
|
437
198
|
# TODO: pass in update timestamp from block edit time
|
|
438
|
-
self.rebuild_system_prompt()
|
|
199
|
+
self.agent_state = self.agent_manager.rebuild_system_prompt(agent_id=self.agent_state.id, actor=self.user)
|
|
439
200
|
|
|
440
201
|
return True
|
|
441
202
|
return False
|
|
@@ -487,109 +248,6 @@ class Agent(BaseAgent):
|
|
|
487
248
|
|
|
488
249
|
return function_response
|
|
489
250
|
|
|
490
|
-
@property
|
|
491
|
-
def messages(self) -> List[dict]:
|
|
492
|
-
"""Getter method that converts the internal Message list into OpenAI-style dicts"""
|
|
493
|
-
return [msg.to_openai_dict() for msg in self._messages]
|
|
494
|
-
|
|
495
|
-
@messages.setter
|
|
496
|
-
def messages(self, value):
|
|
497
|
-
raise Exception("Modifying message list directly not allowed")
|
|
498
|
-
|
|
499
|
-
def _load_messages_from_recall(self, message_ids: List[str]) -> List[Message]:
|
|
500
|
-
"""Load a list of messages from recall storage"""
|
|
501
|
-
|
|
502
|
-
# Pull the message objects from the database
|
|
503
|
-
message_objs = []
|
|
504
|
-
for msg_id in message_ids:
|
|
505
|
-
msg_obj = self.message_manager.get_message_by_id(msg_id, actor=self.user)
|
|
506
|
-
if msg_obj:
|
|
507
|
-
if isinstance(msg_obj, Message):
|
|
508
|
-
message_objs.append(msg_obj)
|
|
509
|
-
else:
|
|
510
|
-
printd(f"Warning - message ID {msg_id} is not a Message object")
|
|
511
|
-
warnings.warn(f"Warning - message ID {msg_id} is not a Message object")
|
|
512
|
-
else:
|
|
513
|
-
printd(f"Warning - message ID {msg_id} not found in recall storage")
|
|
514
|
-
warnings.warn(f"Warning - message ID {msg_id} not found in recall storage")
|
|
515
|
-
|
|
516
|
-
return message_objs
|
|
517
|
-
|
|
518
|
-
def _validate_message_buffer_is_utc(self):
|
|
519
|
-
"""Iterate over the message buffer and force all messages to be UTC stamped"""
|
|
520
|
-
|
|
521
|
-
for m in self._messages:
|
|
522
|
-
# assert is_utc_datetime(m.created_at), f"created_at on message for agent {self.agent_state.name} isn't UTC:\n{vars(m)}"
|
|
523
|
-
# TODO eventually do casting via an edit_message function
|
|
524
|
-
if m.created_at:
|
|
525
|
-
if not is_utc_datetime(m.created_at):
|
|
526
|
-
printd(f"Warning - created_at on message for agent {self.agent_state.name} isn't UTC (text='{m.text}')")
|
|
527
|
-
m.created_at = m.created_at.replace(tzinfo=datetime.timezone.utc)
|
|
528
|
-
|
|
529
|
-
def set_message_buffer(self, message_ids: List[str], force_utc: bool = True):
|
|
530
|
-
"""Set the messages in the buffer to the message IDs list"""
|
|
531
|
-
|
|
532
|
-
message_objs = self._load_messages_from_recall(message_ids=message_ids)
|
|
533
|
-
|
|
534
|
-
# set the objects in the buffer
|
|
535
|
-
self._messages = message_objs
|
|
536
|
-
|
|
537
|
-
# bugfix for old agents that may not have had UTC specified in their timestamps
|
|
538
|
-
if force_utc:
|
|
539
|
-
self._validate_message_buffer_is_utc()
|
|
540
|
-
|
|
541
|
-
# also sync the message IDs attribute
|
|
542
|
-
self.agent_state.message_ids = message_ids
|
|
543
|
-
|
|
544
|
-
def refresh_message_buffer(self):
|
|
545
|
-
"""Refresh the message buffer from the database"""
|
|
546
|
-
|
|
547
|
-
messages_to_sync = self.agent_state.message_ids
|
|
548
|
-
assert messages_to_sync and all([isinstance(msg_id, str) for msg_id in messages_to_sync])
|
|
549
|
-
|
|
550
|
-
self.set_message_buffer(message_ids=messages_to_sync)
|
|
551
|
-
|
|
552
|
-
def _trim_messages(self, num):
|
|
553
|
-
"""Trim messages from the front, not including the system message"""
|
|
554
|
-
new_messages = [self._messages[0]] + self._messages[num:]
|
|
555
|
-
self._messages = new_messages
|
|
556
|
-
|
|
557
|
-
def _prepend_to_messages(self, added_messages: List[Message]):
|
|
558
|
-
"""Wrapper around self.messages.prepend to allow additional calls to a state/persistence manager"""
|
|
559
|
-
assert all([isinstance(msg, Message) for msg in added_messages])
|
|
560
|
-
self.message_manager.create_many_messages(added_messages, actor=self.user)
|
|
561
|
-
|
|
562
|
-
new_messages = [self._messages[0]] + added_messages + self._messages[1:] # prepend (no system)
|
|
563
|
-
self._messages = new_messages
|
|
564
|
-
self.messages_total += len(added_messages) # still should increment the message counter (summaries are additions too)
|
|
565
|
-
|
|
566
|
-
def _append_to_messages(self, added_messages: List[Message]):
|
|
567
|
-
"""Wrapper around self.messages.append to allow additional calls to a state/persistence manager"""
|
|
568
|
-
assert all([isinstance(msg, Message) for msg in added_messages])
|
|
569
|
-
self.message_manager.create_many_messages(added_messages, actor=self.user)
|
|
570
|
-
|
|
571
|
-
# strip extra metadata if it exists
|
|
572
|
-
# for msg in added_messages:
|
|
573
|
-
# msg.pop("api_response", None)
|
|
574
|
-
# msg.pop("api_args", None)
|
|
575
|
-
new_messages = self._messages + added_messages # append
|
|
576
|
-
|
|
577
|
-
self._messages = new_messages
|
|
578
|
-
self.messages_total += len(added_messages)
|
|
579
|
-
|
|
580
|
-
def append_to_messages(self, added_messages: List[dict]):
|
|
581
|
-
"""An external-facing message append, where dict-like messages are first converted to Message objects"""
|
|
582
|
-
added_messages_objs = [
|
|
583
|
-
Message.dict_to_message(
|
|
584
|
-
agent_id=self.agent_state.id,
|
|
585
|
-
user_id=self.agent_state.created_by_id,
|
|
586
|
-
model=self.model,
|
|
587
|
-
openai_message_dict=msg,
|
|
588
|
-
)
|
|
589
|
-
for msg in added_messages
|
|
590
|
-
]
|
|
591
|
-
self._append_to_messages(added_messages_objs)
|
|
592
|
-
|
|
593
251
|
def _get_ai_reply(
|
|
594
252
|
self,
|
|
595
253
|
message_sequence: List[Message],
|
|
@@ -839,7 +497,7 @@ class Agent(BaseAgent):
|
|
|
839
497
|
function_args.pop("self", None)
|
|
840
498
|
# error_msg = f"Error calling function {function_name} with args {function_args}: {str(e)}"
|
|
841
499
|
# Less detailed - don't provide full args, idea is that it should be in recent context so no need (just adds noise)
|
|
842
|
-
error_msg =
|
|
500
|
+
error_msg = get_friendly_error_msg(function_name=function_name, exception_name=type(e).__name__, exception_message=str(e))
|
|
843
501
|
error_msg_user = f"{error_msg}\n{traceback.format_exc()}"
|
|
844
502
|
printd(error_msg_user)
|
|
845
503
|
function_response = package_function_response(False, error_msg)
|
|
@@ -862,8 +520,29 @@ class Agent(BaseAgent):
|
|
|
862
520
|
self.interface.function_message(f"Error: {error_msg}", msg_obj=messages[-1])
|
|
863
521
|
return messages, False, True # force a heartbeat to allow agent to handle error
|
|
864
522
|
|
|
523
|
+
# Step 4: check if function response is an error
|
|
524
|
+
if function_response_string.startswith(ERROR_MESSAGE_PREFIX):
|
|
525
|
+
function_response = package_function_response(False, function_response_string)
|
|
526
|
+
# TODO: truncate error message somehow
|
|
527
|
+
messages.append(
|
|
528
|
+
Message.dict_to_message(
|
|
529
|
+
agent_id=self.agent_state.id,
|
|
530
|
+
user_id=self.agent_state.created_by_id,
|
|
531
|
+
model=self.model,
|
|
532
|
+
openai_message_dict={
|
|
533
|
+
"role": "tool",
|
|
534
|
+
"name": function_name,
|
|
535
|
+
"content": function_response,
|
|
536
|
+
"tool_call_id": tool_call_id,
|
|
537
|
+
},
|
|
538
|
+
)
|
|
539
|
+
) # extend conversation with function response
|
|
540
|
+
self.interface.function_message(f"Ran {function_name}({function_args})", msg_obj=messages[-1])
|
|
541
|
+
self.interface.function_message(f"Error: {function_response_string}", msg_obj=messages[-1])
|
|
542
|
+
return messages, False, True # force a heartbeat to allow agent to handle error
|
|
543
|
+
|
|
865
544
|
# If no failures happened along the way: ...
|
|
866
|
-
# Step
|
|
545
|
+
# Step 5: send the info on the function call and function response to GPT
|
|
867
546
|
messages.append(
|
|
868
547
|
Message.dict_to_message(
|
|
869
548
|
agent_id=self.agent_state.id,
|
|
@@ -898,7 +577,7 @@ class Agent(BaseAgent):
|
|
|
898
577
|
|
|
899
578
|
# rebuild memory
|
|
900
579
|
# TODO: @charles please check this
|
|
901
|
-
self.rebuild_system_prompt()
|
|
580
|
+
self.agent_state = self.agent_manager.rebuild_system_prompt(agent_id=self.agent_state.id, actor=self.user)
|
|
902
581
|
|
|
903
582
|
# Update ToolRulesSolver state with last called function
|
|
904
583
|
self.tool_rules_solver.update_tool_usage(function_name)
|
|
@@ -930,6 +609,7 @@ class Agent(BaseAgent):
|
|
|
930
609
|
messages=next_input_message,
|
|
931
610
|
**kwargs,
|
|
932
611
|
)
|
|
612
|
+
|
|
933
613
|
heartbeat_request = step_response.heartbeat_request
|
|
934
614
|
function_failed = step_response.function_failed
|
|
935
615
|
token_warning = step_response.in_context_memory_warning
|
|
@@ -1021,33 +701,19 @@ class Agent(BaseAgent):
|
|
|
1021
701
|
if not all(isinstance(m, Message) for m in messages):
|
|
1022
702
|
raise ValueError(f"messages should be a Message or a list of Message, got {type(messages)}")
|
|
1023
703
|
|
|
1024
|
-
|
|
704
|
+
in_context_messages = self.agent_manager.get_in_context_messages(agent_id=self.agent_state.id, actor=self.user)
|
|
705
|
+
input_message_sequence = in_context_messages + messages
|
|
1025
706
|
|
|
1026
707
|
if len(input_message_sequence) > 1 and input_message_sequence[-1].role != "user":
|
|
1027
708
|
printd(f"{CLI_WARNING_PREFIX}Attempting to run ChatCompletion without user as the last message in the queue")
|
|
1028
709
|
|
|
1029
710
|
# Step 2: send the conversation and available functions to the LLM
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
)
|
|
1037
|
-
if verify_first_message_correctness(response, require_monologue=self.first_message_verify_mono):
|
|
1038
|
-
break
|
|
1039
|
-
|
|
1040
|
-
counter += 1
|
|
1041
|
-
if counter > first_message_retry_limit:
|
|
1042
|
-
raise Exception(f"Hit first message retry limit ({first_message_retry_limit})")
|
|
1043
|
-
|
|
1044
|
-
else:
|
|
1045
|
-
response = self._get_ai_reply(
|
|
1046
|
-
message_sequence=input_message_sequence,
|
|
1047
|
-
first_message=first_message,
|
|
1048
|
-
stream=stream,
|
|
1049
|
-
step_count=step_count,
|
|
1050
|
-
)
|
|
711
|
+
response = self._get_ai_reply(
|
|
712
|
+
message_sequence=input_message_sequence,
|
|
713
|
+
first_message=first_message,
|
|
714
|
+
stream=stream,
|
|
715
|
+
step_count=step_count,
|
|
716
|
+
)
|
|
1051
717
|
|
|
1052
718
|
# Step 3: check if LLM wanted to call a function
|
|
1053
719
|
# (if yes) Step 4: call the function
|
|
@@ -1095,10 +761,9 @@ class Agent(BaseAgent):
|
|
|
1095
761
|
f"last response total_tokens ({current_total_tokens}) < {MESSAGE_SUMMARY_WARNING_FRAC * int(self.agent_state.llm_config.context_window)}"
|
|
1096
762
|
)
|
|
1097
763
|
|
|
1098
|
-
self.
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
self.update_state()
|
|
764
|
+
self.agent_state = self.agent_manager.append_to_in_context_messages(
|
|
765
|
+
all_new_messages, agent_id=self.agent_state.id, actor=self.user
|
|
766
|
+
)
|
|
1102
767
|
|
|
1103
768
|
return AgentStepResponse(
|
|
1104
769
|
messages=all_new_messages,
|
|
@@ -1113,7 +778,9 @@ class Agent(BaseAgent):
|
|
|
1113
778
|
|
|
1114
779
|
# If we got a context alert, try trimming the messages length, then try again
|
|
1115
780
|
if is_context_overflow_error(e):
|
|
1116
|
-
printd(
|
|
781
|
+
printd(
|
|
782
|
+
f"context window exceeded with limit {self.agent_state.llm_config.context_window}, running summarizer to trim messages"
|
|
783
|
+
)
|
|
1117
784
|
# A separate API call to run a summarizer
|
|
1118
785
|
self.summarize_messages_inplace()
|
|
1119
786
|
|
|
@@ -1165,15 +832,19 @@ class Agent(BaseAgent):
|
|
|
1165
832
|
return self.inner_step(messages=[user_message], **kwargs)
|
|
1166
833
|
|
|
1167
834
|
def summarize_messages_inplace(self, cutoff=None, preserve_last_N_messages=True, disallow_tool_as_first=True):
|
|
1168
|
-
|
|
835
|
+
in_context_messages = self.agent_manager.get_in_context_messages(agent_id=self.agent_state.id, actor=self.user)
|
|
836
|
+
in_context_messages_openai = [m.to_openai_dict() for m in in_context_messages]
|
|
837
|
+
|
|
838
|
+
if in_context_messages_openai[0]["role"] != "system":
|
|
839
|
+
raise RuntimeError(f"in_context_messages_openai[0] should be system (instead got {in_context_messages_openai[0]})")
|
|
1169
840
|
|
|
1170
841
|
# Start at index 1 (past the system message),
|
|
1171
842
|
# and collect messages for summarization until we reach the desired truncation token fraction (eg 50%)
|
|
1172
843
|
# Do not allow truncation of the last N messages, since these are needed for in-context examples of function calling
|
|
1173
|
-
token_counts = [count_tokens(str(msg)) for msg in
|
|
844
|
+
token_counts = [count_tokens(str(msg)) for msg in in_context_messages_openai]
|
|
1174
845
|
message_buffer_token_count = sum(token_counts[1:]) # no system message
|
|
1175
846
|
desired_token_count_to_summarize = int(message_buffer_token_count * MESSAGE_SUMMARY_TRUNC_TOKEN_FRAC)
|
|
1176
|
-
candidate_messages_to_summarize =
|
|
847
|
+
candidate_messages_to_summarize = in_context_messages_openai[1:]
|
|
1177
848
|
token_counts = token_counts[1:]
|
|
1178
849
|
|
|
1179
850
|
if preserve_last_N_messages:
|
|
@@ -1193,7 +864,7 @@ class Agent(BaseAgent):
|
|
|
1193
864
|
"Not enough messages to compress for summarization",
|
|
1194
865
|
details={
|
|
1195
866
|
"num_candidate_messages": len(candidate_messages_to_summarize),
|
|
1196
|
-
"num_total_messages": len(
|
|
867
|
+
"num_total_messages": len(in_context_messages_openai),
|
|
1197
868
|
"preserve_N": MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST,
|
|
1198
869
|
},
|
|
1199
870
|
)
|
|
@@ -1212,9 +883,9 @@ class Agent(BaseAgent):
|
|
|
1212
883
|
# Try to make an assistant message come after the cutoff
|
|
1213
884
|
try:
|
|
1214
885
|
printd(f"Selected cutoff {cutoff} was a 'user', shifting one...")
|
|
1215
|
-
if
|
|
886
|
+
if in_context_messages_openai[cutoff]["role"] == "user":
|
|
1216
887
|
new_cutoff = cutoff + 1
|
|
1217
|
-
if
|
|
888
|
+
if in_context_messages_openai[new_cutoff]["role"] == "user":
|
|
1218
889
|
printd(f"Shifted cutoff {new_cutoff} is still a 'user', ignoring...")
|
|
1219
890
|
cutoff = new_cutoff
|
|
1220
891
|
except IndexError:
|
|
@@ -1222,23 +893,23 @@ class Agent(BaseAgent):
|
|
|
1222
893
|
|
|
1223
894
|
# Make sure the cutoff isn't on a 'tool' or 'function'
|
|
1224
895
|
if disallow_tool_as_first:
|
|
1225
|
-
while
|
|
896
|
+
while in_context_messages_openai[cutoff]["role"] in ["tool", "function"] and cutoff < len(in_context_messages_openai):
|
|
1226
897
|
printd(f"Selected cutoff {cutoff} was a 'tool', shifting one...")
|
|
1227
898
|
cutoff += 1
|
|
1228
899
|
|
|
1229
|
-
message_sequence_to_summarize =
|
|
900
|
+
message_sequence_to_summarize = in_context_messages[1:cutoff] # do NOT get rid of the system message
|
|
1230
901
|
if len(message_sequence_to_summarize) <= 1:
|
|
1231
902
|
# This prevents a potential infinite loop of summarizing the same message over and over
|
|
1232
903
|
raise ContextWindowExceededError(
|
|
1233
904
|
"Not enough messages to compress for summarization after determining cutoff",
|
|
1234
905
|
details={
|
|
1235
906
|
"num_candidate_messages": len(message_sequence_to_summarize),
|
|
1236
|
-
"num_total_messages": len(
|
|
907
|
+
"num_total_messages": len(in_context_messages_openai),
|
|
1237
908
|
"preserve_N": MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST,
|
|
1238
909
|
},
|
|
1239
910
|
)
|
|
1240
911
|
else:
|
|
1241
|
-
printd(f"Attempting to summarize {len(message_sequence_to_summarize)} messages [1:{cutoff}] of {len(
|
|
912
|
+
printd(f"Attempting to summarize {len(message_sequence_to_summarize)} messages [1:{cutoff}] of {len(in_context_messages)}")
|
|
1242
913
|
|
|
1243
914
|
# We can't do summarize logic properly if context_window is undefined
|
|
1244
915
|
if self.agent_state.llm_config.context_window is None:
|
|
@@ -1253,118 +924,33 @@ class Agent(BaseAgent):
|
|
|
1253
924
|
printd(f"Got summary: {summary}")
|
|
1254
925
|
|
|
1255
926
|
# Metadata that's useful for the agent to see
|
|
1256
|
-
all_time_message_count = self.
|
|
1257
|
-
remaining_message_count = len(
|
|
927
|
+
all_time_message_count = self.message_manager.size(agent_id=self.agent_state.id, actor=self.user)
|
|
928
|
+
remaining_message_count = len(in_context_messages_openai[cutoff:])
|
|
1258
929
|
hidden_message_count = all_time_message_count - remaining_message_count
|
|
1259
930
|
summary_message_count = len(message_sequence_to_summarize)
|
|
1260
931
|
summary_message = package_summarize_message(summary, summary_message_count, hidden_message_count, all_time_message_count)
|
|
1261
932
|
printd(f"Packaged into message: {summary_message}")
|
|
1262
933
|
|
|
1263
|
-
prior_len = len(
|
|
1264
|
-
self.
|
|
934
|
+
prior_len = len(in_context_messages_openai)
|
|
935
|
+
self.agent_state = self.agent_manager.trim_older_in_context_messages(cutoff, agent_id=self.agent_state.id, actor=self.user)
|
|
1265
936
|
packed_summary_message = {"role": "user", "content": summary_message}
|
|
1266
|
-
self.
|
|
1267
|
-
[
|
|
937
|
+
self.agent_state = self.agent_manager.prepend_to_in_context_messages(
|
|
938
|
+
messages=[
|
|
1268
939
|
Message.dict_to_message(
|
|
1269
940
|
agent_id=self.agent_state.id,
|
|
1270
941
|
user_id=self.agent_state.created_by_id,
|
|
1271
942
|
model=self.model,
|
|
1272
943
|
openai_message_dict=packed_summary_message,
|
|
1273
944
|
)
|
|
1274
|
-
]
|
|
1275
|
-
)
|
|
1276
|
-
|
|
1277
|
-
# reset alert
|
|
1278
|
-
self.agent_alerted_about_memory_pressure = False
|
|
1279
|
-
|
|
1280
|
-
printd(f"Ran summarizer, messages length {prior_len} -> {len(self.messages)}")
|
|
1281
|
-
|
|
1282
|
-
def _swap_system_message_in_buffer(self, new_system_message: str):
|
|
1283
|
-
"""Update the system message (NOT prompt) of the Agent (requires updating the internal buffer)"""
|
|
1284
|
-
assert isinstance(new_system_message, str)
|
|
1285
|
-
new_system_message_obj = Message.dict_to_message(
|
|
945
|
+
],
|
|
1286
946
|
agent_id=self.agent_state.id,
|
|
1287
|
-
user_id=self.agent_state.created_by_id,
|
|
1288
|
-
model=self.model,
|
|
1289
|
-
openai_message_dict={"role": "system", "content": new_system_message},
|
|
1290
|
-
)
|
|
1291
|
-
|
|
1292
|
-
assert new_system_message_obj.role == "system", new_system_message_obj
|
|
1293
|
-
assert self._messages[0].role == "system", self._messages
|
|
1294
|
-
|
|
1295
|
-
self.message_manager.create_message(new_system_message_obj, actor=self.user)
|
|
1296
|
-
|
|
1297
|
-
new_messages = [new_system_message_obj] + self._messages[1:] # swap index 0 (system)
|
|
1298
|
-
self._messages = new_messages
|
|
1299
|
-
|
|
1300
|
-
def rebuild_system_prompt(self, force=False, update_timestamp=True):
|
|
1301
|
-
"""Rebuilds the system message with the latest memory object and any shared memory block updates
|
|
1302
|
-
|
|
1303
|
-
Updates to core memory blocks should trigger a "rebuild", which itself will create a new message object
|
|
1304
|
-
|
|
1305
|
-
Updates to the memory header should *not* trigger a rebuild, since that will simply flood recall storage with excess messages
|
|
1306
|
-
"""
|
|
1307
|
-
|
|
1308
|
-
curr_system_message = self.messages[0] # this is the system + memory bank, not just the system prompt
|
|
1309
|
-
|
|
1310
|
-
# note: we only update the system prompt if the core memory is changed
|
|
1311
|
-
# this means that the archival/recall memory statistics may be someout out of date
|
|
1312
|
-
curr_memory_str = self.agent_state.memory.compile()
|
|
1313
|
-
if curr_memory_str in curr_system_message["content"] and not force:
|
|
1314
|
-
# NOTE: could this cause issues if a block is removed? (substring match would still work)
|
|
1315
|
-
printd(f"Memory hasn't changed, skipping system prompt rebuild")
|
|
1316
|
-
return
|
|
1317
|
-
|
|
1318
|
-
# If the memory didn't update, we probably don't want to update the timestamp inside
|
|
1319
|
-
# For example, if we're doing a system prompt swap, this should probably be False
|
|
1320
|
-
if update_timestamp:
|
|
1321
|
-
memory_edit_timestamp = get_utc_time()
|
|
1322
|
-
else:
|
|
1323
|
-
# NOTE: a bit of a hack - we pull the timestamp from the message created_by
|
|
1324
|
-
memory_edit_timestamp = self._messages[0].created_at
|
|
1325
|
-
|
|
1326
|
-
# update memory (TODO: potentially update recall/archival stats separately)
|
|
1327
|
-
new_system_message_str = compile_system_message(
|
|
1328
|
-
agent_id=self.agent_state.id,
|
|
1329
|
-
system_prompt=self.agent_state.system,
|
|
1330
|
-
in_context_memory=self.agent_state.memory,
|
|
1331
|
-
in_context_memory_last_edit=memory_edit_timestamp,
|
|
1332
947
|
actor=self.user,
|
|
1333
|
-
agent_manager=self.agent_manager,
|
|
1334
|
-
message_manager=self.message_manager,
|
|
1335
|
-
user_defined_variables=None,
|
|
1336
|
-
append_icm_if_missing=True,
|
|
1337
948
|
)
|
|
1338
|
-
new_system_message = {
|
|
1339
|
-
"role": "system",
|
|
1340
|
-
"content": new_system_message_str,
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
diff = united_diff(curr_system_message["content"], new_system_message["content"])
|
|
1344
|
-
if len(diff) > 0: # there was a diff
|
|
1345
|
-
printd(f"Rebuilding system with new memory...\nDiff:\n{diff}")
|
|
1346
|
-
|
|
1347
|
-
# Swap the system message out (only if there is a diff)
|
|
1348
|
-
self._swap_system_message_in_buffer(new_system_message=new_system_message_str)
|
|
1349
|
-
assert self.messages[0]["content"] == new_system_message["content"], (
|
|
1350
|
-
self.messages[0]["content"],
|
|
1351
|
-
new_system_message["content"],
|
|
1352
|
-
)
|
|
1353
949
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
assert isinstance(new_system_prompt, str)
|
|
1357
|
-
|
|
1358
|
-
if new_system_prompt == self.agent_state.system:
|
|
1359
|
-
return
|
|
1360
|
-
|
|
1361
|
-
self.agent_state.system = new_system_prompt
|
|
1362
|
-
|
|
1363
|
-
# updating the system prompt requires rebuilding the memory block inside the compiled system message
|
|
1364
|
-
self.rebuild_system_prompt(force=True, update_timestamp=False)
|
|
950
|
+
# reset alert
|
|
951
|
+
self.agent_alerted_about_memory_pressure = False
|
|
1365
952
|
|
|
1366
|
-
|
|
1367
|
-
_ = self.update_state()
|
|
953
|
+
printd(f"Ran summarizer, messages length {prior_len} -> {len(in_context_messages_openai)}")
|
|
1368
954
|
|
|
1369
955
|
def add_function(self, function_name: str) -> str:
|
|
1370
956
|
# TODO: refactor
|
|
@@ -1374,20 +960,6 @@ class Agent(BaseAgent):
|
|
|
1374
960
|
# TODO: refactor
|
|
1375
961
|
raise NotImplementedError
|
|
1376
962
|
|
|
1377
|
-
def update_state(self) -> AgentState:
|
|
1378
|
-
# TODO: this should be removed and self._messages should be moved into self.agent_state.in_context_messages
|
|
1379
|
-
message_ids = [msg.id for msg in self._messages]
|
|
1380
|
-
|
|
1381
|
-
# Assert that these are all strings
|
|
1382
|
-
if any(not isinstance(m_id, str) for m_id in message_ids):
|
|
1383
|
-
warnings.warn(f"Non-string message IDs found in agent state: {message_ids}")
|
|
1384
|
-
message_ids = [m_id for m_id in message_ids if isinstance(m_id, str)]
|
|
1385
|
-
|
|
1386
|
-
# override any fields that may have been updated
|
|
1387
|
-
self.agent_state.message_ids = message_ids
|
|
1388
|
-
|
|
1389
|
-
return self.agent_state
|
|
1390
|
-
|
|
1391
963
|
def migrate_embedding(self, embedding_config: EmbeddingConfig):
|
|
1392
964
|
"""Migrate the agent to a new embedding"""
|
|
1393
965
|
# TODO: archival memory
|
|
@@ -1395,149 +967,6 @@ class Agent(BaseAgent):
|
|
|
1395
967
|
# TODO: recall memory
|
|
1396
968
|
raise NotImplementedError()
|
|
1397
969
|
|
|
1398
|
-
def attach_source(
|
|
1399
|
-
self,
|
|
1400
|
-
user: PydanticUser,
|
|
1401
|
-
source_id: str,
|
|
1402
|
-
source_manager: SourceManager,
|
|
1403
|
-
agent_manager: AgentManager,
|
|
1404
|
-
):
|
|
1405
|
-
"""Attach a source to the agent using the SourcesAgents ORM relationship.
|
|
1406
|
-
|
|
1407
|
-
Args:
|
|
1408
|
-
user: User performing the action
|
|
1409
|
-
source_id: ID of the source to attach
|
|
1410
|
-
source_manager: SourceManager instance to verify source exists
|
|
1411
|
-
agent_manager: AgentManager instance to manage agent-source relationship
|
|
1412
|
-
"""
|
|
1413
|
-
# Verify source exists and user has permission to access it
|
|
1414
|
-
source = source_manager.get_source_by_id(source_id=source_id, actor=user)
|
|
1415
|
-
assert source is not None, f"Source {source_id} not found in user's organization ({user.organization_id})"
|
|
1416
|
-
|
|
1417
|
-
# Use the agent_manager to create the relationship
|
|
1418
|
-
agent_manager.attach_source(agent_id=self.agent_state.id, source_id=source_id, actor=user)
|
|
1419
|
-
|
|
1420
|
-
printd(
|
|
1421
|
-
f"Attached data source {source.name} to agent {self.agent_state.name}.",
|
|
1422
|
-
)
|
|
1423
|
-
|
|
1424
|
-
def update_message(self, message_id: str, request: MessageUpdate) -> Message:
|
|
1425
|
-
"""Update the details of a message associated with an agent"""
|
|
1426
|
-
# Save the updated message
|
|
1427
|
-
updated_message = self.message_manager.update_message_by_id(message_id=message_id, message_update=request, actor=self.user)
|
|
1428
|
-
return updated_message
|
|
1429
|
-
|
|
1430
|
-
# TODO(sarah): should we be creating a new message here, or just editing a message?
|
|
1431
|
-
def rethink_message(self, new_thought: str) -> Message:
|
|
1432
|
-
"""Rethink / update the last message"""
|
|
1433
|
-
for x in range(len(self.messages) - 1, 0, -1):
|
|
1434
|
-
msg_obj = self._messages[x]
|
|
1435
|
-
if msg_obj.role == MessageRole.assistant:
|
|
1436
|
-
updated_message = self.update_message(
|
|
1437
|
-
message_id=msg_obj.id,
|
|
1438
|
-
request=MessageUpdate(
|
|
1439
|
-
text=new_thought,
|
|
1440
|
-
),
|
|
1441
|
-
)
|
|
1442
|
-
self.refresh_message_buffer()
|
|
1443
|
-
return updated_message
|
|
1444
|
-
raise ValueError(f"No assistant message found to update")
|
|
1445
|
-
|
|
1446
|
-
# TODO(sarah): should we be creating a new message here, or just editing a message?
|
|
1447
|
-
def rewrite_message(self, new_text: str) -> Message:
|
|
1448
|
-
"""Rewrite / update the send_message text on the last message"""
|
|
1449
|
-
|
|
1450
|
-
# Walk backwards through the messages until we find an assistant message
|
|
1451
|
-
for x in range(len(self._messages) - 1, 0, -1):
|
|
1452
|
-
if self._messages[x].role == MessageRole.assistant:
|
|
1453
|
-
# Get the current message content
|
|
1454
|
-
message_obj = self._messages[x]
|
|
1455
|
-
|
|
1456
|
-
# The rewrite target is the output of send_message
|
|
1457
|
-
if message_obj.tool_calls is not None and len(message_obj.tool_calls) > 0:
|
|
1458
|
-
|
|
1459
|
-
# Check that we hit an assistant send_message call
|
|
1460
|
-
name_string = message_obj.tool_calls[0].function.name
|
|
1461
|
-
if name_string is None or name_string != "send_message":
|
|
1462
|
-
raise ValueError("Assistant missing send_message function call")
|
|
1463
|
-
|
|
1464
|
-
args_string = message_obj.tool_calls[0].function.arguments
|
|
1465
|
-
if args_string is None:
|
|
1466
|
-
raise ValueError("Assistant missing send_message function arguments")
|
|
1467
|
-
|
|
1468
|
-
args_json = json_loads(args_string)
|
|
1469
|
-
if "message" not in args_json:
|
|
1470
|
-
raise ValueError("Assistant missing send_message message argument")
|
|
1471
|
-
|
|
1472
|
-
# Once we found our target, rewrite it
|
|
1473
|
-
args_json["message"] = new_text
|
|
1474
|
-
new_args_string = json_dumps(args_json)
|
|
1475
|
-
message_obj.tool_calls[0].function.arguments = new_args_string
|
|
1476
|
-
|
|
1477
|
-
# Write the update to the DB
|
|
1478
|
-
updated_message = self.update_message(
|
|
1479
|
-
message_id=message_obj.id,
|
|
1480
|
-
request=MessageUpdate(
|
|
1481
|
-
tool_calls=message_obj.tool_calls,
|
|
1482
|
-
),
|
|
1483
|
-
)
|
|
1484
|
-
self.refresh_message_buffer()
|
|
1485
|
-
return updated_message
|
|
1486
|
-
|
|
1487
|
-
raise ValueError("No assistant message found to update")
|
|
1488
|
-
|
|
1489
|
-
def pop_message(self, count: int = 1) -> List[Message]:
|
|
1490
|
-
"""Pop the last N messages from the agent's memory"""
|
|
1491
|
-
n_messages = len(self._messages)
|
|
1492
|
-
popped_messages = []
|
|
1493
|
-
MIN_MESSAGES = 2
|
|
1494
|
-
if n_messages <= MIN_MESSAGES:
|
|
1495
|
-
raise ValueError(f"Agent only has {n_messages} messages in stack, none left to pop")
|
|
1496
|
-
elif n_messages - count < MIN_MESSAGES:
|
|
1497
|
-
raise ValueError(f"Agent only has {n_messages} messages in stack, cannot pop more than {n_messages - MIN_MESSAGES}")
|
|
1498
|
-
else:
|
|
1499
|
-
# print(f"Popping last {count} messages from stack")
|
|
1500
|
-
for _ in range(min(count, len(self._messages))):
|
|
1501
|
-
# remove the message from the internal state of the agent
|
|
1502
|
-
deleted_message = self._messages.pop()
|
|
1503
|
-
# then also remove it from recall storage
|
|
1504
|
-
try:
|
|
1505
|
-
self.message_manager.delete_message_by_id(deleted_message.id, actor=self.user)
|
|
1506
|
-
popped_messages.append(deleted_message)
|
|
1507
|
-
except Exception as e:
|
|
1508
|
-
warnings.warn(f"Error deleting message {deleted_message.id} from recall memory: {e}")
|
|
1509
|
-
self._messages.append(deleted_message)
|
|
1510
|
-
break
|
|
1511
|
-
|
|
1512
|
-
return popped_messages
|
|
1513
|
-
|
|
1514
|
-
def pop_until_user(self) -> List[Message]:
|
|
1515
|
-
"""Pop all messages until the last user message"""
|
|
1516
|
-
if MessageRole.user not in [msg.role for msg in self._messages]:
|
|
1517
|
-
raise ValueError("No user message found in buffer")
|
|
1518
|
-
|
|
1519
|
-
popped_messages = []
|
|
1520
|
-
while len(self._messages) > 0:
|
|
1521
|
-
if self._messages[-1].role == MessageRole.user:
|
|
1522
|
-
# we want to pop up to the last user message
|
|
1523
|
-
return popped_messages
|
|
1524
|
-
else:
|
|
1525
|
-
popped_messages.append(self.pop_message(count=1))
|
|
1526
|
-
|
|
1527
|
-
raise ValueError("No user message found in buffer")
|
|
1528
|
-
|
|
1529
|
-
def retry_message(self) -> List[Message]:
|
|
1530
|
-
"""Retry / regenerate the last message"""
|
|
1531
|
-
self.pop_until_user()
|
|
1532
|
-
user_message = self.pop_message(count=1)[0]
|
|
1533
|
-
assert user_message.text is not None, "User message text is None"
|
|
1534
|
-
step_response = self.step_user_message(user_message_str=user_message.text)
|
|
1535
|
-
messages = step_response.messages
|
|
1536
|
-
|
|
1537
|
-
assert messages is not None
|
|
1538
|
-
assert all(isinstance(msg, Message) for msg in messages), "step() returned non-Message objects"
|
|
1539
|
-
return messages
|
|
1540
|
-
|
|
1541
970
|
def get_context_window(self) -> ContextWindowOverview:
|
|
1542
971
|
"""Get the context window of the agent"""
|
|
1543
972
|
|
|
@@ -1546,24 +975,28 @@ class Agent(BaseAgent):
|
|
|
1546
975
|
core_memory = self.agent_state.memory.compile()
|
|
1547
976
|
num_tokens_core_memory = count_tokens(core_memory)
|
|
1548
977
|
|
|
978
|
+
# Grab the in-context messages
|
|
1549
979
|
# conversion of messages to OpenAI dict format, which is passed to the token counter
|
|
1550
|
-
|
|
980
|
+
in_context_messages = self.agent_manager.get_in_context_messages(agent_id=self.agent_state.id, actor=self.user)
|
|
981
|
+
in_context_messages_openai = [m.to_openai_dict() for m in in_context_messages]
|
|
1551
982
|
|
|
1552
983
|
# Check if there's a summary message in the message queue
|
|
1553
984
|
if (
|
|
1554
|
-
len(
|
|
1555
|
-
and
|
|
1556
|
-
and isinstance(
|
|
985
|
+
len(in_context_messages) > 1
|
|
986
|
+
and in_context_messages[1].role == MessageRole.user
|
|
987
|
+
and isinstance(in_context_messages[1].text, str)
|
|
1557
988
|
# TODO remove hardcoding
|
|
1558
|
-
and "The following is a summary of the previous " in
|
|
989
|
+
and "The following is a summary of the previous " in in_context_messages[1].text
|
|
1559
990
|
):
|
|
1560
991
|
# Summary message exists
|
|
1561
|
-
assert
|
|
1562
|
-
summary_memory =
|
|
1563
|
-
num_tokens_summary_memory = count_tokens(
|
|
992
|
+
assert in_context_messages[1].text is not None
|
|
993
|
+
summary_memory = in_context_messages[1].text
|
|
994
|
+
num_tokens_summary_memory = count_tokens(in_context_messages[1].text)
|
|
1564
995
|
# with a summary message, the real messages start at index 2
|
|
1565
996
|
num_tokens_messages = (
|
|
1566
|
-
num_tokens_from_messages(messages=
|
|
997
|
+
num_tokens_from_messages(messages=in_context_messages_openai[2:], model=self.model)
|
|
998
|
+
if len(in_context_messages_openai) > 2
|
|
999
|
+
else 0
|
|
1567
1000
|
)
|
|
1568
1001
|
|
|
1569
1002
|
else:
|
|
@@ -1571,17 +1004,17 @@ class Agent(BaseAgent):
|
|
|
1571
1004
|
num_tokens_summary_memory = 0
|
|
1572
1005
|
# with no summary message, the real messages start at index 1
|
|
1573
1006
|
num_tokens_messages = (
|
|
1574
|
-
num_tokens_from_messages(messages=
|
|
1007
|
+
num_tokens_from_messages(messages=in_context_messages_openai[1:], model=self.model)
|
|
1008
|
+
if len(in_context_messages_openai) > 1
|
|
1009
|
+
else 0
|
|
1575
1010
|
)
|
|
1576
1011
|
|
|
1577
1012
|
agent_manager_passage_size = self.agent_manager.passage_size(actor=self.user, agent_id=self.agent_state.id)
|
|
1578
1013
|
message_manager_size = self.message_manager.size(actor=self.user, agent_id=self.agent_state.id)
|
|
1579
1014
|
external_memory_summary = compile_memory_metadata_block(
|
|
1580
|
-
|
|
1581
|
-
agent_id=self.agent_state.id,
|
|
1582
|
-
|
|
1583
|
-
agent_manager=self.agent_manager,
|
|
1584
|
-
message_manager=self.message_manager,
|
|
1015
|
+
memory_edit_timestamp=get_utc_time(),
|
|
1016
|
+
previous_message_count=self.message_manager.size(actor=self.user, agent_id=self.agent_state.id),
|
|
1017
|
+
archival_memory_size=self.agent_manager.passage_size(actor=self.user, agent_id=self.agent_state.id),
|
|
1585
1018
|
)
|
|
1586
1019
|
num_tokens_external_memory_summary = count_tokens(external_memory_summary)
|
|
1587
1020
|
|
|
@@ -1606,7 +1039,7 @@ class Agent(BaseAgent):
|
|
|
1606
1039
|
|
|
1607
1040
|
return ContextWindowOverview(
|
|
1608
1041
|
# context window breakdown (in messages)
|
|
1609
|
-
num_messages=len(
|
|
1042
|
+
num_messages=len(in_context_messages),
|
|
1610
1043
|
num_archival_memory=agent_manager_passage_size,
|
|
1611
1044
|
num_recall_memory=message_manager_size,
|
|
1612
1045
|
num_tokens_external_memory_summary=num_tokens_external_memory_summary,
|
|
@@ -1621,7 +1054,7 @@ class Agent(BaseAgent):
|
|
|
1621
1054
|
num_tokens_summary_memory=num_tokens_summary_memory,
|
|
1622
1055
|
summary_memory=summary_memory,
|
|
1623
1056
|
num_tokens_messages=num_tokens_messages,
|
|
1624
|
-
messages=
|
|
1057
|
+
messages=in_context_messages,
|
|
1625
1058
|
# related to functions
|
|
1626
1059
|
num_tokens_functions_definitions=num_tokens_available_functions_definitions,
|
|
1627
1060
|
functions_definitions=available_functions_definitions,
|
|
@@ -1635,7 +1068,6 @@ class Agent(BaseAgent):
|
|
|
1635
1068
|
|
|
1636
1069
|
def save_agent(agent: Agent):
|
|
1637
1070
|
"""Save agent to metadata store"""
|
|
1638
|
-
agent.update_state()
|
|
1639
1071
|
agent_state = agent.agent_state
|
|
1640
1072
|
assert isinstance(agent_state.memory, Memory), f"Memory is not a Memory object: {type(agent_state.memory)}"
|
|
1641
1073
|
|