from kbdebugger.types import EdgeProperties, ExtractionResult, GraphRelation from kbdebugger.compat.langchain import Document from typing import List, Iterable, Mapping, Any, Optional from datetime import datetime def normalize_text(text: str) -> str: """ Normalize a free-text label into a safe identifier: - lowercase - strip punctuation at edges """ clean = " ".join(text.strip().split()).lower() # clean = clean.replace(" ", "_") return clean def map_doc_extracted_triplets_to_graph_relations( extraction: ExtractionResult, source_doc: Document, # *, # include_sentence: bool = True, ) -> List[GraphRelation]: """ Map an ExtractionResult to graph-ready relation dicts. - extraction: {"sentence": str, "triplets": [(subj,obj,rel), ...]} - source_doc: LangChain Document (for provenance: page_content + metadata) """ # defensive: accept partially-typed dicts sentence_text = extraction.get("sentence") triplets = extraction.get("triplets", []) rels: List[GraphRelation] = [] for subj, obj, rel in triplets: props: EdgeProperties = { # for provenance 'sentence': sentence_text, 'original_sentence': getattr(source_doc, "page_content", ""), **getattr(source_doc, "metadata", {}) # type: ignore[arg-type] } # if include_sentence: # # human-readable extracted sentence (from the extractor) # props["sentence"] = sentence_text rels.append({ "source": { "label": normalize_text(subj) }, "target": { "label": normalize_text(obj) }, "edge": { "label": normalize_text(rel), "properties": props }, }) # type: ignore return rels def map_extracted_triplets_to_graph_relations( extraction: ExtractionResult, source: Optional[str] = None, ) -> List[GraphRelation]: """ Map an ExtractionResult to graph-ready relation dicts. - extraction: {"sentence": str, "triplets": [(subj,obj,rel), ...]} - source_doc: LangChain Document (for provenance: page_content + metadata) """ # defensive: accept partially-typed dicts sentence_text = extraction.get("sentence") triplets = extraction.get("triplets", []) rels: List[GraphRelation] = [] for subj, obj, rel in triplets: props: EdgeProperties = { # for provenance 'sentence': sentence_text, **({'source': source} if source else {}) # only include if source is provided # 'original_sentence': getattr(source_doc, "page_content", ""), # **getattr(source_doc, "metadata", {}) # type: ignore[arg-type] } rels.append({ "source": { "label": normalize_text(subj) }, "target": { "label": normalize_text(obj) }, "edge": { "label": normalize_text(rel), "properties": props }, }) # type: ignore return rels def rows_to_graph_relations( rows: Iterable[Mapping[str, Any]], *, source_key: str = "source", target_key: str = "target", predicate_key: str = "predicate", props_key: str = "props", # if we want to enforce required props fields, will do it here ) -> List[GraphRelation]: rels: List[GraphRelation] = [] for row in rows: source = row[source_key] target = row[target_key] predicate = row[predicate_key] props_raw = row.get(props_key, {}) or {} source_id = str(row.get("source_id", "")) target_id = str(row.get("target_id", "")) if not isinstance(props_raw, dict): raise TypeError(f"Expected '{props_key}' to be a dict, got {type(props_raw)}: {props_raw!r}") props: EdgeProperties = {**props_raw} # type: ignore[misc] # Optional: keep predicate redundantly in properties for provenance/compat # (only if you want this invariant) props.setdefault("label", predicate) now = datetime.now().isoformat() rels.append( { "source": { "label": str(source), "id": source_id, }, "target": { "label": str(target), "id": target_id, }, "edge": { "label": str(predicate), "properties": props } } # type: ignore ) return rels