Skip to content

Repository files navigation

AdaGraphRAG

Adaptive Hierarchical Graph-RAG with Incremental Construction and Self-Verified Retrieval

Extends Microsoft's GraphRAG — Edge, D., Trinh, H., Cheng, N., Bradley, J., Chao, A., Mody, A., Truitt, S., Metropolitansky, D., Ness, R. O., & Larson, J. (2024). From Local to Global: A Graph RAG Approach to Query-Focused Summarization. arXiv:2404.16130 — with three contributions:

# Problem Contribution
1 Every query pays for expensive global retrieval, even simple factual ones Adaptive Retrieval Router — classifies each query, picks the cheapest sufficient strategy (none / local / global / hybrid)
2 Full corpus re-indexed on every new document Incremental Graph Construction — new documents are entity-resolved and merged into the existing graph; only changed communities are re-summarized
3 No check that generated answers are grounded in evidence Self-Verification Layer — decomposes answers into claims, checks each against retrieved evidence, can trigger a second targeted retrieval pass on unsupported claims

Architecture

Indexing pipeline — turns a folder of documents into a queryable knowledge graph:

flowchart LR
    A[Documents<br/>.txt / .md] --> B[Chunking]
    B --> C[Entity + Relation<br/>Extraction]
    C --> D[Entity Resolution<br/>merge/create decisions]
    D --> E[(Neo4j<br/>Graph Store)]
    D --> F[(Qdrant<br/>Vector Store)]
    E --> G[Community Detection<br/>Leiden, multi-level]
    G --> H[Community<br/>Summarization]
    H --> E
Loading

Query pipeline — the three contributions in the flow they actually run in:

flowchart LR
    Q[User Query] --> R{Adaptive Router<br/>classifies complexity}
    R -->|no_retrieval| GEN[Generate Answer]
    R -->|simple_factoid| LOC[Local Retrieval<br/>Qdrant vector search]
    R -->|multi_hop| HYB[Hybrid Retrieval<br/>Local + Global]
    R -->|global_sensemaking| GLB[Global Retrieval<br/>Community summaries]
    LOC --> RANK[Fusion + Cross-Encoder<br/>Reranking]
    HYB --> RANK
    GLB --> RANK
    RANK --> GEN
    GEN --> VER{Self-Verification}
    VER -->|claim unsupported| RETRY[Targeted re-retrieval<br/>for that claim]
    RETRY --> VER
    VER -->|all claims checked| OUT[Answer +<br/>per-claim support labels]
Loading

Structure

src/adagraphrag/
├── domain/            entities, interfaces (no dependencies)
├── config/, logging_utils/, utils/
├── llm/, vectorstore/, graphstore/     infrastructure adapters
├── graph_construction/                 Contribution #2
├── retrieval/, reranking/               Contribution #1
├── verification/                         Contribution #3
├── pipeline/                              orchestration + DI factory
└── ui/                                     Streamlit app
tests/        unit + integration tests (offline, no real infra needed)
scripts/      seed_databases.py, build_graph.py, evaluate.py
configs/      YAML configuration

A typer CLI and a FastAPI service are natural next additions but aren't built yet — for now, indexing and querying go through scripts/build_graph.py and the Streamlit UI below.

Install

python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
pip install -e .
cp .env.example .env

Edit .env: set your LLM provider's API key (OPENAI_API_KEY or GEMINI_API_KEY), and NEO4J_PASSWORD if using Neo4j/Qdrant.

Using the Gemini free tier (click to expand)

Free API keys are capped on both requests-per-minute and requests-per-day, and the per-day cap can be surprisingly low — some newer preview-tier models have shipped with free daily caps as low as 20 requests, which graph construction can burn through indexing a handful of short documents. Check your key's actual current limits at aistudio.google.com under "Rate limits" before relying on any numbers here or elsewhere — they vary by model and change over time. This repo defaults to gemini-3.5-flash-lite, a stable (non-preview) Flash-Lite model, which has historically had one of the most generous free tiers — but verify that's still true for your key, and be aware Google periodically retires older model IDs for new API keys/projects, which will surface as a 404 naming its own replacement if it happens again.

Three things in this repo help stay under whatever caps you land on:

  • Indexing multiple documents through index_documents() (the Streamlit "Index uploaded files" button, or scripts/build_graph.py on a folder) re-detects and re-summarizes graph communities once for the whole batch, not once per document — upload/pass all your files together rather than one at a time.
  • llm.gemini.max_requests_per_minute in configs/llm_config.yaml paces every request (including embeddings) so a burst of calls doesn't trip the per-minute limit.
  • runtime.enable_llm_cache in configs/base.yaml caches every LLM call to disk, so re-running indexing/queries on text you've already processed doesn't re-spend the daily quota. Clear it with adagraphrag.utils.caching.clear_cache() if you need a fresh run.

If you still hit the daily cap, index a small corpus (a handful of short documents) rather than a large one — graph construction makes one LLM call per chunk for extraction plus more for entity resolution and community summaries, so it adds up fast on a ~25-100/day free quota.

Neo4j/Qdrant via Docker (click to expand)

