Skip to main content

Mandol — Agent Memory System with hierarchical, episodic and entity-relation triple-tower retrieval.

Project description

Mandol

Mandol: an in-memory semantic memory runtime for agent systems.

License Python Homepage Docs Paper

English | Chinese

Mandol Overview

Current Scope

This repository exposes the mandol Python package under src/mandol and the paper reproduction workflows under benchmark_locomo, benchmark_longmemeval, and benchmark_self_host.

The public Python surface is centered on:

  • MemoryUnit: the basic memory record.
  • MemorySpace: a tree-like logical namespace for unit membership.
  • SemanticMap: in-memory unit storage, embedding generation, FAISS indexing, sparse retrieval support, persistence, and space-filtered similarity search.
  • SemanticGraph: a graph layer over memory units and spaces, with relationship APIs, graph traversal, retrieval helpers, L2 storage support, and sandboxed persistence.
  • MultiRetriever: BM25, SPLADE, cosine, graph expansion, score fusion, and reranker orchestration.
  • TripleTowerRetriever: hierarchical, entity-relation, and episodic retrieval orchestration for already-built memory spaces.
  • memory_router: LoCoMo and LongMemEval routing policies used by the paper router + quantification workflows.

Older notes in the repository may mention MemorySystem, Uid, mandol.ports, or mandol.retrieval.pipeline.HybridRetriever. Those names are not part of the current package exports. Maintained docs and examples use MemoryUnit, SemanticMap, SemanticGraph, MultiRetriever, and the current subpackages directly.

Requirements

  • Python >=3.12,<3.13
  • Linux for the full research/runtime stack
  • uv for reproducible local environments
  • Provider keys for model-backed reproduction runs

The default dependency set in pyproject.toml is intentionally broad. It contains Torch, transformers, sentence-transformers, FAISS CPU, DuckDB, graph libraries, LLM clients, retrieval/rerank tools, benchmark dependencies, and optional integration clients needed by the artifact scripts.

Environment Setup

Install uv if it is not already available:

curl -LsSf https://astral.sh/uv/install.sh | sh

Create the base runtime environment from the repository root:

uv sync

For day-to-day development and documentation work:

uv sync --extra dev --extra docs --group spacy-model

For paper reproduction and performance runs, install the full artifact stack. The performance numbers reported for the paper were measured with the relevant extras installed; use this full path when comparing throughput:

uv sync --extra dev --extra cuda --group spacy-model

If your machine does not have a CUDA/flash-attention-compatible setup, omit --extra cuda. The workflows still run, but retrieval and reranking throughput may differ from the paper performance setting. The cuda extra is pinned to a Linux x86_64 / Python 3.12 / Torch 2.8 / CUDA 12 flash-attention wheel for the paper artifact. If this wheel does not match your platform, omit --extra cuda or install a compatible flash-attn build manually.

After syncing, verify the local editable package:

uv run python -c "import mandol; print(mandol.__version__)"

Installing Mandol As A Package

For development from this checkout, uv sync installs the local src/mandol package into the environment. To build the same artifacts that would be uploaded to PyPI:

uv build

To test the built wheel locally:

uv pip install --force-reinstall dist/mandol-*.whl
uv run python -c "from mandol import MemoryUnit, SemanticGraph, SemanticMap; print('ok')"

After a public PyPI release, users can install the package with:

python -m pip install mandol

The benchmark directories are repository artifacts, not part of the runtime package. Use the source checkout when reproducing paper results.

Optional Acceleration

Mandol runs without acceleration extras, but the paper artifact uses the following optional paths for higher throughput:

  • --extra cuda: installs the flash-attention extra declared in pyproject.toml. The code only passes flash-attention options when the dependency is available.
  • --group spacy-model: installs the large English spaCy model used by some extraction and retrieval utilities. Tokenization falls back where supported, but the full artifact environment should include it.
  • RERANKER_BACKEND=vllm: routes compatible reranker scoring through a vLLM HTTP endpoint when available.
  • Local model caches: pre-download Hugging Face and sentence-transformers models on shared machines to avoid counting first-run downloads in benchmark timing.

Example vLLM reranker configuration:

