diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e753513f..eacd6157 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -10,6 +10,7 @@ on: - "integrations/langchain-memgraph/**" - "integrations/lightrag-memgraph/**" - "unstructured2graph/**" + - "enterprise-context/agent-memory/**" pull_request: paths: - "memgraph-toolbox/**" @@ -17,6 +18,7 @@ on: - "integrations/langchain-memgraph/**" - "integrations/lightrag-memgraph/**" - "unstructured2graph/**" + - "enterprise-context/agent-memory/**" jobs: changes: @@ -27,6 +29,7 @@ jobs: langchain-memgraph: ${{ steps.filter.outputs.langchain-memgraph }} lightrag-memgraph: ${{ steps.filter.outputs.lightrag-memgraph }} unstructured2graph: ${{ steps.filter.outputs.unstructured2graph }} + agent-memory: ${{ steps.filter.outputs.agent-memory }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -50,6 +53,8 @@ jobs: unstructured2graph: - 'unstructured2graph/**' - 'memgraph-toolbox/**' + agent-memory: + - 'enterprise-context/agent-memory/**' test-memgraph-toolbox: needs: changes @@ -187,3 +192,61 @@ jobs: uv venv uv pip install -e .[test] .venv/bin/python -m pytest . + + test-agent-memory-cognee: + needs: changes + if: ${{ needs.changes.outputs.agent-memory == 'true' }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.11.0 + + - name: Start Memgraph container + run: | + docker run -d -p 7687:7687 --name memgraph memgraph/memgraph-mage:latest --schema-info-enabled=True --telemetry-enabled=false + sleep 5 + + - name: Install uv + run: python -m pip install uv + + - name: Install dependencies and run tests + working-directory: enterprise-context/agent-memory + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + uv venv + uv pip install -e .[cognee] + .venv/bin/python -m pytest -m cognee -v + + test-agent-memory-neo4j: + needs: changes + if: ${{ needs.changes.outputs.agent-memory == 'true' }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.11.0 + + - name: Start Memgraph container + run: | + docker run -d -p 7687:7687 --name memgraph memgraph/memgraph-mage:latest --schema-info-enabled=True --telemetry-enabled=false + sleep 5 + + - name: Install uv + run: python -m pip install uv + + - name: Install dependencies and run tests + working-directory: enterprise-context/agent-memory + run: | + uv venv + uv pip install -e .[neo4j-agent-memory] + .venv/bin/python -m pytest -m neo4j_agent_memory -v diff --git a/enterprise-context/.gitignore b/enterprise-context/.gitignore new file mode 100644 index 00000000..9220d27b --- /dev/null +++ b/enterprise-context/.gitignore @@ -0,0 +1,22 @@ +# Virtual environments +.venv/ +venv/ + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ + +# IDE +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Environment +.env diff --git a/enterprise-context/README.md b/enterprise-context/README.md new file mode 100644 index 00000000..5d3110b9 --- /dev/null +++ b/enterprise-context/README.md @@ -0,0 +1,24 @@ +# Enterprise Context + +A collection of standalone components for enriching enterprise data with graph-powered context. Each component is an independent library or CLI tool with its own dependencies and configuration. + +## Components + +| Component | Description | +| ------------------------------ | ---------------------------------------------------------------------------------------- | +| [sic-agent](./sic-agent) | SIC Classification MCP Server using Memgraph vector search | +| [agent-memory](./agent-memory) | Integration tests for memory frameworks (Cognee, Mem0, neo4j-agent-memory) with Memgraph | + +## Structure + +Each component lives in its own subdirectory and includes: + +- `pyproject.toml` — project metadata and dependencies +- `README.md` — component-specific documentation +- `LICENSE` — license file + +Components are independent and can be installed/run separately. + +## Getting Started + +Navigate into any component directory and follow its README for setup instructions. diff --git a/enterprise-context/agent-memory/LICENSE b/enterprise-context/agent-memory/LICENSE new file mode 100644 index 00000000..3f187a66 --- /dev/null +++ b/enterprise-context/agent-memory/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Memgraph + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/enterprise-context/agent-memory/README.md b/enterprise-context/agent-memory/README.md new file mode 100644 index 00000000..3b9e0cc5 --- /dev/null +++ b/enterprise-context/agent-memory/README.md @@ -0,0 +1,49 @@ +# Agent Memory Tests + +Integration tests for memory frameworks with Memgraph. Each test suite validates that a memory framework works correctly using Memgraph as its graph backend. + +## Frameworks + +| Framework | Description | Upstream | +| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------- | +| [Cognee](https://github.com/topoteretes/cognee-community/tree/main/packages/graph/memgraph) | Knowledge graph adapter for the Cognee framework | `cognee-community-graph-adapter-memgraph` | +| [Neo4j Agent Memory](https://github.com/neo4j-labs/agent-memory) | Graph-native memory system for AI agents (testing Bolt-compatibility with Memgraph) | `neo4j-agent-memory` | + +## Prerequisites + +- Python >= 3.10 +- A running Memgraph instance (default: `bolt://localhost:7687`) + +```bash +docker run -p 7687:7687 memgraph/memgraph-mage:latest --schema-info-enabled=True +``` + +## Setup + +Install all test dependencies: + +```bash +uv sync --all-extras +``` + +Or install only a specific framework: + +```bash +uv sync --extra cognee +uv sync --extra neo4j-agent-memory +``` + +## Running Tests + +Run all tests: + +```bash +uv run pytest +``` + +Run tests for a specific framework: + +```bash +uv run pytest -m cognee +uv run pytest -m neo4j_agent_memory +``` diff --git a/enterprise-context/agent-memory/pyproject.toml b/enterprise-context/agent-memory/pyproject.toml new file mode 100644 index 00000000..6f092bed --- /dev/null +++ b/enterprise-context/agent-memory/pyproject.toml @@ -0,0 +1,39 @@ +[project] +name = "agent-memory-tests" +version = "0.1.0" +description = "Integration tests for memory frameworks with Memgraph" +readme = "README.md" +license = { text = "MIT" } +requires-python = ">=3.10" + +dependencies = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.23.0", + "neo4j>=5.20.0", +] + +[project.optional-dependencies] +cognee = [ + "cognee-community-graph-adapter-memgraph>=0.1.0", +] +neo4j-agent-memory = [ + "neo4j-agent-memory>=0.1.0", +] +all = [ + "agent-memory-tests[cognee,neo4j-agent-memory]", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["tests"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] +markers = [ + "cognee: tests for the cognee integration with Memgraph", + "neo4j_agent_memory: tests for the neo4j-agent-memory integration with Memgraph", +] diff --git a/enterprise-context/agent-memory/tests/__init__.py b/enterprise-context/agent-memory/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/enterprise-context/agent-memory/tests/conftest.py b/enterprise-context/agent-memory/tests/conftest.py new file mode 100644 index 00000000..f3c77a64 --- /dev/null +++ b/enterprise-context/agent-memory/tests/conftest.py @@ -0,0 +1,79 @@ +import os + +import pytest +from neo4j import GraphDatabase + +MEMGRAPH_URL = os.environ.get("MEMGRAPH_URL", "bolt://localhost:7687") +MEMGRAPH_USERNAME = os.environ.get("MEMGRAPH_USERNAME", "") +MEMGRAPH_PASSWORD = os.environ.get("MEMGRAPH_PASSWORD", "") + + +@pytest.fixture(scope="session") +def memgraph_url(): + return MEMGRAPH_URL + + +@pytest.fixture(scope="session") +def memgraph_username(): + return MEMGRAPH_USERNAME + + +@pytest.fixture(scope="session") +def memgraph_password(): + return MEMGRAPH_PASSWORD + + +@pytest.fixture(scope="session") +def memgraph_driver(): + """Shared neo4j driver for direct Cypher assertions.""" + driver = GraphDatabase.driver( + MEMGRAPH_URL, auth=(MEMGRAPH_USERNAME, MEMGRAPH_PASSWORD) + ) + yield driver + driver.close() + + +def _node_count(driver) -> int: + with driver.session() as session: + return session.run("MATCH (n) RETURN count(n) AS cnt").single()["cnt"] + + +def _relationship_count(driver) -> int: + with driver.session() as session: + return session.run("MATCH ()-[r]->() RETURN count(r) AS cnt").single()["cnt"] + + +@pytest.fixture() +def assert_graph_not_empty(memgraph_driver): + """Return a callable that asserts Memgraph contains nodes after the framework writes.""" + + def _check(*, min_nodes: int = 1, min_relationships: int = 0): + nodes = _node_count(memgraph_driver) + rels = _relationship_count(memgraph_driver) + assert ( + nodes >= min_nodes + ), f"Expected at least {min_nodes} node(s) in Memgraph, found {nodes}" + assert ( + rels >= min_relationships + ), f"Expected at least {min_relationships} relationship(s) in Memgraph, found {rels}" + + return _check + + +@pytest.fixture() +def run_cypher(memgraph_driver): + """Return a callable that executes an arbitrary Cypher query and returns records.""" + + def _run(query: str, **params): + with memgraph_driver.session() as session: + return list(session.run(query, **params)) + + return _run + + +@pytest.fixture(autouse=True) +def _clean_memgraph(memgraph_driver): + """Wipe all data before each test so tests are isolated.""" + with memgraph_driver.session() as session: + session.run("MATCH (n) DETACH DELETE n") + yield diff --git a/enterprise-context/agent-memory/tests/test_cognee.py b/enterprise-context/agent-memory/tests/test_cognee.py new file mode 100644 index 00000000..8d23e389 --- /dev/null +++ b/enterprise-context/agent-memory/tests/test_cognee.py @@ -0,0 +1,97 @@ +"""Integration tests for the Cognee framework with Memgraph. + +Requires: + pip install cognee-community-graph-adapter-memgraph + A running Memgraph instance. + OPENAI_API_KEY set in the environment (cognee uses an LLM for extraction). +""" + +import asyncio +import os +import pathlib + +import pytest + +cognee = pytest.importorskip("cognee") +register = pytest.importorskip( + "cognee_community_graph_adapter_memgraph", + reason="cognee-community-graph-adapter-memgraph not installed", +).register + +pytestmark = [pytest.mark.cognee, pytest.mark.asyncio] + + +@pytest.fixture(autouse=True) +async def _configure_cognee( + memgraph_url, memgraph_username, memgraph_password, tmp_path +): + """Register the Memgraph adapter and configure cognee for each test.""" + register() + + # The Memgraph adapter does not support cognee's multi-user access + # control mode. Disable it so cognify does not raise an OSError. + os.environ.setdefault("ENABLE_BACKEND_ACCESS_CONTROL", "false") + + cognee.config.set_graph_database_provider("memgraph") + cognee.config.set_graph_db_config( + { + "graph_database_url": memgraph_url, + "graph_database_username": memgraph_username or "memgraph", + "graph_database_password": memgraph_password or "memgraph", + } + ) + + # Cognee caches LLM config at import time so setting the env var + # in a fixture is too late. Use the programmatic setter instead. + api_key = os.environ.get("LLM_API_KEY") or os.environ.get("OPENAI_API_KEY", "") + if api_key: + cognee.config.set_llm_api_key(api_key) + + system_dir = tmp_path / ".cognee_system" + data_dir = tmp_path / ".data_storage" + system_dir.mkdir() + data_dir.mkdir() + cognee.config.system_root_directory(str(system_dir)) + cognee.config.data_root_directory(str(data_dir)) + yield + + +async def test_cognee_add_and_search(assert_graph_not_empty): + """Add sample data, cognify, and verify nodes are persisted in Memgraph.""" + sample_data = [ + "Artificial intelligence is a branch of computer science.", + "Machine learning is a subset of AI that focuses on algorithms that learn from data.", + ] + + await cognee.add(sample_data, "test_knowledge") + await cognee.cognify(["test_knowledge"]) + + # Verify data landed in Memgraph + assert_graph_not_empty(min_nodes=1, min_relationships=1) + + results = await cognee.search( + query_type=cognee.SearchType.GRAPH_COMPLETION, + query_text="artificial intelligence", + ) + + assert results is not None + assert len(results) > 0 + + +async def test_cognee_graph_data_accessible(assert_graph_not_empty, run_cypher): + """Verify that graph data can be retrieved after cognify and nodes exist in Memgraph.""" + from cognee.infrastructure.databases.graph import get_graph_engine + + await cognee.add(["Graph databases store data as nodes and edges."], "graph_data") + await cognee.cognify(["graph_data"]) + + # Verify via direct Cypher that Memgraph has data + assert_graph_not_empty(min_nodes=1) + + records = run_cypher("MATCH (n) RETURN labels(n) AS labels, count(n) AS cnt") + assert len(records) > 0, "Expected at least one label group in Memgraph" + + graph_engine = await get_graph_engine() + graph_data = await graph_engine.get_graph_data() + + assert graph_data is not None diff --git a/enterprise-context/agent-memory/tests/test_mem0.py b/enterprise-context/agent-memory/tests/test_mem0.py new file mode 100644 index 00000000..ce0b354e --- /dev/null +++ b/enterprise-context/agent-memory/tests/test_mem0.py @@ -0,0 +1,110 @@ +"""Integration tests for Mem0 graph memory with Memgraph. + +Requires: + pip install "mem0ai[graph]" + A running Memgraph instance. + OPENAI_API_KEY set in the environment (mem0 uses OpenAI for embeddings and LLM extraction). +""" + +import os +import uuid + +import pytest + +mem0_memory = pytest.importorskip("mem0", reason="mem0ai not installed") + +pytestmark = pytest.mark.mem0 + + +@pytest.fixture() +def memory(memgraph_url, memgraph_username, memgraph_password, tmp_path): + """Create a Mem0 Memory instance configured with Memgraph. + + Uses a unique Qdrant path per test to avoid concurrent access errors, + and configures the full pipeline (LLM + embedder + graph_store) so + that entity extraction actually writes nodes to Memgraph. + """ + from mem0 import Memory + + qdrant_path = str(tmp_path / f"qdrant_{uuid.uuid4().hex}") + + config = { + "llm": { + "provider": "openai", + "config": { + "model": "gpt-4o-mini", + }, + }, + "embedder": { + "provider": "openai", + "config": { + "model": "text-embedding-3-small", + }, + }, + "vector_store": { + "provider": "qdrant", + "config": { + "collection_name": "mem0_test", + "path": qdrant_path, + }, + }, + "graph_store": { + "provider": "memgraph", + "config": { + "url": memgraph_url, + "username": memgraph_username or "memgraph", + "password": memgraph_password or "memgraph", + }, + }, + } + return Memory.from_config(config_dict=config) + + +def test_mem0_add_and_search(memory, assert_graph_not_empty, run_cypher): + """Store messages and verify data is persisted in Memgraph.""" + messages = [ + {"role": "user", "content": "I love hiking in the mountains."}, + { + "role": "assistant", + "content": "That sounds great! Do you have a favorite trail?", + }, + {"role": "user", "content": "Yes, I really enjoy the Appalachian Trail."}, + ] + + result = memory.add(messages, user_id="test_user") + assert result is not None + + # Verify nodes were created in Memgraph + assert_graph_not_empty(min_nodes=1) + + # Check that at least one relationship was created (mem0 builds a graph of entities) + records = run_cypher("MATCH (n) RETURN count(n) AS cnt") + assert records[0]["cnt"] > 0, "mem0 should have created nodes in Memgraph" + + search_results = memory.search("What does the user enjoy?", user_id="test_user") + assert search_results is not None + assert "results" in search_results + assert len(search_results["results"]) > 0 + + +def test_mem0_multiple_users(memory, run_cypher): + """Verify memories are isolated per user_id and stored in Memgraph.""" + memory.add( + [{"role": "user", "content": "I prefer Python for data science."}], + user_id="alice", + ) + memory.add( + [{"role": "user", "content": "I prefer Rust for systems programming."}], + user_id="bob", + ) + + # Verify that nodes exist in Memgraph for both users + records = run_cypher("MATCH (n) RETURN count(n) AS cnt") + assert records[0]["cnt"] >= 2, "Expected nodes for both alice and bob in Memgraph" + + alice_results = memory.search("programming language", user_id="alice") + bob_results = memory.search("programming language", user_id="bob") + + assert alice_results is not None + assert bob_results is not None + assert bob_results is not None diff --git a/enterprise-context/agent-memory/tests/test_neo4j_agent_memory.py b/enterprise-context/agent-memory/tests/test_neo4j_agent_memory.py new file mode 100644 index 00000000..5fd9a7cf --- /dev/null +++ b/enterprise-context/agent-memory/tests/test_neo4j_agent_memory.py @@ -0,0 +1,118 @@ +"""Integration tests for neo4j-agent-memory with Memgraph. + +Tests whether the neo4j-agent-memory library (designed for Neo4j) works +against Memgraph via Bolt protocol compatibility. + +Requires: + pip install neo4j-agent-memory + A running Memgraph instance. +""" + +import pytest + +neo4j_agent_memory = pytest.importorskip( + "neo4j_agent_memory", reason="neo4j-agent-memory not installed" +) + +pytestmark = [ + pytest.mark.neo4j_agent_memory, + pytest.mark.asyncio, + pytest.mark.xfail( + reason=( + "neo4j-agent-memory uses Neo4j-specific constraint DDL " + "(SHOW CONSTRAINTS YIELD) that Memgraph does not support yet" + ), + strict=False, + ), +] + + +@pytest.fixture() +def memory_settings(memgraph_url, memgraph_password): + """Create MemorySettings pointed at Memgraph.""" + from neo4j_agent_memory import MemorySettings, ExtractionConfig, ExtractorType + + return MemorySettings( + neo4j={ + "uri": memgraph_url, + "password": memgraph_password or "", + "database": "memgraph", + }, + extraction=ExtractionConfig(extractor_type=ExtractorType.NONE), + ) + + +async def test_short_term_memory(memory_settings, assert_graph_not_empty, run_cypher): + """Store and retrieve short-term (conversation) memory, verify in Memgraph.""" + from neo4j_agent_memory import MemoryClient + + async with MemoryClient(memory_settings) as memory: + await memory.short_term.add_message( + session_id="session-1", + role="user", + content="Hello, my name is Alice and I work at Memgraph.", + ) + await memory.short_term.add_message( + session_id="session-1", + role="assistant", + content="Nice to meet you, Alice!", + ) + + messages = await memory.short_term.get_messages(session_id="session-1") + assert len(messages) >= 2 + + # Verify messages were persisted as nodes in Memgraph + assert_graph_not_empty(min_nodes=2) + records = run_cypher("MATCH (n) RETURN count(n) AS cnt") + assert records[0]["cnt"] >= 2, "Expected at least 2 message nodes in Memgraph" + + +async def test_long_term_memory_entities( + memory_settings, assert_graph_not_empty, run_cypher +): + """Add entities and preferences to long-term memory, verify in Memgraph.""" + from neo4j_agent_memory import MemoryClient + + async with MemoryClient(memory_settings) as memory: + await memory.long_term.add_entity("Alice", "PERSON") + await memory.long_term.add_preference( + category="technology", + preference="Prefers graph databases", + ) + + context = await memory.get_context( + "What technology does Alice prefer?", + session_id="session-1", + ) + assert context is not None + + # Verify entity and preference nodes exist in Memgraph + assert_graph_not_empty(min_nodes=1) + records = run_cypher("MATCH (n) RETURN labels(n) AS labels, count(n) AS cnt") + assert len(records) > 0, "Expected labeled nodes for entities/preferences" + + +async def test_get_context(memory_settings, assert_graph_not_empty, run_cypher): + """Verify get_context returns combined short+long term results from Memgraph.""" + from neo4j_agent_memory import MemoryClient + + async with MemoryClient(memory_settings) as memory: + await memory.short_term.add_message( + session_id="session-2", + role="user", + content="I enjoy reading about distributed systems.", + ) + await memory.long_term.add_entity("DistributedSystems", "TOPIC") + + context = await memory.get_context( + "distributed systems", + session_id="session-2", + ) + assert context is not None + + # Verify both message and entity nodes exist in Memgraph + assert_graph_not_empty(min_nodes=2) + records = run_cypher("MATCH (n) RETURN count(n) AS cnt") + assert ( + records[0]["cnt"] >= 2 + ), "Expected nodes from both short-term and long-term memory"