跳转至

mem0

第3章 · 用户记忆和知识库 · 配套项目 chapter3/mem0

项目说明

Mem0 Agent with Kimi K3 for LOCOMO Benchmark

A sophisticated AI agent implementation that combines the Mem0 memory framework with the Kimi K3 language model, specifically designed for the LOCOMO (Long-Context Multi-Agent) benchmark.

Overview

This project implements an advanced conversational AI agent that: - Persistent Memory: Uses Mem0 framework for long-term memory management across sessions - Advanced Language Model: Integrates Kimi K3 model (1M-token context window; experiment caps usage at a smaller budget) - LOCOMO Benchmark: Evaluates agent performance on long-context multi-agent communication tasks - Multi-Session Support: Maintains context and consistency across multiple conversation sessions - Multi-Agent Collaboration: Supports multiple agents working together with shared memory

Features

Core Capabilities

  • Dynamic Memory Management: Automatically extracts, consolidates, and retrieves relevant information
  • Context Preservation: Maintains conversation context across sessions and agents
  • Performance Metrics: Tracks consistency, coherence, response time, and memory utilization
  • Flexible Backend: Supports both local and cloud-based memory storage

Benchmark Scenarios

The LOCOMO benchmark includes five scenario types: 1. Collaborative Planning: Multiple agents plan complex projects together 2. Information Sharing: Agents share and synthesize information across sessions 3. Problem Solving: Multi-step problem solving with memory retention 4. Negotiation: Multi-round negotiations with position tracking 5. Teaching & Learning: Educational dialogues with progress tracking

Installation

Prerequisites

  • Python 3.8 or higher
  • Kimi API key (from Moonshot AI)
  • Optional: Mem0 cloud API key for cloud storage

Setup

  1. Clone the repository:

    cd projects/week2/mem0
    

  2. Install dependencies:

    pip install -r requirements.txt
    

  3. Configure environment variables:

    cp env.example .env
    # Edit .env with your API keys and configuration
    

Required environment variables: - KIMI_API_KEY: Your Kimi K3 API key - MODEL_NAME: Model name (default: kimi-k3). This is the raw Moonshot model id (e.g. kimi-k3, kimi-k2.5, kimi-k2.6) passed straight to the API — do NOT use a provider/model slash form here; Mem0 is configured with the openai provider pointed at Moonshot's base_url, so it forwards the string verbatim and kimi/k3 returns "Not found the model". - MEMORY_BACKEND: Storage backend (local/cloud) - MAX_TOKENS: Maximum token limit (default: 128000)

Quick Start

Basic Usage

Run the quickstart examples to see the agent in action:

python quickstart.py

This will demonstrate: - Basic conversation with memory - Multi-session memory persistence - Multi-agent collaboration

Memory Pipeline Demo (提取—对比—决策)

The single clearest demonstration of Mem0's value — its ADD / UPDATE / DELETE / NOOP pipeline and cross-session recall — is the demo mode:

python main.py --mode demo --user-id demo_user

This reproduces the book's centerpiece example (chapter3.md): the user first says they live in Beijing, a later turn says they moved to Shanghai, and Mem0 resolves the conflict with an UPDATE (revising the existing memory) instead of storing two contradictory records. In between, a stored memory is recalled via semantic search, showing memory being used later. (The same routine is memory_pipeline_example() in quickstart.py, run first by python quickstart.py.)

Direct Memory Operations CLI

Mem0's memory API is exposed directly so you can observe each pipeline decision without the chat loop. All flags have Chinese --help (python main.py --help):

# ADD — write a conversation/utterance; prints the ADD/UPDATE/DELETE events
python main.py --mode memory --op add   --text "我住在北京,是一名后端工程师" --user-id u1

# SEARCH — semantic recall
python main.py --mode memory --op search --query "这个用户住在哪里?" --user-id u1

# GET-ALL — list every stored memory (optionally dump to JSON)
python main.py --mode memory --op get-all --user-id u1 --output mem.json

# HISTORY — the change/audit trail of one memory id (shows UPDATE/DELETE over time)
python main.py --mode memory --op history --memory-id <id>

# DELETE — remove one memory by id
python main.py --mode memory --op delete --memory-id <id>

Key flags: --op {add,search,get-all,history,delete}, --text, --query, --memory-id, --user-id, --agent-id, --model (override MODEL_NAME), --output (write result JSON). --text accepts either a raw string or a path to a JSON message list.

These operations, demo mode, and the chat modes all require a working LLM API key (KIMI_API_KEY) and a vector store — Mem0's fact extraction and semantic retrieval are online model calls. With no key the CLI parses arguments and then reports the missing key; no memory output is fabricated.

Interactive Mode

Start an interactive conversation session:

python main.py --mode interactive

Available commands in interactive mode: - help - Show available commands - memories - Display stored memories - metrics - Show performance metrics - save - Save conversation state - load - Load previous state - new - Start a new session - exit - Exit the program

Batch Processing

Process multiple conversations from a JSON file:

python main.py --mode batch --input conversations.json --output results.json

Input format:

[
  {
    "session_id": "session_001",
    "user_id": "user_001",
    "agent_id": "agent_001",
    "turns": [
      "First user message",
      "Second user message"
    ]
  }
]

Running LOCOMO Benchmark

Full Benchmark

Run the complete LOCOMO benchmark evaluation:

python experiment.py --scenarios 10 --output results/

This will: 1. Generate and run 10 benchmark scenarios 2. Evaluate agent performance on each scenario 3. Calculate metrics (consistency, coherence, memory utilization) 4. Generate detailed reports with visualizations

Benchmark Metrics

The benchmark evaluates: - Consistency Score: How well the agent maintains consistent information - Coherence Score: Relevance and logical flow of responses - Memory Retention: Effectiveness of memory storage and retrieval - Response Time: Average generation time per turn - Context Utilization: How well the agent uses available context

Understanding Results

Results are saved in JSON format with the following structure:

{
  "config": {...},
  "scenarios": [
    {
      "scenario": {...},
      "sessions": [...],
      "overall_metrics": {
        "avg_consistency": 0.92,
        "avg_coherence": 0.88,
        "memory_utilization": 42
      }
    }
  ],
  "overall_metrics": {...}
}

Architecture

Components

  1. Agent Module (agent.py)
  2. Mem0Agent: Main agent class with memory integration
  3. KimiK3Client: Client for Kimi K3 model API
  4. AgentContext: Context management for sessions

  5. Configuration (config.py)

  6. KimiConfig: Kimi model settings
  7. Mem0Config: Memory system configuration
  8. LOCOMOConfig: Benchmark parameters

  9. Experiment Framework (experiment.py)

  10. LOCOMOBenchmark: Benchmark implementation
  11. Scenario generation and evaluation
  12. Metrics calculation and reporting

Memory System

The Mem0 framework provides: - Vector Storage: Efficient semantic search using embeddings - Memory Consolidation: Automatic extraction of key information - Context Retrieval: Intelligent retrieval of relevant memories - Multi-level Organization: User, agent, and session-level memories

Advanced Usage

Custom Scenarios

Create custom benchmark scenarios by modifying experiment.py:

custom_scenario = {
    "type": "custom_type",
    "description": "Your scenario description",
    "topics": ["topic1", "topic2"],
    "context_requirements": ["requirement1", "requirement2"]
}

Memory Backends

Local Storage (Chroma)

config.mem0.backend = "local"
config.mem0.vector_store_config = {
    "provider": "chroma",
    "config": {
        "collection_name": "my_collection",
        "path": "./data/chroma_db"
    }
}

Cloud Storage (Mem0 Cloud)

config.mem0.backend = "cloud"
config.mem0.api_key = "your_mem0_api_key"

Performance Tuning

Optimize performance by adjusting: - MAX_TOKENS: Reduce for faster responses - TEMPERATURE: Lower for more consistent outputs - context_window_size: Balance between context and speed - Memory retrieval limit: Adjust in _prepare_messages()

Troubleshooting

Common Issues

  1. API Key Errors
  2. Ensure KIMI_API_KEY is set in .env
  3. Check API key validity and permissions

  4. Memory Backend Issues

  5. For local: Ensure write permissions in ./data/
  6. For cloud: Verify MEM0_API_KEY is correct

  7. Performance Issues

  8. Reduce MAX_TOKENS for faster responses
  9. Use local memory backend for lower latency
  10. Adjust batch sizes in benchmark runs

Debug Mode

Enable detailed logging:

export LOG_LEVEL=DEBUG
python main.py

Development

Project Structure

mem0/
├── agent.py           # Core agent implementation
├── config.py          # Configuration management
├── experiment.py      # LOCOMO benchmark
├── main.py           # Main entry point
├── quickstart.py     # Example demonstrations
├── requirements.txt  # Dependencies
├── env.example      # Environment template
└── README.md        # Documentation

Testing

Run tests (when available):

pytest tests/

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests if applicable
  5. Submit a pull request

Performance Benchmarks

Typical performance metrics on standard hardware: - Average response time: 1-3 seconds - Memory retrieval: <100ms - Consistency score: 0.85-0.95 - Coherence score: 0.80-0.90 - Memory utilization: 20-100 items per session

Limitations

  • Requires active internet connection for API calls
  • Memory storage grows with usage (periodic cleanup recommended)
  • Context window limited to 128K tokens
  • Response quality depends on model availability

License

This project is part of the AI Agent Book training materials.

Acknowledgments

  • Mem0 framework by Mem0 AI
  • Kimi K3 model by Moonshot AI
  • LOCOMO benchmark concept for long-context evaluation

Support

For issues and questions: - Check the troubleshooting section - Review example code in quickstart.py - Refer to the main AI Agent Book documentation

OpenRouter 通用回退 / Universal OpenRouter fallback

