# How Code-Graph-RAG Performs Natural Language Querying: From English to Cypher

> Discover how Code-Graph-RAG enables natural language querying by transforming English questions into executable Cypher queries with an LLM pipeline and Memgraph.

- Repository: [Vitali Avagyan/code-graph-rag](https://github.com/vitali87/code-graph-rag)
- Tags: deep-dive
- Published: 2026-08-11

---

**Code-Graph-RAG converts English questions into executable Cypher queries by orchestrating an LLM-driven generation pipeline, safety-validated execution against Memgraph, and rich console rendering.**

Code-Graph-RAG (CG-RAG) is an open-source tool that enables **natural language querying** of software repositories by translating free-form questions into precise knowledge graph traversals. According to the vitali87/code-graph-rag source code, the system implements a three-stage pipeline that bridges the gap between human intent and structured graph data without requiring manual Cypher syntax knowledge.

## The Natural Language Querying Pipeline

The natural language querying capability relies on three tightly integrated stages that transform user input into tabular results.

### Stage 1: LLM-Driven Cypher Generation

When a user submits a question, the `CypherGenerator` class in [`codebase_rag/services/llm.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/services/llm.py) wraps a large language model and prompts it with repository context. The LLM receives either the *local cypher system prompt* or the generic *cypher system prompt*, instructing it to output a Cypher query that retrieves the requested information from the knowledge graph.

The raw LLM response undergoes cleaning via `_clean_cypher_response`, which strips markdown fences, stray asterisks, and ensures a trailing semicolon. This produces sanitized Cypher ready for validation.

### Stage 2: Safety Validation and Query Execution

Before touching the database, CG-RAG enforces strict safety constraints. The system runs three validation checks:

- **`_validate_cypher_read_only`**: Ensures no write operations (CREATE, DELETE, SET, etc.) are present
- **`_validate_no_unbounded_paths`**: Prevents queries that could scan the entire graph without limits
- **`_validate_call_procedures`**: Restricts procedure calls to allowed prefixes defined in [`codebase_rag/constants/core.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/constants/core.py)

After validation, the query executes against a `QueryProtocol` implementation (typically Memgraph) inside an asynchronous `asyncio.wait_for` block. The execution respects `settings.QUERY_TIMEOUT_S` and caps results at `settings.QUERY_RESULT_ROW_CAP`. Results are then token-truncated via `truncate_results_by_tokens` to maintain manageable LLM context windows.

### Stage 3: Rich Console Rendering

Final results flow through `create_query_tool` in [`codebase_rag/tools/codebase_query.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/tools/codebase_query.py), which formats the data into a `rich.Table` displayed inside a `Panel` titled **QUERY_RESULTS_PANEL_TITLE**. If truncation occurred, the system emits `QUERY_SUMMARY_TRUNCATED` detailing kept rows, total rows, and token usage. Successful queries display `QUERY_SUMMARY_SUCCESS`.

## Key Implementation Files

Understanding the natural language querying architecture requires familiarity with these specific modules:

- **[`codebase_rag/services/llm.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/services/llm.py)**: Houses `CypherGenerator` and `create_rag_orchestrator`, which coordinate LLM interaction and agent assembly
- **[`codebase_rag/tools/codebase_query.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/tools/codebase_query.py)**: Contains `create_query_tool`, wrapping the query execution into a Tool interface for the orchestrator
- **[`codebase_rag/services/__init__.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/services/__init__.py)**: Defines `QueryProtocol`, the abstract interface for graph database operations
- **[`codebase_rag/constants/core.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/constants/core.py)**: Stores forbidden Cypher keywords and allowed procedure prefixes for validation
- **[`codebase_rag/cli.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/cli.py)**: Entry point that parses arguments and invokes the query orchestrator

## Practical Implementation Example

The following programmatic example demonstrates the complete natural language querying flow without the interactive CLI:

```python
import asyncio
from pathlib import Path
from codebase_rag.services.llm import CypherGenerator, create_rag_orchestrator
from codebase_rag.tools.codebase_query import create_query_tool
from codebase_rag.services import QueryProtocol
from rich.console import Console

# 1️⃣ Initialise the Cypher‑generating LLM

cypher_gen = CypherGenerator(active_projects=["my‑project"])

# 2️⃣ Initialise a QueryProtocol (e.g. Memgraph client)

ingestor: QueryProtocol = ...  # obtain a concrete implementation

# 3️⃣ Build the query tool that the orchestrator can invoke

query_tool = create_query_tool(ingestor, cypher_gen, console=Console())

# 4️⃣ Assemble the RAG orchestrator with the tool

agent, _ = create_rag_orchestrator(tools=[query_tool])

# 5️⃣ Ask a natural‑language question

nl_query = "Which functions call `fetch_data` and are not used elsewhere?"
result = asyncio.run(agent.run(nl_query))

# `result` is a `QueryGraphData` instance containing the generated Cypher,

# the raw rows from Memgraph, and a human‑readable summary.

print(result.summary)

```

Running this pipeline sends the English question to the LLM, receives a Cypher query such as `MATCH (f:Function)-[:CALLS]->(c:Function {name: "fetch_data"}) RETURN f.name, f.file`, validates it for safety, executes against Memgraph, and renders a formatted table of matching functions.

## Summary

- **Code-Graph-RAG enables natural language querying** by chaining LLM-based Cypher generation, safety validation, and Memgraph execution.
- **The `CypherGenerator` class** in [`codebase_rag/services/llm.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/services/llm.py) handles prompt management, response cleaning, and read-only validation.
- **Three safety checks** (`_validate_cypher_read_only`, `_validate_no_unbounded_paths`, `_validate_call_procedures`) prevent dangerous queries before execution.
- **Results are automatically truncated** by both row count (`QUERY_RESULT_ROW_CAP`) and token count to protect downstream context limits.
- **`create_query_tool`** wraps the entire pipeline into a Tool interface consumable by the RAG orchestrator built via `create_rag_orchestrator`.

## Frequently Asked Questions

### How does Code-Graph-RAG prevent malicious Cypher queries?

The system implements multi-layer validation in `CypherGenerator` that scans for forbidden keywords (CREATE, DELETE, DROP, etc.), unbounded path patterns, and unauthorized procedure calls. Only queries passing all three checks (`_validate_cypher_read_only`, `_validate_no_unbounded_paths`, `_validate_call_procedures`) execute against the Memgraph instance, ensuring read-only, bounded operations.

### What happens if a natural language query generates invalid Cypher?

If the LLM returns malformed Cypher or violates safety constraints, the validation layer rejects the query before execution. The `_clean_cypher_response` method handles formatting issues like markdown fences, but semantic errors or policy violations trigger early termination with an error message, preventing database errors or unsafe operations.

### Can I use Code-Graph-RAG with graph databases other than Memgraph?

Yes, through the `QueryProtocol` abstraction defined in [`codebase_rag/services/__init__.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/services/__init__.py). While the reference implementation targets Memgraph, any class implementing this protocol can substitute the graph backend. The `create_query_tool` function accepts any `QueryProtocol` instance, decoupling the orchestration logic from specific database implementations.

### How does the system handle large query results?

Results undergo dual truncation: first by row count using `settings.QUERY_RESULT_ROW_CAP`, then by token count via `truncate_results_by_tokens`. This keeps the result set manageable for both console display and subsequent LLM processing. When truncation occurs, the `QUERY_SUMMARY_TRUNCATED` message reports the final statistics to the user.