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

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 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 (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. 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 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:

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:

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 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 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, which defines CypherGenerator and all validation methods. Prompt construction lives in codebase_rag/prompts.py, while 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →