kgrag/graph_search/cypher_fixer.py
2026-06-30 13:35:52 +08:00

241 lines
9.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Cypher 错误修复模块
功能:修复常见的 Cypher 语法错误和格式问题
"""
import re
import logging
logger = logging.getLogger(__name__)
def parse_cypher_error(error_message: str, cypher: str) -> dict:
"""
解析Cypher错误信息提取错误类型、位置等详细信息
"""
error_info = {
"error_type": "unknown",
"error_message": error_message,
"error_position": None,
"suggested_fix": None
}
error_lower = error_message.lower()
# ========== 识别错误类型 ==========
if "unexpected" in error_lower or "expected" in error_lower:
error_info["error_type"] = "syntax_unexpected"
elif "invalid" in error_lower:
error_info["error_type"] = "syntax_invalid"
elif "missing" in error_lower:
error_info["error_type"] = "syntax_missing"
elif "cannot" in error_lower or "can't" in error_lower:
error_info["error_type"] = "semantic_error"
elif "unknown" in error_lower or "not found" in error_lower:
error_info["error_type"] = "schema_error"
elif "syntax" in error_lower:
error_info["error_type"] = "syntax_error"
# ========== 提取错误位置(如果错误信息中包含位置信息) ==========
# Neo4j错误信息可能包含位置如 "line 1, column 10"
position_match = re.search(r'line\s+(\d+)[,\s]+column\s+(\d+)', error_message, re.IGNORECASE)
if position_match:
line_num = int(position_match.group(1))
col_num = int(position_match.group(2))
error_info["error_position"] = {"line": line_num, "column": col_num}
# 提取错误位置的上下文
lines = cypher.split('\n')
if line_num <= len(lines):
error_line = lines[line_num - 1]
error_info["error_context"] = error_line
if col_num <= len(error_line):
# 标记错误位置
marker = ' ' * (col_num - 1) + '^'
error_info["error_marker"] = marker
# ========== 提取错误相关的关键词 ==========
# 提取错误信息中提到的标签、关系、关键字等
keywords = []
# 匹配可能的标签名(大写字母开头的单词)
label_matches = re.findall(r'\b([A-Z][a-zA-Z0-9_]+)\b', error_message)
keywords.extend(label_matches)
# 匹配引号中的内容
quoted_matches = re.findall(r'["\']([^"\']+)["\']', error_message)
keywords.extend(quoted_matches)
error_info["keywords"] = list(set(keywords))
return error_info
def force_undirected_relationships(cypher: str) -> str:
"""
强制将所有有方向的关系转换为无方向关系
"""
if not cypher:
return cypher
# 替换所有有方向的关系为无方向关系
# 匹配模式:-[:RELATIONSHIP]-> 或 <-[:RELATIONSHIP]-
# 替换为:-[:RELATIONSHIP]-
# 替换 -> 为 -(在关系类型之后)
# 需要小心处理,只替换关系箭头,不替换其他地方的 ->
# 使用正则表达式匹配关系模式
fixed = cypher
# 替换 -[:RELATIONSHIP]-> 为 -[:RELATIONSHIP]-
fixed = re.sub(r'(\[:[^\]]+\])->', r'\1-', fixed)
# 替换 <-[:RELATIONSHIP]- 为 -[:RELATIONSHIP]-
fixed = re.sub(r'<-(\[:[^\]]+\])-', r'-\1-', fixed)
# 替换 -> 在关系模式中(处理可变长度关系,如 [*1..3]->
fixed = re.sub(r'(\[[^\]]+\])->', r'\1-', fixed)
# 替换 <- 在关系模式中(处理可变长度关系,如 <-[*1..3]-
fixed = re.sub(r'<-(\[[^\]]+\])-', r'-\1-', fixed)
# 如果进行了替换,记录日志
if fixed != cypher:
logger.info(f"[关系方向转换] 检测到有方向关系,已自动转换为无方向关系")
logger.debug(f"[关系方向转换] 原始: {cypher[:200]}...")
logger.debug(f"[关系方向转换] 转换后: {fixed[:200]}...")
return fixed
def fix_cypher_based_on_error(cypher: str, error_info: dict) -> str:
"""
基于实际错误信息智能修复Cypher
"""
if not error_info or not error_info.get("error_type"):
return None
error_type = error_info["error_type"]
error_message = error_info.get("error_message", "")
fixed = cypher
# ========== 根据错误类型进行修复 ==========
# 1. 语法错误:多余的括号
if "unexpected" in error_message.lower() and ("(" in error_message or ")" in error_message):
# 尝试修复多余的括号
# 匹配 `-[:REL]- ( (n:Label)` 这种模式
fixed = re.sub(r'(\[:[^\]]+\])-\s+\(\s*\(', r'\1-(', fixed)
fixed = re.sub(r'\)\s*-\s*(\[:[^\]]+\])-\s+\(\s*\(', r')-\1-(', fixed)
if fixed != cypher:
logger.info(f"[智能修复] 修复多余括号错误")
return fixed
# 2. 语法错误:缺少括号
if "missing" in error_message.lower() and ("(" in error_message or ")" in error_message):
# 检查括号是否匹配
open_count = fixed.count('(')
close_count = fixed.count(')')
if open_count > close_count:
# 缺少右括号,在末尾添加
fixed = fixed + ')' * (open_count - close_count)
logger.info(f"[智能修复] 添加缺失的右括号: {open_count - close_count}")
return fixed
elif close_count > open_count:
# 多余的右括号但这种情况较难自动修复返回None
logger.warning(f"[智能修复] 检测到多余的右括号,无法自动修复")
return None
# 3. 语法错误:关系定义问题
if "relationship" in error_message.lower() or "relationship type" in error_message.lower():
# 检查关系定义格式
# 修复 `-[:REL]- (n:Label)` -> `-[:REL]-(n:Label)`
fixed = re.sub(r'(\[:[^\]]+\])-\s+\(', r'\1-(', fixed)
if fixed != cypher:
logger.info(f"[智能修复] 修复关系定义格式")
return fixed
# 4. Schema错误节点标签或关系类型不存在
if error_info["error_type"] == "schema_error":
keywords = error_info.get("keywords", [])
# 这里可以尝试其他修复策略
logger.debug(f"[智能修复] Schema错误关键词: {keywords}")
# Schema错误通常需要LLM修正这里不自动修复
return None
# 5. 语法错误:位置相关的修复
if error_info.get("error_position"):
pos = error_info["error_position"]
line_num = pos.get("line", 1)
col_num = pos.get("column", 1)
lines = fixed.split('\n')
if line_num <= len(lines):
error_line = lines[line_num - 1]
# 检查错误位置附近的字符
if col_num <= len(error_line):
char_at_error = error_line[col_num - 1] if col_num > 0 else ''
context = error_line[max(0, col_num-10):min(len(error_line), col_num+10)]
# 如果是多余的字符,尝试删除
if char_at_error in ['(', ')', ' ', '-']:
# 检查是否是明显的多余字符
before = error_line[:col_num-1] if col_num > 0 else ''
after = error_line[col_num:] if col_num < len(error_line) else ''
# 如果前后都是关系连接符,中间的字符可能是多余的
if re.search(r'[-\s]*$', before) and re.search(r'^[-\s]*', after):
# 尝试删除错误位置的字符
new_line = error_line[:col_num-1] + error_line[col_num:]
lines[line_num - 1] = new_line
fixed = '\n'.join(lines)
logger.info(f"[智能修复] 删除错误位置的字符: '{char_at_error}'")
return fixed
# 6. 通用修复:尝试修复常见的格式问题
# 修复关系后的多余空格
fixed = re.sub(r'(\[:[^\]]+\])-\s+\(', r'\1-(', fixed)
# 修复节点定义前的多余括号
fixed = re.sub(r'\(\s+\(([a-zA-Z_][a-zA-Z0-9_]*\s*:)', r'(\1', fixed)
if fixed != cypher:
logger.info(f"[智能修复] 应用通用修复")
return fixed
# 无法自动修复
return None
def fix_common_cypher_errors(cypher: str) -> str:
"""
修复常见的Cypher语法错误
"""
if not cypher:
return cypher
fixed = cypher
# ========== 修复1关系后多余的空格和括号 ==========
# 匹配模式:`-[:REL]- ( (n:Label)` -> `-[:REL]-(n:Label)`
# 或者 `-[:REL]- (n:Label)` -> `-[:REL]-(n:Label)`
fixed = re.sub(r'(\[:[^\]]+\])-\s+\(\s*\(', r'\1-(', fixed) # `-[:REL]- ( (` -> `-[:REL]-(`
fixed = re.sub(r'(\[:[^\]]+\])-\s+\(([a-zA-Z_][a-zA-Z0-9_]*\s*:)', r'\1-(\2', fixed) # `-[:REL]- (n:` -> `-[:REL]-(n:`
# ========== 修复2节点定义前多余的括号 ==========
# 匹配模式:`( (n:Label)` -> `(n:Label)`
fixed = re.sub(r'\(\s+\(([a-zA-Z_][a-zA-Z0-9_]*\s*:)', r'(\1', fixed)
# ========== 修复3MATCH语句中的多余空格和括号 ==========
# 匹配模式:`MATCH ...)-[:REL]- ( (n:Label)` -> `MATCH ...)-[:REL]-(n:Label)`
fixed = re.sub(r'\)\s*-\s*(\[:[^\]]+\])-\s+\(\s*\(', r')-\1-(', fixed)
fixed = re.sub(r'\)\s*-\s*(\[:[^\]]+\])-\s+\(([a-zA-Z_][a-zA-Z0-9_]*\s*:)', r')-\1-(\2', fixed)
# ========== 修复4规范化关系定义后的空格 ==========
# 确保关系定义后直接跟节点定义,如 `-[:REL]-(n:Label)` 而不是 `-[:REL]- (n:Label)`
fixed = re.sub(r'(\[:[^\]]+\])-\s+\(', r'\1-(', fixed)
if fixed != cypher:
logger.info(f"[Cypher修复] 检测到并修复了常见语法错误")
logger.debug(f"[Cypher修复] 原始: {cypher[:200]}...")
logger.debug(f"[Cypher修复] 修复后: {fixed[:200]}...")
return fixed