export RERANKER_BACKEND=vllm
export VLLM_API_URL=http://127.0.0.1:8000/score
export VLLM_API_KEY=EMPTY

Provider Keys

Runtime configuration is read through mandol.utils.config_manager.settings. The project root .env and system environment variables are supported. Common keys include:

export DASHSCOPE_API_KEY=...
export CLOSEAI_API_KEY=...
export OPENAI_API_KEY=...
export OPENROUTER_API_KEY=...
export SILICONFLOW_API_KEY=...
export CSTCLOUD_API_KEY=...
export HF_TOKEN=...

CLOSEAI_API_KEY falls back to OPENAI_API_KEY in the current provider configuration. CLOSEAI_* is an OpenAI-compatible provider alias used by the paper artifact configuration. If you do not use this gateway, set OPENAI_API_KEY or map the model alias to your own provider. Use env.template as the local environment template; never commit .env files.

Dataset Preparation

Large public datasets and generated graph artifacts are intentionally ignored by Git. Each dataset directory contains a README with source links and placement instructions.

LoCoMo10:

mkdir -p benchmark_locomo/dataset/locomo
curl -fL https://raw.githubusercontent.com/snap-research/locomo/main/data/locomo10.json \
  -o benchmark_locomo/dataset/locomo/locomo10.json

mkdir -p benchmark_self_host/locomo10/dataset
cp benchmark_locomo/dataset/locomo/locomo10.json \
  benchmark_self_host/locomo10/dataset/locomo10.json

LongMemEval small split:

mkdir -p benchmark_longmemeval/dataset/LongMemEval
curl -fL https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_s_cleaned.json \
  -o benchmark_longmemeval/dataset/LongMemEval/longmemeval_s_cleaned.json

mkdir -p benchmark_self_host/longmemeval/dataset
cp benchmark_longmemeval/dataset/LongMemEval/longmemeval_s_cleaned.json \
  benchmark_self_host/longmemeval/dataset/longmemeval_s_cleaned.json

LongMemEval medium split is only needed for --dataset-size m:

curl -fL https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_m_cleaned.json \
  -o benchmark_longmemeval/dataset/LongMemEval/longmemeval_m_cleaned.json

Official dataset sources:

Quick Start

This example uses the light MiniLM preset and disables realtime SPLADE vector generation so the first run stays small. Creating a SemanticMap still loads an embedding model; if the model is not already cached, sentence-transformers may download it from Hugging Face.

from mandol import MemoryUnit, SemanticGraph, SemanticMap

semantic_map = SemanticMap(
    embedding_model_name="all-MiniLM-L6-v2",
    use_flash_attention=False,
)
graph = SemanticGraph(semantic_map_instance=semantic_map)

graph.add_unit(
    MemoryUnit(
        uid="msg_001",
        raw_data={"text_content": "Zhang San travelled to Beijing today."},
        metadata={"timestamp": "2026-06-21T09:00:00"},
    ),
    space_names=["demo"],
    generate_sparse_embedding=False,
)
graph.add_unit(
    MemoryUnit(
        uid="msg_002",
        raw_data={"text_content": "He will discuss the Q2 delivery plan."},
        metadata={"timestamp": "2026-06-21T09:05:00"},
    ),
    space_names=["demo"],
    generate_sparse_embedding=False,
)

graph.add_relationship("msg_001", "msg_002", "NEXT")

hits = graph.search_similarity_in_graph(
    query_text="Where did Zhang San go?",
    top_k=3,
    ms_names=["demo"],
    return_score=True,
)

for unit, score in hits:
    print(f"{score:.3f} {unit.uid}: {unit.text_cached}")

For multi-method retrieval:

from mandol.retrieval import MultiRetriever

retriever = MultiRetriever(graph)
results = retriever.smart_search(
    "Where did Zhang San go?",
    methods=["bm25", "cosine"],
    top_k=5,
    rerank_method=None,
    space_names=["demo"],
)

Persistence

Use SemanticGraph.save_graph() and SemanticGraph.load_graph() for complete state snapshots. They preserve graph topology, semantic map data, retrieval indices when built, and the sandboxed DuckDB L2 storage copy.

graph.save_graph("./memory_snapshot", build_sparse_vectors=False)

