Spaces:
Runtime error
Runtime error
File size: 5,172 Bytes
f8f02c0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | 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
|