Files
ai-agent-book/chapter3/contextual-retrieval-for-user-memory/advanced_memory_manager.py
T
liqiang b119135836
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
2026-08-20 13:12:50 +00:00

468 lines
17 KiB
Python

"""Advanced Memory Manager with JSON Cards for User Memory System
This module implements the Advanced JSON Cards approach from week2/user-memory,
which stores structured, summarized core facts about users.
"""
import json
import os
import uuid
import logging
from datetime import datetime
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass, field, asdict
from pathlib import Path
logger = logging.getLogger(__name__)
@dataclass
class AdvancedMemoryCard:
"""
Represents an advanced memory card with complete metadata.
Each card MUST include:
- backstory: Context about when/why this information was learned
- date_created: Timestamp when created
- person: Who this relates to
- relationship: Role/relationship to primary user
- Additional fields based on information type
"""
category: str
card_key: str
backstory: str
date_created: str
person: str
relationship: str
data: Dict[str, Any] = field(default_factory=dict)
_metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for storage"""
result = {
"backstory": self.backstory,
"date_created": self.date_created,
"person": self.person,
"relationship": self.relationship,
**self.data
}
if self._metadata:
result["_metadata"] = self._metadata
return result
@classmethod
def from_dict(cls, category: str, card_key: str, data: Dict[str, Any]) -> 'AdvancedMemoryCard':
"""Create from dictionary"""
metadata = data.pop("_metadata", {})
return cls(
category=category,
card_key=card_key,
backstory=data.get("backstory", ""),
date_created=data.get("date_created", ""),
person=data.get("person", ""),
relationship=data.get("relationship", ""),
data={k: v for k, v in data.items()
if k not in ["backstory", "date_created", "person", "relationship"]},
_metadata=metadata
)
class AdvancedMemoryManager:
"""
Advanced JSON Memory Manager for structured user information.
This manager handles the persistent, structured memory that stays
in the agent's context at all times (the "备忘录" or memo).
"""
def __init__(self, user_id: str, storage_dir: str = "memory_storage"):
"""
Initialize the advanced memory manager.
Args:
user_id: Unique user identifier
storage_dir: Directory for storing memory files
"""
self.user_id = user_id
self.storage_dir = Path(storage_dir)
self.storage_dir.mkdir(parents=True, exist_ok=True)
self.memory_file = self.storage_dir / f"{user_id}_advanced_memory.json"
self.categories: Dict[str, Dict[str, AdvancedMemoryCard]] = {}
self.load_memory()
logger.info(f"Initialized AdvancedMemoryManager for user {user_id}")
def load_memory(self):
"""Load memory cards from storage"""
if self.memory_file.exists():
try:
with open(self.memory_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# Load categories and cards
for category, cards in data.get('categories', {}).items():
self.categories[category] = {}
for card_key, card_data in cards.items():
self.categories[category][card_key] = AdvancedMemoryCard.from_dict(
category, card_key, card_data
)
logger.info(f"Loaded {sum(len(cards) for cards in self.categories.values())} memory cards")
except Exception as e:
logger.error(f"Error loading memory: {e}")
self.categories = {}
else:
logger.info(f"No existing memory file for user {self.user_id}")
def save_memory(self):
"""Save memory cards to storage"""
try:
data = {
'user_id': self.user_id,
'type': 'advanced_json_cards',
'updated_at': datetime.now().isoformat(),
'categories': {}
}
# Convert cards to dict format
for category, cards in self.categories.items():
data['categories'][category] = {}
for card_key, card in cards.items():
data['categories'][category][card_key] = card.to_dict()
with open(self.memory_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
logger.info(f"Saved {sum(len(cards) for cards in self.categories.values())} memory cards")
except Exception as e:
logger.error(f"Error saving memory: {e}")
def add_card(self, card: AdvancedMemoryCard) -> str:
"""
Add a new memory card.
Args:
card: The memory card to add
Returns:
Memory ID in format category.card_key
"""
if card.category not in self.categories:
self.categories[card.category] = {}
# Add metadata
card._metadata = {
'created_at': datetime.now().isoformat(),
'updated_at': datetime.now().isoformat()
}
# Ensure required fields
if not card.backstory:
logger.warning("Memory card missing backstory")
if not card.date_created:
card.date_created = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if not card.person:
card.person = "Primary User"
if not card.relationship:
card.relationship = "primary account holder"
self.categories[card.category][card.card_key] = card
self.save_memory()
memory_id = f"{card.category}.{card.card_key}"
logger.info(f"Added memory card: {memory_id}")
return memory_id
def update_card(self, category: str, card_key: str, updates: Dict[str, Any]) -> bool:
"""
Update an existing memory card.
Args:
category: Card category
card_key: Card key
updates: Fields to update
Returns:
True if successful
"""
if category not in self.categories or card_key not in self.categories[category]:
logger.warning(f"Card not found: {category}.{card_key}")
return False
card = self.categories[category][card_key]
# Update fields
for key, value in updates.items():
if key in ["backstory", "date_created", "person", "relationship"]:
setattr(card, key, value)
elif key != "_metadata":
card.data[key] = value
# Update metadata
if card._metadata:
card._metadata['updated_at'] = datetime.now().isoformat()
self.save_memory()
logger.info(f"Updated memory card: {category}.{card_key}")
return True
def delete_card(self, category: str, card_key: str) -> bool:
"""
Delete a memory card.
Args:
category: Card category
card_key: Card key
Returns:
True if successful
"""
if category in self.categories and card_key in self.categories[category]:
del self.categories[category][card_key]
# Clean up empty categories
if not self.categories[category]:
del self.categories[category]
self.save_memory()
logger.info(f"Deleted memory card: {category}.{card_key}")
return True
logger.warning(f"Card not found for deletion: {category}.{card_key}")
return False
def get_card(self, category: str, card_key: str) -> Optional[AdvancedMemoryCard]:
"""Get a specific memory card"""
if category in self.categories and card_key in self.categories[category]:
return self.categories[category][card_key]
return None
def search_cards(self, query: str) -> List[Tuple[str, AdvancedMemoryCard]]:
"""
Search memory cards by query.
Args:
query: Search query
Returns:
List of (memory_id, card) tuples
"""
query_lower = query.lower()
results = []
for category, cards in self.categories.items():
for card_key, card in cards.items():
memory_id = f"{category}.{card_key}"
# Search in all card fields
card_str = json.dumps(card.to_dict(), ensure_ascii=False).lower()
if (query_lower in category.lower() or
query_lower in card_key.lower() or
query_lower in card_str):
results.append((memory_id, card))
return results
def get_context_string(self, max_cards: Optional[int] = None) -> str:
"""
Get memory cards as formatted string for LLM context.
Args:
max_cards: Maximum number of cards to include (None for all)
Returns:
Formatted string of memory cards
"""
if not self.categories:
return "No advanced memory cards available."
lines = ["=== ADVANCED USER MEMORY CARDS ===\n"]
card_count = 0
for category, cards in self.categories.items():
lines.append(f"\n[Category: {category}]")
for card_key, card in cards.items():
if max_cards and card_count >= max_cards:
remaining = sum(len(c) for c in self.categories.values()) - card_count
lines.append(f"\n... and {remaining} more memory cards")
break
lines.append(f"\n Card '{card_key}':")
# Format card data nicely
card_dict = card.to_dict()
# Remove internal metadata from display
card_dict.pop('_metadata', None)
# Display key fields first
lines.append(f" - Backstory: {card.backstory}")
lines.append(f" - Person: {card.person}")
lines.append(f" - Relationship: {card.relationship}")
lines.append(f" - Date Created: {card.date_created}")
# Display other data fields
for key, value in card.data.items():
if isinstance(value, dict):
lines.append(f" - {key}:")
for k, v in value.items():
lines.append(f" {k}: {v}")
elif isinstance(value, list):
lines.append(f" - {key}: {', '.join(str(v) for v in value)}")
else:
lines.append(f" - {key}: {value}")
card_count += 1
if max_cards and card_count >= max_cards:
break
lines.append(f"\n\n[Total Memory Cards: {sum(len(cards) for cards in self.categories.values())}]")
return "\n".join(lines)
def summarize_for_conversation(self, conversation_id: str) -> Dict[str, Any]:
"""
Summarize memory cards for a specific conversation.
This creates a conversation-specific summary of relevant memory cards
that can be stored alongside the conversation chunks.
Args:
conversation_id: The conversation identifier
Returns:
Summary dictionary
"""
summary = {
"conversation_id": conversation_id,
"timestamp": datetime.now().isoformat(),
"total_cards": sum(len(cards) for cards in self.categories.values()),
"categories": {}
}
# Summarize by category
for category, cards in self.categories.items():
if cards:
summary["categories"][category] = {
"count": len(cards),
"keys": list(cards.keys()),
"sample": {}
}
# Include first 2 cards as samples
for i, (card_key, card) in enumerate(cards.items()):
if i >= 2:
break
summary["categories"][category]["sample"][card_key] = {
"person": card.person,
"backstory": card.backstory[:100] + "..." if len(card.backstory) > 100 else card.backstory
}
return summary
def clear_all_memories(self):
"""Clear all memory cards"""
self.categories = {}
self.save_memory()
logger.info(f"Cleared all memory cards for user {self.user_id}")
def get_statistics(self) -> Dict[str, Any]:
"""Get memory statistics"""
stats = {
"user_id": self.user_id,
"total_cards": sum(len(cards) for cards in self.categories.values()),
"categories": {}
}
for category, cards in self.categories.items():
stats["categories"][category] = {
"count": len(cards),
"cards": list(cards.keys())
}
# Calculate average backstory length
all_backstories = []
for cards in self.categories.values():
for card in cards.values():
if card.backstory:
all_backstories.append(len(card.backstory))
if all_backstories:
stats["avg_backstory_length"] = sum(all_backstories) / len(all_backstories)
else:
stats["avg_backstory_length"] = 0
return stats
def create_sample_cards() -> List[AdvancedMemoryCard]:
"""Create sample memory cards for testing"""
cards = [
AdvancedMemoryCard(
category="financial",
card_key="bank_account_primary",
backstory="User shared their banking details while setting up automatic bill payments",
date_created="2024-01-15 10:30:00",
person="John Smith (primary)",
relationship="primary account holder",
data={
"bank_name": "Chase Bank",
"account_type": "checking",
"account_ending": "4567",
"routing_number": "021000021",
"purpose": "primary checking for bills"
}
),
AdvancedMemoryCard(
category="medical",
card_key="doctor_dermatologist_sarah",
backstory="User needed to schedule a dermatology appointment for their daughter's skin condition",
date_created="2024-01-16 14:00:00",
person="Sarah Smith (daughter)",
relationship="family member",
data={
"doctor_name": "Dr. Emily Johnson",
"specialty": "Pediatric Dermatology",
"clinic": "Children's Health Center",
"phone": "555-0123",
"condition_treated": "eczema"
}
),
AdvancedMemoryCard(
category="travel",
card_key="passport_jessica",
backstory="User mentioned passport expiration while planning international travel",
date_created="2024-12-20 09:00:00",
person="Jessica Thompson (primary)",
relationship="primary account holder",
data={
"passport_number": "XXXXX1234",
"expiration_date": "2025-02-18",
"issuing_country": "USA",
"needs_renewal": True
}
),
AdvancedMemoryCard(
category="travel",
card_key="tokyo_trip_january",
backstory="User booked a trip to Tokyo for a business conference",
date_created="2024-12-22 11:00:00",
person="Jessica Thompson (primary)",
relationship="primary account holder",
data={
"destination": "Tokyo, Japan",
"departure_date": "2025-01-25",
"return_date": "2025-02-01",
"purpose": "business conference",
"hotel": "Hilton Tokyo",
"flight": "UA837"
}
)
]
return cards