Skip to content

Commit a92d83e

Browse files
committed
feat: overhaul the memory system to provide more useful context
1 parent 147f3cf commit a92d83e

File tree

5 files changed

+129
-162
lines changed

5 files changed

+129
-162
lines changed

AgentCrew/modules/agents/local_agent.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,17 @@ def _enhance_agent_context_messages(self, final_messages: List[Dict[str, Any]]):
634634
- Skip agent evaluation if user request is when...,[action]... related to adaptive behaviors call `adapt` tool instead.""",
635635
},
636636
)
637+
if self.services.get("memory"):
638+
memory_headers = self.services["memory"].list_memory_headers(
639+
agent_name=self.name
640+
)
641+
if memory_headers:
642+
adaptive_messages["content"].append(
643+
{
644+
"type": "text",
645+
"text": f"Here are conversations that we have discussed:\n- {'\n- '.join(memory_headers)}",
646+
}
647+
)
637648
if len(adaptive_messages["content"]) > 0:
638649
final_messages.insert(last_user_index, adaptive_messages)
639650

AgentCrew/modules/chat/message/conversation.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,6 @@ def load_conversation(self, conversation_id: str) -> Optional[List[Dict[str, Any
112112
msg["tool_call_id"] = tool_result.get("tool_use_id", "")
113113

114114
self.message_handler.current_conversation_id = conversation_id
115-
if self.message_handler.memory_service:
116-
self.message_handler.memory_service.session_id = (
117-
self.message_handler.current_conversation_id
118-
)
119-
self.message_handler.memory_service.loaded_conversation = True
120-
self.message_handler.memory_service.load_conversation_context(
121-
self.message_handler.current_conversation_id
122-
)
123115
last_agent_name = history[-1].get("agent", "")
124116
if last_agent_name and self.message_handler.agent_manager.select_agent(
125117
last_agent_name
@@ -129,6 +121,15 @@ def load_conversation(self, conversation_id: str) -> Optional[List[Dict[str, Any
129121
)
130122
self.message_handler._notify("agent_changed", last_agent_name)
131123

124+
if self.message_handler.memory_service:
125+
self.message_handler.memory_service.session_id = (
126+
self.message_handler.current_conversation_id
127+
)
128+
self.message_handler.memory_service.loaded_conversation = True
129+
self.message_handler.memory_service.load_conversation_context(
130+
self.message_handler.current_conversation_id, last_agent_name
131+
)
132+
132133
self.message_handler.streamline_messages = history
133134
self.message_handler.agent_manager.rebuild_agents_messages(
134135
self.message_handler.streamline_messages

AgentCrew/modules/memory/base_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def clear_conversation_context(self):
5050
pass
5151

5252
@abstractmethod
53-
def load_conversation_context(self, session_id: str):
53+
def load_conversation_context(self, session_id: str, agent_name: str = "None"):
5454
pass
5555

5656
@abstractmethod
@@ -88,7 +88,7 @@ def retrieve_memory(
8888
pass
8989

9090
@abstractmethod
91-
def list_memory_ids(
91+
def list_memory_headers(
9292
self,
9393
from_date: Optional[int] = None,
9494
to_date: Optional[int] = None,

AgentCrew/modules/memory/chroma_service.py

Lines changed: 80 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def __init__(
5454
## set to groq if key available
5555
if self.llm_service:
5656
if self.llm_service.provider_name == "google":
57-
self.llm_service.model = "gemini-2.5-flash-lite-preview-06-17"
57+
self.llm_service.model = "gemini-2.5-flash-lite"
5858
elif self.llm_service.provider_name == "claude":
5959
self.llm_service.model = "claude-3-5-haiku-latest"
6060
elif self.llm_service.provider_name == "openai":
@@ -241,88 +241,66 @@ async def _store_conversation_internal(self, operation_data: Dict[str, Any]):
241241
session_id = operation_data["session_id"]
242242

243243
# Use the existing storage logic but make it synchronous
244-
ids = []
245-
memory_data = {}
246-
# avaialble_ids = collection.get(
247-
# where={
248-
# "agent": agent_name,
249-
# },
250-
# include=[],
251-
# )["ids"]
244+
memory_data = None
245+
retried = 0
252246
if self.llm_service:
253-
try:
254-
# Process with LLM using asyncio.run to handle async call in worker thread
255-
if self.current_conversation_context.get(session_id, ""):
256-
analyzed_prompt = PRE_ANALYZE_WITH_CONTEXT_PROMPT.replace(
257-
"{conversation_context}",
258-
f"""<PREVIOUS_CONVERSATION_CONTEXT>
259-
{self.current_conversation_context[session_id]}
260-
</PREVIOUS_CONVERSATION_CONTEXT>""",
247+
while retried < 3:
248+
try:
249+
# Process with LLM using asyncio.run to handle async call in worker thread
250+
if self.current_conversation_context.get(session_id, ""):
251+
analyzed_prompt = PRE_ANALYZE_WITH_CONTEXT_PROMPT.replace(
252+
"{conversation_context}",
253+
f"""<PREVIOUS_CONVERSATION_CONTEXT>
254+
{self.current_conversation_context[session_id]}
255+
</PREVIOUS_CONVERSATION_CONTEXT>""",
256+
)
257+
else:
258+
analyzed_prompt = PRE_ANALYZE_PROMPT
259+
analyzed_prompt = (
260+
analyzed_prompt.replace(
261+
"{current_date}",
262+
datetime.today().strftime("%Y-%m-%d %H:%M:%S"),
263+
)
264+
.replace("{user_message}", user_message)
265+
.replace("{assistant_response}", assistant_response)
261266
)
262-
else:
263-
analyzed_prompt = PRE_ANALYZE_PROMPT
264-
analyzed_prompt = (
265-
analyzed_prompt.replace(
266-
"{current_date}",
267-
datetime.today().strftime("%Y-%m-%d %H:%M:%S"),
267+
analyzed_text = await self.llm_service.process_message(
268+
analyzed_prompt
268269
)
269-
.replace("{user_message}", user_message)
270-
.replace("{assistant_response}", assistant_response)
271-
)
272-
analyzed_text = await self.llm_service.process_message(
273-
analyzed_prompt
274-
)
275-
start_xml = analyzed_text.index("<MEMORY>")
276-
end_xml = analyzed_text.index("</MEMORY>")
277-
xml_content = analyzed_text[start_xml : end_xml + len("</MEMORY>")]
278-
xml_content.replace("&", "&amp;").replace("'", "&apos;").replace(
279-
'"', "&quot;"
280-
)
281-
memory_data = xmltodict.parse(xml_content)
282-
if (
283-
"MEMORY" in memory_data
284-
and "ID" in memory_data["MEMORY"]
285-
and memory_data["MEMORY"]["ID"]
286-
):
287-
ids.append(memory_data["MEMORY"]["ID"])
288-
# if (
289-
# "MEMORY" in memory_data
290-
# and "USER_REQUEST" not in memory_data["MEMORY"]
291-
# ):
292-
# memory_data["MEMORY"]["USER_REQUEST"] = user_message
293-
# if (
294-
# "MEMORY" in memory_data
295-
# and "ASSISTANT_RESPONSE" not in memory_data["MEMORY"]
296-
# ):
297-
# memory_data["MEMORY"]["ASSISTANT_RESPONSE"] = assistant_response
298-
299-
except Exception as e:
300-
logger.warning(f"Error processing conversation with LLM: {e}")
301-
# Fallback to simple concatenation if LLM fails
302-
memory_data = {
303-
"MEMORY": {
304-
"DATE": datetime.today().strftime("%Y-%m-%d"),
305-
"USER_REQUEST": user_message,
306-
"ASSISTANT_RESPONSE": assistant_response
307-
if len(assistant_response) < 200
308-
else assistant_response[:197] + "...",
309-
}
310-
}
311-
else:
270+
start_xml = analyzed_text.index("<MEMORY>")
271+
end_xml = analyzed_text.index("</MEMORY>")
272+
xml_content = analyzed_text[
273+
start_xml : end_xml + len("</MEMORY>")
274+
]
275+
xml_content = (
276+
xml_content.replace("&", "&amp;")
277+
.replace("'", "&apos;")
278+
.replace('"', "&quot;")
279+
)
280+
memory_data = xmltodict.parse(xml_content)
281+
break
282+
except Exception as e:
283+
logger.warning(
284+
f"Error processing conversation with LLM: {e} {xml_content}" # type: ignore
285+
)
286+
retried += 1
287+
continue
288+
289+
if memory_data is None:
312290
# Create the memory document by combining user message and response
313291
memory_data = {
314292
"MEMORY": {
315293
"DATE": datetime.today().strftime("%Y-%m-%d"),
316-
"USER_REQUEST": user_message,
317-
"ASSISTANT_RESPONSE": assistant_response
318-
if len(assistant_response) < 200
319-
else assistant_response[:197] + "...",
294+
"CONVERSATION_NOTES": {
295+
"NOTE": [user_message, assistant_response]
296+
},
320297
}
321298
}
322299

323300
# Store in ChromaDB (existing logic)
324-
memory_id = str(uuid.uuid4())
325301
timestamp = datetime.now().timestamp()
302+
303+
memory_header = memory_data["MEMORY"].get("HEAD", None)
326304
conversation_document = xmltodict.unparse(
327305
memory_data, pretty=True, full_document=False
328306
)
@@ -335,27 +313,19 @@ async def _store_conversation_internal(self, operation_data: Dict[str, Any]):
335313

336314
metadata = {
337315
"date": timestamp,
338-
"conversation_id": memory_id,
339316
"session_id": session_id,
340317
"agent": agent_name,
341318
"type": "conversation",
342319
}
343-
344-
# Add to ChromaDB collection (existing logic)
345-
if ids:
346-
collection.upsert(
347-
ids=[ids[0]],
348-
documents=[conversation_document],
349-
embeddings=conversation_embedding,
350-
metadatas=[metadata],
351-
)
352-
else:
353-
collection.add(
354-
documents=[conversation_document],
355-
embeddings=conversation_embedding,
356-
metadatas=[metadata],
357-
ids=[memory_id],
358-
)
320+
if memory_header:
321+
metadata["header"] = memory_header
322+
323+
collection.upsert(
324+
ids=[f"{session_id}_{agent_name}"],
325+
documents=[conversation_document],
326+
embeddings=conversation_embedding,
327+
metadatas=[metadata],
328+
)
359329

360330
logger.debug(f"Stored conversation: {operation_data['operation_id']}")
361331

@@ -388,7 +358,7 @@ def clear_conversation_context(self):
388358
self.current_conversation_context = {}
389359
self.context_embedding = []
390360

391-
def load_conversation_context(self, session_id: str):
361+
def load_conversation_context(self, session_id: str, agent_name: str = "None"):
392362
collection = self._initialize_collection()
393363
latest_memory = collection.get(
394364
where={
@@ -399,6 +369,7 @@ def load_conversation_context(self, session_id: str):
399369
self.current_conversation_context[session_id] = latest_memory["documents"][
400370
-1
401371
]
372+
print(self.current_conversation_context[session_id])
402373

403374
def generate_user_context(self, user_input: str, agent_name: str = "None") -> str:
404375
"""
@@ -425,7 +396,7 @@ async def _semantic_extracting(self, input: str) -> str:
425396
else:
426397
return input
427398

