v3.1 + 批次0: 智能回复重构基线 - ApprovalMatcher + 关键词降级 + 文档速修 + v4.0任务书面化

This commit is contained in:
Simon
2026-07-17 23:08:59 +08:00
parent 5a77a89ab1
commit 3ed86d5fb3
181 changed files with 19738 additions and 2655 deletions
+114 -12
View File
@@ -30,6 +30,26 @@ from app.models.neo4j_schema import ActionNode, IssueNode, RelationEdge
logger = logging.getLogger(__name__)
def _to_python_datetime(value: Any) -> Optional[datetime]:
"""将 Neo4j 返回的 datetime 转换为 Python datetime。
处理 Neo4j 的 neo4j.time.DateTime 类型,转换为 Python datetime。
如果无法转换或值为 None,返回 None。
"""
if value is None:
return None
# Neo4j 返回的是 neo4j.time.DateTime 对象
if hasattr(value, 'to_native'): # neo4j.time.DateTime
try:
return value.to_native()
except Exception:
return None
# 已经是 Python datetime
if isinstance(value, datetime):
return value
return None
class Neo4jClient:
"""Neo4j 图数据库异步客户端。
@@ -167,9 +187,9 @@ class Neo4jClient:
if self._driver is None:
return False
try:
# execute_read_query 已经返回数据列表,不需要再迭代
result = await self.execute_read_query("RETURN 1 AS ok")
records = [record async for record in result]
return len(records) > 0 and records[0].get("ok") == 1
return len(result) > 0 and result[0].get("ok") == 1
except Exception as e:
logger.warning(f"Neo4j 健康检查失败: {e}")
return False
@@ -290,8 +310,8 @@ class Neo4jClient:
uuid=record["uuid"],
name=record["name"],
category=record["category"],
created_at=record["created_at"],
updated_at=record["updated_at"],
created_at=_to_python_datetime(record["created_at"]),
updated_at=_to_python_datetime(record["updated_at"]),
source_suggestion_id=record.get("source_suggestion_id"),
)
@@ -344,8 +364,8 @@ class Neo4jClient:
uuid=record["uuid"],
name=record["name"],
category=record["category"],
created_at=record["created_at"],
updated_at=record["updated_at"],
created_at=_to_python_datetime(record["created_at"]),
updated_at=_to_python_datetime(record["updated_at"]),
source_suggestion_id=record.get("source_suggestion_id"),
)
@@ -375,8 +395,8 @@ class Neo4jClient:
uuid=record["uuid"],
name=record["name"],
category=record["category"],
created_at=record["created_at"],
updated_at=record["updated_at"],
created_at=_to_python_datetime(record["created_at"]),
updated_at=_to_python_datetime(record["updated_at"]),
source_suggestion_id=record.get("source_suggestion_id"),
)
@@ -409,8 +429,55 @@ class Neo4jClient:
uuid=record["uuid"],
name=record["name"],
category=record["category"],
created_at=record["created_at"],
updated_at=record["updated_at"],
created_at=_to_python_datetime(record["created_at"]),
updated_at=_to_python_datetime(record["updated_at"]),
source_suggestion_id=record.get("source_suggestion_id"),
)
for record in data
]
async def find_issues_by_keyword(
self, keyword: str, limit: int = 10
) -> List[IssueNode]:
"""根据关键词模糊搜索Issue节点(图谱查询核心方法)
使用 Cypher CONTAINS 进行模糊匹配,支持中文关键词搜索。
Args:
keyword: 搜索关键词
limit: 返回结果数量限制,默认10
Returns:
List[IssueNode]: 匹配的Issue节点列表
"""
if not keyword or not keyword.strip():
return []
# 修复:只使用正确的 CONTAINS 语法
# Neo4j CONTAINS: i.name CONTAINS $keyword 表示 Issue名称 包含 关键词
keywords = keyword.strip()
# 单向匹配:Issue名称包含关键词("打印机驱动" 匹配 "打印机驱动安装")
cypher_where = "i.name CONTAINS $keyword"
data = await self.execute_read_query(
f"""
MATCH (i:Issue)
WHERE {cypher_where}
RETURN i.uuid AS uuid, i.name AS name, i.category AS category,
i.created_at AS created_at, i.updated_at AS updated_at,
i.source_suggestion_id AS source_suggestion_id
LIMIT $limit
""",
params={"keyword": keywords, "limit": limit},
)
return [
IssueNode(
uuid=record["uuid"],
name=record["name"],
category=record["category"],
created_at=_to_python_datetime(record["created_at"]),
updated_at=_to_python_datetime(record["updated_at"]),
source_suggestion_id=record.get("source_suggestion_id"),
)
for record in data
@@ -450,7 +517,7 @@ class Neo4jClient:
uuid=record["uuid"],
name=record["name"],
description=record.get("description", ""),
created_at=record["created_at"],
created_at=_to_python_datetime(record["created_at"]),
source_suggestion_id=record.get("source_suggestion_id"),
)
@@ -494,10 +561,45 @@ class Neo4jClient:
uuid=record["uuid"],
name=record["name"],
description=record.get("description", ""),
created_at=record["created_at"],
created_at=_to_python_datetime(record["created_at"]),
source_suggestion_id=record.get("source_suggestion_id"),
)
async def find_actions_by_issue(
self, issue_uuid: str, limit: int = 5
) -> List[ActionNode]:
"""根据Issue UUID查找关联的Action解决方案(图谱查询核心方法)
查询与指定Issue节点通过LEADS_TO关系关联的Action解决方案。
Args:
issue_uuid: Issue节点的uuid
limit: 返回结果数量限制,默认5
Returns:
List[ActionNode]: 关联的Action节点列表
"""
data = await self.execute_read_query(
"""
MATCH (i:Issue {uuid: $issue_uuid})-[r:HAS_ACTION|LEADS_TO]->(a:Action)
RETURN a.uuid AS uuid, a.name AS name,
a.description AS description, a.created_at AS created_at,
a.source_suggestion_id AS source_suggestion_id
LIMIT $limit
""",
params={"issue_uuid": issue_uuid, "limit": limit},
)
return [
ActionNode(
uuid=record["uuid"],
name=record["name"],
description=record.get("description", ""),
created_at=_to_python_datetime(record["created_at"]),
source_suggestion_id=record.get("source_suggestion_id"),
)
for record in data
]
# --------------------------------------------------------------------------
# 图关系 CRUD — RelationEdge
# --------------------------------------------------------------------------