memobase¶
第3章 · 用户记忆和知识库 · 配套项目
chapter3/memobase
项目说明¶
Memobase Agent with Kimi K3 for LOCOMO Benchmark¶
An advanced AI agent implementation featuring sophisticated memory management, powered by the Kimi K3 model, and evaluated on the LOCOMO (Long Context and Memory Optimization) benchmark.
Two tracks in this folder — don't confuse them:
- Real Memobase framework demo (
profile_demo.py) — uses the actual open-source Memobase SDK (pip install memobase, packagememobase>=0.0.27) talking to a running Memobase server. This is the canonical demonstration of Memobase's Profile (structured user attributes) + Event Memory (timeline) split described in book chapter 3. See Memobase Profile + Event Demo.- Hand-rolled memory agent (
agent.py/main.py) — a self-contained, Memobase-inspiredMemoryStore(episodic/semantic/procedural/working memory, pickle-persisted) that calls Kimi K3 directly. It needs no Memobase server — only aKIMI_API_KEY. The--modecommands below (interactive / benchmark / demo / task) all drive this agent.
Features¶
🧠 Advanced Memory Management¶
- Multiple Memory Types:
- Episodic Memory: Stores specific task experiences and interactions
- Semantic Memory: Maintains general knowledge and facts
- Procedural Memory: Learns and stores problem-solving patterns
-
Working Memory: Manages short-term task context
-
Memory Operations:
- Automatic memory compression when thresholds are exceeded
- Memory consolidation and pattern extraction
- Importance-based decay and retention
- Clustering of related memories
- Efficient retrieval based on relevance and recency
🚀 Kimi K3 Model Integration¶
- Utilizes Kimi K3, a ~2.8-trillion parameter Mixture-of-Experts model
- 32 billion active parameters per forward pass
- Optimized for agentic capabilities including:
- Advanced tool use
- Multi-step reasoning
- Code synthesis
- Long-context understanding (128k tokens)
📊 LOCOMO Benchmark¶
Comprehensive evaluation across multiple task categories: - Multi-turn Reasoning: Complex problem-solving over multiple interactions - Long Context Q&A: Information extraction from extensive documents - Task Planning: Project and resource planning capabilities - Knowledge Integration: Cross-domain synthesis and analysis - Tool Usage: Effective utilization of external tools and APIs
Installation¶
-
Clone the repository:
-
Install dependencies:
-
Set up environment variables:
Configuration¶
Edit config.py to customize:
- Model parameters (temperature, max tokens, etc.)
- Memory settings (thresholds, retention policies)
- Benchmark configurations
- Logging levels
Usage¶
Interactive Mode¶
Engage in conversation with the agent while it builds and uses memories:
Available commands in interactive mode:
- /help - Show available commands
- /memory - Display memory statistics
- /clear - Clear working memory
- /reset - Reset conversation (preserves long-term memories)
- /learn - Trigger memory consolidation
- /exit - Exit interactive mode
Benchmark Mode¶
Run the LOCOMO benchmark evaluation:
# Run full benchmark
python main.py --mode benchmark
# Run specific category
python main.py --mode benchmark --category multi_turn_reasoning
# Run limited number of tasks
python main.py --mode benchmark --num-tasks 5
Demo Mode¶
Run pre-configured demonstration scenarios:
Demonstrations include: - Memory retention across conversations - Learning from experience - Context management in long interactions
Single Task Mode¶
Execute a specific task:
Additional Options¶
--api-key KEY- Override the API key from environment--no-memory- Start with empty memory store--verbose- Enable detailed logging
Memobase Profile + Event Demo (real SDK)¶
profile_demo.py uses the real open-source Memobase SDK to show its two
memory structures: a Profile (topic → sub-topic → content, e.g.
basic_info→城市, work→职位, interest→游戏偏好) and Event Memory (a
timeline for "when did we discuss the budget?" style questions). It follows
Memobase's buffered pipeline: insert (write to the user buffer) →
flush (trigger one LLM extraction) → profile / event / context (recall).
Prerequisites: a running Memobase server + an extraction LLM¶
Memobase does its memory extraction server-side, so the demo needs a reachable Memobase service (the client does not call the LLM itself):
- Self-hosted: follow memodb-io/memobase
(docker compose). Default endpoint
http://localhost:8019, default tokensecret. The extraction model is configured in the server's.env/config.yaml, not by this client (--modelis informational only). - Cloud: get a
project_url+api_keyfrom https://www.memobase.io.
Point the client at the server via --project-url / --api-key or the
MEMOBASE_PROJECT_URL / MEMOBASE_API_KEY environment variables (see
env.example).
Running it¶
pip install -r requirements.txt # installs the memobase SDK
# End-to-end demo (built-in sample conversation) against a running server:
python profile_demo.py
# Offline preview — shows the conversation + pipeline without a server,
# does not go online and does not fabricate results:
python profile_demo.py --dry-run
# Individual ops once memories exist:
python profile_demo.py --op profile # recall structured user profile
python profile_demo.py --op event # recall the event timeline
python profile_demo.py --op context # assembled memory context string
python profile_demo.py --input chat.json --output result.json
If no server is reachable the demo exits with an actionable message (pointing
to --dry-run or a --project-url) rather than inventing memory output.
Architecture¶
Memory Store (agent.py)¶
The MemoryStore class manages different memory types with:
- Persistent storage using pickle
- Memory compression and clustering
- Importance-based retrieval
- Time-based decay mechanisms
Agent Core (agent.py)¶
The MemobaseAgent class provides:
- Message processing with memory context
- Learning from task outcomes
- Memory-aware response generation
- Performance metrics tracking
Benchmark System (locomo_benchmark.py)¶
The LOCOMOBenchmark class implements:
- Task generation and management
- Response evaluation metrics
- Category-specific scoring
- Results persistence and analysis
Memory Management Strategies¶
Compression¶
When memory exceeds thresholds: 1. Sort memories by importance and recency 2. Keep high-importance memories 3. Compress low-importance memories into clusters 4. Store cluster summaries as compressed memories
Consolidation¶
During idle or triggered consolidation: 1. Apply decay to all memories 2. Remove very low importance memories 3. Extract patterns from episodic memories 4. Create procedural memories from patterns
Retrieval¶
When processing queries: 1. Search for relevant memories by content 2. Retrieve recent episodic memories 3. Include applicable procedural knowledge 4. Format memories for context inclusion
Benchmark Results¶
Results are saved in benchmark_results/ with:
- Task-level scores and timings
- Category-wise performance metrics
- Memory usage statistics
- Detailed error logs
Development¶
Adding Custom Tools¶
Extend the agent with custom tools by modifying the agent class:
Custom Memory Types¶
Add new memory types in config.py:
Extending Benchmark¶
Add custom benchmark tasks in locomo_benchmark.py:
self.tasks.append(BenchmarkTask(
id="custom_001",
category="custom_category",
query="Your custom task query",
expected_capabilities=["capability1", "capability2"]
))
Performance Optimization¶
Memory Efficiency¶
- Batch memory operations for better performance
- Use compression for large memory stores
- Implement periodic consolidation
- Clear working memory after task completion
Response Latency¶
- Pre-fetch likely memories
- Cache embedding computations
- Use parallel processing for memory search
- Optimize context window usage
Troubleshooting¶
Common Issues¶
- API Key Error:
- Ensure
KIMI_API_KEYis set in.env -
Verify API key validity
-
Memory Overflow:
- Adjust
MAX_MEMORY_ENTRIESin config - Enable more aggressive compression
-
Trigger manual consolidation
-
Slow Performance:
- Reduce
MODEL_MAX_TOKENS - Enable caching in config
- Use category-specific benchmarks
Contributing¶
Contributions are welcome! Areas for improvement: - Additional memory compression algorithms - Enhanced retrieval mechanisms - New benchmark categories - Performance optimizations - Tool integrations
License¶
MIT License - See LICENSE file for details
Acknowledgments¶
- Kimi K3 model by Moonshot AI
- Memobase framework concepts
- LOCOMO benchmark design inspirations
OpenRouter 通用回退 / Universal OpenRouter fallback¶
This experiment now supports a universal OpenRouter fallback for its chat LLM.
- If the primary provider key (e.g.
MOONSHOT_API_KEY/KIMI_API_KEY/OPENAI_API_KEY/DOUBAO_API_KEY…) is present, behavior is unchanged. - Else if
OPENROUTER_API_KEYis set, the chat LLM is automatically routed through OpenRouter (https://openrouter.ai/api/v1). Model names are mapped automatically:gpt-*/o1-*→openai/…,claude-*→anthropic/claude-opus-4.8,kimi-*→moonshotai/kimi-k2.6, ids already containing/are kept as-is, and other provider-native ids (e.g.doubao-*) fall back toopenai/gpt-5.6-luna. SetOPENROUTER_MODELto force a specific OpenRouter model id. - Else a clear error lists the accepted keys.
Add OPENROUTER_API_KEY=... to your .env (see env.example) to enable it.
源代码¶
agent.py¶
"""
Memobase Agent Implementation with Kimi K3 Model
Advanced memory management for LOCOMO benchmark
"""
import json
import logging
import time
import hashlib
import pickle
from typing import List, Dict, Any, Optional, Tuple
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from collections import defaultdict
from openai import OpenAI
from config import (
KIMI_API_KEY, KIMI_BASE_URL, KIMI_MODEL,
MODEL_TEMPERATURE, MODEL_MAX_TOKENS, MODEL_TOP_P,
MEMOBASE_CONFIG, MEMORY_DB_PATH, AGENT_CONFIG,
MAX_MEMORY_ENTRIES, MEMORY_COMPRESSION_THRESHOLD,
LOG_LEVEL, LOG_FORMAT
)
def _reasoning_safe_temperature(model, requested=1.0):
"""Reasoning models (Kimi K3, GPT-5, ...) only accept temperature=1.
Return 1 for those; otherwise the requested value so non-reasoning
providers (Doubao, DeepSeek, older Moonshot) are unchanged."""
m = str(model or "").lower().replace("/", "-")
return 1 if ("kimi-k3" in m or "gpt-5" in m) else requested
# Configure logging
logging.basicConfig(level=LOG_LEVEL, format=LOG_FORMAT)
logger = logging.getLogger(__name__)
@dataclass
class Memory:
"""Represents a single memory entry"""
id: str
type: str # episodic, semantic, procedural, working
content: Any
embedding: Optional[List[float]] = None
metadata: Dict[str, Any] = field(default_factory=dict)
created_at: datetime = field(default_factory=datetime.now)
accessed_at: datetime = field(default_factory=datetime.now)
access_count: int = 0
importance_score: float = 1.0
decay_rate: float = 0.1
def __post_init__(self):
if not self.id:
# Generate unique ID based on content
content_str = json.dumps(self.content, sort_keys=True)
self.id = hashlib.md5(content_str.encode()).hexdigest()[:12]
def access(self):
"""Update access statistics"""
self.accessed_at = datetime.now()
self.access_count += 1
# Increase importance with access
self.importance_score = min(10.0, self.importance_score * 1.1)
def decay(self):
"""Apply time-based decay to importance"""
time_since_access = (datetime.now() - self.accessed_at).total_seconds() / 3600
self.importance_score *= (1 - self.decay_rate * min(1, time_since_access / 24))
self.importance_score = max(0.1, self.importance_score)
@dataclass
class MemoryCluster:
"""Represents a cluster of related memories"""
id: str
memories: List[Memory]
summary: Optional[str] = None
centroid_embedding: Optional[List[float]] = None
created_at: datetime = field(default_factory=datetime.now)
def add_memory(self, memory: Memory):
"""Add a memory to the cluster"""
self.memories.append(memory)
# TODO: Update centroid embedding
def compress(self) -> str:
"""Compress cluster into a summary"""
if not self.summary:
# Create summary from memories
contents = [m.content for m in self.memories]
self.summary = f"Cluster of {len(self.memories)} related memories: {contents[:3]}..."
return self.summary
class MemoryStore:
"""Manages different types of memories with persistence"""
def __init__(self, db_path: Path = MEMORY_DB_PATH):
self.db_path = db_path
self.memories: Dict[str, List[Memory]] = defaultdict(list)
self.clusters: List[MemoryCluster] = []
self.embeddings_cache: Dict[str, List[float]] = {}
self._load_memories()
logger.info(f"Initialized MemoryStore at {db_path}")
def _load_memories(self):
"""Load memories from persistent storage"""
memory_file = self.db_path / "memories.pkl"
if memory_file.exists():
try:
with open(memory_file, 'rb') as f:
data = pickle.load(f)
self.memories = data.get('memories', defaultdict(list))
self.clusters = data.get('clusters', [])
logger.info(f"Loaded {sum(len(m) for m in self.memories.values())} memories")
except Exception as e:
logger.error(f"Failed to load memories: {e}")
def _save_memories(self):
"""Save memories to persistent storage"""
memory_file = self.db_path / "memories.pkl"
try:
with open(memory_file, 'wb') as f:
pickle.dump({
'memories': dict(self.memories),
'clusters': self.clusters
}, f)
logger.debug("Saved memories to disk")
except Exception as e:
logger.error(f"Failed to save memories: {e}")
def add_memory(self, memory_type: str, content: Any,
metadata: Optional[Dict] = None,
importance: float = 1.0) -> Memory:
"""Add a new memory"""
memory = Memory(
id="", # Will be auto-generated
type=memory_type,
content=content,
metadata=metadata or {},
importance_score=importance
)
self.memories[memory_type].append(memory)
# Apply compression if needed
if len(self.memories[memory_type]) > MEMORY_COMPRESSION_THRESHOLD:
self._compress_memories(memory_type)
self._save_memories()
logger.debug(f"Added {memory_type} memory: {memory.id}")
return memory
def get_memories(self, memory_type: Optional[str] = None,
limit: int = 10,
min_importance: float = 0.5) -> List[Memory]:
"""Retrieve memories, optionally filtered by type and importance"""
if memory_type:
memories = self.memories.get(memory_type, [])
else:
memories = [m for mlist in self.memories.values() for m in mlist]
# Filter by importance and sort by relevance
memories = [m for m in memories if m.importance_score >= min_importance]
memories.sort(key=lambda m: (m.importance_score, m.access_count), reverse=True)
# Update access stats
for memory in memories[:limit]:
memory.access()
return memories[:limit]
def search_memories(self, query: str, limit: int = 5) -> List[Memory]:
"""Search memories by content similarity"""
results = []
query_lower = query.lower()
for memory_list in self.memories.values():
for memory in memory_list:
# Simple text matching (would use embeddings in production)
content_str = str(memory.content).lower()
if query_lower in content_str:
score = content_str.count(query_lower) * memory.importance_score
results.append((score, memory))
results.sort(key=lambda x: x[0], reverse=True)
# Update access stats
for _, memory in results[:limit]:
memory.access()
return [m for _, m in results[:limit]]
def _compress_memories(self, memory_type: str):
"""Compress old memories to save space"""
memories = self.memories[memory_type]
if len(memories) <= MEMORY_COMPRESSION_THRESHOLD:
return
# Sort by importance and recency
memories.sort(key=lambda m: (m.importance_score, m.accessed_at.timestamp()))
# Keep top memories, compress others
to_keep = memories[-MAX_MEMORY_ENTRIES//2:]
to_compress = memories[:-MAX_MEMORY_ENTRIES//2]
if to_compress:
# Create a cluster from compressed memories
cluster = MemoryCluster(
id=hashlib.md5(f"{memory_type}_{datetime.now()}".encode()).hexdigest()[:12],
memories=to_compress
)
cluster.compress()
self.clusters.append(cluster)
# Replace with compressed version
compressed_memory = Memory(
id=cluster.id,
type=memory_type,
content=cluster.summary,
metadata={"cluster_id": cluster.id, "compressed_count": len(to_compress)},
importance_score=sum(m.importance_score for m in to_compress) / len(to_compress)
)
self.memories[memory_type] = [compressed_memory] + to_keep
logger.info(f"Compressed {len(to_compress)} {memory_type} memories into cluster {cluster.id}")
def consolidate_memories(self):
"""Consolidate and reorganize memories for efficiency"""
for memory_type in self.memories:
memories = self.memories[memory_type]
# Apply decay to all memories
for memory in memories:
memory.decay()
# Remove very low importance memories
self.memories[memory_type] = [
m for m in memories if m.importance_score > 0.1
]
self._save_memories()
logger.info("Consolidated memories")
def clear_working_memory(self):
"""Clear working memory (short-term)"""
self.memories['working'] = []
logger.debug("Cleared working memory")
class MemobaseAgent:
"""
Advanced agent with Memobase memory management for LOCOMO benchmark
"""
def __init__(self, api_key: str = KIMI_API_KEY):
"""Initialize the Memobase agent"""
self.client = OpenAI(
api_key=api_key,
base_url=KIMI_BASE_URL
)
self.model = KIMI_MODEL
self.memory_store = MemoryStore()
self.conversation_history = []
self.current_task = None
self.task_context = {}
# Initialize system prompt
self._init_system_prompt()
logger.info(f"Initialized MemobaseAgent with model {self.model}")
def _init_system_prompt(self):
"""Initialize the system prompt with memory capabilities"""
self.system_prompt = """You are an advanced AI agent with sophisticated memory management capabilities.
You have access to multiple types of memory:
1. **Episodic Memory**: Specific experiences and events from tasks
2. **Semantic Memory**: General knowledge and facts
3. **Procedural Memory**: Learned procedures and problem-solving patterns
4. **Working Memory**: Current task context and temporary information
Memory Management Guidelines:
- Store important information for future reference
- Retrieve relevant memories when solving new problems
- Learn from past experiences to improve performance
- Compress and consolidate memories to maintain efficiency
- Use procedural memories to apply learned strategies
Your goal is to complete tasks efficiently while learning and adapting from experience.
When you encounter similar problems, use your memories to solve them more effectively.
Always think step-by-step and use your memory system strategically."""
def _store_interaction(self, role: str, content: str, memory_type: str = "episodic"):
"""Store an interaction in memory"""
self.memory_store.add_memory(
memory_type=memory_type,
content={
"role": role,
"content": content,
"task": self.current_task,
"timestamp": datetime.now().isoformat()
},
metadata={
"task_id": self.current_task,
"turn": len(self.conversation_history)
}
)
def _retrieve_relevant_memories(self, query: str, limit: int = 5) -> List[Memory]:
"""Retrieve memories relevant to current query"""
# Search across all memory types
relevant_memories = []
# Get recent episodic memories
episodic = self.memory_store.get_memories("episodic", limit=limit//2)
relevant_memories.extend(episodic)
# Search for similar content
searched = self.memory_store.search_memories(query, limit=limit//2)
relevant_memories.extend(searched)
# Get procedural memories if task-related
if "solve" in query.lower() or "how" in query.lower():
procedural = self.memory_store.get_memories("procedural", limit=2)
relevant_memories.extend(procedural)
# Remove duplicates
seen = set()
unique_memories = []
for memory in relevant_memories:
if memory.id not in seen:
seen.add(memory.id)
unique_memories.append(memory)
return unique_memories[:limit]
def _format_memories_for_context(self, memories: List[Memory]) -> str:
"""Format memories for inclusion in context"""
if not memories:
return ""
formatted = "\n=== Relevant Memories ===\n"
for memory in memories:
formatted += f"[{memory.type.upper()}] (importance: {memory.importance_score:.2f})\n"
if isinstance(memory.content, dict):
formatted += json.dumps(memory.content, indent=2)
else:
formatted += str(memory.content)
formatted += "\n---\n"
return formatted
def _learn_from_outcome(self, task: str, approach: str, outcome: str, success: bool):
"""Learn from task outcomes and store procedural knowledge"""
# Store the learning as procedural memory
self.memory_store.add_memory(
memory_type="procedural",
content={
"task_pattern": task,
"approach": approach,
"outcome": outcome,
"success": success,
"learned_at": datetime.now().isoformat()
},
importance=2.0 if success else 1.0,
metadata={"task_id": self.current_task}
)
if success:
logger.info(f"Learned successful approach for task type: {task}")
else:
logger.info(f"Learned from failure in task type: {task}")
def process_message(self, message: str, task_id: Optional[str] = None) -> str:
"""
Process a message with memory-aware reasoning
Args:
message: User message to process
task_id: Optional task identifier for context
Returns:
Agent's response
"""
self.current_task = task_id or f"task_{int(time.time())}"
# Store the query in working memory
self.memory_store.add_memory(
memory_type="working",
content=message,
metadata={"task_id": self.current_task}
)
# Retrieve relevant memories
relevant_memories = self._retrieve_relevant_memories(message)
memory_context = self._format_memories_for_context(relevant_memories)
# Build messages with memory context
messages = [
{"role": "system", "content": self.system_prompt}
]
if memory_context:
messages.append({
"role": "system",
"content": memory_context
})
# Add conversation history
messages.extend(self.conversation_history)
messages.append({"role": "user", "content": message})
try:
# Call Kimi K3 model
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=_reasoning_safe_temperature(self.model, MODEL_TEMPERATURE),
max_tokens=MODEL_MAX_TOKENS,
top_p=MODEL_TOP_P
)
assistant_response = response.choices[0].message.content
# Store the interaction in episodic memory
self._store_interaction("user", message)
self._store_interaction("assistant", assistant_response)
# Update conversation history
self.conversation_history.append({"role": "user", "content": message})
self.conversation_history.append({"role": "assistant", "content": assistant_response})
# Keep conversation history manageable
if len(self.conversation_history) > 20:
# Move old conversations to episodic memory and compress
old_convs = self.conversation_history[:10]
for conv in old_convs:
self.memory_store.add_memory(
memory_type="episodic",
content=conv,
importance=0.5
)
self.conversation_history = self.conversation_history[10:]
return assistant_response
except Exception as e:
logger.error(f"Error processing message: {e}")
return f"Error: {str(e)}"
def execute_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
"""
Execute a LOCOMO benchmark task
Args:
task: Task dictionary with 'id', 'type', 'query', and optional 'context'
Returns:
Result dictionary with 'response', 'memories_used', 'execution_time'
"""
start_time = time.time()
task_id = task.get('id', f"task_{int(time.time())}")
task_type = task.get('type', 'unknown')
query = task['query']
context = task.get('context', '')
self.current_task = task_id
# Check for similar past tasks in procedural memory
similar_tasks = self.memory_store.search_memories(f"{task_type} {query[:50]}", limit=3)
# Build enhanced query with context
enhanced_query = query
if context:
enhanced_query = f"Context: {context}\n\nTask: {query}"
# Process the task
response = self.process_message(enhanced_query, task_id)
# Extract approach and outcome for learning
approach = f"Used {len(similar_tasks)} similar memories"
outcome = response[:100] # First 100 chars as outcome summary
# Learn from this task
self._learn_from_outcome(
task=task_type,
approach=approach,
outcome=outcome,
success=True # Would be determined by evaluation
)
execution_time = time.time() - start_time
return {
"task_id": task_id,
"response": response,
"memories_used": len(similar_tasks),
"execution_time": execution_time,
"memory_stats": {
"episodic": len(self.memory_store.memories.get('episodic', [])),
"semantic": len(self.memory_store.memories.get('semantic', [])),
"procedural": len(self.memory_store.memories.get('procedural', [])),
"working": len(self.memory_store.memories.get('working', []))
}
}
def consolidate_and_learn(self):
"""Consolidate memories and extract learnings"""
logger.info("Starting memory consolidation...")
# Consolidate memories
self.memory_store.consolidate_memories()
# Extract patterns from episodic memories
episodic_memories = self.memory_store.get_memories('episodic', limit=50)
# Group by task type and extract patterns
task_patterns = defaultdict(list)
for memory in episodic_memories:
if isinstance(memory.content, dict):
task = memory.content.get('task', 'unknown')
task_patterns[task].append(memory)
# Create procedural memories from patterns
for task_type, memories in task_patterns.items():
if len(memories) >= 3: # Need multiple examples to learn
# Extract common approach
pattern = {
"task_type": task_type,
"successful_approaches": [],
"common_challenges": [],
"learned_from": len(memories)
}
self.memory_store.add_memory(
memory_type="procedural",
content=pattern,
importance=2.0,
metadata={"consolidation_run": datetime.now().isoformat()}
)
# Clear working memory
self.memory_store.clear_working_memory()
logger.info("Memory consolidation complete")
def reset(self, keep_memories: bool = True):
"""Reset the agent state"""
self.conversation_history = []
self.current_task = None
self.task_context = {}
if not keep_memories:
self.memory_store = MemoryStore()
else:
# Only clear working memory
self.memory_store.clear_working_memory()
logger.info(f"Agent reset (memories kept: {keep_memories})")
def get_performance_metrics(self) -> Dict[str, Any]:
"""Get agent performance metrics"""
memory_stats = {
memory_type: len(memories)
for memory_type, memories in self.memory_store.memories.items()
}
total_memories = sum(memory_stats.values())
cluster_count = len(self.memory_store.clusters)
return {
"total_memories": total_memories,
"memory_distribution": memory_stats,
"clusters_created": cluster_count,
"conversation_length": len(self.conversation_history),
"current_task": self.current_task
}
config.py¶
"""
Configuration for Memobase Agent with Kimi K3 Model
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
def _openrouter_model_id(model) -> str:
"""Map a provider-native model name to an OpenRouter model id, used by the
universal OpenRouter fallback. An explicit OPENROUTER_MODEL env var wins."""
override = os.getenv("OPENROUTER_MODEL")
if override:
return override
m = (model or "").strip()
if not m:
return "openai/gpt-5.6-luna"
if "/" in m:
return m
ml = m.lower()
if ml.startswith(("gpt-", "o1", "o3", "o4", "chatgpt")):
return "openai/" + m
if ml.startswith("claude-"):
return "anthropic/claude-opus-4.8"
if ml.startswith("kimi"):
# kimi-k3 is not on OpenRouter; moonshotai/kimi-k2.6 is the closest hosted id.
return "moonshotai/kimi-k2.6"
return "openai/gpt-5.6-luna"
# Kimi K3 Model Configuration
KIMI_API_KEY = os.getenv("KIMI_API_KEY", "") or os.getenv("MOONSHOT_API_KEY", "")
KIMI_BASE_URL = "https://api.moonshot.cn/v1"
KIMI_MODEL = "kimi-k3" # Kimi K3 model identifier
# Universal OpenRouter fallback: primary key (KIMI/MOONSHOT) absent but
# OPENROUTER_API_KEY present -> route the chat LLM through OpenRouter.
if not KIMI_API_KEY and os.getenv("OPENROUTER_API_KEY"):
KIMI_API_KEY = os.getenv("OPENROUTER_API_KEY")
KIMI_BASE_URL = "https://openrouter.ai/api/v1"
KIMI_MODEL = _openrouter_model_id(KIMI_MODEL)
# Model Parameters
MODEL_TEMPERATURE = 0.7
MODEL_MAX_TOKENS = 4096
MODEL_TOP_P = 0.95
# Context Window Configuration
CONTEXT_WINDOW_SIZE = 128000 # Experiment context budget (K3 itself supports up to 1M tokens)
MAX_MEMORY_ENTRIES = 100
MEMORY_COMPRESSION_THRESHOLD = 50 # Compress when memory exceeds this
# Memobase Configuration
MEMOBASE_CONFIG = {
"memory_types": [
"episodic", # Task-specific memories
"semantic", # General knowledge
"procedural", # Learned procedures and patterns
"working" # Short-term working memory
],
"retention_policy": "adaptive", # adaptive, fixed, or decay
"compression_strategy": "hierarchical", # hierarchical, summary, or selective
"storage_backend": "local", # local, redis, or postgresql
}
# LOCOMO Benchmark Configuration
LOCOMO_CONFIG = {
"benchmark_path": Path("benchmarks/locomo"),
"evaluation_metrics": [
"task_completion",
"reasoning_accuracy",
"memory_utilization",
"context_efficiency",
"adaptation_score"
],
"task_categories": [
"multi_turn_reasoning",
"long_context_qa",
"task_planning",
"knowledge_integration",
"tool_usage"
],
"max_turns": 20,
"timeout_seconds": 300
}
# Memory Database Configuration
MEMORY_DB_PATH = Path("memory_store")
MEMORY_DB_PATH.mkdir(exist_ok=True)
# Logging Configuration
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
LOG_FILE = Path("logs") / "memobase_agent.log"
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# Agent Configuration
AGENT_CONFIG = {
"name": "MemobaseAgent",
"version": "1.0.0",
"capabilities": [
"long_term_memory",
"context_compression",
"adaptive_learning",
"tool_calling",
"multi_turn_reasoning"
],
"max_retries": 3,
"retry_delay": 1.0,
}
# Tool Configuration
ENABLE_WEB_SEARCH = True
ENABLE_CODE_EXECUTION = True
ENABLE_FILE_OPERATIONS = True
ENABLE_DATABASE_ACCESS = True
# Performance Optimization
BATCH_SIZE = 10
CACHE_ENABLED = True
CACHE_TTL = 3600 # 1 hour
PARALLEL_PROCESSING = True
MAX_WORKERS = 4
# Experimental Features
ENABLE_MEMORY_CONSOLIDATION = True # Consolidate memories during idle time
ENABLE_PREDICTIVE_CACHING = True # Pre-fetch likely needed memories
ENABLE_ADAPTIVE_COMPRESSION = True # Adjust compression based on usage patterns
locomo_benchmark.py¶
"""
LOCOMO Benchmark Implementation for Memobase Agent
Evaluates agent performance on long-context and memory-intensive tasks
"""
import json
import time
import logging
from typing import List, Dict, Any, Optional, Tuple
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from collections import defaultdict
import random
from config import LOCOMO_CONFIG, LOG_LEVEL, LOG_FORMAT
# Configure logging
logging.basicConfig(level=LOG_LEVEL, format=LOG_FORMAT)
logger = logging.getLogger(__name__)
@dataclass
class BenchmarkTask:
"""Represents a single benchmark task"""
id: str
category: str # multi_turn_reasoning, long_context_qa, etc.
query: str
context: Optional[str] = None
expected_capabilities: List[str] = field(default_factory=list)
ground_truth: Optional[Any] = None
max_turns: int = 1
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass
class TaskResult:
"""Result of executing a benchmark task"""
task_id: str
response: str
execution_time: float
memory_usage: Dict[str, int]
success: bool
score: float
turns_used: int
errors: List[str] = field(default_factory=list)
class LOCOMOBenchmark:
"""
LOCOMO (Long Context and Memory Optimization) Benchmark
Evaluates agent capabilities in handling long contexts and memory management
"""
def __init__(self, benchmark_path: Optional[Path] = None):
"""Initialize the benchmark suite"""
self.benchmark_path = benchmark_path or LOCOMO_CONFIG["benchmark_path"]
self.tasks: List[BenchmarkTask] = []
self.results: List[TaskResult] = []
self.metrics: Dict[str, Any] = defaultdict(list)
# Load or generate benchmark tasks
self._initialize_tasks()
logger.info(f"Initialized LOCOMO benchmark with {len(self.tasks)} tasks")
def _initialize_tasks(self):
"""Initialize benchmark tasks"""
# Try to load from file
if self.benchmark_path and (self.benchmark_path / "tasks.json").exists():
self._load_tasks_from_file()
else:
# Generate default benchmark tasks
self._generate_default_tasks()
def _load_tasks_from_file(self):
"""Load tasks from JSON file"""
try:
with open(self.benchmark_path / "tasks.json", 'r') as f:
tasks_data = json.load(f)
for task_dict in tasks_data:
self.tasks.append(BenchmarkTask(**task_dict))
logger.info(f"Loaded {len(self.tasks)} tasks from file")
except Exception as e:
logger.error(f"Failed to load tasks: {e}")
self._generate_default_tasks()
def _generate_default_tasks(self):
"""Generate default benchmark tasks for each category"""
# Multi-turn reasoning tasks
self.tasks.extend([
BenchmarkTask(
id="mtr_001",
category="multi_turn_reasoning",
query="Let's solve a complex problem step by step. First, calculate the compound interest on $10,000 at 5% annual rate for 3 years.",
expected_capabilities=["mathematical_reasoning", "multi_step_problem_solving"],
max_turns=5,
ground_truth=11576.25
),
BenchmarkTask(
id="mtr_002",
category="multi_turn_reasoning",
query="Plan a detailed itinerary for a 7-day trip to Japan, considering budget constraints of $3000.",
expected_capabilities=["planning", "constraint_satisfaction", "cultural_knowledge"],
max_turns=8
),
BenchmarkTask(
id="mtr_003",
category="multi_turn_reasoning",
query="Debug this code issue: A recursive function is causing a stack overflow. Help me identify and fix it step by step.",
expected_capabilities=["code_analysis", "debugging", "iterative_refinement"],
max_turns=6
)
])
# Long context Q&A tasks
self.tasks.extend([
BenchmarkTask(
id="lcqa_001",
category="long_context_qa",
query="Based on the provided research papers, summarize the key findings about climate change impacts on ocean acidification.",
context=self._generate_long_context("climate_research", 50000),
expected_capabilities=["information_extraction", "summarization", "scientific_reasoning"]
),
BenchmarkTask(
id="lcqa_002",
category="long_context_qa",
query="Analyze the financial statements and identify the top 3 risk factors for the company.",
context=self._generate_long_context("financial_report", 30000),
expected_capabilities=["financial_analysis", "risk_assessment", "data_interpretation"]
),
BenchmarkTask(
id="lcqa_003",
category="long_context_qa",
query="Review the legal documents and identify any potential conflicts or inconsistencies.",
context=self._generate_long_context("legal_document", 40000),
expected_capabilities=["legal_reasoning", "contradiction_detection", "document_analysis"]
)
])
# Task planning tasks
self.tasks.extend([
BenchmarkTask(
id="tp_001",
category="task_planning",
query="Create a detailed project plan for developing a mobile app, including timeline, resources, and milestones.",
expected_capabilities=["project_management", "resource_allocation", "timeline_planning"]
),
BenchmarkTask(
id="tp_002",
category="task_planning",
query="Design an optimal study plan for learning machine learning in 3 months with 2 hours daily.",
expected_capabilities=["curriculum_design", "learning_optimization", "scheduling"]
)
])
# Knowledge integration tasks
self.tasks.extend([
BenchmarkTask(
id="ki_001",
category="knowledge_integration",
query="Combine insights from psychology, neuroscience, and education to explain how humans learn languages.",
expected_capabilities=["interdisciplinary_thinking", "knowledge_synthesis", "conceptual_integration"]
),
BenchmarkTask(
id="ki_002",
category="knowledge_integration",
query="Integrate historical events, economic theories, and sociological concepts to analyze the 2008 financial crisis.",
expected_capabilities=["historical_analysis", "economic_reasoning", "systemic_thinking"]
)
])
# Tool usage tasks
self.tasks.extend([
BenchmarkTask(
id="tu_001",
category="tool_usage",
query="Use available tools to gather real-time weather data and create a 5-day forecast analysis.",
expected_capabilities=["tool_selection", "data_gathering", "predictive_analysis"]
),
BenchmarkTask(
id="tu_002",
category="tool_usage",
query="Research and compare the top 5 programming languages for web development using current data.",
expected_capabilities=["web_search", "comparative_analysis", "technology_assessment"]
)
])
logger.info(f"Generated {len(self.tasks)} default benchmark tasks")
def _generate_long_context(self, context_type: str, char_count: int) -> str:
"""Generate synthetic long context for testing"""
templates = {
"climate_research": [
"Recent studies on ocean acidification show significant changes in pH levels. ",
"The correlation between CO2 emissions and marine ecosystem degradation is evident. ",
"Temperature variations in deep ocean currents affect global climate patterns. ",
"Coral reef bleaching events have increased by 40% in the last decade. ",
"Phytoplankton populations show remarkable adaptation to changing conditions. "
],
"financial_report": [
"Revenue increased by 15% year-over-year, driven by strong product demand. ",
"Operating expenses rose due to increased R&D investments. ",
"Market volatility poses risks to future earnings projections. ",
"Cash flow remains strong with $2.3B in liquid assets. ",
"Debt-to-equity ratio improved to 0.8 from previous 1.2. "
],
"legal_document": [
"The party of the first part agrees to the terms specified in Section 3.2. ",
"Notwithstanding the above, exceptions may apply under force majeure. ",
"Intellectual property rights remain with the original creator as per Article 7. ",
"Dispute resolution shall follow binding arbitration procedures. ",
"Confidentiality clauses extend for 5 years post-termination. "
]
}
sentences = templates.get(context_type, templates["climate_research"])
context = ""
while len(context) < char_count:
context += random.choice(sentences)
# Add some variation
if random.random() > 0.7:
context += f"In {random.randint(2020, 2024)}, researchers found that "
return context[:char_count]
def evaluate_response(self, task: BenchmarkTask, response: str,
execution_time: float, memory_usage: Dict[str, int]) -> TaskResult:
"""
Evaluate agent response for a task
Args:
task: The benchmark task
response: Agent's response
execution_time: Time taken to generate response
memory_usage: Memory statistics
Returns:
TaskResult with evaluation metrics
"""
score = 0.0
success = False
errors = []
# Basic response validation
if not response or len(response) < 10:
errors.append("Response too short or empty")
score = 0.0
else:
# Category-specific evaluation
if task.category == "multi_turn_reasoning":
score = self._evaluate_reasoning(task, response)
elif task.category == "long_context_qa":
score = self._evaluate_context_qa(task, response)
elif task.category == "task_planning":
score = self._evaluate_planning(task, response)
elif task.category == "knowledge_integration":
score = self._evaluate_integration(task, response)
elif task.category == "tool_usage":
score = self._evaluate_tool_usage(task, response)
else:
score = self._evaluate_generic(task, response)
success = score >= 0.6 # 60% threshold for success
# Bonus for efficiency
if execution_time < 5.0:
score += 0.05
if sum(memory_usage.values()) < 100:
score += 0.05
score = min(1.0, score) # Cap at 1.0
return TaskResult(
task_id=task.id,
response=response,
execution_time=execution_time,
memory_usage=memory_usage,
success=success,
score=score,
turns_used=1, # Will be updated for multi-turn tasks
errors=errors
)
def _evaluate_reasoning(self, task: BenchmarkTask, response: str) -> float:
"""Evaluate multi-turn reasoning response"""
score = 0.0
# Check for step-by-step reasoning
if any(marker in response.lower() for marker in ["step 1", "first", "then", "finally"]):
score += 0.3
# Check for mathematical accuracy (if applicable)
if task.ground_truth and str(task.ground_truth) in response:
score += 0.4
# Check for logical flow
if len(response.split('\n')) > 3:
score += 0.2
# Check for conclusion
if any(word in response.lower() for word in ["therefore", "conclusion", "result"]):
score += 0.1
return score
def _evaluate_context_qa(self, task: BenchmarkTask, response: str) -> float:
"""Evaluate long context Q&A response"""
score = 0.0
# Check for relevant content extraction
keywords = ["finding", "summary", "key point", "important", "significant"]
keyword_matches = sum(1 for kw in keywords if kw in response.lower())
score += min(0.3, keyword_matches * 0.1)
# Check for structured response
if any(marker in response for marker in ["1.", "•", "-", "*"]):
score += 0.2
# Check response length (should be comprehensive but concise)
optimal_length = 500
length_ratio = min(len(response), optimal_length) / optimal_length
score += length_ratio * 0.3
# Check for citations or references to context
if any(phrase in response.lower() for phrase in ["according to", "based on", "the document states"]):
score += 0.2
return score
def _evaluate_planning(self, task: BenchmarkTask, response: str) -> float:
"""Evaluate task planning response"""
score = 0.0
# Check for timeline/schedule
if any(word in response.lower() for word in ["timeline", "schedule", "deadline", "milestone"]):
score += 0.25
# Check for resource consideration
if any(word in response.lower() for word in ["resource", "budget", "cost", "team"]):
score += 0.25
# Check for structured plan
if any(marker in response for marker in ["phase", "stage", "step"]):
score += 0.25
# Check for risk/contingency consideration
if any(word in response.lower() for word in ["risk", "contingency", "backup", "alternative"]):
score += 0.25
return score
def _evaluate_integration(self, task: BenchmarkTask, response: str) -> float:
"""Evaluate knowledge integration response"""
score = 0.0
# Check for multiple domain references
domains = ["psychology", "neuroscience", "education", "history", "economics", "sociology"]
domain_count = sum(1 for domain in domains if domain in response.lower())
score += min(0.4, domain_count * 0.2)
# Check for synthesis language
synthesis_words = ["integrate", "combine", "together", "relationship", "connection"]
if any(word in response.lower() for word in synthesis_words):
score += 0.3
# Check for depth of analysis
if len(response) > 300:
score += 0.3
return score
def _evaluate_tool_usage(self, task: BenchmarkTask, response: str) -> float:
"""Evaluate tool usage response"""
score = 0.0
# Check for tool mentions
if any(word in response.lower() for word in ["search", "query", "fetch", "retrieve", "analyze"]):
score += 0.3
# Check for data presentation
if any(marker in response for marker in ["data", "result", "finding", "information"]):
score += 0.3
# Check for analysis/interpretation
if any(word in response.lower() for word in ["analysis", "comparison", "trend", "pattern"]):
score += 0.4
return score
def _evaluate_generic(self, task: BenchmarkTask, response: str) -> float:
"""Generic evaluation for unspecified task types"""
score = 0.0
# Response length
if len(response) > 100:
score += 0.3
# Coherence (simple check)
if response.count('.') > 2:
score += 0.3
# Relevance (keyword matching)
query_words = task.query.lower().split()
if query_words:
matching_words = sum(1 for word in query_words if word in response.lower())
score += min(0.4, matching_words / len(query_words))
return score
def run_benchmark(self, agent, tasks: Optional[List[BenchmarkTask]] = None,
verbose: bool = True) -> Dict[str, Any]:
"""
Run benchmark evaluation on an agent
Args:
agent: The agent to evaluate (must have execute_task method)
tasks: Optional list of tasks (uses all if None)
verbose: Print progress information
Returns:
Benchmark results and metrics
"""
tasks_to_run = tasks or self.tasks
self.results = []
if verbose:
print(f"\n{'='*60}")
print(f"Running LOCOMO Benchmark: {len(tasks_to_run)} tasks")
print(f"{'='*60}\n")
for i, task in enumerate(tasks_to_run, 1):
if verbose:
print(f"[{i}/{len(tasks_to_run)}] Running task {task.id} ({task.category})...")
try:
# Execute task
start_time = time.time()
result = agent.execute_task({
"id": task.id,
"type": task.category,
"query": task.query,
"context": task.context
})
execution_time = time.time() - start_time
# Evaluate response
task_result = self.evaluate_response(
task=task,
response=result.get("response", ""),
execution_time=execution_time,
memory_usage=result.get("memory_stats", {})
)
self.results.append(task_result)
if verbose:
print(f" ✓ Score: {task_result.score:.2f} | Time: {execution_time:.2f}s | Success: {task_result.success}")
except Exception as e:
logger.error(f"Error running task {task.id}: {e}")
self.results.append(TaskResult(
task_id=task.id,
response="",
execution_time=0,
memory_usage={},
success=False,
score=0.0,
turns_used=0,
errors=[str(e)]
))
if verbose:
print(f" ✗ Error: {str(e)}")
# Calculate metrics
metrics = self._calculate_metrics()
if verbose:
self._print_summary(metrics)
return metrics
def _calculate_metrics(self) -> Dict[str, Any]:
"""Calculate benchmark metrics from results"""
if not self.results:
return {"error": "No results to calculate metrics"}
# Overall metrics
total_tasks = len(self.results)
successful_tasks = sum(1 for r in self.results if r.success)
avg_score = sum(r.score for r in self.results) / total_tasks
avg_time = sum(r.execution_time for r in self.results) / total_tasks
# Category-specific metrics
category_scores = defaultdict(list)
category_times = defaultdict(list)
for task, result in zip(self.tasks, self.results):
category_scores[task.category].append(result.score)
category_times[task.category].append(result.execution_time)
category_metrics = {}
for category in category_scores:
category_metrics[category] = {
"avg_score": sum(category_scores[category]) / len(category_scores[category]),
"avg_time": sum(category_times[category]) / len(category_times[category]),
"task_count": len(category_scores[category])
}
# Memory efficiency
total_memory = {}
for result in self.results:
for mem_type, count in result.memory_usage.items():
total_memory[mem_type] = total_memory.get(mem_type, 0) + count
return {
"overall": {
"total_tasks": total_tasks,
"successful_tasks": successful_tasks,
"success_rate": successful_tasks / total_tasks,
"average_score": avg_score,
"average_time": avg_time
},
"categories": category_metrics,
"memory_usage": total_memory,
"detailed_results": [
{
"task_id": r.task_id,
"score": r.score,
"success": r.success,
"time": r.execution_time,
"errors": r.errors
}
for r in self.results
]
}
def _print_summary(self, metrics: Dict[str, Any]):
"""Print benchmark summary"""
print(f"\n{'='*60}")
print("BENCHMARK SUMMARY")
print(f"{'='*60}")
overall = metrics["overall"]
print(f"\nOverall Performance:")
print(f" • Success Rate: {overall['success_rate']:.1%}")
print(f" • Average Score: {overall['average_score']:.2f}/1.00")
print(f" • Average Time: {overall['average_time']:.2f}s")
print(f" • Tasks Completed: {overall['successful_tasks']}/{overall['total_tasks']}")
print(f"\nCategory Breakdown:")
for category, cat_metrics in metrics["categories"].items():
print(f" {category}:")
print(f" - Score: {cat_metrics['avg_score']:.2f}")
print(f" - Time: {cat_metrics['avg_time']:.2f}s")
print(f" - Tasks: {cat_metrics['task_count']}")
print(f"\nMemory Usage:")
for mem_type, count in metrics["memory_usage"].items():
print(f" • {mem_type}: {count} entries")
print(f"\n{'='*60}\n")
def save_results(self, filepath: Path):
"""Save benchmark results to file"""
results_data = {
"timestamp": datetime.now().isoformat(),
"tasks": [
{
"id": task.id,
"category": task.category,
"query": task.query[:100] # Truncate for storage
}
for task in self.tasks
],
"results": [
{
"task_id": r.task_id,
"score": r.score,
"success": r.success,
"execution_time": r.execution_time,
"memory_usage": r.memory_usage,
"turns_used": r.turns_used,
"errors": r.errors
}
for r in self.results
],
"metrics": self._calculate_metrics()
}
with open(filepath, 'w') as f:
json.dump(results_data, f, indent=2)
logger.info(f"Saved benchmark results to {filepath}")
main.py¶
"""
Main entry point for Memobase Agent with LOCOMO Benchmark
"""
import argparse
import logging
import sys
from pathlib import Path
from datetime import datetime
import json
from typing import Optional
from agent import MemobaseAgent
from locomo_benchmark import LOCOMOBenchmark, BenchmarkTask
from config import (
KIMI_API_KEY, LOG_LEVEL, LOG_FORMAT,
MEMORY_DB_PATH, LOCOMO_CONFIG
)
# Configure logging
logging.basicConfig(level=LOG_LEVEL, format=LOG_FORMAT)
logger = logging.getLogger(__name__)
def run_interactive_mode(agent: MemobaseAgent):
"""Run agent in interactive mode"""
print("\n" + "="*60)
print("MEMOBASE AGENT - Interactive Mode")
print("="*60)
print("\nCommands:")
print(" /help - Show this help message")
print(" /memory - Show memory statistics")
print(" /clear - Clear working memory")
print(" /reset - Reset agent (keep long-term memories)")
print(" /learn - Trigger memory consolidation")
print(" /exit - Exit interactive mode")
print("\nType your message or command:\n")
while True:
try:
user_input = input("\n👤 You: ").strip()
if not user_input:
continue
# Handle commands
if user_input.startswith('/'):
command = user_input.lower()
if command == '/help':
print("\nAvailable commands:")
print(" /memory - Display memory statistics")
print(" /clear - Clear working memory")
print(" /reset - Reset conversation (keep memories)")
print(" /learn - Consolidate and learn from memories")
print(" /exit - Exit interactive mode")
elif command == '/memory':
metrics = agent.get_performance_metrics()
print("\n📊 Memory Statistics:")
print(f" Total memories: {metrics['total_memories']}")
for mem_type, count in metrics['memory_distribution'].items():
print(f" • {mem_type}: {count}")
print(f" Clusters created: {metrics['clusters_created']}")
print(f" Conversation length: {metrics['conversation_length']}")
elif command == '/clear':
agent.memory_store.clear_working_memory()
print("✅ Working memory cleared")
elif command == '/reset':
agent.reset(keep_memories=True)
print("✅ Agent reset (memories preserved)")
elif command == '/learn':
print("🧠 Consolidating memories...")
agent.consolidate_and_learn()
print("✅ Memory consolidation complete")
elif command == '/exit':
print("\n👋 Goodbye!")
break
else:
print(f"❌ Unknown command: {command}")
print(" Type /help for available commands")
else:
# Process regular message
print("\n🤖 Assistant: ", end="", flush=True)
response = agent.process_message(user_input)
print(response)
except KeyboardInterrupt:
print("\n\n👋 Interrupted. Goodbye!")
break
except Exception as e:
logger.error(f"Error in interactive mode: {e}")
print(f"\n❌ Error: {str(e)}")
def run_benchmark_mode(agent: MemobaseAgent,
category: Optional[str] = None,
num_tasks: Optional[int] = None):
"""Run LOCOMO benchmark evaluation"""
print("\n" + "="*60)
print("LOCOMO BENCHMARK EVALUATION")
print("="*60)
# Initialize benchmark
benchmark = LOCOMOBenchmark()
# Filter tasks if category specified
tasks = benchmark.tasks
if category:
tasks = [t for t in tasks if t.category == category]
print(f"\nRunning {category} tasks only")
# Limit number of tasks if specified
if num_tasks:
tasks = tasks[:num_tasks]
print(f"Limited to {num_tasks} tasks")
# Run benchmark
results = benchmark.run_benchmark(agent, tasks=tasks, verbose=True)
# Save results
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
results_dir = Path("benchmark_results")
results_dir.mkdir(exist_ok=True)
results_file = results_dir / f"locomo_results_{timestamp}.json"
benchmark.save_results(results_file)
print(f"\n📊 Results saved to: {results_file}")
return results
def run_demo_mode(agent: MemobaseAgent):
"""Run demonstration scenarios"""
print("\n" + "="*60)
print("MEMOBASE AGENT - Demo Mode")
print("="*60)
demos = [
{
"name": "Memory Retention Demo",
"messages": [
"Remember that my favorite programming language is Python and I prefer functional programming style.",
"What's my favorite programming language?",
"I'm working on a new project. What programming style should I use?"
]
},
{
"name": "Learning from Experience Demo",
"messages": [
"Help me debug this issue: my recursive function is causing a stack overflow.",
"I have another recursive function problem. What should I check first?",
"What are common causes of stack overflow in recursive functions?"
]
},
{
"name": "Context Management Demo",
"messages": [
"Let's plan a machine learning project. We need data collection, preprocessing, model training, and deployment phases.",
"What were the phases we discussed for the ML project?",
"Add evaluation phase between training and deployment.",
"Now summarize the complete project plan with all phases."
]
}
]
for demo in demos:
print(f"\n\n{'='*40}")
print(f"Demo: {demo['name']}")
print(f"{'='*40}")
for i, message in enumerate(demo['messages'], 1):
print(f"\n[{i}/{len(demo['messages'])}] User: {message}")
response = agent.process_message(message)
print(f"Assistant: {response}")
# Show memory stats after each interaction
metrics = agent.get_performance_metrics()
print(f"\n📊 Memory: {metrics['total_memories']} total, Distribution: {metrics['memory_distribution']}")
# Reset for next demo but keep memories
agent.reset(keep_memories=True)
# Final memory consolidation
print("\n\n🧠 Consolidating learnings from all demos...")
agent.consolidate_and_learn()
final_metrics = agent.get_performance_metrics()
print(f"\n📊 Final Memory Statistics:")
print(f" Total memories: {final_metrics['total_memories']}")
for mem_type, count in final_metrics['memory_distribution'].items():
print(f" • {mem_type}: {count}")
print(f" Clusters created: {final_metrics['clusters_created']}")
def run_single_task(agent: MemobaseAgent, task_query: str):
"""Run a single task"""
print("\n" + "="*60)
print("SINGLE TASK EXECUTION")
print("="*60)
print(f"\n📝 Task: {task_query}")
result = agent.execute_task({
"id": f"custom_{int(datetime.now().timestamp())}",
"type": "custom",
"query": task_query
})
print(f"\n🤖 Response: {result['response']}")
print(f"\n📊 Execution Statistics:")
print(f" • Execution time: {result['execution_time']:.2f}s")
print(f" • Memories used: {result['memories_used']}")
print(f" • Memory distribution: {result['memory_stats']}")
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
description="Memobase Agent with LOCOMO Benchmark"
)
parser.add_argument(
"--mode",
choices=["interactive", "benchmark", "demo", "task"],
default="interactive",
help="Execution mode"
)
parser.add_argument(
"--api-key",
type=str,
default=KIMI_API_KEY,
help="Kimi API key"
)
parser.add_argument(
"--category",
type=str,
choices=["multi_turn_reasoning", "long_context_qa", "task_planning",
"knowledge_integration", "tool_usage"],
help="Benchmark category to run (benchmark mode only)"
)
parser.add_argument(
"--num-tasks",
type=int,
help="Number of benchmark tasks to run"
)
parser.add_argument(
"--task",
type=str,
help="Single task query to execute (task mode only)"
)
parser.add_argument(
"--no-memory",
action="store_true",
help="Start with empty memory store"
)
parser.add_argument(
"--verbose",
action="store_true",
help="Enable verbose logging"
)
args = parser.parse_args()
# Set logging level
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
# Initialize agent
print("\n🚀 Initializing Memobase Agent with Kimi K3...")
agent = MemobaseAgent(api_key=args.api_key)
if args.no_memory:
agent.reset(keep_memories=False)
print(" Started with empty memory store")
# Run selected mode
try:
if args.mode == "interactive":
run_interactive_mode(agent)
elif args.mode == "benchmark":
results = run_benchmark_mode(
agent,
category=args.category,
num_tasks=args.num_tasks
)
elif args.mode == "demo":
run_demo_mode(agent)
elif args.mode == "task":
if not args.task:
print("❌ Error: --task argument required for task mode")
sys.exit(1)
run_single_task(agent, args.task)
except KeyboardInterrupt:
print("\n\n👋 Interrupted. Goodbye!")
except Exception as e:
logger.error(f"Fatal error: {e}", exc_info=True)
print(f"\n❌ Fatal error: {str(e)}")
sys.exit(1)
# Final cleanup
print("\n💾 Saving final memory state...")
agent.memory_store._save_memories()
print("✅ Memory state saved")
if __name__ == "__main__":
main()
profile_demo.py¶
"""
Memobase 用户画像(Profile)+ 事件记忆(Event Memory)演示
对应《深入理解 AI Agent》第 3 章"记忆框架案例"中对 Memobase 的介绍:
Memobase(开源项目 memodb-io/memobase)把用户记忆组织为两部分——
* 用户画像(Profile):按"主题—子主题"两级组织的稳定用户属性
(如 basic_info→姓名、interest→游戏偏好、work→职位),从对话中提取;
* 事件记忆(Event Memory):按时间线记录用户经历的事件,
用于回答"我们上次讨论预算是什么时候"这类与时间有关的问题。
工程上 Memobase 采用"缓冲批处理":对话先 insert 进缓冲区累积,
flush 时才统一触发一次 LLM 记忆提取,查询侧(profile/event/context)
只读取已整理好的结果,保证低延迟。
本脚本用真实的 memobase Python SDK 演示这条链路:
insert(写入对话缓冲)→ flush(触发提取)→ profile / event / context(召回)
运行前提:Memobase 需要一个正在运行的服务端 + 用于提取记忆的 LLM。
* 自托管:见 https://github.com/memodb-io/memobase (docker compose 启动,
默认服务地址 http://localhost:8019,默认 token 为 secret);
* 云服务:在 https://www.memobase.io 申请 project_url 与 api_key。
无服务端时可用 --dry-run 查看示例对话与将要执行的操作(不联网、不伪造结果)。
"""
import argparse
import json
import os
import sys
from pathlib import Path
# 示例多轮对话:内容刻意覆盖三个画像主题,便于观察 Profile 的两级结构
SAMPLE_CONVERSATION = [
{"role": "user", "content": "你好,我叫李明,今年 28 岁,住在上海。"},
{"role": "assistant", "content": "你好李明,很高兴认识你!有什么可以帮你的吗?"},
{"role": "user", "content": "我在一家游戏公司做后端工程师,主要用 Go 语言。"},
{"role": "assistant", "content": "后端工程师很酷,Go 在高并发服务里很常用。"},
{"role": "user", "content": "平时最喜欢玩《塞尔达传说》和《艾尔登法环》这类开放世界游戏。"},
{"role": "assistant", "content": "开放世界游戏的探索感确实很棒。"},
{"role": "user", "content": "对了,上周我们把新项目的预算定在了 50 万。"},
{"role": "assistant", "content": "好的,我记住了新项目预算是 50 万。"},
]
# 期望被提取出的画像主题(仅用于 --dry-run 的结构说明,非真实结果)
EXPECTED_TOPICS = {
"basic_info": ["姓名", "年龄", "城市"],
"work": ["职位", "公司", "技术栈"],
"interest": ["游戏偏好"],
}
DEFAULT_PROJECT_URL = os.getenv("MEMOBASE_PROJECT_URL", "http://localhost:8019")
DEFAULT_API_KEY = os.getenv("MEMOBASE_API_KEY", "secret")
def load_conversation(input_path: str | None) -> list[dict]:
"""从 --input 指定的 JSON 文件读取对话,缺省则用内置示例对话。
文件格式:[{"role": "user"|"assistant", "content": "..."}, ...]
"""
if not input_path:
return SAMPLE_CONVERSATION
data = json.loads(Path(input_path).read_text(encoding="utf-8"))
if not isinstance(data, list) or not all(
isinstance(m, dict) and "role" in m and "content" in m for m in data
):
raise ValueError("对话文件应为 [{'role':..., 'content':...}, ...] 形式的 JSON")
return data
def print_conversation(conversation: list[dict]) -> None:
print("\n💬 对话内容:")
for m in conversation:
who = "👤 用户" if m["role"] == "user" else "🤖 助手"
print(f" {who}: {m['content']}")
def build_client(args):
"""构造并连通 Memobase 客户端(联网)。失败时给出可操作的提示。"""
try:
from memobase import MemoBaseClient
except ImportError:
print("❌ 未安装 memobase SDK,请先运行:pip install -r requirements.txt")
sys.exit(1)
client = MemoBaseClient(project_url=args.project_url, api_key=args.api_key)
try:
reachable = client.ping()
except Exception as exc: # 连接被拒绝、超时、DNS 失败等
reachable = False
print(f"❌ 连接 Memobase 服务端时出错:{exc}")
if not reachable:
print("❌ 无法连接 Memobase 服务端:", args.project_url)
print(" 请确认服务已启动(自托管见 memodb-io/memobase 的 docker compose),")
print(" 或用 --project-url / --api-key 指定云服务地址与密钥。")
print(" 仅想查看示例与流程(不联网)可加 --dry-run。")
sys.exit(1)
return client
def render_profiles(profiles) -> list[dict]:
"""把 SDK 返回的 UserProfile 列表整理成 主题→子主题→内容 的结构。"""
rows = []
for p in profiles:
rows.append({"topic": p.topic, "sub_topic": p.sub_topic, "content": p.content})
return rows
def render_events(events) -> list[dict]:
"""把 SDK 返回的 UserEventData 列表整理成可读的时间线。"""
rows = []
for e in events:
tip = None
tags = None
if e.event_data is not None:
tip = e.event_data.event_tip
if e.event_data.event_tags:
tags = {t.tag: t.value for t in e.event_data.event_tags}
rows.append(
{
"created_at": str(e.created_at),
"event_tip": tip,
"event_tags": tags,
}
)
return rows
def op_insert(user, conversation) -> None:
from memobase import ChatBlob
print_conversation(conversation)
bid = user.insert(ChatBlob(messages=conversation))
print(f"\n✅ 已写入对话缓冲区(blob id: {bid})")
print(" 提示:写入只进缓冲区,尚未提取记忆;执行 flush 才会触发一次提取。")
def op_flush(user) -> None:
print("\n🧠 正在 flush 缓冲区,触发记忆提取(由服务端 LLM 完成,可能需要数秒)...")
ok = user.flush(sync=True)
print("✅ 记忆提取完成" if ok else "⚠️ flush 返回 False,请检查服务端日志")
def op_profile(user) -> list[dict]:
profiles = render_profiles(user.profile())
print("\n📇 用户画像(Profile,主题 → 子主题 → 内容):")
if not profiles:
print(" (暂无画像,请先 insert 对话并 flush 提取)")
for r in profiles:
print(f" • [{r['topic']}] {r['sub_topic']}: {r['content']}")
return profiles
def op_event(user) -> list[dict]:
events = render_events(user.event())
print("\n🗓️ 事件记忆(Event Memory,按时间线):")
if not events:
print(" (暂无事件)")
for r in events:
print(f" • {r['created_at']} {r['event_tip'] or ''}")
if r["event_tags"]:
print(f" 标签: {r['event_tags']}")
return events
def op_context(user) -> str:
ctx = user.context()
print("\n🧩 组装好的记忆上下文(context,可直接拼进 LLM 提示词):")
print(ctx if ctx else " (空)")
return ctx
def run_demo(user, conversation, output_path):
"""端到端演示:从对话构建画像,再召回画像 / 事件 / 上下文。"""
print("=" * 64)
print("Memobase 用户画像 + 事件记忆 端到端演示")
print("=" * 64)
op_insert(user, conversation)
op_flush(user)
profiles = op_profile(user)
events = op_event(user)
ctx = op_context(user)
result = {"profiles": profiles, "events": events, "context": ctx}
if output_path:
Path(output_path).write_text(
json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8"
)
print(f"\n💾 结果已写入:{output_path}")
return result
def run_dry_run(conversation):
"""离线路径:不联网,展示示例对话、缓冲批处理流程与期望画像结构。"""
print("=" * 64)
print("Memobase 演示(--dry-run 离线预览,不连接服务端、不伪造结果)")
print("=" * 64)
print_conversation(conversation)
print("\n🔀 将要执行的操作(缓冲批处理流水线):")
print(" 1) insert —— 把上述对话写入用户缓冲区(不触发提取)")
print(" 2) flush —— 统一触发一次 LLM 记忆提取(摊薄调用成本)")
print(" 3) profile —— 读取提取出的结构化用户画像")
print(" 4) event —— 读取按时间线组织的事件记忆")
print(" 5) context —— 读取组装好的记忆上下文")
print("\n🧬 期望被提取的画像结构(主题 → 子主题,示意,非真实结果):")
for topic, subs in EXPECTED_TOPICS.items():
print(f" • {topic}: {', '.join(subs)}")
print("\n▶️ 接入真实服务后,去掉 --dry-run 即可执行上述完整链路。")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Memobase 用户画像(Profile)+ 事件记忆(Event Memory)演示",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"示例:\n"
" python profile_demo.py # 端到端演示(内置示例对话)\n"
" python profile_demo.py --dry-run # 离线预览流程,不连接服务端\n"
" python profile_demo.py --op profile # 只召回已提取的用户画像\n"
" python profile_demo.py --op event # 只查看事件记忆时间线\n"
" python profile_demo.py --input chat.json --output result.json\n"
),
)
parser.add_argument(
"--op",
choices=["demo", "insert", "flush", "profile", "event", "context", "reset"],
default="demo",
help="记忆操作:demo=端到端演示(默认);insert=写入对话缓冲;"
"flush=触发提取;profile=召回画像;event=事件记忆;"
"context=记忆上下文;reset=删除该用户重置",
)
parser.add_argument(
"--input",
type=str,
default=None,
help="对话输入文件(JSON:[{'role','content'}, ...]),缺省用内置示例对话",
)
parser.add_argument(
"--user-id",
type=str,
default="demo_user_liming",
help="用户 ID(默认 demo_user_liming)",
)
parser.add_argument(
"--project-url",
type=str,
default=DEFAULT_PROJECT_URL,
help=f"Memobase 服务地址(默认 {DEFAULT_PROJECT_URL},可用环境变量 MEMOBASE_PROJECT_URL)",
)
parser.add_argument(
"--api-key",
type=str,
default=DEFAULT_API_KEY,
help="Memobase 访问密钥(默认取环境变量 MEMOBASE_API_KEY,自托管默认 secret)",
)
parser.add_argument(
"--model",
type=str,
default=None,
help="记忆提取所用 LLM(仅作说明:Memobase 由服务端配置提取模型,"
"客户端不直接指定;如需更换请改服务端 .env)",
)
parser.add_argument(
"--output",
type=str,
default=None,
help="将画像/事件/上下文结果写入指定 JSON 文件",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="离线预览:仅展示示例对话与操作流程,不连接服务端、不伪造结果",
)
return parser
def main():
args = build_parser().parse_args()
try:
conversation = load_conversation(args.input)
except (OSError, ValueError, json.JSONDecodeError) as exc:
print(f"❌ 读取对话文件失败({args.input}):{exc}")
sys.exit(1)
if args.dry_run:
run_dry_run(conversation)
return
if args.model:
print(f"ℹ️ --model={args.model}:Memobase 的提取模型由服务端配置,此处仅作记录。")
client = build_client(args)
user = client.get_or_create_user(args.user_id)
if args.op == "demo":
run_demo(user, conversation, args.output)
return
result = None
if args.op == "insert":
op_insert(user, conversation)
elif args.op == "flush":
op_flush(user)
elif args.op == "profile":
result = {"profiles": op_profile(user)}
elif args.op == "event":
result = {"events": op_event(user)}
elif args.op == "context":
result = {"context": op_context(user)}
elif args.op == "reset":
client.delete_user(args.user_id)
print(f"🧹 已删除用户 {args.user_id},画像与事件已清空。")
if result and args.output:
Path(args.output).write_text(
json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8"
)
print(f"\n💾 结果已写入:{args.output}")
if __name__ == "__main__":
main()
quickstart.py¶
"""
Quick start script for testing Memobase Agent
"""
import os
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent))
from agent import MemobaseAgent
from locomo_benchmark import LOCOMOBenchmark, BenchmarkTask
def quick_demo():
"""Run a quick demonstration of the agent's capabilities"""
print("=" * 60)
print("MEMOBASE AGENT - Quick Start Demo")
print("=" * 60)
# Check for API key
api_key = os.getenv("KIMI_API_KEY", "")
if not api_key or api_key == "sk-your-kimi-api-key-here":
print("\n⚠️ Warning: KIMI_API_KEY not set properly!")
print("Please set your API key in .env file or as environment variable")
print("\nContinuing with demo setup...")
api_key = "demo-key" # Use demo key for structure demonstration
try:
# Initialize agent
print("\n🚀 Initializing Memobase Agent...")
agent = MemobaseAgent(api_key=api_key)
print("✅ Agent initialized successfully!")
# Show initial memory state
metrics = agent.get_performance_metrics()
print(f"\n📊 Initial Memory State:")
print(f" • Total memories: {metrics['total_memories']}")
print(f" • Memory types: {list(metrics['memory_distribution'].keys())}")
# Demo 1: Simple interaction with memory
print("\n" + "-" * 40)
print("Demo 1: Memory Storage and Retrieval")
print("-" * 40)
test_messages = [
"Remember that I prefer Python for data science and JavaScript for web development.",
"What programming language should I use for data analysis?",
"What about for building a web application?"
]
for msg in test_messages:
print(f"\n👤 User: {msg}")
if api_key != "demo-key":
response = agent.process_message(msg)
print(f"🤖 Agent: {response}")
else:
print("🤖 Agent: [Demo mode - API key required for actual response]")
# Show memory growth
metrics = agent.get_performance_metrics()
print(f" 📊 Memories: {metrics['total_memories']} total")
# Demo 2: Learning from experience
print("\n" + "-" * 40)
print("Demo 2: Learning from Experience")
print("-" * 40)
# Simulate learning
agent._learn_from_outcome(
task="debugging",
approach="Check for infinite loops in recursive functions",
outcome="Successfully identified stack overflow cause",
success=True
)
print("✅ Learned from debugging experience")
agent._learn_from_outcome(
task="optimization",
approach="Use memoization for recursive algorithms",
outcome="Reduced computation time by 80%",
success=True
)
print("✅ Learned optimization technique")
# Show procedural memories
procedural_memories = agent.memory_store.get_memories("procedural", limit=5)
print(f"\n📚 Procedural Knowledge Acquired: {len(procedural_memories)} patterns")
# Demo 3: Memory consolidation
print("\n" + "-" * 40)
print("Demo 3: Memory Consolidation")
print("-" * 40)
print("🧠 Triggering memory consolidation...")
agent.consolidate_and_learn()
print("✅ Consolidation complete")
# Final memory statistics
final_metrics = agent.get_performance_metrics()
print(f"\n📊 Final Memory Statistics:")
print(f" • Total memories: {final_metrics['total_memories']}")
for mem_type, count in final_metrics['memory_distribution'].items():
print(f" - {mem_type}: {count}")
print(f" • Memory clusters: {final_metrics['clusters_created']}")
# Demo 4: Mini benchmark
print("\n" + "-" * 40)
print("Demo 4: Mini Benchmark Test")
print("-" * 40)
# Create a simple benchmark task
task = BenchmarkTask(
id="demo_001",
category="multi_turn_reasoning",
query="What are the key factors to consider when choosing a database for a web application?",
expected_capabilities=["technical_knowledge", "comparative_analysis"]
)
print(f"📝 Task: {task.query}")
if api_key != "demo-key":
# Initialize mini benchmark
benchmark = LOCOMOBenchmark()
benchmark.tasks = [task]
# Run benchmark
print("🏃 Running benchmark...")
results = benchmark.run_benchmark(agent, tasks=[task], verbose=False)
# Show results
print(f"\n📊 Benchmark Results:")
print(f" • Score: {results['overall']['average_score']:.2f}/1.00")
print(f" • Time: {results['overall']['average_time']:.2f}s")
print(f" • Success: {results['overall']['success_rate']:.0%}")
else:
print("📊 [Demo mode - API key required for benchmark execution]")
print("\n" + "=" * 60)
print("✅ Quick Start Demo Complete!")
print("=" * 60)
print("\nNext steps:")
print("1. Set your KIMI_API_KEY in .env file")
print("2. Run 'python main.py --mode interactive' for full interaction")
print("3. Run 'python main.py --mode benchmark' for complete evaluation")
print("4. Check README.md for detailed documentation")
except Exception as e:
print(f"\n❌ Error during demo: {str(e)}")
print("\nTroubleshooting:")
print("1. Ensure all dependencies are installed: pip install -r requirements.txt")
print("2. Check that KIMI_API_KEY is properly set")
print("3. Verify network connectivity for API calls")
if __name__ == "__main__":
quick_demo()
test_evaluate_generic_empty_query.py¶
"""Regression tests: benchmark evaluation must not raise ZeroDivisionError on
tasks with an empty/whitespace query (e.g. loaded from tasks.json) that fall
through to the generic evaluator."""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from locomo_benchmark import BenchmarkTask, LOCOMOBenchmark
LONG_RESPONSE = (
"This is a reasonably long agent response. It has several sentences. "
"And it keeps going with more detail. Even more detail here."
)
def _suite():
return LOCOMOBenchmark.__new__(LOCOMOBenchmark)
def test_evaluate_generic_empty_query():
task = BenchmarkTask(id="t1", category="custom_category", query="")
score = _suite()._evaluate_generic(task, LONG_RESPONSE)
assert 0.0 <= score <= 1.0
def test_evaluate_response_whitespace_query_unknown_category():
task = BenchmarkTask(id="t2", category="custom_category", query=" ")
result = _suite().evaluate_response(
task, response=LONG_RESPONSE, execution_time=1.0, memory_usage={}
)
assert 0.0 <= result.score <= 1.0
def test_evaluate_generic_normal_query_unchanged():
task = BenchmarkTask(id="t3", category="custom_category", query="weather forecast analysis")
score = _suite()._evaluate_generic(task, LONG_RESPONSE.replace("agent", "weather"))
assert score > 0.0
test_memory.py¶
"""
Test script for Memobase memory functionality
"""
import os
import sys
import time
from pathlib import Path
from datetime import datetime
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent))
from agent import Memory, MemoryStore, MemoryCluster, MemobaseAgent
from config import MEMORY_DB_PATH
def test_memory_basics():
"""Test basic memory operations"""
print("\n" + "=" * 50)
print("Testing Basic Memory Operations")
print("=" * 50)
# Create a memory
memory = Memory(
id="",
type="episodic",
content={"event": "User asked about Python", "response": "Python is great for data science"},
importance_score=2.0
)
print(f"✅ Created memory with ID: {memory.id}")
print(f" Type: {memory.type}")
print(f" Importance: {memory.importance_score}")
# Test access
initial_count = memory.access_count
memory.access()
print(f"✅ Memory accessed: count {initial_count} -> {memory.access_count}")
# Test decay
initial_importance = memory.importance_score
time.sleep(0.1) # Small delay
memory.decay()
print(f"✅ Memory decayed: importance {initial_importance:.2f} -> {memory.importance_score:.2f}")
return True
def test_memory_store():
"""Test memory store operations"""
print("\n" + "=" * 50)
print("Testing Memory Store")
print("=" * 50)
# Create temporary store
test_path = Path("test_memory_store")
test_path.mkdir(exist_ok=True)
store = MemoryStore(db_path=test_path)
# Add memories of different types
memories_added = []
# Episodic memory
mem1 = store.add_memory(
memory_type="episodic",
content="First interaction with user about machine learning",
metadata={"session": "test_001"},
importance=2.0
)
memories_added.append(mem1)
print(f"✅ Added episodic memory: {mem1.id}")
# Semantic memory
mem2 = store.add_memory(
memory_type="semantic",
content="Python is a programming language used for data science",
metadata={"domain": "programming"},
importance=1.5
)
memories_added.append(mem2)
print(f"✅ Added semantic memory: {mem2.id}")
# Procedural memory
mem3 = store.add_memory(
memory_type="procedural",
content={"pattern": "debugging", "approach": "check logs first"},
metadata={"learned_from": "experience"},
importance=3.0
)
memories_added.append(mem3)
print(f"✅ Added procedural memory: {mem3.id}")
# Working memory
mem4 = store.add_memory(
memory_type="working",
content="Current task: testing memory system",
importance=1.0
)
memories_added.append(mem4)
print(f"✅ Added working memory: {mem4.id}")
# Test retrieval
print("\n📚 Testing Memory Retrieval:")
# Get episodic memories
episodic = store.get_memories("episodic", limit=5)
print(f" Episodic memories retrieved: {len(episodic)}")
# Search memories
search_results = store.search_memories("Python", limit=3)
print(f" Search for 'Python': {len(search_results)} results")
# Test persistence
store._save_memories()
print("✅ Memories saved to disk")
# Create new store and load
store2 = MemoryStore(db_path=test_path)
total_memories = sum(len(mems) for mems in store2.memories.values())
print(f"✅ Memories loaded from disk: {total_memories} total")
# Test consolidation
store.consolidate_memories()
print("✅ Memory consolidation completed")
# Clear working memory
store.clear_working_memory()
working_after = len(store.memories.get('working', []))
print(f"✅ Working memory cleared: {working_after} remaining")
# Cleanup
import shutil
shutil.rmtree(test_path)
print("✅ Test data cleaned up")
return True
def test_memory_compression():
"""Test memory compression functionality"""
print("\n" + "=" * 50)
print("Testing Memory Compression")
print("=" * 50)
# Create store with low threshold for testing
test_path = Path("test_compression")
test_path.mkdir(exist_ok=True)
store = MemoryStore(db_path=test_path)
# Add many memories to trigger compression
print("Adding memories to trigger compression...")
for i in range(10):
store.add_memory(
memory_type="episodic",
content=f"Memory {i}: Event occurred at time {i}",
importance=1.0 if i < 5 else 2.0
)
# Force compression by adding more with low threshold
original_threshold = 50 # From config
test_threshold = 5
# Manually trigger compression
if len(store.memories['episodic']) > test_threshold:
print(f" Memories before compression: {len(store.memories['episodic'])}")
store._compress_memories('episodic')
print(f" Memories after compression: {len(store.memories['episodic'])}")
print(f" Clusters created: {len(store.clusters)}")
print("✅ Compression test completed")
# Cleanup
import shutil
shutil.rmtree(test_path)
return True
def test_agent_memory_integration():
"""Test agent integration with memory system"""
print("\n" + "=" * 50)
print("Testing Agent Memory Integration")
print("=" * 50)
# Check for API key
api_key = os.getenv("KIMI_API_KEY", "test-key")
# Initialize agent
agent = MemobaseAgent(api_key=api_key)
print("✅ Agent initialized with memory system")
# Test memory operations through agent
print("\n📊 Initial state:")
metrics = agent.get_performance_metrics()
print(f" Total memories: {metrics['total_memories']}")
print(f" Distribution: {metrics['memory_distribution']}")
# Simulate learning
agent._learn_from_outcome(
task="test_task",
approach="test_approach",
outcome="successful",
success=True
)
print("✅ Agent learned from outcome")
# Check procedural memory
procedural = agent.memory_store.get_memories("procedural", limit=1)
if procedural:
print(f"✅ Procedural memory created: {procedural[0].id}")
# Test memory retrieval in context
relevant = agent._retrieve_relevant_memories("test query", limit=3)
print(f"✅ Retrieved {len(relevant)} relevant memories")
# Test consolidation
agent.consolidate_and_learn()
print("✅ Agent consolidation completed")
# Test reset with memory preservation
agent.reset(keep_memories=True)
metrics_after = agent.get_performance_metrics()
print(f"✅ Agent reset (memories preserved): {metrics_after['total_memories']} memories retained")
# Test reset without memory preservation
agent.reset(keep_memories=False)
metrics_final = agent.get_performance_metrics()
print(f"✅ Agent reset (memories cleared): {metrics_final['total_memories']} memories remaining")
return True
def run_all_tests():
"""Run all memory tests"""
print("\n" + "=" * 60)
print("MEMOBASE MEMORY SYSTEM TEST SUITE")
print("=" * 60)
tests = [
("Basic Memory Operations", test_memory_basics),
("Memory Store", test_memory_store),
("Memory Compression", test_memory_compression),
("Agent Integration", test_agent_memory_integration)
]
results = []
for test_name, test_func in tests:
try:
success = test_func()
results.append((test_name, success))
except Exception as e:
print(f"\n❌ Test '{test_name}' failed: {str(e)}")
results.append((test_name, False))
# Summary
print("\n" + "=" * 60)
print("TEST SUMMARY")
print("=" * 60)
passed = sum(1 for _, success in results if success)
total = len(results)
for test_name, success in results:
status = "✅ PASSED" if success else "❌ FAILED"
print(f"{status}: {test_name}")
print(f"\nTotal: {passed}/{total} tests passed")
if passed == total:
print("\n🎉 All tests passed successfully!")
else:
print(f"\n⚠️ {total - passed} test(s) failed")
return passed == total
if __name__ == "__main__":
success = run_all_tests()
sys.exit(0 if success else 1)