If you're running them as Docker containers (docker start <qdrant-container> <neo4j-container>), give Neo4j a few seconds to finish booting before running anything — docker start returns immediately but Neo4j doesn't accept Bolt connections on :7687 right away. If you hit a connection error immediately after starting the containers, wait ~15s and retry, or tail docker logs -f <container> until you see Started..

Run

# tests (fully offline, no infra needed)
pytest tests/ --cov=adagraphrag

# one-time: create the Neo4j schema + Qdrant collection
python scripts/seed_databases.py

# index a folder of .txt/.md files
python scripts/build_graph.py data/raw/my_corpus/

# query + explore the graph
streamlit run src/adagraphrag/ui/streamlit_app.py

Evaluation

scripts/evaluate.py runs two small experiments against an already-indexed corpus. This is a toy-corpus sanity check, not a rigorous benchmark — no held-out dataset, no human-labeled gold answers, no statistical significance testing. Good enough to verify the two headline design decisions actually behave as intended.

python -c "from adagraphrag.utils.caching import clear_cache; print(clear_cache(), 'files deleted')"
python scripts/evaluate.py

Self-verification precision on injected claims

Fed the evidence matcher 3 true claims paraphrased from the indexed corpus and 3 deliberately false ones (facts not in the corpus, or contradicting it):

Claim Expected Predicted Correct
Aspirin can cause stomach irritation. true supported
Aspirin is generally not recommended for children due to Reye's syndrome risk. true supported
Ibuprofen is a nonsteroidal anti-inflammatory drug. true supported
Aspirin is proven to cure cancer. FALSE (injected) unsupported
Ibuprofen is recommended as the safest pain reliever during pregnancy. FALSE (injected) unsupported
Headaches are always caused by brain tumors and require immediate surgery. FALSE (injected) unsupported

Accuracy: 6/6 (100%) on this small claim set.

Adaptive routing vs. always-global

Compares chat-completion call count between the router's chosen strategy and a baseline forced to always use global retrieval (what plain GraphRAG does for every query), with self-verification excluded from both arms to isolate the router's own cost:

Query Strategy LLM calls Latency (s)
What is the capital of France? adaptive → none 2 4.0
What is the capital of France? always-global 1 14.4
What treats headaches? adaptive → none 2 2.3
How does aspirin's side effect risk compare to ibuprofen's? adaptive → hybrid 2 41.5
Summarize everything the corpus says about pain relief options. adaptive → global 2 14.5

On raw chat-completion call count, adaptive routing used more calls (8 vs. 4 total) — every strategy still needs one final generation call, and the router adds one classification call as fixed overhead. This metric is incomplete, though: it doesn't count embedding calls, and the always-global baseline pays for a full 57-community embedding batch (12-14s) on every query regardless of complexity — a cost the none and local strategies skip entirely, visible in the latency column even though it isn't reflected in the call count. A complete cost comparison would need to track embedding calls alongside chat completions; that's a known gap in this evaluation script rather than a claim that adaptive routing is unambiguously cheaper.

Bugs this evaluation surfaced

Worth stating plainly rather than burying: writing this eval script found three real bugs, since fixed — self_verifier.py: an evidence pool was leaking across claims (O(n²) prompt growth, 30-50s latency spikes on later claims); caching.py: clear_cache() used a non-recursive glob and silently missed the LLM response cache's subdirectory; and a Neo4j query pattern that triggered a cartesian-product warning on every relationship write. None affect correctness of the final answers, but the caching one made an earlier "clean" evaluation run silently invalid until caught.

Limitations & Future Work

  • Evaluation scope. The results above come from a 2-3 document toy corpus and a handful of hand-written test queries and injected claims — enough to sanity-check that the router and verifier behave as designed, not enough to make a statistically meaningful claim about accuracy or cost on real-world corpora. A proper evaluation would need a larger, held-out corpus (e.g. a subset of the datasets used in the original GraphRAG paper), more queries per complexity class, and a cost metric that includes embedding calls alongside chat completions (see the routing table above for why that gap matters).
  • Router calibration. During evaluation the router occasionally under-classified a query that the indexed corpus could directly answer (routing it to no_retrieval instead of local), which then caused the self-verification layer to spend many follow-up retrieval passes trying to find evidence for ungrounded claims. The router's confidence threshold or classifier prompt likely needs tuning against a labeled query set rather than relying on the LLM's zero-shot classification.
  • No CLI or API surface yet. Indexing and querying currently only go through scripts/build_graph.py and the Streamlit UI. A typer CLI and a FastAPI service (already anticipated in the code's layered architecture — pipelines are decoupled from the UI) are natural next steps for programmatic or production use.
  • Cross-encoder reranking is optional and untested at scale. It requires the sentence-transformers extra and hasn't been evaluated for whether it changes ranking quality on this project's corpora versus the fusion ranker alone.
  • Single-provider LLM testing. Development and evaluation were done against Gemini; the OpenAI and HuggingFace provider adapters exist in the codebase but haven't been run through the same evaluation.

License

MIT — see LICENSE.

About

Adaptive Graph-RAG extending Microsoft's GraphRAG with query-complexity routing, incremental graph updates, and self-verified retrieval to reduce cost and hallucination.

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages