ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
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

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+249
View File
@@ -0,0 +1,249 @@
# Memobase Agent / Memobase AgentProfile + Event 与手写记忆对照)
> Companion material for *AI Agents in Depth*, Chapter 3 — real Memobase SDK demo **and** a Memobase-inspired hand-rolled memory agent (Experiment 3-2 track).
> 配套《深入理解 AI Agent》第 3 章——真实 Memobase SDK 演示 **与** Memobase 风格自研记忆 Agent(实验 3-2 对照)。
← [Chapter 3 index / 返回第 3 章目录](../README.md)
---
## English
### Two tracks in this folder — dont confuse them
1. **Real Memobase framework demo** (`profile_demo.py`) — uses the actual open-source Memobase SDK (`pip install memobase`, package `memobase>=0.0.27`) against a **running Memobase server**. Canonical demo of Memobases *Profile* (structured user attributes) + *Event Memory* (timeline). See [Memobase Profile + Event Demo](#memobase-profile--event-demo-real-sdk).
2. **Hand-rolled memory agent** (`agent.py` / `main.py`) — self-contained *Memobase-inspired* `MemoryStore` (episodic / semantic / procedural / working, pickle-persisted) calling Kimi directly. **No Memobase server**—only `KIMI_API_KEY`. The `--mode` commands (interactive / benchmark / demo / task) drive this agent.
### Features (hand-rolled agent)
**Memory types:** episodic (task experiences), semantic (facts), procedural (patterns), working (short-term context).
**Operations:** compression when over threshold; consolidation; importance-based decay; clustering; relevance/recency retrieval.
**Model:** Kimi K3 integration (tool use, multi-step reasoning, long context).
**LOCOMO-style categories:** multi-turn reasoning, long-context Q&A, task planning, knowledge integration, tool usage.
### Installation
```bash
# From the repository root: use the shared Chapter 3 environment
uv sync --locked --python 3.12 --extra ch3
# Activate it before changing directories:
# macOS/Linux:
source .venv/bin/activate
# Windows PowerShell: .venv\Scripts\Activate.ps1
# Windows cmd: .venv\Scripts\activate.bat
# pip fallback when uv is not installed:
# python -m pip install -e ".[ch3]"
cd chapter3/memobase
# Single-project compatibility path, still supported during migration:
# python -m pip install -r requirements.txt
cp env.example .env
# Set KIMI_API_KEY, or use LLM_PROVIDER=dashscope/qwen/bailian with DASHSCOPE_API_KEY
```
Edit `config.py` for model, memory thresholds, benchmark, logging.
### Usage (hand-rolled agent)
#### Interactive
```bash
python main.py --mode interactive
```
Commands: `/help`, `/memory`, `/clear`, `/reset`, `/learn`, `/exit`.
#### Benchmark
```bash
python main.py --mode benchmark
python main.py --mode benchmark --category multi_turn_reasoning
python main.py --mode benchmark --num-tasks 5
```
#### Demo / single task
```bash
python main.py --mode demo
python main.py --mode task --task "Plan a 7-day trip to Japan with a $3000 budget"
```
Extra: `--api-key KEY`, `--no-memory`, `--verbose`.
### Memobase Profile + Event Demo (real SDK)
`profile_demo.py` uses the **real** Memobase SDK: **Profile** (topic → sub-topic → content, e.g. `basic_info→城市`, `work→职位`) and **Event Memory** (timeline for “when did we discuss budget?”). Pipeline: `insert``flush``profile` / `event` / `context`.
#### Prerequisites
Memobase extracts **server-side**; you need a reachable service:
- **Self-hosted**: [memodb-io/memobase](https://github.com/memodb-io/memobase) (docker compose). Default `http://localhost:8019`, token `secret`. Extraction model is in the **servers** `.env` / `config.yaml` (`--model` on the client is informational only).
- **Cloud**: `project_url` + `api_key` from https://www.memobase.ai
Client: `--project-url` / `--api-key` or `MEMOBASE_PROJECT_URL` / `MEMOBASE_API_KEY` (see `env.example`).
#### Running
```bash
# From the repository root, after installing and activating the shared `ch3` environment above:
cd chapter3/memobase
# Single-project compatibility path, still supported during migration:
# python -m pip install -r requirements.txt
python profile_demo.py
python profile_demo.py --dry-run
python profile_demo.py --op profile
python profile_demo.py --op event
python profile_demo.py --op context
python profile_demo.py --input chat.json --output result.json
```
If no server is reachable, the demo exits with an actionable message (try `--dry-run` or set `--project-url`)—it does **not** invent memory output.
### Architecture (hand-rolled)
- **MemoryStore** (`agent.py`): pickle persistence, compression, clustering, decay, retrieval
- **MemobaseAgent** (`agent.py`): message processing with memory context, learning, metrics
- **LOCOMOBenchmark** (`locomo_benchmark.py`): tasks, scoring, persistence
### Memory strategies
**Compression:** sort by importance/recency; keep important; cluster low-importance into summaries.
**Consolidation:** decay; drop very low importance; extract patterns → procedural.
**Retrieval:** content search + recent episodic + procedural → format into context.
### Results / development
Benchmark outputs under `benchmark_results/`. Extend tools / memory types in `config.py` / `locomo_benchmark.py` as needed.
### Troubleshooting
1. API key: `KIMI_API_KEY` in `.env`, or `DASHSCOPE_API_KEY` with `LLM_PROVIDER=dashscope`
2. Memory overflow: lower `MAX_MEMORY_ENTRIES`, more aggressive compression, manual consolidation
3. Slow: reduce `MODEL_MAX_TOKENS`, enable cache, category-specific benchmarks
### License / acknowledgments
MIT-style educational use. Kimi by Moonshot AI; Memobase concepts; LOCOMO-inspired design.
---
## 中文
### 本目录两条线——不要搞混
1. **真实 Memobase 框架演示**`profile_demo.py`)——官方开源 SDK`memobase>=0.0.27`)对接**正在运行的 Memobase 服务**,展示书中的 *Profile*(结构化用户属性)+ *Event Memory*(时间线)。见下文 Profile + Event 演示。
2. **手写记忆 Agent**`agent.py` / `main.py`)——自包含、受 Memobase 启发的 `MemoryStore`(情景 / 语义 / 程序 / 工作记忆,pickle 持久化),直接调 Kimi。**不需要 Memobase 服务**,只要 `KIMI_API_KEY``--mode`interactive / benchmark / demo / task)驱动的是这条线。
### 手写 Agent 功能
**记忆类型:** 情景、语义、程序、工作记忆。
**操作:** 超阈值压缩、巩固、重要性衰减、聚类、相关度/近因检索。
**模型:** Kimi K3。
**评测类别:** 多轮推理、长上下文问答、任务规划、知识整合、工具使用。
### 安装
```bash
# 在仓库根目录使用统一的第 3 章环境
uv sync --locked --python 3.12 --extra ch3
# 切换目录前先激活环境:
# macOS/Linux
source .venv/bin/activate
# Windows PowerShell.venv\Scripts\Activate.ps1
# Windows cmd.venv\Scripts\activate.bat
# 未安装 uv 时可用 pip 兜底:
# python -m pip install -e ".[ch3]"
cd chapter3/memobase
# 迁移期间仍支持单项目兼容路径:
# python -m pip install -r requirements.txt
cp env.example .env
# 手写 Agent 可填写 KIMI_API_KEY;也可用 LLM_PROVIDER=dashscope 与 DASHSCOPE_API_KEY
```
`config.py` 中调整模型、记忆阈值、基准与日志。
### 用法(手写 Agent
```bash
python main.py --mode interactive
# /help /memory /clear /reset /learn /exit
python main.py --mode benchmark
python main.py --mode benchmark --category multi_turn_reasoning
python main.py --mode benchmark --num-tasks 5
python main.py --mode demo
python main.py --mode task --task "Plan a 7-day trip to Japan with a $3000 budget"
```
额外:`--api-key``--no-memory``--verbose`
### Memobase Profile + Event 演示(真实 SDK
`profile_demo.py` 展示 **Profile**topic → sub-topic → content)与 **Event Memory**(时间线)。流水线:`insert``flush``profile` / `event` / `context`
#### 前置
抽取在**服务端**完成,需要可访问的 Memobase
- **自托管**[memodb-io/memobase](https://github.com/memodb-io/memobase)docker compose)。默认 `http://localhost:8019`token `secret`。抽取模型在**服务端**配置。
- **云端**https://www.memobase.ai 的 `project_url` + `api_key`
客户端:`--project-url` / `--api-key``MEMOBASE_PROJECT_URL` / `MEMOBASE_API_KEY`
#### 运行
```bash
# 在上方安装并激活统一 `ch3` 环境后,从仓库根目录进入本项目:
cd chapter3/memobase
# 迁移期间仍支持单项目兼容路径:
# python -m pip install -r requirements.txt
python profile_demo.py
python profile_demo.py --dry-run
python profile_demo.py --op profile
python profile_demo.py --op event
python profile_demo.py --op context
python profile_demo.py --input chat.json --output result.json
```
连不上服务时会给出可操作提示(`--dry-run` 或配置 URL),**不会捏造**记忆结果。
### 架构(手写)
- **MemoryStore** / **MemobaseAgent**`agent.py`
- **LOCOMOBenchmark**`locomo_benchmark.py`
压缩 / 巩固 / 检索策略与 English 节相同。
### 结果与排错
结果目录 `benchmark_results/`。常见问题:API Key、记忆溢出(调阈值/压缩)、性能(降 `MODEL_MAX_TOKENS`)。
### 许可
教学用途;致谢 Moonshot / Memobase / LOCOMO 相关设计。
---
## Notes / 说明
### OpenRouter 通用回退 / Universal OpenRouter fallback
If primary keys are absent and `OPENROUTER_API_KEY` is set, chat LLM routes through OpenRouter with automatic model mapping; `OPENROUTER_MODEL` forces an id. See `env.example`.
+580
View File
@@ -0,0 +1,580 @@
"""
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
}
+140
View File
@@ -0,0 +1,140 @@
"""
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"
# Chat model configuration (Kimi by default; DashScope/Bailian aliases supported)
LLM_PROVIDER = os.getenv("LLM_PROVIDER", "kimi").lower()
LLM_PROVIDER = {"qwen": "dashscope", "bailian": "dashscope"}.get(LLM_PROVIDER, LLM_PROVIDER)
if LLM_PROVIDER == "dashscope":
KIMI_API_KEY = os.getenv("DASHSCOPE_API_KEY", "")
KIMI_BASE_URL = os.getenv(
"DASHSCOPE_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1"
)
KIMI_MODEL = os.getenv("MODEL_NAME", "qwen3.7-plus")
else:
KIMI_API_KEY = os.getenv("KIMI_API_KEY", "") or os.getenv("MOONSHOT_API_KEY", "")
KIMI_BASE_URL = "https://api.moonshot.cn/v1"
KIMI_MODEL = os.getenv("MODEL_NAME", "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
+32
View File
@@ -0,0 +1,32 @@
# Provider: kimi (default) or dashscope/qwen/bailian
LLM_PROVIDER=kimi
# Kimi API Configuration
KIMI_API_KEY=your-kimi-api-key
# Alibaba Cloud Model Studio / Bailian (Qwen)
# DASHSCOPE_API_KEY=your-dashscope-api-key
# DASHSCOPE_BASE_URL=https://dashscope-intl.aliyuncs.com/compatible-mode/v1
# OpenRouter universal fallback (optional): if KIMI_API_KEY / MOONSHOT_API_KEY
# is missing but OPENROUTER_API_KEY is set, the chat LLM is routed through
# OpenRouter (kimi-k3 -> moonshotai/kimi-k2.6; OPENROUTER_MODEL overrides).
OPENROUTER_API_KEY=your_openrouter_api_key_here
# OPENROUTER_MODEL=openai/gpt-5.6-luna
# Optional: Logging Configuration
LOG_LEVEL=INFO
# Optional: Memory Settings
MAX_MEMORY_ENTRIES=100
MEMORY_COMPRESSION_THRESHOLD=50
# Optional: Model Settings
MODEL_TEMPERATURE=0.7
MODEL_MAX_TOKENS=4096
# Real Memobase framework demo (profile_demo.py) — points at a running
# Memobase server. Self-hosted default is http://localhost:8019 with token
# "secret"; for Memobase cloud use your project_url + api_key.
MEMOBASE_PROJECT_URL=http://localhost:8019
MEMOBASE_API_KEY=secret
+596
View File
@@ -0,0 +1,596 @@
"""
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}")
+324
View File
@@ -0,0 +1,324 @@
"""
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()
+324
View File
@@ -0,0 +1,324 @@
"""
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.ai 申请 project_url 与 api_key。
无服务端时可用 --dry-run 查看示例对话与将要执行的操作(不联网、不伪造结果)。
"""
import argparse
import json
import os
import sys
from pathlib import Path
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
# 示例多轮对话:内容刻意覆盖三个画像主题,便于观察 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()
+160
View File
@@ -0,0 +1,160 @@
"""
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
provider = os.getenv("LLM_PROVIDER", "kimi").lower()
api_key = os.getenv("DASHSCOPE_API_KEY", "") if provider in {"dashscope", "qwen", "bailian"} else os.getenv("KIMI_API_KEY", "")
if not api_key or api_key == "your-kimi-api-key":
print(f"\n⚠️ Warning: API key for provider '{provider}' 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 DASHSCOPE_API_KEY for dashscope/qwen/bailian or KIMI_API_KEY for kimi in .env")
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()
+16
View File
@@ -0,0 +1,16 @@
# Real Memobase framework SDK — required by profile_demo.py (Profile + Event
# memory). Talks to a running Memobase server (self-hosted via docker compose,
# default http://localhost:8019, or the Memobase cloud). The hand-rolled
# MemoryStore agent (agent.py/main.py) does NOT need this package.
memobase>=0.0.27
openai>=1.45.0
python-dotenv>=1.0.0
numpy>=1.24.0
pandas>=2.0.0
scikit-learn>=1.3.0
tiktoken>=0.5.0
pydantic>=2.0.0
rich>=13.5.0
httpx>=0.25.0
tenacity>=8.2.0
@@ -0,0 +1,38 @@
"""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
+280
View File
@@ -0,0 +1,280 @@
"""
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)