This experiment now supports a universal OpenRouter fallback for its chat LLM.

  • If the primary provider key (e.g. MOONSHOT_API_KEY / KIMI_API_KEY / OPENAI_API_KEY / DOUBAO_API_KEY …) is present, behavior is unchanged.
  • Else if OPENROUTER_API_KEY is set, the chat LLM is automatically routed through OpenRouter (https://openrouter.ai/api/v1). Model names are mapped automatically: gpt-*/o1-*openai/…, claude-*anthropic/claude-opus-4.8, kimi-*moonshotai/kimi-k2.6, ids already containing / are kept as-is, and other provider-native ids (e.g. doubao-*) fall back to openai/gpt-5.6-luna. Set OPENROUTER_MODEL to force a specific OpenRouter model id.
  • Else a clear error lists the accepted keys.

Add OPENROUTER_API_KEY=... to your .env (see env.example) to enable it.

Note: mem0's embedder still uses OpenAI embeddings (OpenRouter has no embeddings endpoint), so OPENAI_API_KEY is still required for storing/retrieving memories. The OpenRouter fallback only covers the chat LLM (fact extraction, ADD/UPDATE/DELETE decisions, and answering).

源代码

agent.py

"""Mem0-powered agent with Kimi K3 integration for LOCOMO benchmark."""

import json
import logging
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass, field
from datetime import datetime
import asyncio
from collections import defaultdict

from mem0 import Memory
from openai import OpenAI
from tenacity import retry, stop_after_attempt, wait_exponential
import numpy as np
from rich.console import Console
from rich.table import Table
from rich.progress import track

from config import Config, config as default_config


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


def _as_memory_list(result: Any) -> List[Dict[str, Any]]:
    """Normalize a mem0 return value to a plain list of memory dicts.

    mem0 >=1.0 returns ``{"results": [...], "relations": [...]}`` from
    ``search``/``get_all``, whereas older versions returned a bare list.
    Accepting either form keeps the agent working across mem0 versions
    (iterating the dict directly would otherwise yield the string keys).
    """
    if isinstance(result, dict):
        return result.get("results", []) or []
    if isinstance(result, list):
        return result
    return []


def _extract_memory_events(add_result: Any) -> List[Dict[str, str]]:
    """Extract ADD/UPDATE/DELETE decisions from a mem0 ``add`` return value.

    Mem0's "extract-compare-decide" pipeline judges every candidate fact
    against existing memories and emits one of ADD (brand-new), UPDATE
    (revise an existing memory), DELETE (retract a contradicted memory)
    or NOOP (duplicate, nothing changes). ``add`` surfaces the non-NOOP
    decisions; this returns them as ``[{"event", "memory", "id"}]`` so the
    book's four-way decision is visible to callers.
    """
    events = []
    for item in _as_memory_list(add_result):
        events.append({
            "event": item.get("event", "ADD"),
            "memory": item.get("memory", item.get("text", "")),
            "id": item.get("id", ""),
        })
    return events


# Set up logging
logging.basicConfig(
    level=getattr(logging, default_config.logging.level),
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
console = Console()


@dataclass
class AgentContext:
    """Context information for an agent in the LOCOMO benchmark."""

    agent_id: str
    user_id: str
    session_id: str
    turn_count: int = 0
    conversation_history: List[Dict[str, str]] = field(default_factory=list)
    metadata: Dict[str, Any] = field(default_factory=dict)

    def add_turn(self, role: str, content: str) -> None:
        """Add a turn to the conversation history."""
        self.conversation_history.append({
            "role": role,
            "content": content,
            "timestamp": datetime.now().isoformat(),
            "turn": self.turn_count
        })
        self.turn_count += 1


class KimiK3Client:
    """Client for interacting with Kimi K3 model."""

    def __init__(self, config: Config):
        self.config = config
        self.client = OpenAI(
            api_key=config.kimi.api_key,
            base_url=config.kimi.api_base
        )

    @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
    def generate(self, messages: List[Dict[str, str]], **kwargs) -> str:
        """Generate response using Kimi K3 model."""
        try:
            response = self.client.chat.completions.create(
                model=self.config.kimi.model_name,
                messages=messages,
                max_tokens=kwargs.get("max_tokens", self.config.kimi.max_tokens),
                temperature=_reasoning_safe_temperature(self.config.kimi.model_name, kwargs.get("temperature", self.config.kimi.temperature)),
                top_p=kwargs.get("top_p", 0.95),
                frequency_penalty=kwargs.get("frequency_penalty", 0),
                presence_penalty=kwargs.get("presence_penalty", 0)
            )
            return response.choices[0].message.content
        except Exception as e:
            logger.error(f"Error generating response with Kimi K3: {e}")
            raise

    async def agenerate(self, messages: List[Dict[str, str]], **kwargs) -> str:
        """Async generate response using Kimi K3 model."""
        return await asyncio.to_thread(self.generate, messages, **kwargs)


class Mem0Agent:
    """Agent powered by Mem0 memory system and Kimi K3 model."""

    def __init__(self, config: Optional[Config] = None):
        self.config = config or default_config
        self.config.validate()

        # Initialize Kimi K3 client
        self.llm_client = KimiK3Client(self.config)

        # Initialize Mem0 memory system
        self._init_memory()

        # Agent state management
        self.active_contexts: Dict[str, AgentContext] = {}
        self.performance_metrics: Dict[str, List[float]] = defaultdict(list)

    def _init_memory(self) -> None:
        """Initialize Mem0 memory system."""
        # Mem0 runs its own LLM calls for fact extraction and the
        # ADD/UPDATE/DELETE decision. Left unset, mem0 defaults to
        # max_tokens=2000 / temperature=0.1, which is unsafe for reasoning
        # models (Kimi K3 wants temperature=1 and enough room for its thinking
        # tokens). Pin both explicitly so the pipeline is reasoning-safe.
        mem0_config = {
            "llm": {
                "provider": "openai",
                "config": {
                    "api_key": self.config.kimi.api_key,
                    # mem0 >=1.0 names this field openai_base_url (not base_url);
                    # it points the OpenAI-compatible client at Moonshot.
                    "openai_base_url": self.config.kimi.api_base,
                    "model": self.config.kimi.model_name,
                    "temperature": _reasoning_safe_temperature(
                        self.config.kimi.model_name, self.config.kimi.temperature
                    ),
                    "max_tokens": max(self.config.kimi.max_tokens, 2048),
                }
            },
            "vector_store": self.config.mem0.vector_store_config,
            "embedder": {
                "provider": "openai",
                "config": {
                    "model": self.config.mem0.embedding_model
                }
            }
        }

        if self.config.mem0.backend == "local":
            self.memory = Memory.from_config(mem0_config)
        else:
            # For cloud backend
            self.memory = Memory(api_key=self.config.mem0.api_key)

        logger.info(f"Initialized Mem0 memory system with {self.config.mem0.backend} backend")

    def create_context(self, agent_id: str, user_id: str, session_id: str) -> AgentContext:
        """Create a new agent context for a session."""
        context = AgentContext(
            agent_id=agent_id,
            user_id=user_id,
            session_id=session_id,
            metadata={
                "created_at": datetime.now().isoformat(),
                "model": self.config.kimi.model_name
            }
        )
        self.active_contexts[session_id] = context
        logger.info(f"Created context for agent {agent_id} in session {session_id}")
        return context

    def get_context(self, session_id: str) -> Optional[AgentContext]:
        """Get agent context for a session."""
        return self.active_contexts.get(session_id)

    def _prepare_messages(self, context: AgentContext, user_input: str) -> List[Dict[str, str]]:
        """Prepare messages for LLM including memory context."""
        messages = []

        # System prompt
        system_prompt = f"""You are an intelligent agent participating in the LOCOMO benchmark.
Your task is to maintain consistent and coherent conversations across multiple sessions.
You have access to a memory system that helps you remember important information.

Agent ID: {context.agent_id}
User ID: {context.user_id}
Session ID: {context.session_id}
Current Turn: {context.turn_count}

Guidelines:
1. Maintain consistency with previous conversations
2. Reference relevant past information when appropriate
3. Build upon established context naturally
4. Be concise but informative in your responses
"""
        messages.append({"role": "system", "content": system_prompt})

        # Retrieve relevant memories
        memories = self.memory.search(
            query=user_input,
            user_id=context.user_id,
            agent_id=context.agent_id,
            limit=5
        )

        if memories and len(memories) > 0:
            memory_context = "\n\nRelevant memories from past interactions:\n"
            for mem in memories:
                memory_context += f"- {mem.get('memory', mem.get('text', ''))}\n"
            messages.append({"role": "system", "content": memory_context})

        # Add recent conversation history (last 10 turns)
        recent_history = context.conversation_history[-10:] if len(context.conversation_history) > 10 else context.conversation_history
        for turn in recent_history:
            messages.append({"role": turn["role"], "content": turn["content"]})

        # Add current user input
        messages.append({"role": "user", "content": user_input})

        return messages

    def process_turn(self, session_id: str, user_input: str) -> Tuple[str, Dict[str, Any]]:
        """Process a single turn in the conversation."""
        context = self.get_context(session_id)
        if not context:
            raise ValueError(f"No context found for session {session_id}")

        # Record user input
        context.add_turn("user", user_input)

        # Prepare messages with memory context
        messages = self._prepare_messages(context, user_input)

        # Generate response using Kimi K3
        start_time = datetime.now()
        response = self.llm_client.generate(messages)
        generation_time = (datetime.now() - start_time).total_seconds()

        # Record assistant response
        context.add_turn("assistant", response)

        # Store interaction in memory. mem0 runs its extract-compare-decide
        # pipeline here and returns the ADD/UPDATE/DELETE decisions it made.
        add_result = self.memory.add(
            messages=[
                {"role": "user", "content": user_input},
                {"role": "assistant", "content": response}
            ],
            user_id=context.user_id,
            agent_id=context.agent_id,
            metadata={
                "session_id": session_id,
                "turn": context.turn_count - 1,
                "timestamp": datetime.now().isoformat()
            }
        )
        memory_events = _extract_memory_events(add_result)

        # Calculate metrics
        metrics = {
            "generation_time": generation_time,
            "response_length": len(response),
            "turn_count": context.turn_count,
            "memory_count": len(_as_memory_list(self.memory.get_all(user_id=context.user_id))),
            "memory_events": memory_events
        }

        # Store performance metrics
        self.performance_metrics[session_id].append(generation_time)

        logger.info(f"Processed turn {context.turn_count} for session {session_id} in {generation_time:.2f}s")

        return response, metrics

    async def process_turn_async(self, session_id: str, user_input: str) -> Tuple[str, Dict[str, Any]]:
        """Async version of process_turn."""
        return await asyncio.to_thread(self.process_turn, session_id, user_input)

    # ------------------------------------------------------------------
    # Direct memory operations (used by the CLI and the pipeline demo)
    # ------------------------------------------------------------------
    def add_memory(self, messages, user_id: str, agent_id: Optional[str] = None,
                   metadata: Optional[Dict[str, Any]] = None) -> List[Dict[str, str]]:
        """Add a message/conversation to memory.

        Returns the ADD/UPDATE/DELETE decisions from mem0's pipeline. An
        empty list means every candidate fact was judged NOOP (duplicate).
        ``messages`` may be a plain string or an OpenAI-style message list.
        """
        add_result = self.memory.add(
            messages=messages,
            user_id=user_id,
            agent_id=agent_id,
            metadata=metadata or {}
        )
        return _extract_memory_events(add_result)

    def search_memory(self, query: str, user_id: str, agent_id: Optional[str] = None,
                      limit: int = 5) -> List[Dict[str, Any]]:
        """Semantically retrieve memories relevant to ``query``."""
        return _as_memory_list(self.memory.search(
            query=query, user_id=user_id, agent_id=agent_id, limit=limit
        ))

    def get_all_memories(self, user_id: str, agent_id: Optional[str] = None) -> List[Dict[str, Any]]:
        """List every stored memory for a user."""
        return _as_memory_list(self.memory.get_all(user_id=user_id, agent_id=agent_id))

    def memory_history(self, memory_id: str) -> List[Dict[str, Any]]:
        """Return the change history (ADD/UPDATE/DELETE audit trail) of one memory."""
        return self.memory.history(memory_id)

    def delete_memory(self, memory_id: str) -> str:
        """Delete a single memory by id."""
        self.memory.delete(memory_id)
        return memory_id

    def evaluate_consistency(self, session_id: str) -> float:
        """Evaluate consistency of responses in a session."""
        context = self.get_context(session_id)
        if not context or len(context.conversation_history) < 2:
            return 1.0

        # Simple consistency check based on response patterns
        responses = [turn["content"] for turn in context.conversation_history if turn["role"] == "assistant"]

        if len(responses) < 2:
            return 1.0

        # Calculate consistency score based on semantic similarity (simplified)
        # In a real implementation, you would use embeddings and cosine similarity
        consistency_scores = []
        for i in range(1, len(responses)):
            # Simplified: check for contradiction keywords
            prev_response = responses[i-1].lower()
            curr_response = responses[i].lower()

            contradiction_words = ["however", "but actually", "correction", "i was wrong", "let me correct"]
            has_contradiction = any(word in curr_response for word in contradiction_words)

            consistency_scores.append(0.5 if has_contradiction else 1.0)

        return np.mean(consistency_scores) if consistency_scores else 1.0

    def evaluate_coherence(self, session_id: str) -> float:
        """Evaluate coherence of the conversation."""
        context = self.get_context(session_id)
        if not context or len(context.conversation_history) < 2:
            return 1.0

        # Simple coherence check based on response relevance
        coherence_scores = []
        for i in range(0, len(context.conversation_history) - 1, 2):
            if i + 1 < len(context.conversation_history):
                user_turn = context.conversation_history[i]["content"]
                assistant_turn = context.conversation_history[i + 1]["content"]

                # Check if response addresses the user input (simplified)
                user_keywords = set(user_turn.lower().split())
                assistant_keywords = set(assistant_turn.lower().split())

                overlap = len(user_keywords.intersection(assistant_keywords))
                score = min(1.0, overlap / max(len(user_keywords), 1) * 2)
                coherence_scores.append(score)

        return np.mean(coherence_scores) if coherence_scores else 1.0

    def evaluate_memory_retention(self, user_id: str) -> float:
        """Evaluate memory retention for a user."""
        memories = _as_memory_list(self.memory.get_all(user_id=user_id))

        if not memories or len(memories) == 0:
            return 0.0

        # Calculate retention score based on memory count and recency
        now = datetime.now()
        retention_scores = []

        for memory in memories:
            created_at = memory.get("created_at", now.isoformat())
            if isinstance(created_at, str):
                created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00"))

            age_hours = (now - created_at).total_seconds() / 3600
            # Decay function: memories lose value over time
            retention_score = np.exp(-age_hours / 24)  # Half-life of 24 hours
            retention_scores.append(retention_score)

        return np.mean(retention_scores)

    def get_performance_summary(self, session_id: Optional[str] = None) -> Dict[str, Any]:
        """Get performance summary for a session or all sessions."""
        if session_id:
            context = self.get_context(session_id)
            if not context:
                return {}

            metrics = self.performance_metrics.get(session_id, [])
            return {
                "session_id": session_id,
                "turn_count": context.turn_count,
                "avg_response_time": np.mean(metrics) if metrics else 0,
                "consistency_score": self.evaluate_consistency(session_id),
                "coherence_score": self.evaluate_coherence(session_id),
                "memory_retention": self.evaluate_memory_retention(context.user_id)
            }
        else:
            # Aggregate metrics for all sessions
            all_metrics = []
            for sid in self.active_contexts:
                all_metrics.append(self.get_performance_summary(sid))

            if not all_metrics:
                return {}

            return {
                "total_sessions": len(all_metrics),
                "avg_turn_count": np.mean([m["turn_count"] for m in all_metrics]),
                "avg_response_time": np.mean([m["avg_response_time"] for m in all_metrics]),
                "avg_consistency": np.mean([m["consistency_score"] for m in all_metrics]),
                "avg_coherence": np.mean([m["coherence_score"] for m in all_metrics]),
                "avg_memory_retention": np.mean([m["memory_retention"] for m in all_metrics])
            }

    def display_metrics(self, session_id: Optional[str] = None) -> None:
        """Display performance metrics in a formatted table."""
        summary = self.get_performance_summary(session_id)

        if not summary:
            console.print("[yellow]No metrics available[/yellow]")
            return

        table = Table(title="Performance Metrics")
        table.add_column("Metric", style="cyan")
        table.add_column("Value", style="green")

        for key, value in summary.items():
            if isinstance(value, float):
                table.add_row(key.replace("_", " ").title(), f"{value:.4f}")
            else:
                table.add_row(key.replace("_", " ").title(), str(value))

        console.print(table)

    def reset(self) -> None:
        """Reset the agent state."""
        self.active_contexts.clear()
        self.performance_metrics.clear()
        logger.info("Agent state reset")

    def save_state(self, filepath: str) -> None:
        """Save agent state to file."""
        state = {
            "contexts": {
                sid: {
                    "agent_id": ctx.agent_id,
                    "user_id": ctx.user_id,
                    "session_id": ctx.session_id,
                    "turn_count": ctx.turn_count,
                    "conversation_history": ctx.conversation_history,
                    "metadata": ctx.metadata
                }
                for sid, ctx in self.active_contexts.items()
            },
            "metrics": dict(self.performance_metrics),
            "timestamp": datetime.now().isoformat()
        }

        with open(filepath, "w") as f:
            json.dump(state, f, indent=2)

        logger.info(f"Agent state saved to {filepath}")

    def load_state(self, filepath: str) -> None:
        """Load agent state from file."""
        with open(filepath, "r") as f:
            state = json.load(f)

        self.active_contexts.clear()
        for sid, ctx_data in state["contexts"].items():
            context = AgentContext(
                agent_id=ctx_data["agent_id"],
                user_id=ctx_data["user_id"],
                session_id=ctx_data["session_id"],
                turn_count=ctx_data["turn_count"],
                conversation_history=ctx_data["conversation_history"],
                metadata=ctx_data["metadata"]
            )
            self.active_contexts[sid] = context

        self.performance_metrics = defaultdict(list, state["metrics"])

        logger.info(f"Agent state loaded from {filepath}")

config.py

"""Configuration module for Mem0 agent with Kimi K3 integration."""

import os
from pathlib import Path
from typing import Optional, Dict, Any
from dataclasses import dataclass, field
from dotenv import load_dotenv

# Load environment variables
load_dotenv()


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


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  # already an OpenRouter-style id (e.g. openai/gpt-5.6-luna)
    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"
    # Provider-native ids (kimi-*/doubao-*/qwen/deepseek-*) not hosted on
    # OpenRouter under the same name -> a widely-available OpenAI chat model.
    return "openai/gpt-5.6-luna"


@dataclass
class KimiConfig:
    """Configuration for Kimi K3 model."""

    api_key: str = field(default_factory=lambda: os.getenv("KIMI_API_KEY", ""))
    model_name: str = field(default_factory=lambda: os.getenv("MODEL_NAME", "kimi-k3"))
    max_tokens: int = field(default_factory=lambda: int(os.getenv("MAX_TOKENS", "128000")))
    temperature: float = field(default_factory=lambda: float(os.getenv("TEMPERATURE", "0.7")))
    api_base: str = field(default_factory=lambda: os.getenv("KIMI_API_BASE", "https://api.moonshot.cn/v1"))

    def __post_init__(self):
        """Universal OpenRouter fallback for the chat LLM: when KIMI_API_KEY is
        absent but OPENROUTER_API_KEY is present, route the chat model (used by
        KimiK3Client and threaded into mem0's own LLM config) through OpenRouter.
        NB: mem0's embedder still uses OpenAI embeddings (OpenRouter has no
        embeddings endpoint), so OPENAI_API_KEY remains needed for memory add."""
        if not self.api_key and os.getenv("OPENROUTER_API_KEY"):
            self.api_key = os.getenv("OPENROUTER_API_KEY")
            self.api_base = "https://openrouter.ai/api/v1"
            self.model_name = _openrouter_model_id(self.model_name)

    def validate(self) -> bool:
        """Validate Kimi configuration."""
        if not self.api_key:
            raise ValueError("KIMI_API_KEY is required (or set OPENROUTER_API_KEY for the fallback)")
        if self.max_tokens <= 0 or self.max_tokens > 128000:
            raise ValueError("MAX_TOKENS must be between 1 and 128000")
        if self.temperature < 0 or self.temperature > 2:
            raise ValueError("TEMPERATURE must be between 0 and 2")
        return True


@dataclass
class Mem0Config:
    """Configuration for Mem0 memory system."""

    api_key: Optional[str] = field(default_factory=lambda: os.getenv("MEM0_API_KEY"))
    backend: str = field(default_factory=lambda: os.getenv("MEMORY_BACKEND", "local"))
    collection_name: str = field(default_factory=lambda: os.getenv("MEMORY_COLLECTION", "locomo_benchmark"))
    embedding_model: str = field(default_factory=lambda: os.getenv("MEMORY_EMBEDDING_MODEL", "text-embedding-3-small"))
    vector_store_config: Dict[str, Any] = field(default_factory=dict)

    def __post_init__(self):
        """Initialize vector store configuration based on backend."""
        if self.backend == "local":
            # NB: mem0 >=1.0 validates the chroma config against a fixed field
            # set (collection_name/path/host/port/api_key/tenant/client). The
            # embedding model belongs to the top-level "embedder" block (set in
            # agent.py), NOT here — passing embedding_function raises a
            # MemoryConfig validation error.
            self.vector_store_config = {
                "provider": "chroma",
                "config": {
                    "collection_name": self.collection_name,
                    "path": "./data/chroma_db",
                }
            }
        elif self.backend == "cloud":
            if not self.api_key:
                raise ValueError("MEM0_API_KEY is required for cloud backend")
            self.vector_store_config = {
                "provider": "mem0_cloud",
                "config": {
                    "api_key": self.api_key,
                    "collection_name": self.collection_name
                }
            }
        else:
            raise ValueError(f"Invalid backend: {self.backend}. Must be 'local' or 'cloud'")

    def validate(self) -> bool:
        """Validate Mem0 configuration."""
        if self.backend not in ["local", "cloud"]:
            raise ValueError("MEMORY_BACKEND must be 'local' or 'cloud'")
        if self.backend == "cloud" and not self.api_key:
            raise ValueError("MEM0_API_KEY is required for cloud backend")
        return True


@dataclass
class LOCOMOConfig:
    """Configuration for LOCOMO benchmark."""

    data_path: Path = field(default_factory=lambda: Path(os.getenv("BENCHMARK_DATA_PATH", "./data/locomo")))
    max_sessions: int = field(default_factory=lambda: int(os.getenv("MAX_SESSIONS", "100")))
    max_agents: int = field(default_factory=lambda: int(os.getenv("MAX_AGENTS", "10")))
    context_window_size: int = field(default_factory=lambda: int(os.getenv("CONTEXT_WINDOW_SIZE", "128000")))
    evaluation_metrics: list = field(default_factory=lambda: [
        "consistency_score",
        "coherence_score",
        "memory_retention",
        "context_utilization",
        "response_relevance"
    ])

    def __post_init__(self):
        """Ensure data path exists."""
        self.data_path.mkdir(parents=True, exist_ok=True)

    def validate(self) -> bool:
        """Validate LOCOMO configuration."""
        if self.max_sessions <= 0:
            raise ValueError("MAX_SESSIONS must be positive")
        if self.max_agents <= 0:
            raise ValueError("MAX_AGENTS must be positive")
        if self.context_window_size <= 0:
            raise ValueError("CONTEXT_WINDOW_SIZE must be positive")
        return True


@dataclass
class LoggingConfig:
    """Configuration for logging."""

    level: str = field(default_factory=lambda: os.getenv("LOG_LEVEL", "INFO"))
    file_path: Optional[Path] = field(default_factory=lambda: Path(os.getenv("LOG_FILE", "./logs/mem0_agent.log")) if os.getenv("LOG_FILE") else None)

    def __post_init__(self):
        """Ensure log directory exists."""
        if self.file_path:
            self.file_path.parent.mkdir(parents=True, exist_ok=True)


@dataclass
class Config:
    """Main configuration class."""

    kimi: KimiConfig = field(default_factory=KimiConfig)
    mem0: Mem0Config = field(default_factory=Mem0Config)
    locomo: LOCOMOConfig = field(default_factory=LOCOMOConfig)
    logging: LoggingConfig = field(default_factory=LoggingConfig)

    def validate(self) -> bool:
        """Validate all configurations."""
        self.kimi.validate()
        self.mem0.validate()
        self.locomo.validate()
        return True

    @classmethod
    def from_env(cls) -> "Config":
        """Create configuration from environment variables."""
        return cls()

    def to_dict(self) -> Dict[str, Any]:
        """Convert configuration to dictionary."""
        return {
            "kimi": {
                "model_name": self.kimi.model_name,
                "max_tokens": self.kimi.max_tokens,
                "temperature": _reasoning_safe_temperature(self.kimi.model_name, self.kimi.temperature),
                "api_base": self.kimi.api_base
            },
            "mem0": {
                "backend": self.mem0.backend,
                "collection_name": self.mem0.collection_name,
                "embedding_model": self.mem0.embedding_model
            },
            "locomo": {
                "data_path": str(self.locomo.data_path),
                "max_sessions": self.locomo.max_sessions,
                "max_agents": self.locomo.max_agents,
                "context_window_size": self.locomo.context_window_size,
                "evaluation_metrics": self.locomo.evaluation_metrics
            },
            "logging": {
                "level": self.logging.level,
                "file_path": str(self.logging.file_path) if self.logging.file_path else None
            }
        }


# Global configuration instance
config = Config.from_env()

experiment.py

"""LOCOMO benchmark experiment runner for Mem0 agent."""

import asyncio
import json
import random
import time
from pathlib import Path
from typing import List, Dict, Any, Tuple
from datetime import datetime
import argparse

import numpy as np
import pandas as pd
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
from rich.table import Table
from rich.panel import Panel
from rich.layout import Layout
import matplotlib.pyplot as plt
import seaborn as sns

from agent import Mem0Agent, AgentContext
from config import Config


console = Console()


class LOCOMOBenchmark:
    """LOCOMO benchmark implementation for evaluating long-context multi-agent communication."""

    def __init__(self, agent: Mem0Agent, config: Config):
        self.agent = agent
        self.config = config
        self.results = []
        self.scenario_data = []

    def generate_scenario(self, scenario_id: int, num_agents: int = 3) -> Dict[str, Any]:
        """Generate a LOCOMO benchmark scenario."""
        scenarios = [
            {
                "type": "collaborative_planning",
                "description": "Multiple agents collaborate to plan a complex project",
                "topics": ["project timeline", "resource allocation", "task dependencies", "risk assessment"],
                "context_requirements": ["maintain consistency across planning decisions", "remember previous constraints", "coordinate between agents"]
            },
            {
                "type": "information_sharing",
                "description": "Agents share and synthesize information across sessions",
                "topics": ["research findings", "data analysis", "hypothesis formation", "conclusion drawing"],
                "context_requirements": ["retain factual information", "build upon previous insights", "cross-reference between sources"]
            },
            {
                "type": "problem_solving",
                "description": "Agents work together to solve multi-step problems",
                "topics": ["problem decomposition", "solution strategies", "intermediate results", "final synthesis"],
                "context_requirements": ["remember partial solutions", "maintain logical consistency", "track progress across sessions"]
            },
            {
                "type": "negotiation",
                "description": "Agents engage in multi-round negotiations",
                "topics": ["initial positions", "concessions", "agreements", "conflict resolution"],
                "context_requirements": ["remember previous offers", "maintain negotiation stance", "track agreement points"]
            },
            {
                "type": "teaching_learning",
                "description": "Agents engage in educational dialogue",
                "topics": ["concept explanation", "question answering", "knowledge verification", "skill progression"],
                "context_requirements": ["track learning progress", "adapt to understanding level", "remember misconceptions"]
            }
        ]

        scenario = scenarios[scenario_id % len(scenarios)].copy()
        scenario["scenario_id"] = f"scenario_{scenario_id:03d}"
        scenario["num_agents"] = num_agents
        scenario["agents"] = [f"agent_{i:02d}" for i in range(num_agents)]
        scenario["num_sessions"] = random.randint(3, 8)
        scenario["turns_per_session"] = random.randint(5, 15)

        return scenario

    def generate_conversation_prompts(self, scenario: Dict[str, Any], session_num: int) -> List[str]:
        """Generate conversation prompts for a scenario session."""
        prompts = []
        topic = random.choice(scenario["topics"])

        base_prompts = {
            "collaborative_planning": [
                f"Let's discuss the {topic} for our project. What are your thoughts?",
                f"Based on our previous discussion, how should we adjust the {topic}?",
                f"Can you summarize what we've decided about {topic} so far?",
                f"What challenges do you foresee with the current {topic}?",
                f"How does the {topic} align with our overall objectives?"
            ],
            "information_sharing": [
                f"What new information do you have about {topic}?",
                f"How does this relate to what we discussed about {topic} before?",
                f"Can you integrate the findings about {topic} with our previous data?",
                f"What patterns are emerging from our {topic} analysis?",
                f"What conclusions can we draw about {topic} at this point?"
            ],
            "problem_solving": [
                f"What's our current approach to {topic}?",
                f"Have we made progress on {topic} since last time?",
                f"What obstacles are we facing with {topic}?",
                f"Can you propose an alternative solution for {topic}?",
                f"How can we validate our solution for {topic}?"
            ],
            "negotiation": [
                f"What's your position on {topic}?",
                f"Can we find middle ground on {topic}?",
                f"What concessions are you willing to make regarding {topic}?",
                f"How does this affect our previous agreement on {topic}?",
                f"Let's finalize our agreement on {topic}."
            ],
            "teaching_learning": [
                f"Can you explain {topic} in simple terms?",
                f"What questions do you have about {topic}?",
                f"How would you apply {topic} in practice?",
                f"What did we learn about {topic} last time?",
                f"Can you give an example of {topic}?"
            ]
        }

        scenario_prompts = base_prompts.get(scenario["type"], base_prompts["information_sharing"])

        # Add session-specific context
        for i in range(scenario["turns_per_session"]):
            if session_num == 0:
                prompt = f"[Session {session_num + 1}, Turn {i + 1}] {random.choice(scenario_prompts)}"
            else:
                prompt = f"[Session {session_num + 1}, Turn {i + 1}] Continuing from our previous session, {random.choice(scenario_prompts)}"
            prompts.append(prompt)

        return prompts

    async def run_scenario_session(self, scenario: Dict[str, Any], session_num: int) -> Dict[str, Any]:
        """Run a single session of a scenario."""
        session_id = f"{scenario['scenario_id']}_session_{session_num:02d}"
        user_id = f"user_{scenario['scenario_id']}"

        # Create contexts for all agents
        agent_contexts = {}
        for agent_id in scenario["agents"]:
            context = self.agent.create_context(
                agent_id=agent_id,
                user_id=user_id,
                session_id=f"{session_id}_{agent_id}"
            )
            agent_contexts[agent_id] = context

        # Generate conversation prompts
        prompts = self.generate_conversation_prompts(scenario, session_num)

        session_results = {
            "session_id": session_id,
            "session_num": session_num,
            "turns": [],
            "metrics": {}
        }

        # Run conversation turns
        for turn_idx, prompt in enumerate(prompts):
            # Randomly select which agent responds
            responding_agent = random.choice(scenario["agents"])
            agent_session_id = f"{session_id}_{responding_agent}"

            # Process turn
            response, metrics = await self.agent.process_turn_async(agent_session_id, prompt)

            session_results["turns"].append({
                "turn": turn_idx,
                "agent": responding_agent,
                "prompt": prompt,
                "response": response,
                "metrics": metrics
            })

            # Small delay to simulate realistic conversation
            await asyncio.sleep(0.1)

        # Calculate session-level metrics
        session_results["metrics"] = {
            "total_turns": len(prompts),
            "avg_response_time": np.mean([t["metrics"]["generation_time"] for t in session_results["turns"]]),
            "avg_response_length": np.mean([t["metrics"]["response_length"] for t in session_results["turns"]]),
            "consistency_scores": {
                agent_id: self.agent.evaluate_consistency(f"{session_id}_{agent_id}")
                for agent_id in scenario["agents"]
            },
            "coherence_scores": {
                agent_id: self.agent.evaluate_coherence(f"{session_id}_{agent_id}")
                for agent_id in scenario["agents"]
            }
        }

        return session_results

    async def run_scenario(self, scenario_id: int) -> Dict[str, Any]:
        """Run a complete LOCOMO scenario."""
        scenario = self.generate_scenario(scenario_id)

        console.print(f"\n[cyan]Running Scenario {scenario['scenario_id']}[/cyan]")
        console.print(f"Type: {scenario['type']}")
        console.print(f"Agents: {', '.join(scenario['agents'])}")
        console.print(f"Sessions: {scenario['num_sessions']}")

        scenario_results = {
            "scenario": scenario,
            "sessions": [],
            "start_time": datetime.now().isoformat()
        }

        # Run all sessions
        with Progress(
            SpinnerColumn(),
            TextColumn("[progress.description]{task.description}"),
            BarColumn(),
            TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
            TimeElapsedColumn(),
            console=console
        ) as progress:
            task = progress.add_task(
                f"Running {scenario['num_sessions']} sessions...",
                total=scenario["num_sessions"]
            )

            for session_num in range(scenario["num_sessions"]):
                session_results = await self.run_scenario_session(scenario, session_num)
                scenario_results["sessions"].append(session_results)
                progress.update(task, advance=1)

                # Delay between sessions
                await asyncio.sleep(0.5)

        scenario_results["end_time"] = datetime.now().isoformat()

        # Calculate scenario-level metrics
        scenario_results["overall_metrics"] = self.calculate_scenario_metrics(scenario_results)

        return scenario_results

    def calculate_scenario_metrics(self, scenario_results: Dict[str, Any]) -> Dict[str, Any]:
        """Calculate overall metrics for a scenario."""
        all_turns = []
        for session in scenario_results["sessions"]:
            all_turns.extend(session["turns"])

        consistency_scores = []
        coherence_scores = []
        for session in scenario_results["sessions"]:
            consistency_scores.extend(list(session["metrics"]["consistency_scores"].values()))
            coherence_scores.extend(list(session["metrics"]["coherence_scores"].values()))

        metrics = {
            "total_turns": len(all_turns),
            "total_sessions": len(scenario_results["sessions"]),
            "avg_response_time": np.mean([t["metrics"]["generation_time"] for t in all_turns]),
            "std_response_time": np.std([t["metrics"]["generation_time"] for t in all_turns]),
            "avg_response_length": np.mean([t["metrics"]["response_length"] for t in all_turns]),
            "avg_consistency": np.mean(consistency_scores) if consistency_scores else 0,
            "avg_coherence": np.mean(coherence_scores) if coherence_scores else 0,
            "memory_utilization": len(self.agent.memory.get_all(user_id=f"user_{scenario_results['scenario']['scenario_id']}"))
        }

        return metrics

    async def run_benchmark(self, num_scenarios: int = 5) -> Dict[str, Any]:
        """Run the complete LOCOMO benchmark."""
        console.print(Panel.fit(
            f"[bold cyan]LOCOMO Benchmark[/bold cyan]\n"
            f"Scenarios: {num_scenarios}\n"
            f"Model: {self.config.kimi.model_name}\n"
            f"Memory Backend: {self.config.mem0.backend}",
            title="Benchmark Configuration"
        ))

        benchmark_results = {
            "config": self.config.to_dict(),
            "start_time": datetime.now().isoformat(),
            "scenarios": []
        }

        for i in range(num_scenarios):
            scenario_results = await self.run_scenario(i)
            benchmark_results["scenarios"].append(scenario_results)
            self.results.append(scenario_results)

            # Display interim results
            self.display_scenario_results(scenario_results)

        benchmark_results["end_time"] = datetime.now().isoformat()
        benchmark_results["overall_metrics"] = self.calculate_overall_metrics(benchmark_results)

        return benchmark_results

    def calculate_overall_metrics(self, benchmark_results: Dict[str, Any]) -> Dict[str, Any]:
        """Calculate overall benchmark metrics."""
        all_metrics = [s["overall_metrics"] for s in benchmark_results["scenarios"]]

        return {
            "total_scenarios": len(benchmark_results["scenarios"]),
            "avg_response_time": np.mean([m["avg_response_time"] for m in all_metrics]),
            "std_response_time": np.std([m["avg_response_time"] for m in all_metrics]),
            "avg_consistency": np.mean([m["avg_consistency"] for m in all_metrics]),
            "std_consistency": np.std([m["avg_consistency"] for m in all_metrics]),
            "avg_coherence": np.mean([m["avg_coherence"] for m in all_metrics]),
            "std_coherence": np.std([m["avg_coherence"] for m in all_metrics]),
            "avg_memory_utilization": np.mean([m["memory_utilization"] for m in all_metrics]),
            "total_turns": sum([m["total_turns"] for m in all_metrics])
        }

    def display_scenario_results(self, scenario_results: Dict[str, Any]) -> None:
        """Display results for a single scenario."""
        metrics = scenario_results["overall_metrics"]

        table = Table(title=f"Scenario {scenario_results['scenario']['scenario_id']} Results")
        table.add_column("Metric", style="cyan")
        table.add_column("Value", style="green")

        table.add_row("Type", scenario_results["scenario"]["type"])
        table.add_row("Sessions", str(metrics["total_sessions"]))
        table.add_row("Total Turns", str(metrics["total_turns"]))
        table.add_row("Avg Response Time", f"{metrics['avg_response_time']:.3f}s")
        table.add_row("Consistency Score", f"{metrics['avg_consistency']:.3f}")
        table.add_row("Coherence Score", f"{metrics['avg_coherence']:.3f}")
        table.add_row("Memory Utilization", str(metrics["memory_utilization"]))

        console.print(table)

    def display_overall_results(self, benchmark_results: Dict[str, Any]) -> None:
        """Display overall benchmark results."""
        metrics = benchmark_results["overall_metrics"]

        console.print("\n")
        console.print(Panel.fit(
            f"[bold green]Benchmark Complete![/bold green]\n"
            f"Total Scenarios: {metrics['total_scenarios']}\n"
            f"Total Turns: {metrics['total_turns']}\n"
            f"Avg Response Time: {metrics['avg_response_time']:.3f}s ± {metrics['std_response_time']:.3f}s\n"
            f"Avg Consistency: {metrics['avg_consistency']:.3f} ± {metrics['std_consistency']:.3f}\n"
            f"Avg Coherence: {metrics['avg_coherence']:.3f} ± {metrics['std_coherence']:.3f}\n"
            f"Avg Memory Utilization: {metrics['avg_memory_utilization']:.1f}",
            title="Overall Results"
        ))

    def save_results(self, benchmark_results: Dict[str, Any], filepath: Path) -> None:
        """Save benchmark results to file."""
        filepath.parent.mkdir(parents=True, exist_ok=True)
        with open(filepath, "w") as f:
            json.dump(benchmark_results, f, indent=2)
        console.print(f"[green]Results saved to {filepath}[/green]")

    def generate_report(self, benchmark_results: Dict[str, Any], output_dir: Path) -> None:
        """Generate a detailed report with visualizations."""
        output_dir.mkdir(parents=True, exist_ok=True)

        # Extract data for visualization
        scenarios_df = pd.DataFrame([
            {
                "scenario_id": s["scenario"]["scenario_id"],
                "type": s["scenario"]["type"],
                "consistency": s["overall_metrics"]["avg_consistency"],
                "coherence": s["overall_metrics"]["avg_coherence"],
                "response_time": s["overall_metrics"]["avg_response_time"],
                "memory_utilization": s["overall_metrics"]["memory_utilization"]
            }
            for s in benchmark_results["scenarios"]
        ])

        # Create visualizations
        fig, axes = plt.subplots(2, 2, figsize=(12, 10))

        # Consistency scores by scenario type
        sns.boxplot(data=scenarios_df, x="type", y="consistency", ax=axes[0, 0])
        axes[0, 0].set_title("Consistency Scores by Scenario Type")
        axes[0, 0].set_xticklabels(axes[0, 0].get_xticklabels(), rotation=45)

        # Coherence scores by scenario type
        sns.boxplot(data=scenarios_df, x="type", y="coherence", ax=axes[0, 1])
        axes[0, 1].set_title("Coherence Scores by Scenario Type")
        axes[0, 1].set_xticklabels(axes[0, 1].get_xticklabels(), rotation=45)

        # Response time distribution
        axes[1, 0].hist(scenarios_df["response_time"], bins=20, edgecolor='black')
        axes[1, 0].set_title("Response Time Distribution")
        axes[1, 0].set_xlabel("Response Time (s)")
        axes[1, 0].set_ylabel("Frequency")

        # Memory utilization vs performance
        axes[1, 1].scatter(scenarios_df["memory_utilization"], 
                          scenarios_df["consistency"], 
                          alpha=0.6, label="Consistency")
        axes[1, 1].scatter(scenarios_df["memory_utilization"], 
                          scenarios_df["coherence"], 
                          alpha=0.6, label="Coherence")
        axes[1, 1].set_title("Memory Utilization vs Performance")
        axes[1, 1].set_xlabel("Memory Utilization")
        axes[1, 1].set_ylabel("Score")
        axes[1, 1].legend()

        plt.tight_layout()
        plt.savefig(output_dir / "benchmark_results.png", dpi=300)
        console.print(f"[green]Report generated in {output_dir}[/green]")


async def main():
    """Main function to run the LOCOMO benchmark."""
    parser = argparse.ArgumentParser(description="Run LOCOMO benchmark for Mem0 agent")
    parser.add_argument("--scenarios", type=int, default=5, help="Number of scenarios to run")
    parser.add_argument("--output", type=str, default="results", help="Output directory for results")
    parser.add_argument("--config", type=str, help="Path to configuration file")
    args = parser.parse_args()

    # Initialize configuration
    config = Config.from_env()

    # Initialize agent
    console.print("[yellow]Initializing Mem0 agent...[/yellow]")
    agent = Mem0Agent(config)

    # Initialize benchmark
    benchmark = LOCOMOBenchmark(agent, config)

    # Run benchmark
    console.print(f"[yellow]Starting benchmark with {args.scenarios} scenarios...[/yellow]")
    benchmark_results = await benchmark.run_benchmark(num_scenarios=args.scenarios)

    # Save results
    output_dir = Path(args.output)
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    results_file = output_dir / f"locomo_results_{timestamp}.json"
    benchmark.save_results(benchmark_results, results_file)

    # Generate report
    report_dir = output_dir / f"report_{timestamp}"
    benchmark.generate_report(benchmark_results, report_dir)

    # Display overall results
    benchmark.display_overall_results(benchmark_results)


if __name__ == "__main__":
    asyncio.run(main())

main.py

"""Main entry point for the Mem0 agent with Kimi K3."""

import asyncio
import argparse
import json
import os
from pathlib import Path
from typing import Optional
import sys

from rich.console import Console
from rich.prompt import Prompt, Confirm
from rich.panel import Panel
from rich.markdown import Markdown

from agent import Mem0Agent
from config import Config


console = Console()


class InteractiveSession:
    """Interactive session manager for Mem0 agent."""

    def __init__(self, agent: Mem0Agent):
        self.agent = agent
        self.current_session = None
        self.current_user = None
        self.current_agent_id = None

    def start_session(self) -> None:
        """Start a new interactive session."""
        console.print(Panel.fit(
            "[bold cyan]Mem0 Agent Interactive Session[/bold cyan]\n"
            "Type 'help' for commands, 'exit' to quit",
            title="Welcome"
        ))

        # Get session details
        self.current_user = Prompt.ask("Enter user ID", default="user_001")
        self.current_agent_id = Prompt.ask("Enter agent ID", default="agent_001")
        session_id = Prompt.ask("Enter session ID", default="session_001")

        # Create context
        context = self.agent.create_context(
            agent_id=self.current_agent_id,
            user_id=self.current_user,
            session_id=session_id
        )
        self.current_session = session_id

        console.print(f"[green]Session started:[/green] {session_id}")
        console.print(f"[green]User:[/green] {self.current_user}")
        console.print(f"[green]Agent:[/green] {self.current_agent_id}")

    def show_help(self) -> None:
        """Display help information."""
        help_text = """
# Available Commands

- **help** - Show this help message
- **exit/quit** - Exit the session
- **clear** - Clear the screen
- **metrics** - Show performance metrics
- **memories** - Show stored memories
- **save** - Save conversation state
- **load** - Load conversation state
- **reset** - Reset the agent state
- **new** - Start a new session
        """
        console.print(Markdown(help_text))

    def show_memories(self) -> None:
        """Display stored memories."""
        if not self.current_user:
            console.print("[yellow]No active session[/yellow]")
            return

        memories = self.agent.get_all_memories(user_id=self.current_user)

        if not memories:
            console.print("[yellow]No memories found[/yellow]")
            return

        console.print(f"\n[cyan]Memories for {self.current_user}:[/cyan]")
        for i, memory in enumerate(memories, 1):
            console.print(f"{i}. {memory.get('memory', memory.get('text', 'N/A'))}")

    def save_state(self) -> None:
        """Save the current state."""
        filepath = Prompt.ask("Enter filepath to save", default="state.json")
        self.agent.save_state(filepath)
        console.print(f"[green]State saved to {filepath}[/green]")

    def load_state(self) -> None:
        """Load a saved state."""
        filepath = Prompt.ask("Enter filepath to load", default="state.json")
        if Path(filepath).exists():
            self.agent.load_state(filepath)
            console.print(f"[green]State loaded from {filepath}[/green]")
        else:
            console.print(f"[red]File not found: {filepath}[/red]")

    async def run(self) -> None:
        """Run the interactive session."""
        self.start_session()

        while True:
            try:
                # Get user input
                user_input = Prompt.ask("\n[bold]You[/bold]")

                # Check for commands
                if user_input.lower() in ["exit", "quit"]:
                    if Confirm.ask("Are you sure you want to exit?"):
                        break
                elif user_input.lower() == "help":
                    self.show_help()
                    continue
                elif user_input.lower() == "clear":
                    console.clear()
                    continue
                elif user_input.lower() == "metrics":
                    self.agent.display_metrics(self.current_session)
                    continue
                elif user_input.lower() == "memories":
                    self.show_memories()
                    continue
                elif user_input.lower() == "save":
                    self.save_state()
                    continue
                elif user_input.lower() == "load":
                    self.load_state()
                    continue
                elif user_input.lower() == "reset":
                    if Confirm.ask("Reset agent state?"):
                        self.agent.reset()
                        console.print("[green]Agent state reset[/green]")
                    continue
                elif user_input.lower() == "new":
                    self.start_session()
                    continue

                # Process the input through the agent
                console.print("[dim]Processing...[/dim]")
                response, metrics = await self.agent.process_turn_async(
                    self.current_session, 
                    user_input
                )

                # Display response
                console.print(f"\n[bold cyan]Agent[/bold cyan]: {response}")

                # Display metrics (optional)
                if metrics.get("generation_time"):
                    console.print(
                        f"[dim]Generated in {metrics['generation_time']:.2f}s | "
                        f"Turn {metrics['turn_count']} | "
                        f"Memories: {metrics['memory_count']}[/dim]"
                    )

            except KeyboardInterrupt:
                console.print("\n[yellow]Interrupted[/yellow]")
                if Confirm.ask("Exit session?"):
                    break
            except Exception as e:
                console.print(f"[red]Error: {e}[/red]")

        console.print("\n[cyan]Session ended. Goodbye![/cyan]")


async def run_batch_mode(agent: Mem0Agent, input_file: Path, output_file: Path) -> None:
    """Run the agent in batch mode."""
    console.print(f"[yellow]Processing batch file: {input_file}[/yellow]")

    # Read input file
    with open(input_file, "r") as f:
        batch_data = f.read()

    # Parse batch data (assuming JSON format)
    import json
    try:
        sessions = json.loads(batch_data)
    except json.JSONDecodeError:
        console.print("[red]Invalid JSON in input file[/red]")
        return

    results = []

    # Process each session
    for session_data in sessions:
        session_id = session_data.get("session_id", "batch_session")
        user_id = session_data.get("user_id", "batch_user")
        agent_id = session_data.get("agent_id", "batch_agent")
        turns = session_data.get("turns", [])

        # Create context
        context = agent.create_context(
            agent_id=agent_id,
            user_id=user_id,
            session_id=session_id
        )

        session_results = {
            "session_id": session_id,
            "user_id": user_id,
            "agent_id": agent_id,
            "turns": []
        }

        # Process turns
        for turn in turns:
            response, metrics = await agent.process_turn_async(session_id, turn)
            session_results["turns"].append({
                "input": turn,
                "response": response,
                "metrics": metrics
            })

        results.append(session_results)

    # Save results
    with open(output_file, "w") as f:
        json.dump(results, f, indent=2)

    console.print(f"[green]Results saved to {output_file}[/green]")


def _load_add_messages(text: str):
    """Resolve the --text argument for a memory add operation.

    If it points to an existing JSON file, load it (expects a message list
    or a string); otherwise treat the argument itself as a user utterance.
    """
    if os.path.exists(text):
        with open(text, "r", encoding="utf-8") as f:
            return json.load(f)
    return text


async def run_memory_op(agent: Mem0Agent, args) -> None:
    """Run a single direct memory operation (add/search/get-all/history/delete).

    This exposes mem0's memory API on the command line so the extract-
    compare-decide pipeline (ADD/UPDATE/DELETE/NOOP) is observable turn by
    turn, independent of the chat loop.
    """
    op = args.op
    if not op:
        console.print("[red]memory 模式需要 --op 参数(add/search/get-all/history/delete)[/red]")
        sys.exit(1)

    result = None

    if op == "add":
        if not args.text:
            console.print("[red]add 操作需要 --text 参数(一段对话文本,或 JSON 消息文件路径)[/red]")
            sys.exit(1)
        messages = _load_add_messages(args.text)
        events = await asyncio.to_thread(agent.add_memory, messages, args.user_id, args.agent_id)
        console.print(f"[green]写入完成,记忆流水线(提取—对比—决策)产生的事件:[/green]")
        if events:
            for ev in events:
                console.print(f"  [{ev['event']}] {ev['memory']}  [dim](id={ev['id']})[/dim]")
        else:
            console.print("  [dim](没有 ADD/UPDATE/DELETE —— 候选事实被判定为 NOOP 重复信息)[/dim]")
        result = {"op": "add", "user_id": args.user_id, "events": events}

    elif op == "search":
        if not args.query:
            console.print("[red]search 操作需要 --query 参数[/red]")
            sys.exit(1)
        hits = await asyncio.to_thread(agent.search_memory, args.query, args.user_id, args.agent_id)
        console.print(f"[green]检索到 {len(hits)} 条相关记忆:[/green]")
        for mem in hits:
            console.print(f"  - {mem.get('memory', mem.get('text', 'N/A'))}  [dim](id={mem.get('id','')})[/dim]")
        result = {"op": "search", "query": args.query, "user_id": args.user_id, "memories": hits}

    elif op == "get-all":
        memories = await asyncio.to_thread(agent.get_all_memories, args.user_id, args.agent_id)
        console.print(f"[green]用户 {args.user_id} 共有 {len(memories)} 条记忆:[/green]")
        for i, mem in enumerate(memories, 1):
            console.print(f"  {i}. {mem.get('memory', mem.get('text', 'N/A'))}  [dim](id={mem.get('id','')})[/dim]")
        result = {"op": "get-all", "user_id": args.user_id, "memories": memories}

    elif op == "history":
        if not args.memory_id:
            console.print("[red]history 操作需要 --memory-id 参数[/red]")
            sys.exit(1)
        history = await asyncio.to_thread(agent.memory_history, args.memory_id)
        console.print(f"[green]记忆 {args.memory_id} 的修改历史:[/green]")
        for entry in history:
            console.print(f"  - {entry}")
        result = {"op": "history", "memory_id": args.memory_id, "history": history}

    elif op == "delete":
        if not args.memory_id:
            console.print("[red]delete 操作需要 --memory-id 参数[/red]")
            sys.exit(1)
        await asyncio.to_thread(agent.delete_memory, args.memory_id)
        console.print(f"[green]已删除记忆 {args.memory_id}[/green]")
        result = {"op": "delete", "memory_id": args.memory_id}

    if args.output and result is not None:
        with open(args.output, "w", encoding="utf-8") as f:
            json.dump(result, f, ensure_ascii=False, indent=2, default=str)
        console.print(f"[green]结果已写入 {args.output}[/green]")


CLI_EPILOG = """\
示例:
  python main.py                              # 默认进入交互式对话(记忆随对话自动写入/检索)
  python main.py --mode demo --user-id u1     # 运行“北京→上海”记忆流水线演示(ADD/UPDATE/DELETE/NOOP)
  python main.py --mode memory --op add   --text "我住在北京,是一名后端工程师" --user-id u1
  python main.py --mode memory --op search --query "这个用户住在哪里?" --user-id u1
  python main.py --mode memory --op get-all --user-id u1 --output mem.json
  python main.py --mode batch  --input conversations.json --output results.json
  python main.py --mode benchmark --model kimi-k3

说明:memory / demo / interactive / batch / benchmark 均需要可用的 LLM API(KIMI_API_KEY)
及向量存储;Mem0 的记忆提取与检索依赖在线模型调用。
"""


async def main():
    """Main entry point."""
    parser = argparse.ArgumentParser(
        description="Mem0 记忆智能体(Kimi K3)— 演示 Mem0 的“提取—对比—决策”记忆流水线",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=CLI_EPILOG,
    )
    parser.add_argument(
        "--mode",
        choices=["interactive", "batch", "benchmark", "memory", "demo"],
        default="interactive",
        help="运行模式:interactive 交互对话(默认)/ batch 批量对话 / benchmark 跑 LOCOMO 基准 / "
             "memory 直接调用记忆操作 / demo 记忆流水线演示",
    )
    parser.add_argument(
        "--op",
        choices=["add", "search", "get-all", "history", "delete"],
        help="memory 模式下的记忆操作:add 写入 / search 检索 / get-all 列出全部 / "
             "history 查看某条记忆的修改历史 / delete 删除",
    )
    parser.add_argument("--text", type=str,
                        help="add 操作的对话输入:一段文本,或指向 JSON 消息列表文件的路径")
    parser.add_argument("--query", type=str, help="search 操作的查询语句")
    parser.add_argument("--memory-id", type=str, help="history / delete 操作针对的记忆 ID")
    parser.add_argument("--user-id", type=str, default="user_001",
                        help="记忆归属的用户 ID(默认 user_001)")
    parser.add_argument("--agent-id", type=str, default="agent_001",
                        help="智能体 ID(默认 agent_001)")
    parser.add_argument("--model", type=str,
                        help="覆盖 MODEL_NAME,指定对话模型(如 kimi-k3)")
    parser.add_argument("--input", type=str, help="batch 模式的输入 JSON 文件")
    parser.add_argument("--output", type=str,
                        help="将结果写入的 JSON 文件(memory / batch 模式)")
    parser.add_argument("--config", type=str, help="配置文件路径(预留)")
    args = parser.parse_args()

    # Initialize configuration
    config = Config.from_env()
    if args.model:
        config.kimi.model_name = args.model

    # Initialize agent
    console.print("[yellow]Initializing Mem0 agent...[/yellow]")
    try:
        agent = Mem0Agent(config)
        console.print("[green]Agent initialized successfully[/green]")
    except Exception as e:
        console.print(f"[red]Failed to initialize agent: {e}[/red]")
        sys.exit(1)

    # Run based on mode
    if args.mode == "interactive":
        session = InteractiveSession(agent)
        await session.run()
    elif args.mode == "batch":
        if not args.input or not args.output:
            console.print("[red]Batch mode requires --input and --output arguments[/red]")
            sys.exit(1)
        await run_batch_mode(agent, Path(args.input), Path(args.output))
    elif args.mode == "memory":
        await run_memory_op(agent, args)
    elif args.mode == "demo":
        from quickstart import memory_pipeline_example
        await memory_pipeline_example(agent=agent, user_id=args.user_id)
    elif args.mode == "benchmark":
        # Import and run benchmark
        from experiment import LOCOMOBenchmark
        benchmark = LOCOMOBenchmark(agent, config)
        results = await benchmark.run_benchmark(num_scenarios=3)
        benchmark.display_overall_results(results)


if __name__ == "__main__":
    asyncio.run(main())

quickstart.py

"""Quick start example for Mem0 agent with Kimi K3."""

import asyncio
import os
from dotenv import load_dotenv
from rich.console import Console

from agent import Mem0Agent
from config import Config


# Load environment variables
load_dotenv()

console = Console()


async def basic_example():
    """Basic example of using Mem0 agent."""
    console.print("[bold cyan]Basic Mem0 Agent Example[/bold cyan]\n")

    # Initialize configuration
    config = Config.from_env()

    # Initialize agent
    console.print("[yellow]Initializing agent...[/yellow]")
    agent = Mem0Agent(config)

    # Create a session context
    session_id = "quickstart_session"
    user_id = "quickstart_user"
    agent_id = "quickstart_agent"

    context = agent.create_context(
        agent_id=agent_id,
        user_id=user_id,
        session_id=session_id
    )

    console.print(f"[green]Session created: {session_id}[/green]\n")

    # Example conversation
    conversations = [
        "Hello! I'm interested in learning about machine learning.",
        "I prefer Python for programming and have experience with scikit-learn.",
        "What would you recommend as the next step in my ML journey?",
        "Can you remind me what programming language I mentioned earlier?",
        "What libraries have I mentioned using?"
    ]

    for i, user_input in enumerate(conversations, 1):
        console.print(f"[bold]Turn {i} - User:[/bold] {user_input}")

        # Process the turn
        response, metrics = await agent.process_turn_async(session_id, user_input)

        console.print(f"[cyan]Agent:[/cyan] {response}")
        console.print(f"[dim]Response time: {metrics['generation_time']:.2f}s[/dim]\n")

        # Small delay for readability
        await asyncio.sleep(0.5)

    # Display final metrics
    console.print("\n[bold]Session Metrics:[/bold]")
    agent.display_metrics(session_id)

    # Show stored memories
    console.print("\n[bold]Stored Memories:[/bold]")
    memories = agent.memory.get_all(user_id=user_id)
    for memory in memories:
        console.print(f"- {memory.get('memory', memory.get('text', 'N/A'))}")


async def memory_pipeline_example(agent=None, user_id: str = "pipeline_user"):
    """Demonstrate Mem0's extract-compare-decide pipeline (ADD/UPDATE/DELETE/NOOP).

    This reproduces the book's centerpiece example: the user first says they
    live in Beijing, later says they moved to Shanghai, and Mem0 resolves the
    conflict by UPDATE-ing the existing memory instead of keeping two
    contradictory records. It also shows a stored memory being recalled in a
    *later* session, which is the whole point of a memory framework.

    Requires a working LLM API (KIMI_API_KEY) and vector store — Mem0's fact
    extraction and semantic retrieval are online model calls.
    """
    console.print("\n[bold cyan]Memory Pipeline Example (提取—对比—决策)[/bold cyan]\n")

    if agent is None:
        agent = Mem0Agent(Config.from_env())

    def show_events(label, events):
        console.print(f"[bold]{label}[/bold]")
        if events:
            for ev in events:
                console.print(f"  [magenta][{ev['event']}][/magenta] {ev['memory']} "
                              f"[dim](id={ev['id']})[/dim]")
        else:
            console.print("  [dim](NOOP — 没有产生新的记忆变更)[/dim]")
        console.print()

    # --- Session 1: establish facts about the user ---------------------------
    console.print("[yellow]Session 1 —— 首次对话,建立用户画像[/yellow]")
    events = await asyncio.to_thread(
        agent.add_memory,
        "我住在北京,在一家 AI 创业公司做后端工程师。",
        user_id,
    )
    show_events("写入「我住在北京 / 后端工程师」的记忆决策:", events)

    events = await asyncio.to_thread(
        agent.add_memory,
        "我平时喜欢周末去爬山,也在学弹吉他。",
        user_id,
    )
    show_events("写入「爱好」的记忆决策:", events)

    # --- Recall the stored memory (used later, across the session) -----------
    console.print("[yellow]检索 —— 从记忆中回忆用户信息(跨轮次复用)[/yellow]")
    hits = await asyncio.to_thread(
        agent.search_memory, "这个用户住在哪座城市?做什么工作?", user_id
    )
    console.print(f"[bold]检索到 {len(hits)} 条相关记忆:[/bold]")
    for mem in hits:
        console.print(f"  - {mem.get('memory', mem.get('text', 'N/A'))}")
    console.print()

    # --- Session 2 (later): conflicting fact triggers UPDATE -----------------
    console.print("[yellow]Session 2(一段时间后)—— 用户搬家,出现冲突信息[/yellow]")
    events = await asyncio.to_thread(
        agent.add_memory,
        "更新一下,我上个月从北京搬到上海了。",
        user_id,
    )
    show_events("写入「搬到上海」后的记忆决策(预期出现 UPDATE,而非新增矛盾条目):", events)

    # --- Verify consolidation: no contradictory Beijing/Shanghai pair --------
    console.print("[yellow]核对 —— 记忆库应当保持一致,而不是同时保留北京与上海[/yellow]")
    memories = await asyncio.to_thread(agent.get_all_memories, user_id)
    console.print(f"[bold]用户 {user_id} 当前全部记忆({len(memories)} 条):[/bold]")
    for i, mem in enumerate(memories, 1):
        console.print(f"  {i}. {mem.get('memory', mem.get('text', 'N/A'))}")
    console.print()
    console.print("[dim]提示:观察居住地记忆是否已被 UPDATE 为“上海”,且没有残留矛盾的“北京”条目。[/dim]")


async def multi_session_example():
    """Example showing memory persistence across sessions."""
    console.print("\n[bold cyan]Multi-Session Memory Example[/bold cyan]\n")

    # Initialize agent
    config = Config.from_env()
    agent = Mem0Agent(config)

    user_id = "persistent_user"

    # First session
    console.print("[yellow]Starting Session 1...[/yellow]")
    session1_id = "session_001"
    context1 = agent.create_context(
        agent_id="agent_001",
        user_id=user_id,
        session_id=session1_id
    )

    # First session conversation
    response1, _ = await agent.process_turn_async(
        session1_id, 
        "Hi! I'm working on a project about renewable energy, specifically solar panels."
    )
    console.print(f"[cyan]Session 1 Response:[/cyan] {response1}\n")

    response2, _ = await agent.process_turn_async(
        session1_id,
        "I need to analyze efficiency data from different manufacturers."
    )
    console.print(f"[cyan]Session 1 Response:[/cyan] {response2}\n")

    # Second session (different session, same user)
    console.print("[yellow]Starting Session 2 (after some time)...[/yellow]")
    session2_id = "session_002"
    context2 = agent.create_context(
        agent_id="agent_001",
        user_id=user_id,
        session_id=session2_id
    )

    # Second session should remember context from first session
    response3, _ = await agent.process_turn_async(
        session2_id,
        "What was I working on last time we talked?"
    )
    console.print(f"[cyan]Session 2 Response:[/cyan] {response3}\n")

    response4, _ = await agent.process_turn_async(
        session2_id,
        "Can you help me continue with that project?"
    )
    console.print(f"[cyan]Session 2 Response:[/cyan] {response4}\n")

    # Show all memories
    console.print("[bold]All Memories for User:[/bold]")
    memories = agent.memory.get_all(user_id=user_id)
    for memory in memories:
        console.print(f"- {memory.get('memory', memory.get('text', 'N/A'))}")


async def multi_agent_example():
    """Example with multiple agents collaborating."""
    console.print("\n[bold cyan]Multi-Agent Collaboration Example[/bold cyan]\n")

    # Initialize agent
    config = Config.from_env()
    agent = Mem0Agent(config)

    user_id = "collaboration_user"
    session_id = "collab_session"

    # Create contexts for multiple agents
    agents = ["researcher", "analyst", "advisor"]
    contexts = {}

    for agent_id in agents:
        contexts[agent_id] = agent.create_context(
            agent_id=agent_id,
            user_id=user_id,
            session_id=f"{session_id}_{agent_id}"
        )
        console.print(f"[green]Created context for {agent_id}[/green]")

    # Collaborative conversation
    console.print("\n[yellow]Starting collaborative discussion...[/yellow]\n")

    # Researcher starts
    response1, _ = await agent.process_turn_async(
        f"{session_id}_researcher",
        "I've found some interesting data on climate change impacts on agriculture."
    )
    console.print(f"[cyan]Researcher:[/cyan] {response1}\n")

    # Analyst responds
    response2, _ = await agent.process_turn_async(
        f"{session_id}_analyst",
        "Based on what the researcher mentioned, what are the key metrics we should analyze?"
    )
    console.print(f"[cyan]Analyst:[/cyan] {response2}\n")

    # Advisor provides guidance
    response3, _ = await agent.process_turn_async(
        f"{session_id}_advisor",
        "Considering both the research and analysis perspectives, what recommendations can we make?"
    )
    console.print(f"[cyan]Advisor:[/cyan] {response3}\n")

    # Show metrics for all agents
    console.print("[bold]Performance Metrics:[/bold]")
    for agent_id in agents:
        console.print(f"\n[yellow]{agent_id.capitalize()}:[/yellow]")
        summary = agent.get_performance_summary(f"{session_id}_{agent_id}")
        for key, value in summary.items():
            if isinstance(value, float):
                console.print(f"  {key}: {value:.3f}")
            else:
                console.print(f"  {key}: {value}")


async def main():
    """Run all examples."""
    console.print(Panel.fit(
        "[bold]Mem0 Agent Quickstart Examples[/bold]\n"
        "Demonstrating various capabilities of the Mem0 agent with Kimi K3",
        title="Welcome"
    ))

    # Check for API key
    if not os.getenv("KIMI_API_KEY"):
        console.print("[red]Error: KIMI_API_KEY not found in environment[/red]")
        console.print("Please set your Kimi API key in the .env file")
        return

    try:
        # Run examples
        await memory_pipeline_example()
        await asyncio.sleep(1)

        await basic_example()
        await asyncio.sleep(1)

        await multi_session_example()
        await asyncio.sleep(1)

        await multi_agent_example()

        console.print("\n[green]All examples completed successfully![/green]")

    except Exception as e:
        console.print(f"[red]Error running examples: {e}[/red]")
        import traceback
        traceback.print_exc()


if __name__ == "__main__":
    from rich.panel import Panel
    asyncio.run(main())

test_simple.py

"""Simple test to verify the Mem0 agent setup."""

import os
import asyncio
from dotenv import load_dotenv

# Load environment variables
load_dotenv()


def test_imports():
    """Test that all modules can be imported."""
    print("Testing imports...")
    try:
        from agent import Mem0Agent, AgentContext, KimiK3Client
        print("✓ Agent module imported successfully")

        from config import Config, KimiConfig, Mem0Config, LOCOMOConfig
        print("✓ Config module imported successfully")

        from experiment import LOCOMOBenchmark
        print("✓ Experiment module imported successfully")

        import mem0
        print("✓ Mem0 library available")

        import openai
        print("✓ OpenAI library available")

        return True
    except ImportError as e:
        print(f"✗ Import error: {e}")
        return False


def test_configuration():
    """Test configuration loading."""
    print("\nTesting configuration...")
    try:
        from config import Config

        config = Config.from_env()

        # Check if API key is set
        if not config.kimi.api_key:
            print("⚠ Warning: KIMI_API_KEY not set in environment")
            print("  Please create a .env file from env.example and add your API key")
            return False
        else:
            print(f"✓ Kimi API key configured (length: {len(config.kimi.api_key)})")

        print(f"✓ Model: {config.kimi.model_name}")
        print(f"✓ Max tokens: {config.kimi.max_tokens}")
        print(f"✓ Memory backend: {config.mem0.backend}")
        print(f"✓ LOCOMO data path: {config.locomo.data_path}")

        # Validate configuration
        config.validate()
        print("✓ Configuration validation passed")

        return True
    except Exception as e:
        print(f"✗ Configuration error: {e}")
        return False


async def test_agent_initialization():
    """Test agent initialization."""
    print("\nTesting agent initialization...")
    try:
        from agent import Mem0Agent
        from config import Config

        config = Config.from_env()

        # Skip if no API key
        if not config.kimi.api_key:
            print("⚠ Skipping agent test (no API key)")
            return False

        agent = Mem0Agent(config)
        print("✓ Agent initialized successfully")

        # Create a test context
        context = agent.create_context(
            agent_id="test_agent",
            user_id="test_user",
            session_id="test_session"
        )
        print(f"✓ Context created: {context.session_id}")

        return True
    except Exception as e:
        print(f"✗ Agent initialization error: {e}")
        import traceback
        traceback.print_exc()
        return False


async def test_memory_system():
    """Test memory system initialization."""
    print("\nTesting memory system...")
    try:
        from mem0 import Memory
        from config import Config

        config = Config.from_env()

        if config.mem0.backend == "local":
            # Test local memory initialization
            mem0_config = {
                "vector_store": {
                    "provider": "chroma",
                    "config": {
                        "collection_name": "test_collection",
                        "path": "./data/test_chroma_db"
                    }
                }
            }

            memory = Memory.from_config(mem0_config)
            print("✓ Local memory system initialized")

            # Clean up test database
            import shutil
            if os.path.exists("./data/test_chroma_db"):
                shutil.rmtree("./data/test_chroma_db")
                print("✓ Test database cleaned up")
        else:
            print("✓ Cloud memory backend configured")

        return True
    except Exception as e:
        print(f"✗ Memory system error: {e}")
        return False


def main():
    """Run all tests."""
    print("=" * 50)
    print("Mem0 Agent Setup Test")
    print("=" * 50)

    all_passed = True

    # Run tests
    if not test_imports():
        all_passed = False

    if not test_configuration():
        all_passed = False

    # Run async tests
    loop = asyncio.get_event_loop()

    if not loop.run_until_complete(test_agent_initialization()):
        all_passed = False

    if not loop.run_until_complete(test_memory_system()):
        all_passed = False

    # Summary
    print("\n" + "=" * 50)
    if all_passed:
        print("✅ All tests passed! The agent is ready to use.")
        print("\nNext steps:")
        print("1. Run quickstart examples: python quickstart.py")
        print("2. Try interactive mode: python main.py")
        print("3. Run LOCOMO benchmark: python experiment.py")
    else:
        print("⚠️ Some tests failed. Please check the configuration.")
        print("\nTroubleshooting:")
        print("1. Create .env file from env.example")
        print("2. Add your KIMI_API_KEY to .env")
        print("3. Install all requirements: pip install -r requirements.txt")
    print("=" * 50)


if __name__ == "__main__":
    main()