diff --git a/README.md b/README.md index 1e04373..02bac24 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,13 @@ LangMem uses a hybrid approach combining: ```bash git clone -cd langmem-project +cd langmem ``` ### 2. Start Development Environment ```bash -./start-dev.sh +./scripts/start-dev.sh ``` This will: @@ -52,7 +52,7 @@ This will: ### 3. Test the API ```bash -./test.sh +./scripts/test.sh ``` ## API Endpoints @@ -139,39 +139,60 @@ NEO4J_PASSWORD=langmem_neo4j_password ### Project Structure ``` -langmem-project/ -├── src/ -│ └── api/ -│ └── main.py # Main API application -├── tests/ -│ ├── test_api.py # API unit tests -│ ├── test_integration.py # Integration tests -│ └── conftest.py # Test configuration -├── docker-compose.yml # Docker services -├── Dockerfile # API container -├── requirements.txt # Python dependencies -├── start-dev.sh # Development startup -├── test.sh # Test runner -└── README.md # This file +langmem/ +├── src/ # Source code +│ ├── api/ # FastAPI application +│ │ ├── main.py # Main API server +│ │ ├── fact_extraction.py # Fact-based memory logic +│ │ └── memory_manager.py # Memory management +│ └── mcp/ # Model Context Protocol +│ ├── server.py # MCP server for Claude Code +│ └── requirements.txt +├── scripts/ # Utility scripts +│ ├── start-dev.sh # Development startup +│ ├── start-mcp-server.sh # MCP server startup +│ ├── start-docs-server.sh # Documentation server +│ ├── docs_server.py # Authenticated docs server +│ ├── get-claude-token.py # Matrix setup utility +│ └── test.sh # Test runner +├── tests/ # Test suite +│ ├── test_api.py # API tests +│ ├── test_integration.py # Integration tests +│ ├── test_fact_based_memory.py # Fact extraction tests +│ ├── debug_*.py # Debug utilities +│ └── conftest.py # Test configuration +├── docs/ # Documentation website +│ ├── index.html # Main documentation +│ ├── api/ # API documentation +│ ├── architecture/ # Architecture docs +│ └── implementation/ # Setup guides +├── config/ # Configuration files +│ ├── mcp_config.json # MCP server config +│ ├── claude-matrix-config.json # Matrix setup +│ └── caddyfile-docs-update.txt # Caddy config +├── docker-compose.yml # Docker services +├── Dockerfile # API container +├── requirements.txt # Python dependencies +└── README.md # This file ``` ### Running Tests ```bash # All tests -./test.sh all +./scripts/test.sh all # Unit tests only -./test.sh unit +./scripts/test.sh unit # Integration tests only -./test.sh integration +./scripts/test.sh integration # Quick tests (no slow tests) -./test.sh quick +./scripts/test.sh quick # With coverage -./test.sh coverage +./scripts/test.sh coverage ``` ### Local Development @@ -289,10 +310,10 @@ Start the authenticated documentation server: ```bash # Start documentation server on port 8080 (default) -./start-docs-server.sh +./scripts/start-docs-server.sh # Or specify a custom port -./start-docs-server.sh 8090 +./scripts/start-docs-server.sh 8090 ``` **Access Credentials:** @@ -310,7 +331,7 @@ Start the authenticated documentation server: You can also run the documentation server directly: ```bash -python3 docs_server.py [port] +python3 scripts/docs_server.py [port] ``` Then visit: `http://localhost:8080` (or your specified port) diff --git a/caddyfile-docs-update.txt b/config/caddyfile-docs-update.txt similarity index 100% rename from caddyfile-docs-update.txt rename to config/caddyfile-docs-update.txt diff --git a/claude-matrix-config.json b/config/claude-matrix-config.json similarity index 100% rename from claude-matrix-config.json rename to config/claude-matrix-config.json diff --git a/mcp_config.json b/config/mcp_config.json similarity index 100% rename from mcp_config.json rename to config/mcp_config.json diff --git a/create-claude-matrix-user.md b/docs/create-claude-matrix-user.md similarity index 100% rename from create-claude-matrix-user.md rename to docs/create-claude-matrix-user.md diff --git a/docs_server.py b/scripts/docs_server.py similarity index 100% rename from docs_server.py rename to scripts/docs_server.py diff --git a/get-claude-token.py b/scripts/get-claude-token.py similarity index 100% rename from get-claude-token.py rename to scripts/get-claude-token.py diff --git a/start-dev.sh b/scripts/start-dev.sh similarity index 98% rename from start-dev.sh rename to scripts/start-dev.sh index 9ac55f7..9758279 100755 --- a/start-dev.sh +++ b/scripts/start-dev.sh @@ -79,7 +79,7 @@ echo " - Health Check: http://localhost:8765/health" echo "" echo "🔧 Useful Commands:" echo " - View logs: docker compose logs -f langmem-api" -echo " - Run tests: ./test.sh" +echo " - Run tests: ./scripts/test.sh" echo " - Stop services: docker compose down" echo " - Restart API: docker compose restart langmem-api" echo "" diff --git a/start-docs-server.sh b/scripts/start-docs-server.sh similarity index 74% rename from start-docs-server.sh rename to scripts/start-docs-server.sh index ad6753a..95c2e03 100755 --- a/start-docs-server.sh +++ b/scripts/start-docs-server.sh @@ -5,8 +5,11 @@ echo "🚀 Starting LangMem Documentation Server..." echo "🔐 Authentication enabled with basic auth" echo "" +# Change to project root +cd "$(dirname "$0")/.." + # Default port PORT=${1:-8080} # Start the authenticated server -python3 docs_server.py $PORT \ No newline at end of file +python3 scripts/docs_server.py $PORT \ No newline at end of file diff --git a/start-mcp-server.sh b/scripts/start-mcp-server.sh similarity index 91% rename from start-mcp-server.sh rename to scripts/start-mcp-server.sh index 0d2b17b..94e5020 100755 --- a/start-mcp-server.sh +++ b/scripts/start-mcp-server.sh @@ -5,6 +5,9 @@ echo "🚀 Starting LangMem MCP Server..." +# Change to project root +cd "$(dirname "$0")/.." + # Check if virtual environment exists if [ ! -d "venv" ]; then echo "📦 Creating virtual environment..." @@ -36,8 +39,8 @@ export LANGMEM_API_KEY="langmem_api_key_2025" # Start MCP server echo "🏃 Starting MCP server..." echo "🔗 Connect from Claude Code using:" -echo " Server command: python /home/klas/langmem-project/src/mcp/server.py" -echo " Working directory: /home/klas/langmem-project" +echo " Server command: python /home/klas/langmem/src/mcp/server.py" +echo " Working directory: /home/klas/langmem" echo "" echo "📖 Available tools:" echo " - store_memory: Store memories with AI relationship extraction" diff --git a/test.sh b/scripts/test.sh similarity index 85% rename from test.sh rename to scripts/test.sh index 60cd8d8..a1e9891 100755 --- a/test.sh +++ b/scripts/test.sh @@ -6,9 +6,12 @@ set -e echo "🧪 Running LangMem API Tests" +# Change to project root +cd "$(dirname "$0")/.." + # Check if services are running if ! curl -s http://localhost:8765/health > /dev/null; then - echo "❌ LangMem API is not running. Please start with ./start-dev.sh first." + echo "❌ LangMem API is not running. Please start with ./scripts/start-dev.sh first." exit 1 fi @@ -40,7 +43,7 @@ case "${1:-all}" in ;; *) echo "❌ Unknown test type: $1" - echo "Usage: ./test.sh [unit|integration|all|quick|coverage]" + echo "Usage: ./scripts/test.sh [unit|integration|all|quick|coverage]" exit 1 ;; esac diff --git a/test_api.py b/test_api.py deleted file mode 100644 index d5275e0..0000000 --- a/test_api.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env python3 -""" -Test the LangMem API endpoints -""" - -import asyncio -import httpx -import json -from uuid import uuid4 - -# Configuration -API_BASE_URL = "http://localhost:8765" -API_KEY = "langmem_api_key_2025" -TEST_USER_ID = f"test_user_{uuid4()}" - -async def test_api_endpoints(): - """Test all API endpoints""" - print("🧪 Testing LangMem API Endpoints") - print("=" * 50) - - headers = {"Authorization": f"Bearer {API_KEY}"} - - async with httpx.AsyncClient() as client: - # Test 1: Root endpoint - print("\n1. Testing root endpoint...") - try: - response = await client.get(f"{API_BASE_URL}/") - print(f"✅ Root endpoint: {response.status_code}") - print(f" Response: {response.json()}") - except Exception as e: - print(f"❌ Root endpoint failed: {e}") - - # Test 2: Health check - print("\n2. Testing health check...") - try: - response = await client.get(f"{API_BASE_URL}/health") - print(f"✅ Health check: {response.status_code}") - data = response.json() - print(f" Overall status: {data.get('status')}") - for service, status in data.get('services', {}).items(): - print(f" {service}: {status}") - except Exception as e: - print(f"❌ Health check failed: {e}") - - # Test 3: Store memory - print("\n3. Testing memory storage...") - try: - memory_data = { - "content": "FastAPI is a modern web framework for building APIs with Python", - "user_id": TEST_USER_ID, - "session_id": "test_session_1", - "metadata": { - "category": "programming", - "language": "python", - "framework": "fastapi" - } - } - - response = await client.post( - f"{API_BASE_URL}/v1/memories/store", - json=memory_data, - headers=headers, - timeout=30.0 - ) - - if response.status_code == 200: - data = response.json() - memory_id = data["id"] - print(f"✅ Memory stored successfully: {memory_id}") - print(f" Status: {data['status']}") - else: - print(f"❌ Memory storage failed: {response.status_code}") - print(f" Response: {response.text}") - return - - except Exception as e: - print(f"❌ Memory storage failed: {e}") - return - - # Test 4: Search memories - print("\n4. Testing memory search...") - try: - search_data = { - "query": "Python web framework", - "user_id": TEST_USER_ID, - "limit": 5, - "threshold": 0.5 - } - - response = await client.post( - f"{API_BASE_URL}/v1/memories/search", - json=search_data, - headers=headers, - timeout=30.0 - ) - - if response.status_code == 200: - data = response.json() - print(f"✅ Memory search successful") - print(f" Found {data['total_count']} memories") - for memory in data['memories']: - print(f" - {memory['content'][:50]}... (similarity: {memory['similarity']:.3f})") - else: - print(f"❌ Memory search failed: {response.status_code}") - print(f" Response: {response.text}") - - except Exception as e: - print(f"❌ Memory search failed: {e}") - - # Test 5: Retrieve memories for conversation - print("\n5. Testing memory retrieval...") - try: - retrieve_data = { - "messages": [ - {"role": "user", "content": "I want to learn about web development"}, - {"role": "assistant", "content": "Great! What technology are you interested in?"}, - {"role": "user", "content": "I heard Python is good for web APIs"} - ], - "user_id": TEST_USER_ID, - "session_id": "test_session_1" - } - - response = await client.post( - f"{API_BASE_URL}/v1/memories/retrieve", - json=retrieve_data, - headers=headers, - timeout=30.0 - ) - - if response.status_code == 200: - data = response.json() - print(f"✅ Memory retrieval successful") - print(f" Retrieved {data['total_count']} relevant memories") - for memory in data['memories']: - print(f" - {memory['content'][:50]}... (similarity: {memory['similarity']:.3f})") - else: - print(f"❌ Memory retrieval failed: {response.status_code}") - print(f" Response: {response.text}") - - except Exception as e: - print(f"❌ Memory retrieval failed: {e}") - - # Test 6: Get user memories - print("\n6. Testing user memory listing...") - try: - response = await client.get( - f"{API_BASE_URL}/v1/memories/users/{TEST_USER_ID}", - headers=headers, - timeout=30.0 - ) - - if response.status_code == 200: - data = response.json() - print(f"✅ User memory listing successful") - print(f" User has {data['total_count']} memories") - for memory in data['memories']: - print(f" - {memory['content'][:50]}... (created: {memory['created_at'][:19]})") - else: - print(f"❌ User memory listing failed: {response.status_code}") - print(f" Response: {response.text}") - - except Exception as e: - print(f"❌ User memory listing failed: {e}") - - print("\n" + "=" * 50) - print("🎉 API Testing Complete!") - -if __name__ == "__main__": - asyncio.run(test_api_endpoints()) \ No newline at end of file diff --git a/check_neo4j_data.py b/tests/check_neo4j_data.py similarity index 100% rename from check_neo4j_data.py rename to tests/check_neo4j_data.py diff --git a/clear_all_databases.py b/tests/clear_all_databases.py similarity index 100% rename from clear_all_databases.py rename to tests/clear_all_databases.py diff --git a/debug_fact_extraction.py b/tests/debug_fact_extraction.py similarity index 100% rename from debug_fact_extraction.py rename to tests/debug_fact_extraction.py diff --git a/debug_neo4j_relationships.py b/tests/debug_neo4j_relationships.py similarity index 100% rename from debug_neo4j_relationships.py rename to tests/debug_neo4j_relationships.py diff --git a/populate_test_data.py b/tests/populate_test_data.py similarity index 100% rename from populate_test_data.py rename to tests/populate_test_data.py diff --git a/simple_test.py b/tests/simple_test.py similarity index 100% rename from simple_test.py rename to tests/simple_test.py diff --git a/store_personal_info.py b/tests/store_personal_info.py similarity index 100% rename from store_personal_info.py rename to tests/store_personal_info.py diff --git a/test_ai_relationships.py b/tests/test_ai_relationships.py similarity index 100% rename from test_ai_relationships.py rename to tests/test_ai_relationships.py diff --git a/tests/test_api.py b/tests/test_api.py index 94b63c3..d5275e0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,204 +1,169 @@ #!/usr/bin/env python3 """ -Test suite for LangMem API +Test the LangMem API endpoints """ import asyncio -import json -import pytest import httpx +import json from uuid import uuid4 # Configuration API_BASE_URL = "http://localhost:8765" API_KEY = "langmem_api_key_2025" +TEST_USER_ID = f"test_user_{uuid4()}" -class TestLangMemAPI: - """Test suite for LangMem API endpoints""" +async def test_api_endpoints(): + """Test all API endpoints""" + print("🧪 Testing LangMem API Endpoints") + print("=" * 50) - def setup_method(self): - """Setup test client""" - self.client = httpx.AsyncClient(base_url=API_BASE_URL) - self.headers = {"Authorization": f"Bearer {API_KEY}"} - self.test_user_id = f"test_user_{uuid4()}" + headers = {"Authorization": f"Bearer {API_KEY}"} - async def teardown_method(self): - """Cleanup test client""" - await self.client.aclose() - - @pytest.mark.asyncio - async def test_root_endpoint(self): - """Test root endpoint""" - response = await self.client.get("/") - assert response.status_code == 200 - data = response.json() - assert data["message"] == "LangMem API - Long-term Memory System" - assert data["version"] == "1.0.0" - - @pytest.mark.asyncio - async def test_health_check(self): - """Test health check endpoint""" - response = await self.client.get("/health") - assert response.status_code == 200 - data = response.json() - assert "status" in data - assert "services" in data - assert "timestamp" in data - - @pytest.mark.asyncio - async def test_store_memory(self): - """Test storing a memory""" - memory_data = { - "content": "This is a test memory about Python programming", - "user_id": self.test_user_id, - "session_id": "test_session_1", - "metadata": { - "category": "programming", - "language": "python", - "importance": "high" + async with httpx.AsyncClient() as client: + # Test 1: Root endpoint + print("\n1. Testing root endpoint...") + try: + response = await client.get(f"{API_BASE_URL}/") + print(f"✅ Root endpoint: {response.status_code}") + print(f" Response: {response.json()}") + except Exception as e: + print(f"❌ Root endpoint failed: {e}") + + # Test 2: Health check + print("\n2. Testing health check...") + try: + response = await client.get(f"{API_BASE_URL}/health") + print(f"✅ Health check: {response.status_code}") + data = response.json() + print(f" Overall status: {data.get('status')}") + for service, status in data.get('services', {}).items(): + print(f" {service}: {status}") + except Exception as e: + print(f"❌ Health check failed: {e}") + + # Test 3: Store memory + print("\n3. Testing memory storage...") + try: + memory_data = { + "content": "FastAPI is a modern web framework for building APIs with Python", + "user_id": TEST_USER_ID, + "session_id": "test_session_1", + "metadata": { + "category": "programming", + "language": "python", + "framework": "fastapi" + } } - } + + response = await client.post( + f"{API_BASE_URL}/v1/memories/store", + json=memory_data, + headers=headers, + timeout=30.0 + ) + + if response.status_code == 200: + data = response.json() + memory_id = data["id"] + print(f"✅ Memory stored successfully: {memory_id}") + print(f" Status: {data['status']}") + else: + print(f"❌ Memory storage failed: {response.status_code}") + print(f" Response: {response.text}") + return + + except Exception as e: + print(f"❌ Memory storage failed: {e}") + return - response = await self.client.post( - "/v1/memories/store", - json=memory_data, - headers=self.headers - ) + # Test 4: Search memories + print("\n4. Testing memory search...") + try: + search_data = { + "query": "Python web framework", + "user_id": TEST_USER_ID, + "limit": 5, + "threshold": 0.5 + } + + response = await client.post( + f"{API_BASE_URL}/v1/memories/search", + json=search_data, + headers=headers, + timeout=30.0 + ) + + if response.status_code == 200: + data = response.json() + print(f"✅ Memory search successful") + print(f" Found {data['total_count']} memories") + for memory in data['memories']: + print(f" - {memory['content'][:50]}... (similarity: {memory['similarity']:.3f})") + else: + print(f"❌ Memory search failed: {response.status_code}") + print(f" Response: {response.text}") + + except Exception as e: + print(f"❌ Memory search failed: {e}") - assert response.status_code == 200 - data = response.json() - assert data["status"] == "stored" - assert data["user_id"] == self.test_user_id - assert "id" in data - assert "created_at" in data + # Test 5: Retrieve memories for conversation + print("\n5. Testing memory retrieval...") + try: + retrieve_data = { + "messages": [ + {"role": "user", "content": "I want to learn about web development"}, + {"role": "assistant", "content": "Great! What technology are you interested in?"}, + {"role": "user", "content": "I heard Python is good for web APIs"} + ], + "user_id": TEST_USER_ID, + "session_id": "test_session_1" + } + + response = await client.post( + f"{API_BASE_URL}/v1/memories/retrieve", + json=retrieve_data, + headers=headers, + timeout=30.0 + ) + + if response.status_code == 200: + data = response.json() + print(f"✅ Memory retrieval successful") + print(f" Retrieved {data['total_count']} relevant memories") + for memory in data['memories']: + print(f" - {memory['content'][:50]}... (similarity: {memory['similarity']:.3f})") + else: + print(f"❌ Memory retrieval failed: {response.status_code}") + print(f" Response: {response.text}") + + except Exception as e: + print(f"❌ Memory retrieval failed: {e}") - return data["id"] - - @pytest.mark.asyncio - async def test_search_memories(self): - """Test searching memories""" - # First store a memory - memory_id = await self.test_store_memory() + # Test 6: Get user memories + print("\n6. Testing user memory listing...") + try: + response = await client.get( + f"{API_BASE_URL}/v1/memories/users/{TEST_USER_ID}", + headers=headers, + timeout=30.0 + ) + + if response.status_code == 200: + data = response.json() + print(f"✅ User memory listing successful") + print(f" User has {data['total_count']} memories") + for memory in data['memories']: + print(f" - {memory['content'][:50]}... (created: {memory['created_at'][:19]})") + else: + print(f"❌ User memory listing failed: {response.status_code}") + print(f" Response: {response.text}") + + except Exception as e: + print(f"❌ User memory listing failed: {e}") - # Wait a moment for indexing - await asyncio.sleep(1) - - # Search for the memory - search_data = { - "query": "Python programming", - "user_id": self.test_user_id, - "limit": 10, - "threshold": 0.5, - "include_graph": True - } - - response = await self.client.post( - "/v1/memories/search", - json=search_data, - headers=self.headers - ) - - assert response.status_code == 200 - data = response.json() - assert "memories" in data - assert "context" in data - assert "total_count" in data - assert data["total_count"] > 0 - - # Check first memory result - if data["memories"]: - memory = data["memories"][0] - assert "id" in memory - assert "content" in memory - assert "similarity" in memory - assert memory["user_id"] == self.test_user_id - - @pytest.mark.asyncio - async def test_retrieve_memories(self): - """Test retrieving memories for conversation context""" - # Store a memory first - await self.test_store_memory() - - # Wait a moment for indexing - await asyncio.sleep(1) - - # Retrieve memories based on conversation - retrieve_data = { - "messages": [ - {"role": "user", "content": "I want to learn about Python"}, - {"role": "assistant", "content": "Python is a great programming language"}, - {"role": "user", "content": "Tell me more about Python programming"} - ], - "user_id": self.test_user_id, - "session_id": "test_session_1" - } - - response = await self.client.post( - "/v1/memories/retrieve", - json=retrieve_data, - headers=self.headers - ) - - assert response.status_code == 200 - data = response.json() - assert "memories" in data - assert "context" in data - assert "total_count" in data - - @pytest.mark.asyncio - async def test_get_user_memories(self): - """Test getting all memories for a user""" - # Store a memory first - await self.test_store_memory() - - response = await self.client.get( - f"/v1/memories/users/{self.test_user_id}", - headers=self.headers - ) - - assert response.status_code == 200 - data = response.json() - assert "memories" in data - assert "total_count" in data - assert data["total_count"] > 0 - - # Check memory structure - if data["memories"]: - memory = data["memories"][0] - assert "id" in memory - assert "content" in memory - assert "user_id" in memory - assert "created_at" in memory - - @pytest.mark.asyncio - async def test_delete_memory(self): - """Test deleting a memory""" - # Store a memory first - memory_id = await self.test_store_memory() - - # Delete the memory - response = await self.client.delete( - f"/v1/memories/{memory_id}", - headers=self.headers - ) - - assert response.status_code == 200 - data = response.json() - assert data["status"] == "deleted" - assert data["id"] == memory_id - - @pytest.mark.asyncio - async def test_authentication_required(self): - """Test that authentication is required""" - response = await self.client.get("/v1/memories/users/test_user") - assert response.status_code == 401 - - @pytest.mark.asyncio - async def test_invalid_api_key(self): - """Test invalid API key""" - headers = {"Authorization": "Bearer invalid_key"} - response = await self.client.get("/v1/memories/users/test_user", headers=headers) - assert response.status_code == 401 + print("\n" + "=" * 50) + print("🎉 API Testing Complete!") if __name__ == "__main__": - pytest.main([__file__, "-v"]) \ No newline at end of file + asyncio.run(test_api_endpoints()) \ No newline at end of file diff --git a/test_fact_based_memory.py b/tests/test_fact_based_memory.py similarity index 100% rename from test_fact_based_memory.py rename to tests/test_fact_based_memory.py diff --git a/test_langmem_with_neo4j.py b/tests/test_langmem_with_neo4j.py similarity index 100% rename from test_langmem_with_neo4j.py rename to tests/test_langmem_with_neo4j.py diff --git a/test_mcp_server.py b/tests/test_mcp_server.py similarity index 100% rename from test_mcp_server.py rename to tests/test_mcp_server.py diff --git a/test_neo4j.py b/tests/test_neo4j.py similarity index 100% rename from test_neo4j.py rename to tests/test_neo4j.py