Files
wehub-resource-sync 1443d3fdf9
Ruff Format Check / Ruff Format & Lint (push) Failing after 7m39s
Deploy VitePress site to Pages / build (push) Failing after 9m11s
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:26 +08:00

166 lines
5.5 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.
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Callable
from typing import Any
from yuxi.knowledge.graphs.graph_utils import normalize_entity_name
class GraphExtractor(ABC):
extractor_type: str
def __init__(self, options: dict[str, Any] | None = None):
self.options = options or {}
@abstractmethod
async def extract(self, text: str, *, chunk_metadata: dict[str, Any] | None = None) -> dict[str, Any]:
pass
def validate_options(self) -> None:
return None
def normalize_extraction_result(result: dict[str, Any], extractor_type: str) -> dict[str, Any]:
if not isinstance(result, dict):
raise ValueError("extraction_result 必须是对象")
entities = result.get("entities") or []
relations = result.get("relations") or []
if not isinstance(entities, list) or not isinstance(relations, list):
raise ValueError("extraction_result.entities 和 relations 必须是数组")
normalized_entities_by_key: dict[tuple[str, str], dict[str, Any]] = {}
entity_refs: dict[str, dict[str, Any]] = {}
def add_entity(entity: Any, path: str) -> dict[str, Any]:
normalized_entity = _normalize_entity(entity, path)
key = _entity_key(normalized_entity)
existing = normalized_entities_by_key.get(key)
if existing is None:
normalized_entities_by_key[key] = normalized_entity
existing = normalized_entity
else:
_merge_attributes(existing, normalized_entity)
for ref in _entity_refs(entity, existing):
entity_refs[ref] = existing
return existing
for index, entity in enumerate(entities):
add_entity(entity, f"entities[{index}]")
normalized_relations = []
for index, relation in enumerate(relations):
if not isinstance(relation, dict):
raise ValueError("relations 元素必须是对象")
source = _normalize_relation_endpoint(
relation.get("source"),
entity_refs,
add_entity,
result,
f"relations[{index}].source",
)
target = _normalize_relation_endpoint(
relation.get("target"),
entity_refs,
add_entity,
result,
f"relations[{index}].target",
)
text = str(relation.get("text") or "").strip()
if not text:
raise ValueError("relations[].text 不能为空")
normalized_relations.append(
{
"source": source,
"target": target,
"text": text,
"label": str(relation.get("label") or "RELATED_TO").strip() or "RELATED_TO",
}
)
metadata = dict(result.get("metadata") or {})
metadata.setdefault("extractor_type", extractor_type)
metadata.setdefault("schema_version", 1)
return {
"entities": list(normalized_entities_by_key.values()),
"relations": normalized_relations,
"metadata": metadata,
}
def _normalize_relation_endpoint(
endpoint: Any,
entity_refs: dict[str, dict[str, Any]],
add_entity: Callable[[Any, str], dict[str, Any]],
result: dict[str, Any],
path: str,
) -> dict[str, Any]:
if isinstance(endpoint, dict):
return add_entity(endpoint, path)
endpoint_ref = str(endpoint or "").strip()
entity = entity_refs.get(endpoint_ref)
if entity is None:
raise ValueError(
f"relations[].source/target 必须是实体对象,或引用 entities[].text/id"
f"未找到: {path}={endpoint_ref}, Result: {result}"
)
return entity
def _normalize_entity(entity: Any, path: str) -> dict[str, Any]:
if not isinstance(entity, dict):
raise ValueError(f"{path} 必须是对象")
text = str(entity.get("text") or "").strip()
if not text:
raise ValueError(f"{path}.text 不能为空")
attributes = entity.get("attributes") or []
if not isinstance(attributes, list):
raise ValueError(f"{path}.attributes 必须是数组")
normalized_attributes = []
for attribute in attributes:
if not isinstance(attribute, dict):
raise ValueError(f"{path}.attributes 元素必须是对象")
attr_text = str(attribute.get("text") or "").strip()
if not attr_text:
continue
normalized_attributes.append(
{
"text": attr_text,
"label": str(attribute.get("label") or "Attribute").strip() or "Attribute",
}
)
return {
"text": text,
"label": str(entity.get("label") or "Entity").strip() or "Entity",
"attributes": normalized_attributes,
}
def _entity_key(entity: dict[str, Any]) -> tuple[str, str]:
return (normalize_entity_name(entity["text"]), entity["label"])
def _entity_refs(raw_entity: Any, entity: dict[str, Any]) -> list[str]:
refs = [entity["text"]]
if isinstance(raw_entity, dict):
entity_id = str(raw_entity.get("id") or "").strip()
if entity_id:
refs.append(entity_id)
return refs
def _merge_attributes(target: dict[str, Any], source: dict[str, Any]) -> None:
known_attributes = {(attr["text"], attr["label"]) for attr in target.get("attributes") or []}
for attribute in source.get("attributes") or []:
attribute_key = (attribute["text"], attribute["label"])
if attribute_key not in known_attributes:
target.setdefault("attributes", []).append(attribute)
known_attributes.add(attribute_key)