restored = SemanticGraph.load_graph(
    "./memory_snapshot",
    embedding_model_name="all-MiniLM-L6-v2",
    use_flash_attention=False,
)

SemanticMap.save_map() and SemanticMap.load_map() also exist, but they are map-only APIs and do not preserve the SemanticGraph topology.

Model Configuration

SemanticMap has a built-in model registry in src/mandol/core/semantic_map.py. Current presets include:

Model name Type Dim Notes
Qwen/Qwen3-Embedding-0.6B local 1024 Default text embedding model
Qwen/Qwen3-Embedding-4B local 2560 Larger local text model
Qwen/Qwen3-Embedding-8B local 4096 Larger local text model
Qwen/Qwen3-Embedding-0.6B-remote cloud 1024 SiliconFlow adapter
BAAI/bge-m3 / bge-m3 local 1024 Text embedding model
all-MiniLM-L6-v2 local 384 Lightweight CPU-friendly option
jinaai/jina-clip-v2 local 1024 Text and image modalities
jinaai/jina-embeddings-v4 local 2048 Text and image modalities

Reproduction Workflows

The paper accuracy numbers use router + quantification workflows over generated three-tower memory spaces:

The self-host workflows use Mandol's own high-level memory-generation path without router + quantification:

Recommended smoke checks before long runs:

uv run python -m benchmark_locomo.task_eval.locomo_triple_router_quantification --help
uv run python -m benchmark_longmemeval.task_eval.benchmark_triple_router_quantification --help
uv run python -m benchmark_self_host.locomo10.build_graph --help
uv run python -m benchmark_self_host.longmemeval.build_graph --help

After the required graph artifacts exist, run a bounded real-LLM task-eval smoke before launching full benchmark jobs:

uv run python -m benchmark_locomo.task_eval.locomo_triple_router_quantification \
  --sample-ids conv-30 \
  --max-questions 1 \
  --llm-model gpt-4.1-mini-closeai \
  --llm-evaluate-model gpt-4o-mini-closeai \
  --output-dir benchmark_locomo/task_eval/results/smoke/gpt41_mini

uv run python -m benchmark_longmemeval.task_eval.benchmark_triple_router_quantification \
  --dataset-size s \
  --start-qa 0 \
  --end-qa 0 \
  --max-tests 1 \
  --llm-model gpt-4.1-mini-closeai \
  --llm-evaluate-model gpt-4o-mini-closeai \
  --output-dir benchmark_longmemeval/task_eval/results/smoke/gpt41_mini

Paper model roles:

The names below are Mandol provider aliases resolved by the repository configuration. When using a different provider gateway, keep the roles fixed but map each alias to an equivalent model endpoint in your local configuration.

  • LoCoMo memory/extraction generation: qwen-3.5-plus-thinking
  • LongMemEval memory/extraction generation: qwen-3-plus
  • Deduplication: deepseek-v3.2-dashscope
  • Task-eval evaluated models: gpt-4.1-mini-closeai and gpt-4o-mini-closeai
  • Task-eval judge model: gpt-4o-mini-closeai

The model names above should be kept fixed when reproducing the paper tables. The Qwen/DeepSeek models are used for memory generation and deduplication; the GPT models are used for task evaluation and judging.

Notes and Limitations

This repository is released as a research artifact and Python reference implementation for the Mandol paper. It is not intended to be a production-ready service.

  • Full reproduction requires external model providers and local model downloads.
  • Reported numbers may vary slightly across hardware, dependency versions, model provider versions, and random seeds.
  • The cuda extra is platform-specific and can be omitted when flash-attention is unavailable.
  • Large datasets, generated graphs, model caches, and benchmark outputs are intentionally excluded from Git.

Performance Measurement Scope

LoCoMo retrieval-performance tests require unified per-sample graphs. Build them after the three offline towers have been generated and before running the fixed-QPS search benchmark:

bash benchmark_locomo/dataset_maker/build_unified.sh

The wrapper calls benchmark_locomo/dataset_maker/build_unified_graph.py and writes unified graph folders to:

benchmark_locomo/dataset/locomo/unified_per_sample_graphs

