kgrag/graph_search/result_formatter.py
2026-07-29 18:10:19 +08:00

808 lines
33 KiB
Python
Raw 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.

"""
结果格式化模块
功能:从 Neo4j 查询结果中提取节点和路径信息
"""
import logging
import re
from typing import Dict, List, Any, Optional, Set
logger = logging.getLogger(__name__)
EXCLUDED_RESULT_NODE_LABELS = {"维修工作", "操作程序", "操作使用"}
_FILTERED_OUT = object()
class SimpleNode:
"""
简单的Node模拟类用于将节点数据包装为Neo4j Node对象格式降级策略
"""
def __init__(self, node_data: dict, labels: List[str], node_id: str):
"""
Args:
node_data: 节点数据字典
labels: 节点标签列表
node_id: 节点ID
"""
self._data = node_data
self._labels = labels
self._node_id = node_id
@property
def element_id(self):
"""返回节点ID"""
return self._node_id
@property
def id(self):
"""返回节点ID兼容旧版本"""
return self._node_id
@property
def labels(self):
"""返回节点标签"""
return self._labels
def get(self, key, default=None):
"""获取节点属性"""
return self._data.get(key, default)
def __iter__(self):
"""使节点可以转换为字典"""
return iter(self._data.items())
def __getitem__(self, key):
"""使节点可以像字典一样访问"""
return self._data[key]
class SimplePath:
"""
简单的Path模拟类用于将节点包装为path结构降级策略
包含nodes和relationships属性其中relationships为空列表
"""
def __init__(self, nodes: List):
"""
Args:
nodes: 节点列表SimpleNode对象列表
"""
self.nodes = nodes
self.relationships = [] # 降级策略中,只有节点,没有关系
# def extract_knowledge_source(knowledge_source_value: Any) -> List[Dict[str, str]]:
# """
# 从节点属性中的 knowledge_source 字段提取信息并格式化为标准格式
#
# Args:
# knowledge_source_value: 节点属性中的 knowledge_source 值(可能是字符串、字典、列表等)
#
# Returns:
# List[Dict]: 格式化后的 knowledge_source 列表,格式为 [{"filename":"","url":"","info":""}]
# """
# if knowledge_source_value is None:
# return [{"filename": "", "url": "", "info": ""}]
#
# result = []
#
# # 如果是字符串尝试解析为JSON或直接使用
# if isinstance(knowledge_source_value, str):
# # 尝试解析JSON字符串
# try:
# import json
# parsed = json.loads(knowledge_source_value)
# if isinstance(parsed, list):
# knowledge_source_value = parsed
# elif isinstance(parsed, dict):
# knowledge_source_value = [parsed]
# else:
# # 如果不是JSON将字符串作为info
# return [{"filename": "", "url": "", "info": knowledge_source_value}]
# except:
# # 解析失败将字符串作为info
# return [{"filename": "", "url": "", "info": knowledge_source_value}]
#
# # 如果是字典,转换为列表
# if isinstance(knowledge_source_value, dict):
# knowledge_source_value = [knowledge_source_value]
#
# # 如果是列表,处理每个元素
# if isinstance(knowledge_source_value, list):
# for item in knowledge_source_value:
# if isinstance(item, dict):
# # 从字典中提取字段
# formatted_item = {
# "filename": str(item.get("filename", item.get("file_name", item.get("file", "")))),
# "url": str(item.get("url", item.get("link", item.get("uri", "")))),
# "info": str(item.get("info", item.get("information", item.get("description", ""))))
# }
# result.append(formatted_item)
# elif isinstance(item, str):
# # 如果是字符串作为info
# result.append({"filename": "", "url": "", "info": item})
# else:
# # 其他类型转换为字符串作为info
# result.append({"filename": "", "url": "", "info": str(item)})
# else:
# # 其他类型转换为字符串作为info
# result.append({"filename": "", "url": "", "info": str(knowledge_source_value)})
#
# # 如果没有结果,返回默认值
# if not result:
# result = [{"filename": "", "url": "", "info": ""}]
#
# return result
def extract_knowledge_source(knowledge_source_value: Any) -> List[Dict[str, str]]:
"""
从节点属性中的 knowledge_source 字段提取信息并格式化为标准格式(不含 info 字段)
Args:
knowledge_source_value: 节点属性中的 knowledge_source 值(可能是字符串、字典、列表等)
Returns:
List[Dict]: 格式化后的 knowledge_source 列表,格式为 [{"filename":"","url":""}]
"""
if knowledge_source_value is None:
return [{"filename": "", "url": ""}]
result = []
# 如果是字符串尝试解析为JSON或直接使用
if isinstance(knowledge_source_value, str):
# 尝试解析JSON字符串
try:
import json
parsed = json.loads(knowledge_source_value)
if isinstance(parsed, list):
knowledge_source_value = parsed
elif isinstance(parsed, dict):
knowledge_source_value = [parsed]
else:
# 如果不是JSON跳过该字符串
return [{"filename": "", "url": ""}]
except:
# 解析失败,跳过该字符串
return [{"filename": "", "url": ""}]
# 如果是字典,转换为列表
if isinstance(knowledge_source_value, dict):
knowledge_source_value = [knowledge_source_value]
# 如果是列表,处理每个元素
if isinstance(knowledge_source_value, list):
for item in knowledge_source_value:
if isinstance(item, dict):
# 从字典中提取字段(不含 info
formatted_item = {
"filename": str(item.get("filename", item.get("file_name", item.get("file", "")))),
"url": str(item.get("url", item.get("link", item.get("uri", ""))))
}
result.append(formatted_item)
elif isinstance(item, str):
# 如果是字符串,跳过
continue
else:
# 其他类型,跳过
continue
else:
# 其他类型,跳过
return [{"filename": "", "url": ""}]
# 如果没有结果,返回默认值
if not result:
result = [{"filename": "", "url": ""}]
return result
def remove_sensitive_fields(data: Any) -> Any:
"""
递归删除数据中的 embedding、fulltext 和 path 字段
参考 test.py 的实现方式:在转换为 dict 后立即 pop 掉敏感字段
Args:
data: 要处理的数据可以是字典、列表、Neo4j对象或基本类型
Returns:
清理后的数据
"""
# 如果是 Neo4j Node 对象,转换为字典并删除敏感字段(参考 test.py
if hasattr(data, 'labels') and (hasattr(data, 'element_id') or hasattr(data, 'id')):
try:
props = dict(data)
props.pop('embedding', None)
props.pop('fulltext', None)
# 递归处理属性中的嵌套结构
return remove_sensitive_fields(props)
except:
return data
# 如果是 Neo4j Relationship 对象,转换为字典并删除敏感字段
if hasattr(data, 'type') and (hasattr(data, 'element_id') or hasattr(data, 'id')):
try:
props = dict(data)
props.pop('embedding', None)
props.pop('fulltext', None)
# 递归处理属性中的嵌套结构
return remove_sensitive_fields(props)
except:
return data
if isinstance(data, dict):
# 创建新字典,排除 embedding、fulltext 和 path
result = {}
for key, value in data.items():
if key not in ['embedding', 'fulltext', 'path']:
# 递归处理嵌套的字典、列表和 Neo4j 对象
result[key] = remove_sensitive_fields(value)
return result
elif isinstance(data, list):
# 递归处理列表中的每个元素
return [remove_sensitive_fields(item) for item in data]
else:
# 基本类型直接返回
return data
def format_results(results: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
格式化图谱查询结果,提取所有节点和路径(自动去重)
从 rerank 后的结果字典列表中提取 path 信息,生成 nodes 和 links
Args:
results: rerank 后的结果列表(字典列表,每个字典包含 rerank_score 和 path 等信息)
Returns:
Dict: 包含 nodes 和 links 的字典(已去重,只保留最高分)
"""
if not results:
return {"nodes": [], "links": []}
all_nodes = {} # key: node_id(str), value: node dict
all_links = {} # key: rel_id(str), value: link dict
node_scores = {} # key: node_id(str), value: highest rerank score for the node
def process_path(path, current_score):
"""处理单个 Path 对象"""
if path is None:
return
def process_node(node):
"""处理单个节点的辅助函数"""
if node is None:
return
node_id = str(node.element_id) if hasattr(node, 'element_id') else str(node.id)
if node_id not in all_nodes:
props = dict(node)
props.pop('embedding', None)
props.pop('fulltext', None)
props.pop('切片', None)
# 提取 name 用于外部字段从props中提取后移除避免出现在properties中
node_name = props.pop('name', None) or node.get("名称", f"Node_{node_id}")
# 直接过滤掉 last_updated 和 created_at
props.pop('last_updated', None)
props.pop('created_at', None)
# 提取并格式化 knowledge_source从props中提取后移除原始值
knowledge_source_value = props.pop('knowledge_source', None)
formatted_knowledge_source = extract_knowledge_source(knowledge_source_value)
all_nodes[node_id] = {
"id": node_id,
"name": node_name,
"labels": list(node.labels),
"properties": props,
"knowledge_source": formatted_knowledge_source
}
# 初始化节点的最高分数
if current_score > 0:
node_scores[node_id] = current_score
else:
# 如果节点已存在,比较并保留最高分数
existing_score = node_scores.get(node_id, 0)
if current_score > existing_score:
node_scores[node_id] = current_score
try:
# 处理节点:优先使用 path.nodes如果为空则使用 start/end 或 start_node/end_node
nodes_processed = False
# 首先尝试使用 path.nodes
if hasattr(path, 'nodes'):
try:
# 直接迭代 path.nodes可能是生成器
for node in path.nodes:
process_node(node)
nodes_processed = True
except Exception as e:
logger.debug(f"迭代 path.nodes 时出错: {e}")
# 如果 path.nodes 没有处理任何节点size=0的情况尝试使用 start/end 节点
if not nodes_processed:
start_node = None
end_node = None
# 尝试多种方式获取起始节点(优先使用 start_node其次使用 start
if hasattr(path, 'start_node'):
try:
start_node = path.start_node
except Exception:
pass
elif hasattr(path, 'start'):
try:
start_node = path.start
except Exception:
pass
# 尝试多种方式获取结束节点(优先使用 end_node其次使用 end
if hasattr(path, 'end_node'):
try:
end_node = path.end_node
except Exception:
pass
elif hasattr(path, 'end'):
try:
end_node = path.end
except Exception:
pass
# 处理起始节点
if start_node is not None:
process_node(start_node)
# 处理结束节点(只有当与起始节点不同时才处理,避免重复)
if end_node is not None and (start_node is None or end_node != start_node):
process_node(end_node)
# 处理关系
for rel in path.relationships:
rel_id = str(rel.element_id) if hasattr(rel, 'element_id') else str(rel.id)
if rel_id not in all_links:
rel_props = dict(rel)
rel_props.pop('embedding', None)
rel_props.pop('fulltext', None)
start_node_id = str(rel.start_node.element_id) if hasattr(rel.start_node, 'element_id') else str(rel.start_node.id)
end_node_id = str(rel.end_node.element_id) if hasattr(rel.end_node, 'element_id') else str(rel.end_node.id)
all_links[rel_id] = {
"id": rel_id,
"label": rel.type,
"source": start_node_id,
"target": end_node_id,
"properties": rel_props
}
except Exception as e:
logger.warning(f"处理 Path 对象时出错: {e}", exc_info=True)
for result in results:
try:
# 获取当前记录的rerank_score
current_rerank_score = result.get('rerank_score', 0)
# 查找 path 字段(可能是 path 或 paths
path_value = None
if 'path' in result:
path_value = result['path']
elif 'paths' in result:
path_value = result['paths']
else:
# 尝试从所有值中查找 Path 对象
for key, value in result.items():
if value is not None and hasattr(value, 'nodes') and hasattr(value, 'relationships'):
path_value = value
break
# 处理 path_value
if path_value is not None:
# 处理单个 Path 对象
if hasattr(path_value, 'nodes') and hasattr(path_value, 'relationships'):
process_path(path_value, current_rerank_score)
# 处理路径列表
elif isinstance(path_value, list):
if path_value: # 列表不为空
for path in path_value:
if path is not None and hasattr(path, 'nodes') and hasattr(path, 'relationships'):
process_path(path, current_rerank_score)
except Exception as e:
logger.warning(f"格式化结果记录失败: {e}", exc_info=True)
# 为每个节点添加最高rerank分数到其属性中只保留高分
for node_id, score in node_scores.items():
if node_id in all_nodes:
node = all_nodes[node_id]
if 'properties' not in node:
node['properties'] = {}
# 只保留最高分,使用 分数 作为 key
node['properties']['分数'] = score
# 最终清理:确保所有嵌套数据中的敏感字段都被删除
cleaned_nodes = [remove_sensitive_fields(node) for node in all_nodes.values()]
cleaned_links = [remove_sensitive_fields(link) for link in all_links.values()]
return {
"nodes": cleaned_nodes,
"links": cleaned_links
}
def format_entry_nodes_as_results(entry_nodes: dict) -> List[Dict[str, Any]]:
"""
将入口节点信息格式化为检索结果(降级策略)
将节点作为path节点作为结果
Args:
entry_nodes: 入口节点字典,格式如 {"设备": [{"name": "xxx", "score": 0.9, "node": {...}}]}
Returns:
List[Dict]: 结果列表每个元素是一个结果记录包含path字段用于 graph_retrieval.py 的降级策略)
"""
results = []
if not entry_nodes:
return results
for label, nodes in entry_nodes.items():
if not isinstance(nodes, list) or not nodes:
continue
# 标签列表优先使用entry_nodes中的label如果节点数据中有labels则使用节点数据的labels
label_list = [label] if label else []
for node_info in nodes:
node_name = node_info.get("name") or node_info.get("_name") or (node_info.get("node", {}).get("名称")) or str(node_info.get("node", ""))
node_data = node_info.get("node", {})
node_score = node_info.get("score", 0.8)
# 如果node_data是Neo4j Node对象转换为字典
if hasattr(node_data, 'labels') and (hasattr(node_data, 'element_id') or hasattr(node_data, 'id')):
# 从Neo4j Node对象提取labels
node_labels = list(node_data.labels) if hasattr(node_data, 'labels') else label_list
if node_labels:
label_list = node_labels
# 转换为字典
node_data = dict(node_data)
# 跳过无效节点
if not isinstance(node_data, dict) and not node_name:
continue
# 提取节点ID兜底情况下使用简单的ID格式
if isinstance(node_data, dict):
# 优先使用element_id或idNeo4j原生ID
if node_data.get("element_id"):
node_id = str(node_data.get("element_id"))
elif node_data.get("id"):
node_id = str(node_data.get("id"))
else:
# 兜底情况使用节点的名称属性作为简单ID
# 优先使用中文属性名"名称",其次使用"name"
node_id = node_data.get("名称") or node_data.get("name") or node_name
if node_id:
node_id = str(node_id)
else:
# 如果都没有生成一个简单的ID
node_id = f"{label}_{len(results)}"
else:
node_id = str(node_name) if node_name else f"{label}_{len(results)}"
# 提取节点属性(排除内部字段和指定字段)
node_properties = {}
if isinstance(node_data, dict):
for key, value in node_data.items():
if key not in ["element_id", "id", "labels", "embedding", "fulltext", "name", "切片"]:
node_properties[key] = value
# 如果节点数据中有labels使用它
if "labels" in node_data and node_data["labels"]:
if isinstance(node_data["labels"], list):
label_list = node_data["labels"]
elif hasattr(node_data["labels"], '__iter__'):
label_list = list(node_data["labels"])
# 如果没有属性,跳过
if not node_properties:
continue
# 直接过滤掉 last_updated 和 created_at
node_properties.pop('last_updated', None)
node_properties.pop('created_at', None)
# 使用remove_sensitive_fields递归删除embedding和fulltext字段确保嵌套结构也被清理
node_properties = remove_sensitive_fields(node_properties)
# 提取并格式化 knowledge_source
knowledge_source_value = node_properties.get('knowledge_source')
formatted_knowledge_source = extract_knowledge_source(knowledge_source_value)
# 创建SimpleNode对象knowledge_source 会在 format_results 的 process_node 中再次处理)
simple_node = SimpleNode(
node_data=node_properties,
labels=label_list,
node_id=node_id
)
# 创建SimplePath对象只包含一个节点
simple_path = SimplePath(nodes=[simple_node])
# 构建结果字典
# 包含path字段节点作为path同时将节点属性作为result字段
result = {
"path": simple_path,
"result": node_properties # 节点作为结果已删除embedding和fulltext
}
results.append(result)
return results
def _normalize_label_values(labels: Any) -> Set[str]:
"""将 labels/标签/type 等字段统一成标签集合。"""
if labels is None:
return set()
if isinstance(labels, str):
parts = re.split(r"[,,、/\s]+", labels)
return {part.strip("[]'\" ") for part in parts if part.strip("[]'\" ")}
if isinstance(labels, (list, tuple, set, frozenset)):
return {str(label).strip() for label in labels if str(label).strip()}
return {str(labels).strip()} if str(labels).strip() else set()
def _dict_label_values(data: Dict[str, Any]) -> Set[str]:
label_keys = ("labels", "标签", "label", "类型", "type", "节点类型", "node_type")
labels = set()
for key in label_keys:
if key in data:
labels.update(_normalize_label_values(data.get(key)))
return labels
def _has_excluded_result_label(value: Any) -> bool:
"""判断一个结果对象是否明确属于需要从 results 隐藏的节点类型。"""
labels = set()
if isinstance(value, dict):
labels.update(_dict_label_values(value))
elif hasattr(value, "labels") and (hasattr(value, "element_id") or hasattr(value, "id")):
try:
labels.update(_normalize_label_values(value.labels))
except Exception:
pass
return bool(labels & EXCLUDED_RESULT_NODE_LABELS)
def _mentions_excluded_result_label(text: Any) -> bool:
if text is None:
return False
return any(label in str(text) for label in EXCLUDED_RESULT_NODE_LABELS)
def _is_empty_result_content(value: Any) -> bool:
return value in (None, {}, [])
def _filter_excluded_result_content(value: Any, parent_key: Any = None) -> Any:
"""
只用于 results 字段:过滤掉维修工作、操作程序、操作使用这类节点内容。
nodes/links 的原始返回不会经过这个函数。
"""
if parent_key is not None and _mentions_excluded_result_label(parent_key):
return _FILTERED_OUT
if _has_excluded_result_label(value):
return _FILTERED_OUT
if isinstance(value, dict):
name_value = value.get("名称", value.get("name"))
if _mentions_excluded_result_label(name_value):
return _FILTERED_OUT
filtered = {}
for key, child in value.items():
if _mentions_excluded_result_label(key):
continue
filtered_child = _filter_excluded_result_content(child, key)
if filtered_child is not _FILTERED_OUT:
filtered[key] = filtered_child
return _FILTERED_OUT if _is_empty_result_content(filtered) else filtered
if isinstance(value, list):
filtered_items = []
for item in value:
filtered_item = _filter_excluded_result_content(item)
if filtered_item is not _FILTERED_OUT and not _is_empty_result_content(filtered_item):
filtered_items.append(filtered_item)
return filtered_items
return value
def _filter_results_for_output(results: Any) -> List[Any]:
filtered = _filter_excluded_result_content(results)
if filtered is _FILTERED_OUT or _is_empty_result_content(filtered):
return []
if isinstance(filtered, list):
return filtered
return [filtered]
def _filter_result_nodes_for_output(nodes: Any) -> List[Dict[str, Any]]:
if not isinstance(nodes, list):
return []
return [
node for node in nodes
if isinstance(node, dict) and not _has_excluded_result_label(node)
]
def _format_value(value: Any) -> str:
"""
将任意值转为简洁字符串,自适应处理:
- 数字或字符串:直接使用
- 字典:检查是否有 result 字段,分解 result 字段,去除 knowledge_source
- 列表:递归处理每个元素
"""
# 如果直接是数字或字符串,直接返回
if isinstance(value, (str, int, float)):
return str(value)
# 如果是字典,检查是否有 result 字段
elif isinstance(value, dict):
# 如果字典中有 result 字段,优先处理 result 字段的内容
if 'result' in value:
result_value = value['result']
# 处理 result 字段(去除 knowledge_source 和指定字段)
if isinstance(result_value, dict):
# 复制字典,去除 knowledge_source、last_updated、created_at 和指定字段
skip_keys = {'knowledge_source', 'name', '切片', 'last_updated', 'created_at'}
cleaned_result = {k: v for k, v in result_value.items() if k not in skip_keys}
return _format_value(cleaned_result)
elif isinstance(result_value, list):
# 如果是列表,递归处理每个元素(去除 knowledge_source
if len(result_value) == 0:
return "[]"
# 如果是简单列表,直接连接
if all(isinstance(x, (str, int, float)) for x in result_value[:3]):
return ", ".join(str(x) for x in result_value[:10]) + ("..." if len(result_value) > 10 else "")
# 复杂列表,递归处理每个元素
cleaned_items = []
for item in result_value[:10]:
if isinstance(item, dict):
# 去除 knowledge_source、last_updated、created_at 和指定字段
skip_keys = {'knowledge_source', 'name', '切片', 'last_updated', 'created_at'}
cleaned_item = {k: v for k, v in item.items() if k not in skip_keys}
cleaned_items.append(_format_value(cleaned_item))
else:
cleaned_items.append(_format_value(item))
result_str = "; ".join(cleaned_items)
if len(result_value) > 10:
result_str += f" ...(共 {len(result_value)} 条)"
return result_str
else:
# result 字段是其他类型,直接格式化
return _format_value(result_value)
else:
# 没有 result 字段,正常处理字典,去除 knowledge_source、last_updated、created_at 和指定字段
skip_keys = {'knowledge_source', 'name', '切片', 'last_updated', 'created_at'}
cleaned_dict = {k: v for k, v in value.items() if k not in skip_keys}
# 展平字典为 key: value 形式,避免嵌套 JSON
parts = []
for k, v in cleaned_dict.items():
if not isinstance(v, (dict, list)) or len(str(v)) < 100: # 避免大对象
parts.append(f"{k}: {_format_value(v)}")
return "; ".join(parts) if parts else "{...}"
# 如果是列表
elif isinstance(value, list):
if len(value) == 0:
return "[]"
# 如果是简单列表(如 [1,2,3] 或 ["a","b"]
if all(isinstance(x, (str, int, float)) for x in value[:3]):
return ", ".join(str(x) for x in value[:10]) + ("..." if len(value) > 10 else "")
else:
# 复杂对象列表,检查是否有 {'result': {...}} 格式的元素
# 检查前几个元素,判断是否都是 {'result': {...}} 格式
has_result_format = False
if value and isinstance(value[0], dict) and 'result' in value[0]:
# 检查是否所有元素都是 {'result': {...}} 格式
has_result_format = all(
isinstance(item, dict) and 'result' in item and isinstance(item.get('result'), dict)
for item in value[:3] # 只检查前3个元素来判断模式
)
formatted_items = []
for item in value[:10]: # 最多处理10个元素
if has_result_format and isinstance(item, dict) and 'result' in item:
# 如果是 {'result': {...}} 格式,分解 result 字段并去除 knowledge_source 和指定字段
result_value = item['result']
if isinstance(result_value, dict):
# 去除 knowledge_source、last_updated、created_at 和指定字段
skip_keys = {'knowledge_source', 'name', '切片', 'last_updated', 'created_at'}
cleaned_result = {k: v for k, v in result_value.items() if k not in skip_keys}
formatted_items.append(_format_value(cleaned_result))
else:
formatted_items.append(_format_value(result_value))
elif isinstance(item, dict):
# 如果不是 {'result': {...}} 格式,去除字典中的 knowledge_source、last_updated、created_at 和指定字段
skip_keys = {'knowledge_source', 'name', '切片', 'last_updated', 'created_at'}
cleaned_item = {k: v for k, v in item.items() if k not in skip_keys}
formatted_items.append(_format_value(cleaned_item))
else:
# 其他类型,正常处理
formatted_items.append(_format_value(item))
result = "; ".join(formatted_items)
if len(value) > 10:
result += f" ...(共 {len(value)} 条)"
return result
else:
return str(value)[:200] # 截断超长内容
def serialize_graph_for_llm(graph_response: dict) -> str:
"""
通用图谱结果序列化器,支持:
- nodes实体详情
- results任意查询返回值聚合/记录/字符串等)
"""
data = graph_response.get("data", {})
# 这里的过滤只影响 results 字段里的文本展示,不改动接口返回的 data.nodes/data.links。
nodes = _filter_result_nodes_for_output(data.get("nodes", []))
results = _filter_results_for_output(data.get("results", []))
output_lines = []
# ========== 1. 处理 results核心查询返回值==========
if results:
output_lines.append("【图谱查询直接结果】")
if len(results) == 1 and not isinstance(results[0], (dict, list)):
# 单值结果(如 count, max, 字符串)
output_lines.append(f" {results[0]}")
else:
# 多条记录或复杂结构
for i, res in enumerate(results): # 最多展示10条
formatted = _format_value(res)
output_lines.append(f" [{i + 1}] {formatted}")
output_lines.append("")
# ========== 2. 处理 nodes完整实体信息==========
if nodes:
output_lines.append("【相关图谱实体详情】")
for node in nodes: # 防止过长最多15个节点
name = node.get("name", "Unnamed")
labels = node.get("labels", [])
props = node.get("properties", {})
output_lines.append(f" - 名称: {name}")
if labels:
output_lines.append(f" 标签: {', '.join(labels)}")
# 过滤掉低价值字段(如 分数, 内部 id, name, 切片, last_updated, created_at
skip_props = {"分数", "id", "name", "切片", "last_updated", "created_at", "最后更新时间", "创建时间"}
for k, v in props.items():
if k not in skip_props and v not in (None, ""):
output_lines.append(f" {k}: {v}")
# 不再显示手册来源knowledge_source因为用户要求在 results 字段中不显示
output_lines.append("") # 节点间空行
# ========== 返回结果 ==========
if not output_lines:
return "图谱查询完成,但未返回有效数据。"
return "\n".join(output_lines).rstrip()