From 29304f2238f88ada0816a79e73302b0c4d44e6e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Nov 2025 14:42:56 +0000 Subject: [PATCH 1/3] docs: add comprehensive CLAUDE.md guide for AI assistants Add detailed documentation to help AI assistants understand and work with the Memori codebase, including: - Complete architecture overview and component relationships - Detailed codebase structure with 73+ Python modules - Development workflows and code quality standards - Database schema design and multi-tenant patterns - LLM integration patterns (OpenAI, Anthropic, LiteLLM) - Security best practices and common pitfalls - Testing guidelines and debugging tips - Pull request checklist and contribution guide This guide serves as a comprehensive reference for AI assistants to understand conventions, make safe changes, and maintain code quality. --- CLAUDE.md | 897 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 897 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..054e2116 --- /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 From 949302b72ef2b5283a1b192fd1c836368943f6ec Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Nov 2025 14:50:24 +0000 Subject: [PATCH 2/3] feat: add Model Context Protocol (MCP) server implementation Add comprehensive MCP server to expose Memori's persistent memory capabilities to any MCP-compatible AI assistant like Claude Desktop. Features: - 6 MCP tools for memory operations (record, search, retrieve, stats, etc.) - 2 MCP resources for read-only data access - 2 MCP prompt templates for common workflows - Multi-tenant isolation with user_id/session_id support - Support for all Memori database backends (SQLite, PostgreSQL, MySQL) - Automatic memory processing with OpenAI integration Files added: - mcp/memori_mcp_server.py - Main MCP server implementation - mcp/README.md - Comprehensive documentation - mcp/QUICKSTART.md - 5-minute setup guide - mcp/claude_desktop_config.json - Configuration template - mcp/__init__.py - Package initialization - examples/mcp/basic_usage_example.py - Basic usage examples - examples/mcp/advanced_workflow_example.py - Advanced workflows Tools provided: 1. record_conversation - Store conversations with automatic processing 2. search_memories - Intelligent memory search with filtering 3. get_recent_memories - Retrieve recent context 4. get_memory_statistics - Memory analytics and insights 5. get_conversation_history - Access conversation history 6. clear_session_memories - Reset session context Configuration: - Added mcp optional dependency group to pyproject.toml - Requires: mcp>=1.0.0, fastmcp>=2.0.0 - Uses uv for dependency management - Environment-based configuration (MEMORI_DATABASE_URL, OPENAI_API_KEY) Usage: 1. Install uv package manager 2. Configure Claude Desktop with provided JSON 3. Restart Claude Desktop 4. Use hammer icon to access Memori tools This enables Claude and other MCP clients to have persistent, queryable memory across all conversations with full SQL database backing. --- examples/mcp/advanced_workflow_example.py | 236 ++++++++ examples/mcp/basic_usage_example.py | 122 ++++ mcp/QUICKSTART.md | 205 +++++++ mcp/README.md | 427 ++++++++++++++ mcp/__init__.py | 8 + mcp/claude_desktop_config.json | 24 + mcp/memori_mcp_server.py | 654 ++++++++++++++++++++++ pyproject.toml | 9 + 8 files changed, 1685 insertions(+) create mode 100644 examples/mcp/advanced_workflow_example.py create mode 100644 examples/mcp/basic_usage_example.py create mode 100644 mcp/QUICKSTART.md create mode 100644 mcp/README.md create mode 100644 mcp/__init__.py create mode 100644 mcp/claude_desktop_config.json create mode 100644 mcp/memori_mcp_server.py diff --git a/examples/mcp/advanced_workflow_example.py b/examples/mcp/advanced_workflow_example.py new file mode 100644 index 00000000..c6771d1f --- /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 00000000..3b9b0fa3 --- /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/mcp/QUICKSTART.md b/mcp/QUICKSTART.md new file mode 100644 index 00000000..160e74f2 --- /dev/null +++ b/mcp/QUICKSTART.md @@ -0,0 +1,205 @@ +# 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?" + +## 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 00000000..04e0c99c --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,427 @@ +# 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 + +### Environment Variables + +Set these in the `env` section of your Claude Desktop config: + +| Variable | Description | Default | Required | +|----------|-------------|---------|----------| +| `MEMORI_DATABASE_URL` | Database connection string | `sqlite:///memori_mcp.db` | No | +| `OPENAI_API_KEY` | OpenAI API key for memory processing | None | Yes | + +### 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 00000000..a79ce3e1 --- /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 00000000..823c786f --- /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/memori_mcp_server.py b/mcp/memori_mcp_server.py new file mode 100644 index 00000000..98362cd6 --- /dev/null +++ b/mcp/memori_mcp_server.py @@ -0,0 +1,654 @@ +#!/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. + + 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" + ) + + # 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) + openai_api_key=os.getenv("OPENAI_API_KEY"), + ) + + return _memori_instances[key] + + +# ============================================================================ +# 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 50714781..22e7429e 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] From c823a69bf589295e18ade8f5e240387e74e04f27 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Nov 2025 15:22:04 +0000 Subject: [PATCH 3/3] feat: add OpenRouter and multi-provider LLM support to MCP server Add comprehensive support for multiple LLM providers in the Memori MCP server, with OpenRouter as the recommended option for accessing 100+ models. Features: - OpenRouter support (100+ models including Claude, GPT-4, Llama, Mistral) - Azure OpenAI support (enterprise deployments) - Custom OpenAI-compatible endpoints (Ollama, LM Studio, etc.) - Automatic provider detection from environment variables - Priority-based configuration (OpenRouter > Azure > Custom > OpenAI) Provider detection priority: 1. OpenRouter (OPENROUTER_API_KEY) - access to 100+ models 2. Azure OpenAI (AZURE_OPENAI_API_KEY) - enterprise 3. Custom endpoint (LLM_BASE_URL) - local/self-hosted 4. OpenAI (OPENAI_API_KEY) - default Files modified: - mcp/memori_mcp_server.py - Added _detect_llm_provider() function - mcp/README.md - Comprehensive LLM provider configuration guide - mcp/QUICKSTART.md - Quick provider selection guide - mcp/claude_desktop_config_openrouter.json - OpenRouter config template - examples/mcp/openrouter_example.py - Full OpenRouter usage example Environment variables added: - OPENROUTER_API_KEY - OpenRouter API key - OPENROUTER_MODEL - Model to use (default: openai/gpt-4o) - OPENROUTER_BASE_URL - API base URL (default: https://openrouter.ai/api/v1) - OPENROUTER_APP_NAME - Optional app name for rankings - OPENROUTER_SITE_URL - Optional site URL for rankings - AZURE_OPENAI_* - Azure OpenAI configuration - LLM_BASE_URL - Custom endpoint URL - LLM_API_KEY - Custom API key - LLM_MODEL - Custom model name Benefits: - Access 100+ models through OpenRouter (Claude, GPT-4, Llama, etc.) - Use free models (Llama 3.1 70B, Mistral, etc.) - Run local models with Ollama/LM Studio - Enterprise Azure OpenAI support - Cost optimization through model selection - No vendor lock-in Popular OpenRouter 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 - google/gemini-pro-1.5 - Google Gemini - mistralai/mixtral-8x7b-instruct - Cost-effective This enables users to choose the best model for their needs and budget, from premium models to free open-source options. --- examples/mcp/openrouter_example.py | 221 ++++++++++++++++++++++ mcp/QUICKSTART.md | 50 +++++ mcp/README.md | 86 ++++++++- mcp/claude_desktop_config_openrouter.json | 27 +++ mcp/memori_mcp_server.py | 63 +++++- 5 files changed, 443 insertions(+), 4 deletions(-) create mode 100644 examples/mcp/openrouter_example.py create mode 100644 mcp/claude_desktop_config_openrouter.json diff --git a/examples/mcp/openrouter_example.py b/examples/mcp/openrouter_example.py new file mode 100644 index 00000000..659f8aa0 --- /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 index 160e74f2..53295386 100644 --- a/mcp/QUICKSTART.md +++ b/mcp/QUICKSTART.md @@ -132,6 +132,56 @@ export PATH="$HOME/.cargo/bin:$PATH" - 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) diff --git a/mcp/README.md b/mcp/README.md index 04e0c99c..3979a2af 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -122,14 +122,94 @@ Look for the 🔨 hammer icon in Claude Desktop. Click it to see available MCP s ## Configuration -### Environment Variables +### LLM Provider Configuration -Set these in the `env` section of your Claude Desktop config: +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_API_KEY` | OpenAI API key for memory processing | None | Yes | +| **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 diff --git a/mcp/claude_desktop_config_openrouter.json b/mcp/claude_desktop_config_openrouter.json new file mode 100644 index 00000000..4eb5a70c --- /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 index 98362cd6..dfc5dbc1 100644 --- a/mcp/memori_mcp_server.py +++ b/mcp/memori_mcp_server.py @@ -49,6 +49,12 @@ def get_memori_instance( """ 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 @@ -67,6 +73,9 @@ def get_memori_instance( "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, @@ -75,12 +84,64 @@ def get_memori_instance( session_id=session_id, conscious_ingest=True, # Enable conscious memory injection auto_ingest=False, # Disable auto-injection (manual via MCP) - openai_api_key=os.getenv("OPENAI_API_KEY"), + **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 # ============================================================================