Spaces:
Runtime error
Runtime error
| 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, | |
| ) | |
| 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, | |
| ) | |
| 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 | |
| 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 | |