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
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:
@@ -0,0 +1,249 @@
|
||||
# Memobase Agent / Memobase Agent(Profile + 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 — don’t 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 Memobase’s *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 **server’s** `.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`.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user