428-
def list_memory_ids(
399+
def list_memory_headers(
429400
self,
430401
from_date: Optional[int] = None,
431402
to_date: Optional[int] = None,
@@ -450,9 +421,14 @@ def list_memory_ids(
450421
else and_conditions[0]
451422
if and_conditions
452423
else None,
453-
include=[],
424+
include=["metadatas"],
454425
)
455-
return list_memory["ids"]
426+
headers = []
427+
if list_memory and list_memory["metadatas"]:
428+
for metadata in list_memory["metadatas"]:
429+
if metadata.get("header", None):
430+
headers.append(metadata.get("header"))
431+
return headers
456432

457433
def retrieve_memory(
458434
self,
@@ -498,36 +474,30 @@ def retrieve_memory(
498474
if not results["documents"] or not results["documents"][0]:
499475
return "No relevant memories found."
500476

501-
# Group chunks by conversation_id
502-
conversation_chunks = {}
503-
for i, (doc, metadata) in enumerate(
504-
zip(results["documents"][0], results["metadatas"][0]) # type:ignore
477+
conversation_chunks = []
478+
for i, (id, doc, metadata) in enumerate(
479+
zip(results["ids"][0], results["documents"][0], results["metadatas"][0]) # type:ignore
505480
):
506-
conv_id = metadata.get("conversation_id", "unknown")
507-
if conv_id not in conversation_chunks:
508-
conversation_chunks[conv_id] = {
509-
"chunks": [],
481+
conversation_chunks.append(
482+
{
483+
"id": id,
484+
"document": doc,
510485
"timestamp": metadata.get("date", None)
511486
or metadata.get("timestamp", "unknown"),
512487
"relevance": results["distances"][0][i]
513488
if results["distances"]
514489
else 99,
515490
}
516-
conversation_chunks[conv_id]["chunks"].append(
517-
(metadata.get("chunk_index", 0), doc)
518491
)
519492

520493
# Sort conversations by relevance
521-
sorted_conversations = sorted(
522-
conversation_chunks.items(), key=lambda x: x[1]["relevance"]
523-
)
494+
sorted_conversations = sorted(conversation_chunks, key=lambda x: x["relevance"])
524495

525496
# Format the output
526497
output = []
527-
for conv_id, conv_data in sorted_conversations:
498+
for conv_data in sorted_conversations:
528499
# Sort chunks by index
529-
sorted_chunks = sorted(conv_data["chunks"], key=lambda x: x[0])
530-
conversation_text = "\n".join([chunk for _, chunk in sorted_chunks])
500+
conversation_text = conv_data["document"]
531501
if conv_data["relevance"] > RELEVANT_THRESHOLD:
532502
continue
533503
# Format timestamp
@@ -544,22 +514,10 @@ def retrieve_memory(
544514
timestamp = conv_data["timestamp"]
545515

546516
output.append(
547-
f"--- Memory from {timestamp} (relevance point(lower is better): {conv_data['relevance']}) ---\n{conversation_text}\n---"
517+
f"--- Memory from {timestamp} [id:{conv_data['id']}] ---\n{conversation_text}\n---"
548518
)
549519

550520
memories = "\n\n".join(output)
551-
# if self.llm_service:
552-
# try:
553-
# return await self.llm_service.process_message(
554-
# POST_RETRIEVE_MEMORY.replace("{keywords}", keywords).replace(
555-
# "{memory_list}", memories
556-
# )
557-
# )
558-
# except Exception as e:
559-
# logger.warning(f"Error processing retrieved memories with LLM: {e}")
560-
# # Fallback to returning raw memories if LLM processing fails
561-
# return memories
562-
# else:
563521
return memories
564522

565523
def _cosine_similarity(self, vec_a, vec_b):
@@ -663,23 +621,8 @@ def forget_topic(
663621
"count": 0,
664622
}
665623

666-
# Collect all conversation IDs related to the topic
667-
conversation_ids = set()
668-
if results["metadatas"] and results["metadatas"][0]:
669-
for metadata in results["metadatas"][0]:
670-
conv_id = metadata.get("conversation_id")
671-
if conv_id:
672-
conversation_ids.add(conv_id)
673-
674-
# Get all memories to find those with matching conversation IDs
675-
all_memories = collection.get()
676-
677624
# Find IDs to remove
678-
ids_to_remove = []
679-
if all_memories["metadatas"]:
680-
for i, metadata in enumerate(all_memories["metadatas"]):
681-
if metadata.get("conversation_id") in conversation_ids:
682-
ids_to_remove.append(all_memories["ids"][i])
625+
ids_to_remove = results["ids"][0]
683626

684627
# Remove the memories
685628
if ids_to_remove:
@@ -689,7 +632,6 @@ def forget_topic(
689632
"success": True,
690633
"message": f"Successfully removed {len(ids_to_remove)} memory chunks related to '{topic}'",
691634
"count": len(ids_to_remove),
692-
"conversations_affected": len(conversation_ids),
693635
}
694636

695637
except Exception as e:

0 commit comments

Comments
 (0)