The two reported LoCoMo performance entrypoints measure different API scopes:

  • Insertion latency: benchmark_locomo/task_eval/locomo_triple_input_speed.py schedules requests at the target QPS and measures only the body of each SemanticGraph.add_unit(...) call with index_update_mode="incremental" and generate_sparse_embedding=True. This timed call includes dense embedding generation, realtime SPLADE sparse embedding generation, and incremental index updates performed by the add path. The reported latency_ms excludes request scheduling sleep, memory-pool construction, graph initialization, warmup, and result-file writing.
  • Search latency: benchmark_locomo/task_eval/locomo_triple_smart_search_qps.py loads a unified graph, runs warmup requests, then measures each scheduled MultiRetriever.smart_search(...) or smart_search_async(...) call. The reported latency_ms covers query dispatch through BM25, cosine, SPLADE, score fusion, reranking when --rerank-method is set, response parsing, and Python async/thread wrapper overhead inside one request. The provided speed scripts pass --rerank-method baai, so the current smart-search QPS numbers include reranking. The metric excludes graph loading, warmup, fixed QPS scheduling sleep, and report writing. The report also records retrieval_time_ms for the base retrieval phase and rerank_time_ms for the reranking phase.

Package Layout

src/mandol/
  core/                MemoryUnit, MemorySpace, SemanticMap, SemanticGraph
  retrieval/           MultiRetriever, BM25, SPLADE, cosine, fusion, rerankers
  triple_retrieval/    Three-tower retrieval orchestration
  hierarchical/        Retrieval-facing hierarchical memory components
  entity_relation/     Retrieval-facing entity/relation graph components
  episodic/            Episodic memory retriever
  quantification/      Query expansion, pruning, semantic quantification
  memory_router/       LoCoMo and LongMemEval tower routers
  llm/                 LLM clients and provider wrappers
  storage/             DuckDB and tiered storage helpers
  cluster/             Leiden and DBSCAN clustering helpers
  utils/               Configuration, logging, model management

Documentation

The maintained documentation entry point is docs/index.rst. Build it with:

uv sync --extra docs
uv run sphinx-build -b html docs docs/_build/html

The Docusaurus website lives in website/ and is a separate static front page:

cd website
npm install
npm run build

Citation

If you use Mandol in your research, please cite the arXiv paper:

@misc{zhang2026mandol,
  title         = {Mandol: An Agglomerative Agent Memory System for Long-Term Conversations},
  author        = {Yuhan Zhang and Zhiyuan Guo and Ziheng Zeng and Wei Wang and Wentao Wu and Lijie Xu},
  year          = {2026},
  eprint        = {2606.29778},
  archivePrefix = {arXiv},
  primaryClass  = {cs.DB},
  doi           = {10.48550/arXiv.2606.29778},
  url           = {https://arxiv.org/abs/2606.29778}
}

License

Mandol is released under the Apache License 2.0. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mandol-0.1.0a1.tar.gz (333.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mandol-0.1.0a1-py3-none-any.whl (365.4 kB view details)

Uploaded Python 3

File details

Details for the file mandol-0.1.0a1.tar.gz.

File metadata

  • Download URL: mandol-0.1.0a1.tar.gz
  • Upload date:
  • Size: 333.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for mandol-0.1.0a1.tar.gz
Algorithm Hash digest
SHA256 72766b28d231cbe1ad0ca70cb1c25858b49dde101d85af1a47a817fcf34bdfb6
MD5 4b98d6817418267b6b10fbe62fd95bb6
BLAKE2b-256 acb98c3fa6ffc7e1028003b773a37b2135bbba32d9d2c72bebf7ae2674e9d947

See more details on using hashes here.

File details

Details for the file mandol-0.1.0a1-py3-none-any.whl.

File metadata

  • Download URL: mandol-0.1.0a1-py3-none-any.whl
  • Upload date:
  • Size: 365.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for mandol-0.1.0a1-py3-none-any.whl
Algorithm Hash digest
SHA256 529bfc3321f988e391c8b7a53ec728b1a8aa1f8564ea874c5c81c013c0e01195
MD5 2946730ced2db31b17caa01269a3909e
BLAKE2b-256 c1b5287ce901496349570e503a1483237e29ef15499438c72874778f7224b05e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page