Skip to content

Commit 3a00aca

Browse files
authored
feat(agent): Optimize agent memory (#2665)
1 parent b901cbc commit 3a00aca

8 files changed

Lines changed: 10653 additions & 6039 deletions

File tree

packages/dbgpt-core/src/dbgpt/agent/core/base_agent.py

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import asyncio
66
import json
77
import logging
8+
import time
89
from concurrent.futures import Executor, ThreadPoolExecutor
910
from datetime import datetime
1011
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, final
@@ -45,6 +46,7 @@ class ConversableAgent(Role, Agent):
4546
bind_prompt: Optional[PromptTemplate] = None
4647
run_mode: Optional[AgentRunMode] = Field(default=None, description="Run mode")
4748
max_retry_count: int = 3
49+
max_timeout: int = 600
4850
llm_client: Optional[AIWrapper] = None
4951
# 确认当前Agent是否需要进行流式输出
5052
stream_out: bool = True
@@ -363,6 +365,7 @@ async def generate_reply(
363365

364366
fail_reason = None
365367
current_retry_counter = 0
368+
start_time = time.time()
366369
is_success = True
367370
observation = received_message.content or ""
368371
while current_retry_counter < self.max_retry_count:
@@ -402,10 +405,12 @@ async def generate_reply(
402405
thinking_messages, resource_info = await self._load_thinking_messages(
403406
received_message=received_message,
404407
sender=sender,
408+
observation=observation,
405409
rely_messages=rely_messages,
406410
historical_dialogues=historical_dialogues,
407411
context=reply_message.get_dict_context(),
408412
is_retry_chat=is_retry_chat,
413+
current_retry_counter=current_retry_counter,
409414
)
410415
with root_tracer.start_span(
411416
"agent.generate_reply.thinking",
@@ -493,6 +498,7 @@ async def generate_reply(
493498
logger.warning("No retry available!")
494499
break
495500
fail_reason = reason
501+
observation = fail_reason
496502
await self.write_memories(
497503
question=question,
498504
ai_message=ai_message,
@@ -514,6 +520,13 @@ async def generate_reply(
514520
if self.run_mode != AgentRunMode.LOOP or act_out.terminate:
515521
logger.debug(f"Agent {self.name} reply success!{reply_message}")
516522
break
523+
time_cost = time.time() - start_time
524+
if time_cost > self.max_timeout:
525+
logger.warning(
526+
f"Agent {self.name} run time out!{time_cost} > "
527+
f"{self.max_timeout}"
528+
)
529+
break
517530

518531
# Continue to run the next round
519532
current_retry_counter += 1
@@ -1072,15 +1085,25 @@ async def _load_thinking_messages(
10721085
self,
10731086
received_message: AgentMessage,
10741087
sender: Agent,
1088+
observation: Optional[str] = None,
10751089
rely_messages: Optional[List[AgentMessage]] = None,
10761090
historical_dialogues: Optional[List[AgentMessage]] = None,
10771091
context: Optional[Dict[str, Any]] = None,
10781092
is_retry_chat: bool = False,
1093+
current_retry_counter: Optional[int] = None,
10791094
) -> Tuple[List[AgentMessage], Optional[Dict]]:
1080-
observation = received_message.content
1081-
if not observation:
1095+
question = received_message.content
1096+
observation = observation or question
1097+
if not question:
10821098
raise ValueError("The received message content is empty!")
1099+
most_recent_memories = ""
1100+
memory_list = []
1101+
# Read the memories according to the current observation
10831102
memories = await self.read_memories(observation)
1103+
if isinstance(memories, list):
1104+
memory_list = memories
1105+
else:
1106+
most_recent_memories = memories
10841107
has_memories = True if memories else False
10851108
reply_message_str = ""
10861109
if context is None:
@@ -1102,8 +1125,9 @@ async def _load_thinking_messages(
11021125
elif message.role == ModelMessageRoleType.AI:
11031126
reply_message_str += f"Observation: {message.content}\n"
11041127
if reply_message_str:
1105-
memories += "\n" + reply_message_str
1128+
most_recent_memories += "\n" + reply_message_str
11061129
try:
1130+
# Load the resource prompt according to the current observation
11071131
resource_prompt_str, resource_references = await self.load_resource(
11081132
observation, is_retry_chat=is_retry_chat
11091133
)
@@ -1114,21 +1138,19 @@ async def _load_thinking_messages(
11141138
resource_vars = await self.generate_resource_variables(resource_prompt_str)
11151139

11161140
system_prompt = await self.build_system_prompt(
1117-
question=observation,
1118-
most_recent_memories=memories,
1141+
question=question,
1142+
most_recent_memories=most_recent_memories,
11191143
resource_vars=resource_vars,
11201144
context=context,
11211145
is_retry_chat=is_retry_chat,
11221146
)
11231147
user_prompt = await self.build_prompt(
1124-
question=observation,
1148+
question=question,
11251149
is_system=False,
1126-
most_recent_memories=memories,
1150+
most_recent_memories=most_recent_memories,
11271151
resource_vars=resource_vars,
11281152
**context,
11291153
)
1130-
if not user_prompt:
1131-
user_prompt = f"Observation: {observation}"
11321154

11331155
agent_messages = []
11341156
if system_prompt:
@@ -1153,14 +1175,21 @@ async def _load_thinking_messages(
11531175
message.role = ModelMessageRoleType.AI
11541176
agent_messages.append(message)
11551177

1178+
if memory_list:
1179+
agent_messages.extend(memory_list)
1180+
11561181
# Current user input information
1157-
agent_messages.append(
1158-
AgentMessage(
1159-
content=user_prompt,
1160-
role=ModelMessageRoleType.HUMAN,
1182+
if not user_prompt and (not memory_list or not current_retry_counter):
1183+
# The user prompt is empty, and the current retry count is 0 or the memory
1184+
# is empty
1185+
user_prompt = f"Observation: {observation}"
1186+
if user_prompt:
1187+
agent_messages.append(
1188+
AgentMessage(
1189+
content=user_prompt,
1190+
role=ModelMessageRoleType.HUMAN,
1191+
)
11611192
)
1162-
)
1163-
11641193
return agent_messages, resource_references
11651194

11661195

packages/dbgpt-core/src/dbgpt/agent/core/base_team.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,12 @@ async def _load_thinking_messages(
160160
self,
161161
received_message: AgentMessage,
162162
sender: Agent,
163+
observation: Optional[str] = None,
163164
rely_messages: Optional[List[AgentMessage]] = None,
164165
historical_dialogues: Optional[List[AgentMessage]] = None,
165166
context: Optional[Dict[str, Any]] = None,
166167
is_retry_chat: bool = False,
168+
current_retry_counter: Optional[int] = None,
167169
) -> Tuple[List[AgentMessage], Optional[Dict]]:
168170
"""Load messages for thinking."""
169171
return [AgentMessage(content=received_message.content)], None

packages/dbgpt-core/src/dbgpt/agent/core/memory/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
"""Memory module for the agent."""
22

3-
from .agent_memory import AgentMemory, AgentMemoryFragment # noqa: F401
3+
from .agent_memory import ( # noqa: F401
4+
AgentMemory,
5+
AgentMemoryFragment,
6+
StructuredAgentMemoryFragment,
7+
)
48
from .base import ( # noqa: F401
59
ImportanceScorer,
610
InsightExtractor,

packages/dbgpt-core/src/dbgpt/agent/core/memory/agent_memory.py

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"""Agent memory module."""
22

3+
import json
4+
import logging
35
from datetime import datetime
4-
from typing import Callable, List, Optional, Type, cast
6+
from typing import Callable, List, Optional, Type, Union, cast
7+
8+
from typing_extensions import TypedDict
59

610
from dbgpt.core import LLMClient
711
from dbgpt.util.annotations import immutable, mutable
@@ -18,6 +22,18 @@
1822
)
1923
from .gpts import GptsMemory, GptsMessageMemory, GptsPlansMemory
2024

25+
logger = logging.getLogger(__name__)
26+
27+
28+
class StructuredObservation(TypedDict):
29+
"""Structured observation for agent memory."""
30+
31+
question: Optional[str]
32+
thought: Optional[str]
33+
action: Optional[str]
34+
action_input: Optional[str]
35+
observation: Optional[str]
36+
2137

2238
class AgentMemoryFragment(MemoryFragment):
2339
"""Default memory fragment for agent memory."""
@@ -168,6 +184,94 @@ def copy(self: "AgentMemoryFragment") -> "AgentMemoryFragment":
168184
)
169185

170186

187+
class StructuredAgentMemoryFragment(AgentMemoryFragment):
188+
"""Structured memory fragment for agent memory."""
189+
190+
def __init__(
191+
self,
192+
observation: Union[StructuredObservation, List[StructuredObservation]],
193+
embeddings: Optional[List[float]] = None,
194+
memory_id: Optional[int] = None,
195+
importance: Optional[float] = None,
196+
last_accessed_time: Optional[datetime] = None,
197+
is_insight: bool = False,
198+
):
199+
"""Create a structured memory fragment."""
200+
super().__init__(
201+
observation=self.to_raw_observation(observation),
202+
embeddings=embeddings,
203+
memory_id=memory_id,
204+
importance=importance,
205+
last_accessed_time=last_accessed_time,
206+
is_insight=is_insight,
207+
)
208+
self._structured_observation = observation
209+
210+
def to_raw_observation(
211+
self, observation: Union[StructuredObservation, List[StructuredObservation]]
212+
) -> str:
213+
"""Convert the structured observation to a raw observation.
214+
215+
Args:
216+
observation(StructuredObservation): Structured observation
217+
218+
Returns:
219+
str: Raw observation
220+
"""
221+
return json.dumps(observation, ensure_ascii=False)
222+
223+
@classmethod
224+
def build_from(
225+
cls: Type["AgentMemoryFragment"],
226+
observation: Union[str, StructuredObservation],
227+
embeddings: Optional[List[float]] = None,
228+
memory_id: Optional[int] = None,
229+
importance: Optional[float] = None,
230+
is_insight: bool = False,
231+
last_accessed_time: Optional[datetime] = None,
232+
**kwargs,
233+
) -> "AgentMemoryFragment":
234+
"""Build a memory fragment from the given parameters."""
235+
if isinstance(observation, str):
236+
observation = json.loads(observation)
237+
return cls(
238+
observation=observation,
239+
embeddings=embeddings,
240+
memory_id=memory_id,
241+
importance=importance,
242+
last_accessed_time=last_accessed_time,
243+
is_insight=is_insight,
244+
)
245+
246+
def reduce(
247+
self, memory_fragments: List["StructuredAgentMemoryFragment"], **kwargs
248+
) -> "StructuredAgentMemoryFragment":
249+
"""Reduce memory fragments to a single memory fragment.
250+
251+
Args:
252+
memory_fragments(List[T]): Memory fragments
253+
254+
Returns:
255+
T: The reduced memory fragment
256+
"""
257+
if len(memory_fragments) == 0:
258+
raise ValueError("Memory fragments is empty.")
259+
if len(memory_fragments) == 1:
260+
return memory_fragments[0]
261+
262+
obs = []
263+
for memory_fragment in memory_fragments:
264+
try:
265+
obs.append(json.loads(memory_fragment.raw_observation))
266+
except Exception as e:
267+
logger.warning(
268+
"Failed to parse observation %s: %s",
269+
memory_fragment.raw_observation,
270+
e,
271+
)
272+
return self.current_class.build_from(obs, **kwargs) # type: ignore
273+
274+
171275
class AgentMemory(Memory[AgentMemoryFragment]):
172276
"""Agent memory."""
173277

0 commit comments

Comments
 (0)