prediqai / RULE /query_engine /query_executor.py
ganesh-vilje's picture
Deploy to Hugging Face Main
f8f02c0
Raw
History Blame Contribute Delete
5.17 kB
from __future__ import annotations
"""
query_engine/query_executor.py - QueryWeaver: NL → Cypher → FalkorDB execution.
Responsibility:
1. Build an LLM prompt using the graph schema description.
2. Generate a Cypher query.
3. Validate it is read-only (block DELETE / DROP / SET / REMOVE / MERGE writes).
4. Execute against FalkorDB and return structured results.
"""
import logging
import re
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from RULE.query_engine.falkordb_client import FalkorDBClient
from RULE.query_engine.schema_manager import SchemaManager
logger = logging.getLogger(__name__)
__all__ = ["ExecutionResult", "QueryExecutor"]
# ---------------------------------------------------------------------------
# Safety: patterns that must NOT appear in generated Cypher
# ---------------------------------------------------------------------------
_BLOCKED_PATTERNS = re.compile(
r"\b(DELETE|DROP|SET\s+\w|REMOVE|CREATE|MERGE|DETACH\s+DELETE)\b",
re.IGNORECASE,
)
@dataclass
class ExecutionResult:
"""Result of a QueryExecutor.execute() call."""
success: bool
cypher: str
rows: List[Dict[str, Any]] = field(default_factory=list)
error: Optional[str] = None
graph_name: str = ""
def row_count(self) -> int:
return len(self.rows)
class QueryExecutor:
"""
Translates NL → Cypher and executes against FalkorDB.
Args:
client: Connected (or gracefully degraded) FalkorDBClient.
schema_manager: SchemaManager instance for schema-aware prompts.
"""
def __init__(self, client: Optional[FalkorDBClient], schema_manager: SchemaManager) -> None:
self._client = client
self._schema_manager = schema_manager
def execute(self, nl_query: str, graph_name: str) -> ExecutionResult:
"""
Execute nl_query as a Cypher query against graph_name.
Pipeline:
1. Build schema-aware LLM prompt.
2. Generate Cypher.
3. Validate safety.
4. Run against FalkorDB.
Returns:
ExecutionResult — always returned; check success flag.
"""
schema_desc = self._schema_manager.describe_schema(graph_name)
cypher = self._generate_cypher(nl_query, schema_desc, graph_name)
if not cypher:
return ExecutionResult(
success=False,
cypher="",
error="LLM did not generate a valid Cypher query.",
graph_name=graph_name,
)
logger.debug("[QueryExecutor] Generated Cypher:\n%s", cypher)
if not self._validate_cypher(cypher):
logger.warning("[QueryExecutor] Cypher blocked by safety validator: %s", cypher)
return ExecutionResult(
success=False,
cypher=cypher,
error=(
"Generated Cypher contains write operations "
"(DELETE/DROP/SET/CREATE/MERGE). Only read queries are permitted."
),
graph_name=graph_name,
)
if self._client is None or not self._client.is_connected():
return ExecutionResult(
success=False,
cypher=cypher,
error="FalkorDB is not connected. Start FalkorDB and retry.",
graph_name=graph_name,
)
rows = self._client.execute_cypher(cypher, graph_name=graph_name)
return ExecutionResult(
success=True,
cypher=cypher,
rows=rows,
graph_name=graph_name,
)
@staticmethod
def _call_llm(prompt: str) -> str:
"""Invoke LLM via RULE's call_gemini."""
try:
from pandas_rule import call_gemini
return call_gemini(prompt)
except Exception as exc:
logger.debug("[QueryExecutor] LLM call failed: %s", exc)
return ""
def _generate_cypher(self, nl_query: str, schema_desc: str, graph_name: str) -> str:
"""Ask the LLM to generate a Cypher query for nl_query."""
prompt = f"""You are a Cypher query generator for FalkorDB.
Graph Schema:
{schema_desc}
User request: "{nl_query}"
Rules:
- Generate ONLY a single valid Cypher query.
- Use only MATCH and RETURN clauses (read-only).
- Do NOT use DELETE, DROP, SET, CREATE, MERGE, or REMOVE.
- Do NOT wrap the query in markdown code fences.
- Do NOT add any explanation. Output ONLY the Cypher query.
Example:
User: "Which rules depend on AML_RULE?"
Cypher: MATCH (r:Rule)-[:DEPENDS_ON]->(p:Rule {{name:'AML_RULE'}}) RETURN r.name, r.severity
Cypher:"""
raw = self._call_llm(prompt).strip()
raw = re.sub(r"```(?:cypher)?", "", raw, flags=re.IGNORECASE).strip()
raw = raw.strip("`").strip()
raw = raw.split(";")[0].strip()
return raw
@staticmethod
def _validate_cypher(cypher: str) -> bool:
"""Return True if the Cypher query is safe (read-only)."""
if not cypher or not cypher.strip():
return False
return _BLOCKED_PATTERNS.search(cypher) is None