# How Cypher Query Generation from Natural Language Works in Code‑Graph‑RAG

> Discover how Code-Graph-RAG generates Cypher queries from natural language. This LLM pipeline ensures schema compliance and prevents destructive operations for safe code analysis.

- Repository: [Vitali Avagyan/code-graph-rag](https://github.com/vitali87/code-graph-rag)
- Tags: how-to-guide
- Published: 2026-08-13

---

**Code‑Graph‑RAG converts natural‑language questions about codebases into safe, read‑only Neo4j Cypher queries using a controlled LLM pipeline that enforces schema compliance and blocks destructive operations.**

Code‑Graph‑RAG is an open‑source retrieval‑augmented generation system that bridges human language and property graph databases for code analysis. Understanding **Cypher query generation from natural language** reveals a carefully engineered pipeline that balances LLM flexibility with strict safety constraints. The implementation centers on the `CypherGenerator` class within the [`codebase_rag/services/llm.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/services/llm.py) module.

## The Seven‑Step Cypher Generation Pipeline

The transformation from user question to executable query follows a rigorous sequence designed to prevent hallucinations and security risks.

### Schema‑Aware System Prompt Construction

Every generation begins with `build_cypher_system_prompt()` located in [`codebase_rag/prompts.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/prompts.py) (lines 54–62). This function assembles a comprehensive prompt embedding `GRAPH_SCHEMA_DEFINITION`, `CYPHER_QUERY_RULES`, and concrete examples drawn from [`codebase_rag/cypher_queries.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/cypher_queries.py). The prompt instructs the model on project scoping, query optimization, and the specific node and relationship types available in the code graph.

### LLM Agent Initialization

The `CypherGenerator.__init__` method in [`codebase_rag/services/llm.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/services/llm.py) loads the active model configuration and instantiates a provider‑specific backend via `_create_provider_model`. It then creates a `pydantic_ai.Agent` bound to the system prompt with an output type constrained to `str`, ensuring the LLM emits only raw Cypher without markdown explanations or conversational filler.

### Query Generation and Basic Validation

The `generate()` coroutine submits the natural‑language query to the LLM using `await self.agent.run(natural_language_query)`. Immediately upon receiving the response, the system checks for the mandatory `CYPHER_MATCH_KEYWORD` (the string `"MATCH"`). If this keyword is absent, the pipeline raises `LLMGenerationError` and halts execution before any further processing occurs.

### Response Cleaning

Raw LLM outputs often contain markdown fences or explanatory text. The `_clean_cypher_response()` method strips backticks, removes stray `"Cypher:"` prefixes, and ensures the query terminates with a semicolon. This produces a normalized string suitable for validation and execution.

### Multi‑Layer Safety Validation

Before returning the query, three sequential validators enforce security boundaries:

- **`_validate_cypher_read_only`** (lines 80–87): Scans for destructive keywords such as `DELETE`, `MERGE`, `CREATE`, and `DROP` defined in the constants module. Any match triggers an immediate `LLMGenerationError`.
- **`_validate_no_unbounded_paths`** (lines 89–98): Rejects variable‑length patterns like `()-[*]->()` that lack explicit upper bounds, preventing performance‑degrading traversals on large code graphs.
- **`_validate_call_procedures`** (lines 100–108): Whitelists allowed Neo4j procedure prefixes, ensuring the LLM cannot invoke unauthorized or sensitive database operations.

Only after passing all three checks is the query deemed safe for execution.

### Project Scoping and Final Return

The generator respects the `active_projects` parameter passed during initialization, embedding project‑specific constraints into the system prompt. After logging the validated query, `generate()` returns the cleaned Cypher string to the caller, typically a higher‑level orchestrator such as `create_rag_orchestrator`.

## Practical Implementation

Below is a complete example demonstrating the pipeline:

```python
import asyncio
from codebase_rag.services.llm import CypherGenerator

async def demo():
    # Initialize with optional project scoping

    gen = CypherGenerator(active_projects=["myproject"])
    
    # Natural language input

    nl_query = "How many Python classes define a method called 'run'?"
    
    # Generate validated Cypher

    cypher = await gen.generate(nl_query)
    print("Generated Cypher:", cypher)

asyncio.run(demo())

```

**Typical output:**

```text
Generated Cypher: MATCH (c:Class)-[:DEFINES_METHOD]->(m:Function)
WHERE m.name = 'run' AND c.language = 'python'
RETURN count(c) AS total;

```

The resulting query is guaranteed to be read‑only, schema‑compliant, and bounded by the safety validators described above.

## Summary

- **Schema‑Embedded Prompting**: The `build_cypher_system_prompt()` function in [`codebase_rag/prompts.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/prompts.py) grounds the LLM in the exact graph structure and query constraints.
- **Constrained Generation**: `CypherGenerator` uses `pydantic_ai.Agent` with string output types to eliminate conversational drift and markdown artifacts.
- **Mandatory Keyword Validation**: Every response must contain the `MATCH` keyword before proceeding to cleaning and safety checks.
- **Triple Safety Layer**: Read‑only verification, unbounded path detection, and procedure whitelisting ensure the graph remains secure and performant.
- **Project Isolation**: The `active_projects` parameter injects scoping rules that restrict queries to specific repositories.

## Frequently Asked Questions

### What prevents the LLM from generating destructive Cypher queries?

The `_validate_cypher_read_only` method in [`codebase_rag/services/llm.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/services/llm.py) scans generated queries against a blacklist of dangerous keywords including `DELETE`, `MERGE`, `CREATE`, and `DROP`. Any occurrence raises `LLMGenerationError`, ensuring the pipeline remains read‑only regardless of the LLM’s output.

### How does the system prevent performance issues from unbounded traversals?

The `_validate_no_unbounded_paths` validator specifically detects variable‑length path patterns without explicit upper bounds (e.g., `-[*]->`). Such patterns are rejected immediately to prevent the Neo4j engine from executing expensive traversals across large code graphs.

### Which files contain the core Cypher generation logic?

The primary implementation resides in [`codebase_rag/services/llm.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/services/llm.py), which defines `CypherGenerator` and all validation methods. Prompt construction lives in [`codebase_rag/prompts.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/prompts.py), while [`codebase_rag/cypher_queries.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/cypher_queries.py) supplies few‑shot examples. Security constants are centralized in `codebase_rag/constants/*.py`.

### Can I restrict queries to specific code repositories?

Yes. Pass a list of project identifiers to the `active_projects` parameter when instantiating `CypherGenerator`. The system prompt automatically includes scoping rules that instruct the LLM to append `WHERE project = 'identifier'` clauses, enforcing multi‑tenant isolation at the query level.