diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..054e211 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,897 @@ +# CLAUDE.md - AI Assistant Guide for Memori + +This document provides comprehensive guidance for AI assistants working with the Memori codebase. + +## Project Overview + +**Memori** is an open-source SQL-native memory engine for AI agents that enables persistent, queryable memory using standard SQL databases. It provides a one-line integration (`memori.enable()`) that gives any LLM persistent memory across conversations. + +- **Package Name**: `memorisdk` +- **Current Version**: 2.3.2 +- **Python Support**: 3.10+ +- **License**: Apache 2.0 +- **Repository**: https://github.com/GibsonAI/memori + +## Architecture Quick Reference + +``` +┌─────────────────────────────────────────────────────────┐ +│ User Application (OpenAI/Anthropic/etc) │ +└────────────────────┬────────────────────────────────────┘ + │ + ┌────────────▼────────────────────┐ + │ Memori Core (memory.py) │ ← Main entry point + │ - Recording & Retrieval │ + │ - Context Injection │ + └────────┬───────────┬────────────┘ + │ │ + ┌────────────▼──┐ ┌────▼─────────────┐ + │ Memory Agents │ │ Integrations │ + │ - Processing │ │ - OpenAI │ + │ - Retrieval │ │ - Anthropic │ + │ - Conscious │ │ - LiteLLM │ + └────────┬──────┘ └──────────────────┘ + │ + ┌────────▼──────────────────────┐ + │ SQLAlchemyDatabaseManager │ ← Database abstraction + │ - Multi-database support │ + │ - Search (FTS/FULLTEXT/etc) │ + └────────┬──────────────────────┘ + │ + ┌────────▼──────────┐ + │ SQLite/Postgres/ │ ← You control the data + │ MySQL/MongoDB │ + └───────────────────┘ +``` + +## Core Codebase Structure + +### Directory Layout + +``` +memori/ +├── __init__.py # Public API exports +├── core/ # Core functionality +│ ├── memory.py # Memori class (main entry point) +│ ├── database.py # Legacy DatabaseManager +│ ├── conversation.py # ConversationManager (session tracking) +│ └── providers.py # ProviderConfig (LLM provider config) +├── config/ # Configuration management +│ ├── manager.py # ConfigManager (singleton) +│ ├── settings.py # Pydantic settings models +│ └── memory_manager.py # MemoryManager +├── database/ # Database layer +│ ├── sqlalchemy_manager.py # Main DB interface (SQLAlchemy) +│ ├── models.py # ORM models +│ ├── search_service.py # Cross-DB search +│ ├── query_translator.py # Dialect abstraction +│ ├── connectors/ # DB connectors +│ ├── adapters/ # DB-specific adapters +│ ├── queries/ # SQL query builders +│ └── templates/ # SQL schema templates +├── agents/ # AI agents +│ ├── memory_agent.py # LLM-based memory processing +│ ├── retrieval_agent.py # Intelligent memory search +│ └── conscious_agent.py # Context promotion agent +├── integrations/ # LLM provider integrations +│ ├── openai_integration.py # OpenAI wrapper + interception +│ ├── anthropic_integration.py # Anthropic wrapper +│ └── litellm_integration.py # LiteLLM callbacks +├── tools/ # LLM tools +│ └── memory_tool.py # MemoryTool for LLM function calling +├── security/ # Security features +│ └── auth.py # AuthProvider, JWT +└── utils/ # Utilities + ├── pydantic_models.py # Data models + ├── exceptions.py # Custom exceptions + ├── validators.py # Input validation + ├── helpers.py # Utility functions + ├── logging.py # Logging setup + └── ... +``` + +### Key Files and Their Purposes + +| File | Primary Class | Purpose | +|------|---------------|---------| +| `core/memory.py` | `Memori` | Main API; orchestrates all memory operations | +| `database/sqlalchemy_manager.py` | `SQLAlchemyDatabaseManager` | Cross-database ORM and CRUD operations | +| `database/models.py` | `ChatHistory`, `ShortTermMemory`, `LongTermMemory` | SQLAlchemy ORM models | +| `agents/memory_agent.py` | `MemoryAgent` | LLM-powered memory processing and extraction | +| `agents/retrieval_agent.py` | `MemorySearchEngine` | Intelligent memory search and retrieval | +| `agents/conscious_agent.py` | `ConsciouscAgent` | Promotes conscious memories to short-term | +| `config/manager.py` | `ConfigManager` | Configuration loading (file/env) | +| `integrations/openai_integration.py` | `MemoriOpenAI`, `MemoriOpenAIInterceptor` | OpenAI integration layer | +| `utils/pydantic_models.py` | `ProcessedMemory`, etc. | Memory data structures | + +## Database Schema + +The system uses three primary tables: + +### 1. `chat_history` - Conversation Records +- Stores all conversations (user input + AI output) +- Multi-tenant fields: `user_id`, `assistant_id`, `session_id` +- Metadata: `model`, `tokens`, `timestamp` + +### 2. `short_term_memory` - Recent Working Memory (~7 days) +- Processed, categorized memories with expiration +- Fields: `importance_score`, `category_primary`, `retention_type`, `expires_at` +- Searchable via `searchable_content` field +- Indexes on: user_id, category, importance, expires_at + +### 3. `long_term_memory` - Consolidated Permanent Memory +- Deduplicated, scored, high-value memories +- Scoring: importance, novelty, relevance, actionability +- Classification: ESSENTIAL, CONTEXTUAL, CONVERSATIONAL, etc. +- Entity extraction: `entities_json`, `keywords_json` + +### Multi-Database Support + +| Database | Connection String Example | Full-Text Search | +|----------|---------------------------|------------------| +| SQLite | `sqlite:///memory.db` | FTS5 | +| PostgreSQL | `postgresql://user:pass@host/db` | tsvector + tsquery | +| MySQL | `mysql://user:pass@host/db` | FULLTEXT indexes | +| MongoDB | `mongodb://user:pass@host/db` | Text indexes | + +## Development Workflows + +### Setting Up Development Environment + +```bash +# Clone repository +git clone https://github.com/GibsonAI/memori.git +cd memori + +# Create virtual environment +python -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate + +# Install with dev dependencies +pip install -e ".[dev]" + +# Install pre-commit hooks (if available) +pre-commit install +``` + +### Code Quality Standards + +The project uses multiple tools for code quality: + +#### 1. **Black** - Code Formatting +```bash +black memori/ tests/ +``` +- Line length: 88 characters +- Target: Python 3.10+ +- Config: `[tool.black]` in `pyproject.toml` + +#### 2. **Ruff** - Linting +```bash +ruff check memori/ tests/ --fix +``` +- Checks: pycodestyle (E/W), pyflakes (F), isort (I), bugbear (B), comprehensions (C4), pyupgrade (UP) +- Ignores: E501 (line too long - handled by black) +- Config: `[tool.ruff]` in `pyproject.toml` + +#### 3. **isort** - Import Sorting +```bash +isort memori/ tests/ +``` +- Profile: black +- Config: `[tool.isort]` in `pyproject.toml` + +#### 4. **mypy** - Type Checking +```bash +mypy memori/ +``` +- Target: Python 3.10 +- **Note**: Currently relaxed for CI compatibility (see `[tool.mypy]` in `pyproject.toml`) +- Future: Gradually enable stricter typing + +### Running Tests + +```bash +# Run all tests +pytest + +# Run specific test categories +pytest -m unit # Unit tests only +pytest -m integration # Integration tests only +pytest -m "not slow" # Skip slow tests + +# Run with coverage +pytest --cov=memori --cov-report=html +``` + +### Commit Conventions + +Use conventional commit format: + +``` +: + +[optional body] + +[optional footer] +``` + +**Types**: +- `feat`: New feature +- `fix`: Bug fix +- `docs`: Documentation changes +- `refactor`: Code refactoring +- `test`: Adding/updating tests +- `chore`: Maintenance tasks +- `perf`: Performance improvements + +**Examples**: +``` +feat: add MongoDB support for memory storage +fix: resolve PostgreSQL connection pool timeout +docs: update installation guide for Python 3.12 +refactor: extract search logic into SearchService +test: add integration tests for multi-tenant isolation +``` + +## Key Conventions and Patterns + +### 1. Multi-Tenant Isolation + +**CRITICAL**: All database operations MUST filter by tenant identifiers: + +```python +# Always include these filters +filters = { + "user_id": user_id, # Required + "assistant_id": assistant_id, # Optional but recommended + "session_id": session_id # Optional +} +``` + +**Why**: Prevents data leakage between users/assistants/sessions. + +### 2. Memory Categories + +Memories are categorized into 5 types: + +```python +class MemoryCategoryType(str, Enum): + FACT = "fact" # Factual information + PREFERENCE = "preference" # User preferences + SKILL = "skill" # Capabilities/skills + CONTEXT = "context" # Contextual information + RULE = "rule" # Rules/constraints +``` + +### 3. Memory Classification + +```python +class MemoryClassification(str, Enum): + ESSENTIAL = "ESSENTIAL" # Critical, always retrieve + CONTEXTUAL = "CONTEXTUAL" # Retrieve when relevant + CONVERSATIONAL = "CONVERSATIONAL" # Recent conversation flow + REFERENCE = "REFERENCE" # Background information + PERSONAL = "PERSONAL" # Personal details + CONSCIOUS_INFO = "CONSCIOUS_INFO" # User-flagged important +``` + +### 4. Retention Types + +```python +class RetentionType(str, Enum): + SHORT_TERM = "short_term" # ~7 days, working memory + LONG_TERM = "long_term" # Permanent, consolidated + PERMANENT = "permanent" # Never expires +``` + +### 5. Context Injection Modes + +**Conscious Ingest** (`conscious_ingest=True`): +- One-time at startup +- Copies conscious-labeled memories to short-term +- Lower overhead + +**Auto Ingest** (`auto_ingest=True`): +- Dynamic search before every LLM call +- Retrieves most relevant memories +- Higher accuracy, more API calls + +**Combined** (`conscious_ingest=True, auto_ingest=True`): +- Best of both worlds +- Recommended for production + +### 6. Configuration Loading Priority + +``` +1. Explicit parameters in Memori() constructor +2. Environment variables (MEMORI_*) +3. Configuration files: + - $MEMORI_CONFIG_PATH + - ./memori.json, ./memori.yaml + - ./config/memori.* + - ~/.memori/config.json + - /etc/memori/config.json +4. Default values +``` + +### 7. Error Handling + +The project defines 13+ custom exceptions in `utils/exceptions.py`: + +```python +# Common exceptions +MemoriError # Base exception +ConfigurationError # Config issues +DatabaseConnectionError # DB connection failures +DatabaseOperationError # DB operation failures +MemoryProcessingError # Processing failures +SearchError # Search failures +ValidationError # Input validation failures +``` + +**Always catch specific exceptions**, not generic `Exception`. + +### 8. Logging + +Use the centralized logging system: + +```python +from memori.utils.logging import get_logger + +logger = get_logger(__name__) + +logger.debug("Detailed debug info") +logger.info("General info") +logger.warning("Warning message") +logger.error("Error occurred") +logger.exception("Error with traceback") +``` + +**DO NOT** use `print()` statements in production code. + +### 9. Database Transactions + +For multi-statement operations, use transactions: + +```python +from memori.utils.transaction_manager import TransactionManager + +with TransactionManager(db_session) as txn: + # Multiple DB operations + db_session.add(record1) + db_session.add(record2) + # Auto-commits on success, rolls back on error +``` + +### 10. Async/Await Patterns + +Some agents use async operations: + +```python +# memory_agent.py uses async +async def process_memory(self, ...): + result = await self._call_llm_async(...) + return result + +# Call from sync context +import asyncio +result = asyncio.run(agent.process_memory(...)) +``` + +## Common Development Tasks + +### Adding a New Database Connector + +1. Create connector in `database/connectors/`: + ```python + # your_db_connector.py + from .base_connector import BaseDatabaseConnector + + class YourDBConnector(BaseDatabaseConnector): + def connect(self) -> Any: + # Implementation + pass + ``` + +2. Create adapter in `database/adapters/`: + ```python + # your_db_adapter.py + class YourDBSearchAdapter: + def search(self, query: str) -> List[Dict]: + # Implementation + pass + ``` + +3. Register in `database/sqlalchemy_manager.py`: + - Update `_create_engine()` method + - Add to `SearchService` initialization + +4. Add tests in `tests/your_db_support/` + +5. Add example in `examples/databases/your_db_demo.py` + +### Adding a New LLM Integration + +1. Create integration file in `integrations/`: + ```python + # your_llm_integration.py + from memori.core.memory import Memori + + class MemoriYourLLM: + def __init__(self, memori: Memori, **kwargs): + self.memori = memori + # Setup wrapper + ``` + +2. Implement pre-call and post-call hooks: + - Pre-call: Inject context from `memori.retrieve_context()` + - Post-call: Record with `memori.record()` + +3. Export in `integrations/__init__.py` + +4. Add test in `tests/your_llm_support/` + +5. Add example in `examples/integrations/your_llm_example.py` + +### Adding a New Memory Category + +1. Update `utils/pydantic_models.py`: + ```python + class MemoryCategoryType(str, Enum): + FACT = "fact" + # ... existing ... + YOUR_CATEGORY = "your_category" + ``` + +2. Update `agents/memory_agent.py` system prompt to handle new category + +3. Add tests for new category classification + +4. Update documentation + +### Modifying Database Schema + +**⚠️ IMPORTANT**: Schema changes require migration strategy! + +1. **Never modify existing columns** - add new ones +2. Create migration in `database/migrations/` +3. Update ORM models in `database/models.py` +4. Update SQL templates in `database/templates/schemas/` +5. Test with all supported databases +6. Document migration in `CHANGELOG.md` + +## Testing Guidelines + +### Test Structure + +``` +tests/ +├── unit/ # Unit tests (fast, isolated) +├── integration/ # Integration tests (DB, API) +├── openai/ # OpenAI integration tests +├── mysql_support/ # MySQL-specific tests +├── postgresql_support/ # PostgreSQL-specific tests +├── litellm_support/ # LiteLLM tests +└── utils/ # Utility tests +``` + +### Test Markers + +Use pytest markers to categorize tests: + +```python +@pytest.mark.unit +def test_memory_validation(): + # Fast, isolated test + pass + +@pytest.mark.integration +def test_database_connection(): + # Integration test + pass + +@pytest.mark.slow +def test_full_workflow(): + # Slow end-to-end test + pass +``` + +### Writing Good Tests + +**DO**: +- Test one thing per test function +- Use descriptive test names: `test_memory_agent_extracts_entities_from_conversation` +- Use fixtures for common setup +- Mock external API calls (OpenAI, Anthropic) +- Clean up test data (use transactions that rollback) + +**DON'T**: +- Test implementation details +- Leave test data in databases +- Make tests depend on each other +- Use hardcoded API keys (use env vars or mocks) + +### Example Test Pattern + +```python +import pytest +from memori import Memori +from unittest.mock import Mock, patch + +@pytest.fixture +def memori_instance(): + """Fixture providing a test Memori instance.""" + return Memori( + database_connect="sqlite:///:memory:", + user_id="test_user", + openai_api_key="test_key" + ) + +@pytest.mark.unit +def test_memory_recording(memori_instance): + """Test that conversations are recorded correctly.""" + # Arrange + user_input = "Hello" + ai_response = "Hi there!" + + # Act + memori_instance.record( + user_input=user_input, + ai_response=ai_response + ) + + # Assert + history = memori_instance.db_manager.get_chat_history( + user_id="test_user" + ) + assert len(history) == 1 + assert history[0].user_input == user_input +``` + +## Security Considerations + +### 1. SQL Injection Prevention + +**Always use parameterized queries**: + +```python +# GOOD +session.execute( + select(ChatHistory).where(ChatHistory.user_id == user_id) +) + +# BAD - Never do this! +session.execute(f"SELECT * FROM chat_history WHERE user_id = '{user_id}'") +``` + +### 2. Multi-Tenant Isolation + +**Always validate tenant identifiers**: + +```python +from memori.utils.validators import DataValidator + +# Validate user_id before use +if not DataValidator.is_valid_uuid(user_id): + raise ValidationError("Invalid user_id format") +``` + +### 3. API Key Management + +**Never hardcode API keys**: + +```python +# GOOD +import os +api_key = os.getenv("OPENAI_API_KEY") + +# BAD +api_key = "sk-..." +``` + +### 4. Sensitive Data Handling + +**Sanitize logs**: + +```python +# The LoggingManager automatically sanitizes sensitive data +from memori.utils.logging import get_logger + +logger = get_logger(__name__) +logger.info(f"Processing for user {user_id}") # Safe +# API keys, passwords automatically redacted from logs +``` + +### 5. Input Validation + +**Validate all external input**: + +```python +from memori.utils.validators import MemoryValidator + +# Validate memory data before storage +MemoryValidator.validate_memory_input(memory_data) +``` + +## Performance Optimization + +### 1. Database Connection Pooling + +```python +# Configured in pyproject.toml / settings.py +pool_size = 2 # Base connections +max_overflow = 3 # Extra connections +pool_timeout = 30 # Wait time (seconds) +pool_recycle = 3600 # Recycle after (seconds) +pool_pre_ping = True # Verify before use +``` + +### 2. Batch Operations + +For bulk inserts, use batch operations: + +```python +# GOOD - Batch insert +db_manager.batch_insert_memories(memories_list) + +# AVOID - Individual inserts in loop +for memory in memories_list: + db_manager.insert_memory(memory) +``` + +### 3. Index Usage + +Ensure queries use indexes: + +- `user_id` - Always indexed +- `category_primary` - Indexed for filtering +- `importance_score` - Indexed for sorting +- `expires_at` - Indexed for cleanup +- `searchable_content` - Full-text indexed + +### 4. Memory Cleanup + +Short-term memories auto-expire. For manual cleanup: + +```python +# Runs automatically, but can be triggered +db_manager.cleanup_expired_memories() +``` + +### 5. Caching + +Consider caching for frequently accessed data: + +```python +# ConfigManager uses singleton pattern +from memori.config import ConfigManager + +config = ConfigManager() # Reuses existing instance +``` + +## Debugging Tips + +### 1. Enable SQL Echo + +See all SQL queries: + +```python +memori = Memori( + database_connect="sqlite:///debug.db?echo=true" +) +``` + +Or in settings: +```python +DatabaseSettings(echo_sql=True) +``` + +### 2. Increase Log Level + +```python +from memori.utils.logging import LoggingManager + +LoggingManager.set_log_level("DEBUG") +``` + +### 3. Inspect Memory Processing + +```python +# See what the MemoryAgent extracted +from memori.agents import MemoryAgent + +agent = MemoryAgent(openai_api_key="...") +result = await agent.process_conversation( + user_input="I love Python", + ai_response="That's great!" +) +print(result) # Shows entities, categories, scores +``` + +### 4. Test Database State + +```python +# Check what's in the database +with memori.db_manager.get_session() as session: + memories = session.query(ShortTermMemory).all() + for mem in memories: + print(f"{mem.category_primary}: {mem.summary}") +``` + +### 5. Mock LLM Calls + +For testing without API calls: + +```python +from unittest.mock import patch + +with patch('openai.ChatCompletion.create') as mock_create: + mock_create.return_value = {"choices": [...]} + # Your test code +``` + +## Common Pitfalls and Solutions + +### ❌ Pitfall 1: Forgetting Multi-Tenant Filters + +```python +# WRONG - Leaks data between users +memories = session.query(ShortTermMemory).all() + +# RIGHT - Filter by user_id +memories = session.query(ShortTermMemory).filter_by( + user_id=user_id +).all() +``` + +### ❌ Pitfall 2: Not Handling Async Properly + +```python +# WRONG - Async function not awaited +result = memory_agent.process_memory(...) + +# RIGHT +import asyncio +result = asyncio.run(memory_agent.process_memory(...)) +``` + +### ❌ Pitfall 3: Hardcoding Database Paths + +```python +# WRONG +memori = Memori(database_connect="sqlite:///memori.db") + +# RIGHT - Use environment or config +import os +db_url = os.getenv("MEMORI_DB_URL", "sqlite:///memori.db") +memori = Memori(database_connect=db_url) +``` + +### ❌ Pitfall 4: Not Cleaning Up Test Data + +```python +# WRONG - Leaves data in DB +def test_something(): + memori.record(...) + # Test assertions + # No cleanup! + +# RIGHT - Use transactions or cleanup +def test_something(): + memori.record(...) + # Test assertions + memori.db_manager.delete_all_for_user(test_user_id) +``` + +### ❌ Pitfall 5: Ignoring Connection Pool Limits + +```python +# WRONG - Creates new connection per call +for i in range(1000): + memori = Memori(...) # New connection each time! + memori.record(...) + +# RIGHT - Reuse instance +memori = Memori(...) +for i in range(1000): + memori.record(...) +``` + +## File Modification Guidelines + +### High-Risk Files (Modify with Extreme Care) + +These files affect core functionality. Changes require extensive testing: + +- `core/memory.py` - Main API, used by all integrations +- `database/sqlalchemy_manager.py` - Database operations +- `database/models.py` - Schema changes require migrations +- `integrations/*_integration.py` - Breaking changes affect users +- `utils/pydantic_models.py` - Data structure changes cascade + +### Medium-Risk Files (Test Thoroughly) + +- `agents/*.py` - Agent behavior changes +- `database/search_service.py` - Search functionality +- `config/manager.py` - Configuration loading +- `utils/validators.py` - Validation logic + +### Low-Risk Files (Safer to Modify) + +- `examples/*.py` - Example code +- `docs/*.md` - Documentation +- `tests/*.py` - Tests (but don't break them!) +- `utils/helpers.py` - Utility functions (if well-isolated) + +## Pull Request Checklist + +Before submitting a PR: + +- [ ] Code formatted with `black memori/ tests/` +- [ ] Linting passes: `ruff check memori/ tests/ --fix` +- [ ] Imports sorted: `isort memori/ tests/` +- [ ] Type hints added for new functions +- [ ] Tests written and passing: `pytest` +- [ ] Tests cover new code (check with `pytest --cov`) +- [ ] Documentation updated (docstrings, README, CHANGELOG) +- [ ] Examples updated if API changed +- [ ] Commit messages follow conventional format +- [ ] No hardcoded secrets or API keys +- [ ] Multi-tenant isolation maintained +- [ ] Database migrations provided if schema changed +- [ ] Works with all supported databases (if DB change) +- [ ] Works with all supported Python versions (3.10+) + +## Resources + +### Documentation +- Main Docs: https://www.gibsonai.com/docs/memori +- GitHub: https://github.com/GibsonAI/memori +- PyPI: https://pypi.org/project/memorisdk/ + +### Community +- Discord: https://discord.gg/abD4eGym6v +- Issues: https://github.com/GibsonAI/memori/issues + +### Key Dependencies Documentation +- SQLAlchemy 2.0: https://docs.sqlalchemy.org/ +- Pydantic: https://docs.pydantic.dev/ +- LiteLLM: https://docs.litellm.ai/ +- Loguru: https://loguru.readthedocs.io/ + +## Version History + +See [CHANGELOG.md](./CHANGELOG.md) for detailed version history. + +## Quick Reference: Common Commands + +```bash +# Setup +pip install -e ".[dev]" + +# Code Quality +black memori/ tests/ +ruff check memori/ tests/ --fix +isort memori/ tests/ +mypy memori/ + +# Testing +pytest # All tests +pytest -m unit # Unit tests only +pytest -m "not slow" # Skip slow tests +pytest --cov=memori # With coverage + +# Documentation +mkdocs serve # Local docs server + +# Package +python -m build # Build distribution +pip install -e . # Install locally +``` + +--- + +**Last Updated**: 2025-11-14 +**For**: AI Assistants working with Memori codebase +**Maintained By**: Memori Labs Team diff --git a/examples/mcp/advanced_workflow_example.py b/examples/mcp/advanced_workflow_example.py new file mode 100644 index 0000000..c6771d1 --- /dev/null +++ b/examples/mcp/advanced_workflow_example.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +""" +Advanced MCP Workflow Example + +This example demonstrates more complex workflows using the Memori MCP server, +including: +- Multi-session management +- Category-based filtering +- Importance-based search +- Memory lifecycle management +""" + +import sys +import os +from datetime import datetime + +# Add parent directory to path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +# Set environment variables +os.environ["MEMORI_DATABASE_URL"] = "sqlite:///advanced_mcp.db" +os.environ["OPENAI_API_KEY"] = "your-api-key-here" + +from mcp.memori_mcp_server import ( + record_conversation, + search_memories, + get_recent_memories, + get_memory_statistics, + clear_session_memories, +) + + +def simulate_project_conversation(): + """Simulate a conversation about a software project""" + + print("\n" + "=" * 80) + print("SCENARIO 1: Software Project Discussion") + print("=" * 80) + + user_id = "developer_123" + session_id = "project_planning" + + # Day 1: Initial project discussion + print("\nDay 1: Initial Planning") + conversations = [ + { + "user": "I'm starting a new e-commerce platform project", + "ai": "Exciting! Let's plan this carefully. What's your tech stack preference?", + }, + { + "user": "I want to use Next.js for frontend and Python FastAPI for backend", + "ai": "Great choices! Next.js provides excellent SEO and FastAPI is very performant.", + }, + { + "user": "I'll need user authentication, product catalog, and payment processing", + "ai": "Those are the core features. For auth, I'd suggest JWT tokens. For payments, Stripe is a solid choice.", + }, + ] + + for conv in conversations: + result = record_conversation( + user_input=conv["user"], + ai_response=conv["ai"], + user_id=user_id, + session_id=session_id, + ) + print(f" Recorded: {conv['user'][:50]}...") + + # Check what was recorded + stats = get_memory_statistics(user_id=user_id) + print(f"\n Stats after Day 1: {stats['statistics']['total_memories']} memories") + + +def simulate_preference_learning(): + """Simulate learning user preferences over time""" + + print("\n" + "=" * 80) + print("SCENARIO 2: Learning User Preferences") + print("=" * 80) + + user_id = "power_user_456" + + preferences = [ + { + "user": "I always prefer TypeScript over JavaScript", + "ai": "Noted! I'll suggest TypeScript solutions from now on.", + }, + { + "user": "I like to write comprehensive tests for everything", + "ai": "Great practice! I'll make sure to include test examples in my suggestions.", + }, + { + "user": "I prefer functional programming patterns when possible", + "ai": "Understood! I'll focus on functional approaches and avoid mutations.", + }, + { + "user": "I use VS Code with Vim keybindings", + "ai": "Nice setup! I'll keep that in mind when suggesting editor configurations.", + }, + ] + + for pref in preferences: + result = record_conversation( + user_input=pref["user"], + ai_response=pref["ai"], + user_id=user_id, + ) + print(f" Learned: {pref['user']}") + + # Search for preferences + print("\n Searching for programming preferences...") + results = search_memories( + query="programming preferences", + user_id=user_id, + category="preference", + limit=10, + ) + + print(f" Found {results['total_results']} preferences:") + for mem in results.get('results', []): + print(f" - {mem.get('summary', 'N/A')}") + + +def simulate_multi_session_workflow(): + """Simulate working across multiple sessions""" + + print("\n" + "=" * 80) + print("SCENARIO 3: Multi-Session Workflow") + print("=" * 80) + + user_id = "researcher_789" + + # Session 1: Research on AI + print("\n Session 1: AI Research") + session1_id = "ai_research" + for conv in [ + ("Tell me about transformer architectures", "Transformers revolutionized NLP..."), + ("How do attention mechanisms work?", "Attention allows models to focus on relevant parts..."), + ]: + record_conversation(conv[0], conv[1], user_id, session_id=session1_id) + print(f" Recorded: {conv[0][:40]}...") + + # Session 2: Research on databases + print("\n Session 2: Database Research") + session2_id = "database_research" + for conv in [ + ("What are the benefits of PostgreSQL?", "PostgreSQL offers ACID compliance, advanced features..."), + ("Explain database indexing", "Indexes speed up queries by creating lookup structures..."), + ]: + record_conversation(conv[0], conv[1], user_id, session_id=session2_id) + print(f" Recorded: {conv[0][:40]}...") + + # Session 3: Temporary brainstorming + print("\n Session 3: Temporary Brainstorming") + session3_id = "temp_brainstorm" + for conv in [ + ("Random idea: AI-powered code reviewer", "Interesting! That could help catch bugs..."), + ("Another idea: Automated documentation generator", "That would save a lot of time..."), + ]: + record_conversation(conv[0], conv[1], user_id, session_id=session3_id) + print(f" Recorded: {conv[0][:40]}...") + + # Now clear the temporary session + print("\n Clearing temporary brainstorming session...") + clear_result = clear_session_memories( + session_id=session3_id, + user_id=user_id, + ) + print(f" {clear_result.get('message', 'Done')}") + + # Check remaining memories + stats = get_memory_statistics(user_id=user_id) + print(f"\n Final stats: {stats['statistics']['total_memories']} memories (temp session cleared)") + + +def demonstrate_search_filtering(): + """Demonstrate advanced search with filtering""" + + print("\n" + "=" * 80) + print("SCENARIO 4: Advanced Search Filtering") + print("=" * 80) + + user_id = "data_scientist_101" + + # Record various types of information + print("\n Recording diverse information...") + + facts = [ + "I work with pandas and numpy daily", + "My current dataset has 10 million rows", + "I'm using scikit-learn for machine learning", + ] + + for fact in facts: + record_conversation(fact, "Got it, I'll remember that.", user_id) + + # Search with different filters + print("\n Searching for 'data' with category filter...") + results = search_memories( + query="data science tools", + user_id=user_id, + category="fact", + limit=5, + ) + print(f" Found {results['total_results']} fact-type memories") + + print("\n Searching for high-importance memories...") + results = search_memories( + query="machine learning", + user_id=user_id, + min_importance=0.7, + limit=5, + ) + print(f" Found {results['total_results']} high-importance memories") + + +def main(): + """Run all advanced workflow examples""" + + print("=" * 80) + print("Memori MCP Server - Advanced Workflow Examples") + print("=" * 80) + + # Run each scenario + simulate_project_conversation() + simulate_preference_learning() + simulate_multi_session_workflow() + demonstrate_search_filtering() + + print("\n" + "=" * 80) + print("All scenarios completed! Check advanced_mcp.db for stored data.") + print("=" * 80) + + +if __name__ == "__main__": + main() diff --git a/examples/mcp/basic_usage_example.py b/examples/mcp/basic_usage_example.py new file mode 100644 index 0000000..3b9b0fa --- /dev/null +++ b/examples/mcp/basic_usage_example.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +""" +Basic MCP Usage Example + +This example demonstrates how to test the Memori MCP server tools directly +without needing a full MCP client like Claude Desktop. + +This is useful for: +- Testing the MCP server functionality +- Understanding how the tools work +- Debugging issues +- Development and iteration +""" + +import sys +import os + +# Add parent directory to path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +# Set environment variables +os.environ["MEMORI_DATABASE_URL"] = "sqlite:///test_mcp.db" +os.environ["OPENAI_API_KEY"] = "your-api-key-here" + +# Import the MCP server tools +from mcp.memori_mcp_server import ( + record_conversation, + search_memories, + get_recent_memories, + get_memory_statistics, + get_conversation_history, +) + + +def main(): + """Run basic MCP tool examples""" + + print("=" * 80) + print("Memori MCP Server - Basic Usage Example") + print("=" * 80) + print() + + user_id = "demo_user" + + # Example 1: Record a conversation + print("1. Recording a conversation...") + result = record_conversation( + user_input="I'm building a web application with FastAPI and PostgreSQL", + ai_response="That's great! FastAPI is excellent for building high-performance APIs. Would you like help with the database setup?", + user_id=user_id, + ) + print(f" Result: {result}") + print() + + # Example 2: Record another conversation + print("2. Recording another conversation...") + result = record_conversation( + user_input="Yes, I need help setting up SQLAlchemy models", + ai_response="I can help you with that. Let's start by defining your database models using SQLAlchemy ORM.", + user_id=user_id, + ) + print(f" Result: {result}") + print() + + # Example 3: Record a preference + print("3. Recording a user preference...") + result = record_conversation( + user_input="I prefer using async/await patterns in Python", + ai_response="Noted! I'll keep that in mind and suggest async patterns when appropriate.", + user_id=user_id, + ) + print(f" Result: {result}") + print() + + # Example 4: Get memory statistics + print("4. Getting memory statistics...") + stats = get_memory_statistics(user_id=user_id) + print(f" Statistics: {stats}") + print() + + # Example 5: Search for memories + print("5. Searching for memories about 'Python'...") + search_results = search_memories( + query="Python programming", + user_id=user_id, + limit=5, + ) + print(f" Found {search_results.get('total_results', 0)} results") + for i, memory in enumerate(search_results.get('results', []), 1): + print(f" {i}. {memory.get('summary', 'N/A')} (Category: {memory.get('category', 'N/A')})") + print() + + # Example 6: Get recent memories + print("6. Getting recent memories...") + recent = get_recent_memories( + user_id=user_id, + limit=5, + ) + print(f" Found {recent.get('total_results', 0)} recent memories") + for i, memory in enumerate(recent.get('memories', []), 1): + print(f" {i}. {memory.get('summary', 'N/A')}") + print() + + # Example 7: Get conversation history + print("7. Getting conversation history...") + history = get_conversation_history( + user_id=user_id, + limit=10, + ) + print(f" Found {history.get('total_conversations', 0)} conversations") + for i, conv in enumerate(history.get('conversations', []), 1): + print(f" {i}. User: {conv.get('user_input', 'N/A')[:50]}...") + print(f" AI: {conv.get('ai_output', 'N/A')[:50]}...") + print() + + print("=" * 80) + print("Example completed! Check test_mcp.db for stored data.") + print("=" * 80) + + +if __name__ == "__main__": + main() diff --git a/examples/mcp/openrouter_example.py b/examples/mcp/openrouter_example.py new file mode 100644 index 0000000..659f8aa --- /dev/null +++ b/examples/mcp/openrouter_example.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +""" +OpenRouter MCP Example + +This example demonstrates how to use the Memori MCP server with OpenRouter, +which provides access to 100+ LLMs including Claude, GPT-4, Llama, Mistral, and more. + +Benefits of using OpenRouter: +- Access to 100+ models through a single API +- Competitive pricing with automatic fallbacks +- No need for multiple API keys +- Free models available (Llama, Mistral, etc.) +- Usage tracking and analytics + +Setup: +1. Get your OpenRouter API key from https://openrouter.ai/keys +2. Set environment variables +3. Run this script to test + +Cost comparison (approximate): +- Claude 3.5 Sonnet: $0.003/1K tokens (input), $0.015/1K tokens (output) +- GPT-4o: $0.005/1K tokens (input), $0.015/1K tokens (output) +- Llama 3.1 70B: FREE (community-hosted) +- Mistral 8x7B: $0.00024/1K tokens (input), $0.00024/1K tokens (output) +""" + +import sys +import os + +# Add parent directory to path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +# Configure environment for OpenRouter +print("=" * 80) +print("Memori MCP Server - OpenRouter Example") +print("=" * 80) +print() + +# Check for OpenRouter API key +openrouter_key = os.getenv("OPENROUTER_API_KEY") +if not openrouter_key: + print("❌ OPENROUTER_API_KEY not set!") + print() + print("To use OpenRouter:") + print("1. Get your API key from https://openrouter.ai/keys") + print("2. Set environment variable:") + print(" export OPENROUTER_API_KEY='sk-or-v1-your-key-here'") + print() + print("Or for testing, set it in this script:") + print(" os.environ['OPENROUTER_API_KEY'] = 'sk-or-v1-your-key-here'") + print() + sys.exit(1) + +# Configure OpenRouter settings +os.environ["MEMORI_DATABASE_URL"] = "sqlite:///openrouter_mcp_test.db" +os.environ["OPENROUTER_API_KEY"] = openrouter_key + +# Choose your model +# Popular options: +# - anthropic/claude-3.5-sonnet (best for structured tasks) +# - anthropic/claude-3-opus (most capable) +# - openai/gpt-4o (fastest GPT-4) +# - meta-llama/llama-3.1-70b-instruct (FREE!) +# - google/gemini-pro-1.5 (Google's Gemini) +# - mistralai/mixtral-8x7b-instruct (cost-effective) + +model = os.getenv("OPENROUTER_MODEL", "anthropic/claude-3.5-sonnet") +os.environ["OPENROUTER_MODEL"] = model + +# Optional: Set app name for OpenRouter rankings +os.environ["OPENROUTER_APP_NAME"] = "Memori MCP Example" +os.environ["OPENROUTER_SITE_URL"] = "https://github.com/GibsonAI/memori" + +print(f"✓ Using OpenRouter with model: {model}") +print(f"✓ Database: openrouter_mcp_test.db") +print() + +# Import the MCP server tools +from mcp.memori_mcp_server import ( + record_conversation, + search_memories, + get_recent_memories, + get_memory_statistics, +) + + +def main(): + """Run OpenRouter MCP examples""" + + user_id = "openrouter_demo_user" + + print("=" * 80) + print("Testing Memori MCP with OpenRouter") + print("=" * 80) + print() + + # Example 1: Record some conversations + print("1. Recording conversations with OpenRouter model...") + print() + + conversations = [ + { + "user": "I'm interested in machine learning and AI", + "ai": "That's great! ML and AI are exciting fields. What aspects interest you most?", + }, + { + "user": "I'm particularly interested in large language models and how they work", + "ai": "LLMs are fascinating! They use transformer architectures with attention mechanisms. Would you like to learn about the technical details?", + }, + { + "user": "Yes, and I also want to build practical applications with them", + "ai": "Excellent! Building with LLMs involves understanding prompting, fine-tuning, and RAG patterns. Let's explore these together.", + }, + ] + + for i, conv in enumerate(conversations, 1): + print(f" Recording conversation {i}...") + result = record_conversation( + user_input=conv["user"], + ai_response=conv["ai"], + user_id=user_id, + ) + + if result.get("success"): + print(f" ✓ Recorded (chat_id: {result.get('chat_id')})") + else: + print(f" ✗ Failed: {result.get('error')}") + print() + + print() + + # Example 2: Get memory statistics + print("2. Getting memory statistics...") + stats = get_memory_statistics(user_id=user_id) + + if stats.get("success"): + s = stats["statistics"] + print(f" ✓ Total conversations: {s['total_conversations']}") + print(f" ✓ Total memories: {s['total_memories']}") + print(f" ✓ Short-term: {s['short_term_memories']}") + print(f" ✓ Long-term: {s['long_term_memories']}") + + if s["category_distribution"]: + print(f" ✓ Categories: {', '.join(s['category_distribution'].keys())}") + else: + print(f" ✗ Failed: {stats.get('error')}") + + print() + + # Example 3: Search memories + print("3. Searching for memories about 'machine learning'...") + search_results = search_memories( + query="machine learning and AI", + user_id=user_id, + limit=5, + ) + + if search_results.get("success"): + print(f" ✓ Found {search_results.get('total_results')} results") + for i, mem in enumerate(search_results.get('results', []), 1): + print(f" {i}. {mem.get('summary', 'N/A')[:60]}...") + print(f" Category: {mem.get('category')} | Importance: {mem.get('importance_score', 0):.2f}") + else: + print(f" ✗ Failed: {search_results.get('error')}") + + print() + + # Example 4: Get recent memories + print("4. Getting recent memories...") + recent = get_recent_memories(user_id=user_id, limit=5) + + if recent.get("success"): + print(f" ✓ Found {recent.get('total_results')} recent memories") + for i, mem in enumerate(recent.get('memories', []), 1): + print(f" {i}. {mem.get('summary', 'N/A')[:60]}...") + else: + print(f" ✗ Failed: {recent.get('error')}") + + print() + + # Example 5: Model comparison tip + print("=" * 80) + print("💡 Model Selection Tips") + print("=" * 80) + print() + print("For memory processing (entity extraction, categorization):") + print() + print("Best quality:") + print(" - anthropic/claude-3.5-sonnet ($$$)") + print(" - openai/gpt-4o ($$$)") + print() + print("Good balance:") + print(" - anthropic/claude-3-haiku ($$)") + print(" - openai/gpt-4o-mini ($$)") + print() + print("Free/cheap:") + print(" - meta-llama/llama-3.1-70b-instruct (FREE)") + print(" - mistralai/mixtral-8x7b-instruct ($)") + print() + print("To change model, set environment variable:") + print(" export OPENROUTER_MODEL='anthropic/claude-3.5-sonnet'") + print() + print("See all models: https://openrouter.ai/models") + print() + + print("=" * 80) + print("Example completed!") + print("=" * 80) + print() + print(f"Database: openrouter_mcp_test.db") + print(f"Model used: {model}") + print() + print("Next steps:") + print("1. Try different models by changing OPENROUTER_MODEL") + print("2. Check your usage at https://openrouter.ai/activity") + print("3. Configure in Claude Desktop - see mcp/claude_desktop_config_openrouter.json") + print() + + +if __name__ == "__main__": + main() diff --git a/mcp/QUICKSTART.md b/mcp/QUICKSTART.md new file mode 100644 index 0000000..5329538 --- /dev/null +++ b/mcp/QUICKSTART.md @@ -0,0 +1,255 @@ +# Memori MCP Server - Quick Start Guide + +Get the Memori MCP server running in Claude Desktop in under 5 minutes! + +## Prerequisites + +- Claude Desktop installed +- Python 3.10+ installed +- OpenAI API key (for memory processing) + +## 5-Minute Setup + +### Step 1: Install uv (30 seconds) + +```bash +# macOS/Linux +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Windows +powershell -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +### Step 2: Clone Memori (1 minute) + +```bash +git clone https://github.com/GibsonAI/memori.git +cd memori +``` + +### Step 3: Configure Claude Desktop (2 minutes) + +**macOS**: Open `~/Library/Application Support/Claude/claude_desktop_config.json` + +**Windows**: Open `%APPDATA%\Claude\claude_desktop_config.json` + +**Linux**: Open `~/.config/Claude/claude_desktop_config.json` + +Add this configuration (replace `/absolute/path/to/memori` and `your-api-key`): + +```json +{ + "mcpServers": { + "memori": { + "command": "uv", + "args": [ + "--directory", + "/absolute/path/to/memori", + "run", + "--with", + "mcp", + "--with", + "fastmcp", + "--with-editable", + ".", + "python", + "mcp/memori_mcp_server.py" + ], + "env": { + "MEMORI_DATABASE_URL": "sqlite:///memori_mcp.db", + "OPENAI_API_KEY": "your-openai-api-key-here" + } + } + } +} +``` + +### Step 4: Restart Claude Desktop (30 seconds) + +Completely quit and restart Claude Desktop. + +### Step 5: Verify (30 seconds) + +Look for the 🔨 hammer icon in Claude Desktop. You should see the "memori" server with 6 tools available. + +## Try It Out! + +Start a conversation in Claude: + +**You**: "Record this: I'm working on a Python FastAPI project with PostgreSQL" + +**Claude**: [Uses the record_conversation tool and confirms] + +**You**: "What do you remember about my projects?" + +**Claude**: [Uses search_memories to recall what you told it] + +That's it! Claude now has persistent memory. + +## What's Happening? + +1. **When you talk to Claude**, it can use Memori tools to remember important information +2. **Conversations are stored** in a local SQLite database (memori_mcp.db) +3. **Memories are processed** using OpenAI to extract entities, categories, and importance +4. **Claude can search** these memories in future conversations + +## Common Issues + +### "Server not found" or hammer icon doesn't appear + +- Check the config file path is correct for your OS +- Verify the JSON syntax is valid (use a JSON validator) +- Make sure the absolute path to memori is correct +- Restart Claude Desktop completely + +### "Command not found: uv" + +```bash +# Install uv +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Add to PATH (if needed) +export PATH="$HOME/.cargo/bin:$PATH" +``` + +### "OpenAI API Error" + +- Check your OPENAI_API_KEY is correct +- Verify you have API credits +- Ensure the key has necessary permissions + +### "Database errors" + +- Check the MEMORI_DATABASE_URL path is writable +- For SQLite, ensure the directory exists +- For PostgreSQL/MySQL, verify connection credentials + +## Next Steps + +- Read the [full MCP README](README.md) for advanced usage +- Try the [example scripts](../examples/mcp/) +- Configure a production database (PostgreSQL/MySQL) +- Explore the 6 available tools in Claude +- Check memory statistics with "How many memories do you have about me?" + +## LLM Provider Options + +By default, the quickstart uses OpenAI. But you can use other providers: + +### OpenRouter (100+ Models - Recommended) + +Use Claude, GPT-4, Llama, Mistral, and 100+ other models through a single API: + +```json +"env": { + "MEMORI_DATABASE_URL": "sqlite:///memori_mcp.db", + "OPENROUTER_API_KEY": "sk-or-v1-your-key-here", + "OPENROUTER_MODEL": "anthropic/claude-3.5-sonnet" +} +``` + +**Get API key:** https://openrouter.ai/keys + +**Popular models:** +- `anthropic/claude-3.5-sonnet` - Best for structured tasks +- `openai/gpt-4o` - OpenAI's fastest GPT-4 +- `meta-llama/llama-3.1-70b-instruct` - Free, open-source + +### Local Models (Free) + +Run models locally with Ollama: + +```json +"env": { + "MEMORI_DATABASE_URL": "sqlite:///memori_mcp.db", + "LLM_BASE_URL": "http://localhost:11434/v1", + "LLM_MODEL": "llama3.1:8b" +} +``` + +First install Ollama: https://ollama.ai + +### Azure OpenAI + +```json +"env": { + "MEMORI_DATABASE_URL": "sqlite:///memori_mcp.db", + "AZURE_OPENAI_API_KEY": "your-key", + "AZURE_OPENAI_ENDPOINT": "https://your-resource.openai.azure.com", + "AZURE_OPENAI_DEPLOYMENT": "your-deployment" +} +``` + +See [full configuration guide](README.md#llm-provider-configuration) for more options. + +## Database Options + +### Local SQLite (Default - Good for personal use) +```json +"MEMORI_DATABASE_URL": "sqlite:///memori_mcp.db" +``` + +### PostgreSQL (Production) +```json +"MEMORI_DATABASE_URL": "postgresql://user:password@localhost/memori" +``` + +### MySQL (Production) +```json +"MEMORI_DATABASE_URL": "mysql://user:password@localhost/memori" +``` + +### Cloud PostgreSQL (Neon, Supabase, etc.) +```json +"MEMORI_DATABASE_URL": "postgresql://user:password@host.region.provider.com/memori" +``` + +## Available Tools + +Once configured, Claude can use these tools: + +1. **record_conversation** - Store conversations +2. **search_memories** - Find relevant memories +3. **get_recent_memories** - Get recent context +4. **get_memory_statistics** - View memory stats +5. **get_conversation_history** - See past conversations +6. **clear_session_memories** - Reset session + +## Multi-User Setup + +Each user gets isolated memories by default. To use with different users: + +**In Claude**: "Record this for user 'alice': She prefers Python over JavaScript" + +The server automatically handles multi-tenant isolation. + +## Security Notes + +- Your database is LOCAL (SQLite) or on YOUR server (PostgreSQL/MySQL) +- OpenAI API is only used for processing (extracting entities/importance) +- No data is sent to Memori servers (it's open-source, runs locally) +- Multi-tenant isolation ensures user privacy + +## Cost Estimate + +- **Memori software**: Free (open-source) +- **Database**: Free (SQLite) or your hosting cost +- **OpenAI API**: ~$0.01-0.10 per conversation for processing +- **Total**: Minimal cost, full control + +## Support + +- **GitHub Issues**: https://github.com/GibsonAI/memori/issues +- **Discord**: https://discord.gg/abD4eGym6v +- **Documentation**: https://www.gibsonai.com/docs/memori + +## What's Next? + +Explore advanced features: +- Session management for different contexts +- Category filtering (facts, preferences, skills) +- Importance-based search +- Long-term vs short-term memory +- Custom memory processing + +Happy remembering! 🧠 diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 0000000..3979a2a --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,507 @@ +# Memori MCP Server + +A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that exposes Memori's persistent memory capabilities to any MCP-compatible AI assistant like Claude Desktop. + +## What is MCP? + +The Model Context Protocol (MCP) is an open protocol that enables AI assistants to securely access external tools and data sources. Think of it as a standardized way for AI systems to "remember" and access information across conversations. + +## What Does This Server Do? + +The Memori MCP Server gives Claude (or any MCP-compatible AI) the ability to: + +- **Remember conversations** across sessions +- **Store and retrieve facts** about users, projects, and preferences +- **Search past memories** intelligently +- **Track conversation history** with full context +- **Manage memory lifecycle** (short-term vs long-term) +- **Get insights** from stored memories + +## Features + +### Tools (Actions) + +1. **record_conversation** - Store user/AI conversation turns +2. **search_memories** - Intelligent memory search with filters +3. **get_recent_memories** - Get recent context +4. **get_memory_statistics** - Memory analytics and insights +5. **get_conversation_history** - Raw conversation history +6. **clear_session_memories** - Reset session context + +### Resources (Data Access) + +1. **memori://memories/{user_id}** - View all memories for a user +2. **memori://stats/{user_id}** - View memory statistics + +### Prompts (Templates) + +1. **memory_search_prompt** - Search and summarize memories +2. **conversation_context_prompt** - Get recent context + +## Installation + +### Prerequisites + +- Python 3.10+ +- [uv](https://docs.astral.sh/uv/) package manager +- Claude Desktop (or any MCP-compatible client) + +### Step 1: Install uv + +```bash +# macOS/Linux +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Windows +powershell -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +### Step 2: Clone Memori Repository + +```bash +git clone https://github.com/GibsonAI/memori.git +cd memori +``` + +### Step 3: Install Memori with MCP Support + +```bash +# Install with MCP dependencies +pip install -e ".[all]" +pip install mcp fastmcp +``` + +### Step 4: Configure Claude Desktop + +#### macOS + +Edit `~/Library/Application Support/Claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "memori": { + "command": "uv", + "args": [ + "--directory", + "/absolute/path/to/memori", + "run", + "--with", + "mcp", + "--with", + "fastmcp", + "--with-editable", + ".", + "python", + "mcp/memori_mcp_server.py" + ], + "env": { + "MEMORI_DATABASE_URL": "sqlite:///memori_mcp.db", + "OPENAI_API_KEY": "your-openai-api-key-here" + } + } + } +} +``` + +#### Windows + +Edit `%APPDATA%\Claude\claude_desktop_config.json` with the same content (use Windows-style paths). + +#### Linux + +Edit `~/.config/Claude/claude_desktop_config.json` with the same content. + +### Step 5: Restart Claude Desktop + +After saving the configuration, restart Claude Desktop completely (quit and reopen). + +### Step 6: Verify Installation + +Look for the 🔨 hammer icon in Claude Desktop. Click it to see available MCP servers. You should see "memori" listed with 6 tools available. + +## Configuration + +### LLM Provider Configuration + +The MCP server supports multiple LLM providers for memory processing. Choose one: + +#### Option 1: OpenAI (Default) + +```json +"env": { + "MEMORI_DATABASE_URL": "sqlite:///memori_mcp.db", + "OPENAI_API_KEY": "sk-your-api-key-here" +} +``` + +#### Option 2: OpenRouter (Recommended - Access 100+ Models) + +OpenRouter provides access to 100+ LLMs including Claude, GPT-4, Llama, Mistral, and more through a single API. + +```json +"env": { + "MEMORI_DATABASE_URL": "sqlite:///memori_mcp.db", + "OPENROUTER_API_KEY": "sk-or-v1-your-api-key-here", + "OPENROUTER_MODEL": "anthropic/claude-3.5-sonnet", + "OPENROUTER_APP_NAME": "Memori MCP Server", + "OPENROUTER_SITE_URL": "https://github.com/GibsonAI/memori" +} +``` + +**Popular OpenRouter Models:** +- `anthropic/claude-3.5-sonnet` - Best for structured tasks +- `anthropic/claude-3-opus` - Most capable Claude model +- `openai/gpt-4o` - OpenAI's fastest GPT-4 +- `openai/gpt-4-turbo` - GPT-4 with 128k context +- `meta-llama/llama-3.1-70b-instruct` - Free, open-source +- `google/gemini-pro-1.5` - Google's Gemini Pro +- `mistralai/mixtral-8x7b-instruct` - Fast, cost-effective + +**Get your OpenRouter API key:** https://openrouter.ai/keys + +#### Option 3: Azure OpenAI + +```json +"env": { + "MEMORI_DATABASE_URL": "sqlite:///memori_mcp.db", + "AZURE_OPENAI_API_KEY": "your-azure-key", + "AZURE_OPENAI_ENDPOINT": "https://your-resource.openai.azure.com", + "AZURE_OPENAI_DEPLOYMENT": "your-deployment-name", + "AZURE_OPENAI_API_VERSION": "2024-02-15-preview", + "AZURE_OPENAI_MODEL": "gpt-4o" +} +``` + +#### Option 4: Custom OpenAI-Compatible Endpoint + +For Ollama, LM Studio, LocalAI, or any OpenAI-compatible API: + +```json +"env": { + "MEMORI_DATABASE_URL": "sqlite:///memori_mcp.db", + "LLM_BASE_URL": "http://localhost:11434/v1", + "LLM_API_KEY": "not-needed-for-local", + "LLM_MODEL": "llama3.1:8b" +} +``` + +### Environment Variables Reference + +| Variable | Description | Default | Required | +|----------|-------------|---------|----------| +| `MEMORI_DATABASE_URL` | Database connection string | `sqlite:///memori_mcp.db` | No | +| **OpenAI** ||| +| `OPENAI_API_KEY` | OpenAI API key | None | Yes (if using OpenAI) | +| `OPENAI_MODEL` | Model to use | `gpt-4o` | No | +| **OpenRouter** ||| +| `OPENROUTER_API_KEY` | OpenRouter API key | None | Yes (if using OpenRouter) | +| `OPENROUTER_MODEL` | Model to use | `openai/gpt-4o` | No | +| `OPENROUTER_BASE_URL` | API base URL | `https://openrouter.ai/api/v1` | No | +| `OPENROUTER_APP_NAME` | Your app name (for rankings) | None | No | +| `OPENROUTER_SITE_URL` | Your site URL (for rankings) | None | No | +| **Azure OpenAI** ||| +| `AZURE_OPENAI_API_KEY` | Azure API key | None | Yes (if using Azure) | +| `AZURE_OPENAI_ENDPOINT` | Azure endpoint URL | None | Yes (if using Azure) | +| `AZURE_OPENAI_DEPLOYMENT` | Deployment name | None | Yes (if using Azure) | +| `AZURE_OPENAI_API_VERSION` | API version | `2024-02-15-preview` | No | +| `AZURE_OPENAI_MODEL` | Model name | `gpt-4o` | No | +| **Custom/Local** ||| +| `LLM_BASE_URL` | Custom API base URL | None | Yes (if using custom) | +| `LLM_API_KEY` | Custom API key | None | No (for local) | +| `LLM_MODEL` | Model to use | `gpt-4o` | No | + +### Database Options + +The server supports all Memori database backends: + +#### SQLite (Default - Local File) +```json +"env": { + "MEMORI_DATABASE_URL": "sqlite:///memori_mcp.db" +} +``` + +#### PostgreSQL (Production) +```json +"env": { + "MEMORI_DATABASE_URL": "postgresql://user:password@localhost/memori" +} +``` + +#### MySQL (Production) +```json +"env": { + "MEMORI_DATABASE_URL": "mysql://user:password@localhost/memori" +} +``` + +## Usage Examples + +### Recording a Conversation + +In Claude Desktop, you can say: + +> "Record this conversation: I told you I'm working on a Python web app using FastAPI, and you suggested using SQLAlchemy for the database." + +Claude will use the `record_conversation` tool to store this memory. + +### Searching Memories + +> "What do you remember about my Python projects?" + +Claude will use `search_memories` to find relevant information. + +### Getting Context + +> "What were we talking about recently?" + +Claude will use `get_recent_memories` to retrieve recent context. + +### Memory Statistics + +> "How many memories do you have about me?" + +Claude will use `get_memory_statistics` to provide insights. + +## Multi-User Support + +The MCP server supports multi-tenant isolation using `user_id`. Each user's memories are completely isolated. + +By default, the server uses: +- `user_id`: "default_user" +- `assistant_id`: "mcp_assistant" +- `session_id`: Optional (for conversation grouping) + +You can specify different user IDs when using the tools: + +```python +# In Claude, you might say: +"Record this for user 'alice': She prefers Python over JavaScript" +``` + +## Security Considerations + +### API Keys + +**Never commit your OpenAI API key to version control.** Always use environment variables or secure configuration management. + +### Database Access + +The MCP server has full access to the configured database. Ensure: +- Database credentials are kept secure +- Connection strings use authentication +- Multi-tenant isolation is maintained via `user_id` + +### Multi-Tenant Isolation + +All tools enforce user isolation. Memories from one user cannot be accessed by another user (unless explicitly shared). + +## Development + +### Running the Server Standalone + +You can test the server without Claude Desktop: + +```bash +# From the memori directory +cd mcp +python memori_mcp_server.py +``` + +This starts the MCP server in stdio mode, ready to accept MCP protocol messages. + +### Testing Tools + +Use the MCP Inspector for interactive testing: + +```bash +npx @modelcontextprotocol/inspector uv --directory /path/to/memori run --with mcp --with fastmcp --with-editable . python mcp/memori_mcp_server.py +``` + +### Adding New Tools + +To add a new tool to the MCP server: + +1. Add a function decorated with `@mcp.tool()`: + +```python +@mcp.tool() +def my_new_tool(param1: str, param2: int) -> Dict[str, Any]: + """ + Description of what this tool does. + + Args: + param1: Description + param2: Description + + Returns: + Result dictionary + """ + # Implementation + return {"success": True, "result": "..."} +``` + +2. Restart the MCP server (restart Claude Desktop) + +### Adding New Resources + +```python +@mcp.resource("memori://my-resource/{identifier}") +def get_my_resource(identifier: str) -> str: + """Resource description""" + return f"Resource data for {identifier}" +``` + +### Adding New Prompts + +```python +@mcp.prompt() +def my_prompt_template(param: str) -> str: + """Prompt description""" + return f"Generated prompt using {param}" +``` + +## Troubleshooting + +### Server Not Appearing in Claude + +1. Check the config file location and syntax (valid JSON) +2. Ensure `uv` is installed and in PATH +3. Verify the absolute path to memori directory +4. Restart Claude Desktop completely +5. Check Claude Desktop logs: + - macOS: `~/Library/Logs/Claude/` + - Windows: `%APPDATA%\Claude\logs\` + +### "Command not found: uv" + +Install uv: +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +### Database Connection Errors + +- Check the `MEMORI_DATABASE_URL` format +- Ensure database server is running (for PostgreSQL/MySQL) +- Verify credentials are correct +- Check file permissions (for SQLite) + +### Import Errors + +Ensure Memori is installed in editable mode: +```bash +cd /path/to/memori +pip install -e . +``` + +### API Key Errors + +- Verify `OPENAI_API_KEY` is set in the config +- Check the API key is valid +- Ensure you have credits/quota remaining + +## Architecture + +``` +┌─────────────────────────────────────────┐ +│ Claude Desktop / MCP Client │ +└────────────────┬────────────────────────┘ + │ MCP Protocol (stdio) + ┌────────▼──────────┐ + │ Memori MCP Server│ + │ (FastMCP) │ + └────────┬──────────┘ + │ + ┌────────▼──────────┐ + │ Memori SDK │ + │ - Memory Agent │ + │ - Search Engine │ + │ - DB Manager │ + └────────┬──────────┘ + │ + ┌────────▼──────────┐ + │ SQL Database │ + │ (SQLite/PG/MySQL)│ + └───────────────────┘ +``` + +## Performance Considerations + +### Database Choice + +- **SQLite**: Great for personal use, single user, local storage +- **PostgreSQL**: Best for production, multi-user, cloud deployment +- **MySQL**: Alternative for production workloads + +### Memory Management + +The server caches Memori instances per user to avoid repeated initialization. Instances are kept in memory for the server's lifetime. + +### Search Performance + +- Uses database-native full-text search (FTS5, FULLTEXT, tsvector) +- Indexed on user_id, category, importance +- Limit results to avoid overwhelming the AI + +## Examples + +See the `examples/mcp/` directory for: +- `basic_usage.py` - Simple MCP client example +- `advanced_workflow.py` - Complex multi-tool workflows +- `custom_integration.py` - Integrating with your own MCP client + +## FAQ + +### Q: Can I use this with other AI assistants besides Claude? + +Yes! Any MCP-compatible client can use this server. The protocol is open and client-agnostic. + +### Q: How much does it cost to run? + +The server itself is free (open-source). Costs: +- Database: Free (SQLite) or hosting costs (PostgreSQL/MySQL) +- OpenAI API: Used for memory processing (~$0.01-0.10 per conversation) + +### Q: Can multiple users share memories? + +By default, no. Each `user_id` has isolated memories. You could build cross-user sharing by creating a "shared" user_id. + +### Q: How do I backup my memories? + +Memories are stored in your database. Backup strategies: +- SQLite: Copy the .db file +- PostgreSQL/MySQL: Use standard database backup tools (pg_dump, mysqldump) + +### Q: Can I export my memories? + +Yes! Memories are stored in standard SQL tables. You can: +- Query directly with SQL +- Export to CSV/JSON +- Use Memori's export tools (coming soon) + +### Q: How long are memories retained? + +- **Short-term**: ~7 days (configurable via `expires_at`) +- **Long-term**: Permanent (promoted from short-term based on importance) +- **Chat history**: Permanent (unless manually deleted) + +## Resources + +- **Memori Documentation**: https://www.gibsonai.com/docs/memori +- **MCP Documentation**: https://modelcontextprotocol.io +- **FastMCP**: https://github.com/jlowin/fastmcp +- **Discord Community**: https://discord.gg/abD4eGym6v + +## Contributing + +Contributions welcome! See [CONTRIBUTING.md](../CONTRIBUTING.md) for guidelines. + +## License + +Apache 2.0 - see [LICENSE](../LICENSE) + +--- + +**Questions or issues?** Open an issue on [GitHub](https://github.com/GibsonAI/memori/issues) or join our [Discord](https://discord.gg/abD4eGym6v). diff --git a/mcp/__init__.py b/mcp/__init__.py new file mode 100644 index 0000000..a79ce3e --- /dev/null +++ b/mcp/__init__.py @@ -0,0 +1,8 @@ +""" +Memori MCP Server + +Model Context Protocol (MCP) server implementation for Memori, +enabling AI assistants to have persistent memory capabilities. +""" + +__version__ = "1.0.0" diff --git a/mcp/claude_desktop_config.json b/mcp/claude_desktop_config.json new file mode 100644 index 0000000..823c786 --- /dev/null +++ b/mcp/claude_desktop_config.json @@ -0,0 +1,24 @@ +{ + "mcpServers": { + "memori": { + "command": "uv", + "args": [ + "--directory", + "/absolute/path/to/memori", + "run", + "--with", + "mcp", + "--with", + "fastmcp", + "--with-editable", + ".", + "python", + "mcp/memori_mcp_server.py" + ], + "env": { + "MEMORI_DATABASE_URL": "sqlite:///memori_mcp.db", + "OPENAI_API_KEY": "your-openai-api-key-here" + } + } + } +} diff --git a/mcp/claude_desktop_config_openrouter.json b/mcp/claude_desktop_config_openrouter.json new file mode 100644 index 0000000..4eb5a70 --- /dev/null +++ b/mcp/claude_desktop_config_openrouter.json @@ -0,0 +1,27 @@ +{ + "mcpServers": { + "memori-openrouter": { + "command": "uv", + "args": [ + "--directory", + "/absolute/path/to/memori", + "run", + "--with", + "mcp", + "--with", + "fastmcp", + "--with-editable", + ".", + "python", + "mcp/memori_mcp_server.py" + ], + "env": { + "MEMORI_DATABASE_URL": "sqlite:///memori_mcp.db", + "OPENROUTER_API_KEY": "your-openrouter-api-key-here", + "OPENROUTER_MODEL": "anthropic/claude-3.5-sonnet", + "OPENROUTER_APP_NAME": "Memori MCP Server", + "OPENROUTER_SITE_URL": "https://github.com/GibsonAI/memori" + } + } + } +} diff --git a/mcp/memori_mcp_server.py b/mcp/memori_mcp_server.py new file mode 100644 index 0000000..dfc5dbc --- /dev/null +++ b/mcp/memori_mcp_server.py @@ -0,0 +1,715 @@ +#!/usr/bin/env python3 +""" +Memori MCP Server + +A Model Context Protocol (MCP) server that exposes Memori's persistent memory +capabilities to any MCP-compatible AI assistant like Claude. + +This server provides tools for: +- Recording conversations and memories +- Searching and retrieving memories +- Managing memory lifecycle +- Getting memory statistics and insights +""" + +import os +import sys +from datetime import datetime +from typing import Any, Dict, List, Optional + +from mcp.server.fastmcp import FastMCP + +# Add parent directory to path to import memori +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from memori import Memori +from memori.utils.pydantic_models import ( + MemoryCategoryType, + MemoryClassification, + MemoryImportance, +) + +# Initialize FastMCP server +mcp = FastMCP( + name="memori", + version="1.0.0", + description="Persistent memory engine for AI assistants using SQL databases", +) + +# Global Memori instance - will be configured per user +_memori_instances: Dict[str, Memori] = {} + + +def get_memori_instance( + user_id: str = "default_user", + assistant_id: str = "mcp_assistant", + session_id: Optional[str] = None, + database_connect: Optional[str] = None, +) -> Memori: + """ + Get or create a Memori instance for the given user. + + Supports multiple LLM providers: + - OpenAI (default) + - OpenRouter (set OPENROUTER_API_KEY) + - Azure OpenAI (set AZURE_OPENAI_API_KEY) + - Custom OpenAI-compatible endpoints (set LLM_BASE_URL) + + Args: + user_id: User identifier for multi-tenant isolation + assistant_id: Assistant identifier + session_id: Optional session identifier + database_connect: Optional database connection string + + Returns: + Memori instance configured for the user + """ + # Create a unique key for this configuration + key = f"{user_id}:{assistant_id}:{session_id or 'default'}" + + if key not in _memori_instances: + # Get database connection from environment or use default SQLite + db_connect = database_connect or os.getenv( + "MEMORI_DATABASE_URL", "sqlite:///memori_mcp.db" + ) + + # Detect LLM provider configuration from environment + llm_config = _detect_llm_provider() + + # Create new Memori instance + _memori_instances[key] = Memori( + database_connect=db_connect, + user_id=user_id, + assistant_id=assistant_id, + session_id=session_id, + conscious_ingest=True, # Enable conscious memory injection + auto_ingest=False, # Disable auto-injection (manual via MCP) + **llm_config, # Unpack provider configuration + ) + + return _memori_instances[key] + + +def _detect_llm_provider() -> Dict[str, Any]: + """ + Detect LLM provider from environment variables and return configuration. + + Priority order: + 1. OpenRouter (OPENROUTER_API_KEY) + 2. Azure OpenAI (AZURE_OPENAI_API_KEY) + 3. Custom endpoint (LLM_BASE_URL) + 4. OpenAI (OPENAI_API_KEY) - default + + Returns: + Dictionary of configuration parameters for Memori.__init__ + """ + config = {} + + # Priority 1: OpenRouter + openrouter_key = os.getenv("OPENROUTER_API_KEY") + if openrouter_key: + config["api_key"] = openrouter_key + config["base_url"] = os.getenv( + "OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1" + ) + config["model"] = os.getenv("OPENROUTER_MODEL", "openai/gpt-4o") + return config + + # Priority 2: Azure OpenAI + azure_key = os.getenv("AZURE_OPENAI_API_KEY") + if azure_key: + config["api_key"] = azure_key + config["api_type"] = "azure" + config["azure_endpoint"] = os.getenv("AZURE_OPENAI_ENDPOINT") + config["azure_deployment"] = os.getenv("AZURE_OPENAI_DEPLOYMENT") + config["api_version"] = os.getenv("AZURE_OPENAI_API_VERSION", "2024-02-15-preview") + config["model"] = os.getenv("AZURE_OPENAI_MODEL", "gpt-4o") + return config + + # Priority 3: Custom OpenAI-compatible endpoint + custom_base_url = os.getenv("LLM_BASE_URL") + if custom_base_url: + config["api_key"] = os.getenv("LLM_API_KEY") + config["base_url"] = custom_base_url + config["model"] = os.getenv("LLM_MODEL", "gpt-4o") + return config + + # Priority 4: Default OpenAI + config["openai_api_key"] = os.getenv("OPENAI_API_KEY") + if os.getenv("OPENAI_MODEL"): + config["model"] = os.getenv("OPENAI_MODEL") + + return config + + +# ============================================================================ +# TOOLS - Actions that can be performed +# ============================================================================ + + +@mcp.tool() +def record_conversation( + user_input: str, + ai_response: str, + user_id: str = "default_user", + assistant_id: str = "mcp_assistant", + session_id: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + """ + Record a conversation turn in Memori's persistent memory. + + This stores both the user input and AI response, processes them to extract + entities, categories, and importance, and makes them available for future + retrieval. + + Args: + user_input: The user's message or question + ai_response: The AI's response to the user + user_id: User identifier (default: "default_user") + assistant_id: Assistant identifier (default: "mcp_assistant") + session_id: Optional session identifier for conversation grouping + metadata: Optional metadata dictionary to attach to the conversation + + Returns: + Dictionary with recording status and chat_id + + Example: + record_conversation( + user_input="I'm working on a Python project", + ai_response="That's great! What kind of Python project?", + user_id="user123" + ) + """ + try: + memori = get_memori_instance(user_id, assistant_id, session_id) + + # Record the conversation + chat_id = memori.record( + user_input=user_input, + ai_response=ai_response, + metadata=metadata or {}, + ) + + return { + "success": True, + "message": "Conversation recorded successfully", + "chat_id": chat_id, + "user_id": user_id, + "timestamp": datetime.utcnow().isoformat(), + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "Failed to record conversation", + } + + +@mcp.tool() +def search_memories( + query: str, + user_id: str = "default_user", + assistant_id: str = "mcp_assistant", + limit: int = 10, + category: Optional[str] = None, + min_importance: Optional[float] = None, +) -> Dict[str, Any]: + """ + Search for relevant memories based on a query. + + Uses intelligent search to find memories that are semantically relevant to + the query. Can filter by category and importance level. + + Args: + query: The search query or topic + user_id: User identifier + assistant_id: Assistant identifier + limit: Maximum number of results to return (default: 10) + category: Optional filter by category (fact, preference, skill, context, rule) + min_importance: Optional minimum importance score (0.0 to 1.0) + + Returns: + Dictionary with search results and metadata + + Example: + search_memories( + query="Python projects", + user_id="user123", + category="fact", + limit=5 + ) + """ + try: + memori = get_memori_instance(user_id, assistant_id) + + # Build search filters + filters = {} + if category: + filters["category_primary"] = category + if min_importance is not None: + filters["importance_score_min"] = min_importance + + # Search memories + results = memori.db_manager.search_memories( + query=query, user_id=user_id, limit=limit, **filters + ) + + # Format results + formatted_results = [] + for memory in results: + formatted_results.append( + { + "memory_id": memory.memory_id, + "summary": memory.summary, + "category": memory.category_primary, + "importance_score": memory.importance_score, + "created_at": ( + memory.created_at.isoformat() if memory.created_at else None + ), + "entities": ( + memory.entities_json if hasattr(memory, "entities_json") else [] + ), + } + ) + + return { + "success": True, + "query": query, + "total_results": len(formatted_results), + "results": formatted_results, + "filters_applied": filters, + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "Failed to search memories", + } + + +@mcp.tool() +def get_recent_memories( + user_id: str = "default_user", + assistant_id: str = "mcp_assistant", + limit: int = 20, + memory_type: str = "short_term", +) -> Dict[str, Any]: + """ + Get the most recent memories for a user. + + Retrieves memories in reverse chronological order, useful for understanding + recent context and conversation history. + + Args: + user_id: User identifier + assistant_id: Assistant identifier + limit: Maximum number of memories to return (default: 20) + memory_type: Type of memory to retrieve ("short_term" or "long_term") + + Returns: + Dictionary with recent memories + + Example: + get_recent_memories(user_id="user123", limit=10) + """ + try: + memori = get_memori_instance(user_id, assistant_id) + + # Get recent memories from database + with memori.db_manager.get_session() as session: + if memory_type == "short_term": + from memori.database.models import ShortTermMemory + + memories = ( + session.query(ShortTermMemory) + .filter_by(user_id=user_id, assistant_id=assistant_id) + .order_by(ShortTermMemory.created_at.desc()) + .limit(limit) + .all() + ) + else: + from memori.database.models import LongTermMemory + + memories = ( + session.query(LongTermMemory) + .filter_by(user_id=user_id, assistant_id=assistant_id) + .order_by(LongTermMemory.created_at.desc()) + .limit(limit) + .all() + ) + + # Format results + formatted_memories = [] + for mem in memories: + formatted_memories.append( + { + "memory_id": mem.memory_id, + "summary": mem.summary, + "category": mem.category_primary, + "importance_score": mem.importance_score, + "created_at": mem.created_at.isoformat() if mem.created_at else None, + } + ) + + return { + "success": True, + "memory_type": memory_type, + "total_results": len(formatted_memories), + "memories": formatted_memories, + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "Failed to retrieve recent memories", + } + + +@mcp.tool() +def get_memory_statistics( + user_id: str = "default_user", + assistant_id: str = "mcp_assistant", +) -> Dict[str, Any]: + """ + Get statistics about stored memories for a user. + + Provides insights into memory distribution by category, importance levels, + total counts, and other useful metrics. + + Args: + user_id: User identifier + assistant_id: Assistant identifier + + Returns: + Dictionary with memory statistics + + Example: + get_memory_statistics(user_id="user123") + """ + try: + memori = get_memori_instance(user_id, assistant_id) + + with memori.db_manager.get_session() as session: + from memori.database.models import ( + ChatHistory, + LongTermMemory, + ShortTermMemory, + ) + from sqlalchemy import func + + # Count total conversations + total_conversations = ( + session.query(func.count(ChatHistory.chat_id)) + .filter_by(user_id=user_id, assistant_id=assistant_id) + .scalar() + ) + + # Count short-term memories + short_term_count = ( + session.query(func.count(ShortTermMemory.memory_id)) + .filter_by(user_id=user_id, assistant_id=assistant_id) + .scalar() + ) + + # Count long-term memories + long_term_count = ( + session.query(func.count(LongTermMemory.memory_id)) + .filter_by(user_id=user_id, assistant_id=assistant_id) + .scalar() + ) + + # Get category distribution (short-term) + category_dist = ( + session.query( + ShortTermMemory.category_primary, + func.count(ShortTermMemory.memory_id), + ) + .filter_by(user_id=user_id, assistant_id=assistant_id) + .group_by(ShortTermMemory.category_primary) + .all() + ) + + categories = {cat: count for cat, count in category_dist if cat} + + return { + "success": True, + "user_id": user_id, + "statistics": { + "total_conversations": total_conversations or 0, + "short_term_memories": short_term_count or 0, + "long_term_memories": long_term_count or 0, + "total_memories": (short_term_count or 0) + (long_term_count or 0), + "category_distribution": categories, + }, + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "Failed to retrieve statistics", + } + + +@mcp.tool() +def get_conversation_history( + user_id: str = "default_user", + assistant_id: str = "mcp_assistant", + session_id: Optional[str] = None, + limit: int = 50, +) -> Dict[str, Any]: + """ + Get conversation history for a user or session. + + Retrieves the raw conversation turns (user input and AI responses) in + chronological order. + + Args: + user_id: User identifier + assistant_id: Assistant identifier + session_id: Optional session identifier to filter by + limit: Maximum number of conversation turns to return (default: 50) + + Returns: + Dictionary with conversation history + + Example: + get_conversation_history(user_id="user123", limit=20) + """ + try: + memori = get_memori_instance(user_id, assistant_id, session_id) + + # Get chat history + history = memori.db_manager.get_chat_history( + user_id=user_id, assistant_id=assistant_id, session_id=session_id, limit=limit + ) + + # Format results + formatted_history = [] + for chat in history: + formatted_history.append( + { + "chat_id": chat.chat_id, + "user_input": chat.user_input, + "ai_output": chat.ai_output, + "model": chat.model, + "timestamp": chat.created_at.isoformat() if chat.created_at else None, + } + ) + + return { + "success": True, + "total_conversations": len(formatted_history), + "conversations": formatted_history, + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "Failed to retrieve conversation history", + } + + +@mcp.tool() +def clear_session_memories( + session_id: str, + user_id: str = "default_user", + assistant_id: str = "mcp_assistant", +) -> Dict[str, Any]: + """ + Clear all memories for a specific session. + + Useful for resetting conversation context or removing temporary memories. + Only affects the specified session, not the user's entire memory. + + Args: + session_id: Session identifier to clear + user_id: User identifier + assistant_id: Assistant identifier + + Returns: + Dictionary with deletion status + + Example: + clear_session_memories(session_id="temp_session", user_id="user123") + """ + try: + memori = get_memori_instance(user_id, assistant_id, session_id) + + with memori.db_manager.get_session() as session: + from memori.database.models import ChatHistory, ShortTermMemory + + # Delete chat history for session + deleted_chats = ( + session.query(ChatHistory) + .filter_by( + user_id=user_id, assistant_id=assistant_id, session_id=session_id + ) + .delete() + ) + + # Delete short-term memories for session + deleted_memories = ( + session.query(ShortTermMemory) + .filter_by( + user_id=user_id, assistant_id=assistant_id, session_id=session_id + ) + .delete() + ) + + session.commit() + + return { + "success": True, + "message": f"Cleared {deleted_chats} conversations and {deleted_memories} memories", + "deleted_conversations": deleted_chats, + "deleted_memories": deleted_memories, + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "Failed to clear session memories", + } + + +# ============================================================================ +# RESOURCES - Read-only data access +# ============================================================================ + + +@mcp.resource("memori://memories/{user_id}") +def get_user_memories(user_id: str) -> str: + """ + Get all memories for a specific user as a formatted text resource. + + Args: + user_id: User identifier + + Returns: + Formatted text representation of user memories + """ + try: + memori = get_memori_instance(user_id) + + with memori.db_manager.get_session() as session: + from memori.database.models import ShortTermMemory + + memories = ( + session.query(ShortTermMemory) + .filter_by(user_id=user_id) + .order_by(ShortTermMemory.importance_score.desc()) + .limit(100) + .all() + ) + + if not memories: + return f"No memories found for user: {user_id}" + + # Format as text + output = f"# Memories for {user_id}\n\n" + output += f"Total memories: {len(memories)}\n\n" + + for mem in memories: + output += f"## {mem.category_primary.upper() if mem.category_primary else 'UNCATEGORIZED'}\n" + output += f"**Summary**: {mem.summary}\n" + output += f"**Importance**: {mem.importance_score:.2f}\n" + output += f"**Created**: {mem.created_at.isoformat() if mem.created_at else 'Unknown'}\n" + output += "\n---\n\n" + + return output + except Exception as e: + return f"Error retrieving memories: {str(e)}" + + +@mcp.resource("memori://stats/{user_id}") +def get_user_stats(user_id: str) -> str: + """ + Get memory statistics for a user as a formatted text resource. + + Args: + user_id: User identifier + + Returns: + Formatted text representation of statistics + """ + stats = get_memory_statistics(user_id) + + if not stats.get("success"): + return f"Error: {stats.get('error', 'Unknown error')}" + + s = stats["statistics"] + output = f"# Memory Statistics for {user_id}\n\n" + output += f"- **Total Conversations**: {s['total_conversations']}\n" + output += f"- **Short-term Memories**: {s['short_term_memories']}\n" + output += f"- **Long-term Memories**: {s['long_term_memories']}\n" + output += f"- **Total Memories**: {s['total_memories']}\n\n" + + if s["category_distribution"]: + output += "## Category Distribution\n\n" + for cat, count in s["category_distribution"].items(): + output += f"- **{cat}**: {count}\n" + + return output + + +# ============================================================================ +# PROMPTS - Reusable interaction patterns +# ============================================================================ + + +@mcp.prompt() +def memory_search_prompt(topic: str, user_id: str = "default_user") -> str: + """ + Generate a prompt to search and summarize memories about a topic. + + Args: + topic: The topic to search for + user_id: User identifier + + Returns: + Formatted prompt for the AI + """ + return f"""Please search the user's memories for information about "{topic}" and provide a comprehensive summary. + +User ID: {user_id} + +Use the search_memories tool to find relevant memories, then: +1. Summarize what you found +2. Highlight the most important facts +3. Note any preferences or context that might be relevant +4. Suggest follow-up questions if appropriate +""" + + +@mcp.prompt() +def conversation_context_prompt(user_id: str = "default_user", limit: int = 10) -> str: + """ + Generate a prompt to retrieve recent conversation context. + + Args: + user_id: User identifier + limit: Number of recent memories to retrieve + + Returns: + Formatted prompt for the AI + """ + return f"""Please retrieve the recent conversation context for this user. + +User ID: {user_id} + +Use the get_recent_memories tool (limit={limit}) to: +1. Get the last {limit} memories +2. Summarize the key topics discussed +3. Note any ongoing tasks or preferences +4. Provide context for continuing the conversation +""" + + +# ============================================================================ +# Main entry point +# ============================================================================ + +if __name__ == "__main__": + # Run the MCP server + mcp.run() diff --git a/pyproject.toml b/pyproject.toml index 5071478..22e7429 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,12 @@ demos = [ "crewai-tools>=0.59.0", ] +# MCP (Model Context Protocol) dependencies +mcp = [ + "mcp>=1.0.0", + "fastmcp>=2.0.0", +] + # All optional dependencies all = [ # Dev tools @@ -109,6 +115,9 @@ all = [ "streamlit>=1.28.0", "pandas>=2.0.0", "plotly>=5.17.0", + # MCP dependencies + "mcp>=1.0.0", + "fastmcp>=2.0.0", ] [project.urls]