Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions graphify/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import networkx as nx
from .ids import make_id, normalize_id as _normalize_id
from .paths import default_graph_json as _default_graph_json
from .validate import validate_extraction
from .validate import validate_extraction, VALID_FILE_TYPES


# Language interop families, keyed by extension, for the cross-language phantom-edge
Expand Down Expand Up @@ -72,6 +72,20 @@
"framework": "concept",
}

# Biomedical entity categories that semantic subagents are told to record in a
# node's `entry_type` (see the biomedical extraction rules in the /graphify
# skill). Models sometimes leak the category into `file_type` instead, where it
# is not a valid file_type. Unlike the generic synonyms above — which collapse
# unknown types to `concept` and discard the original token — these categories
# carry real information, so we preserve them in `entry_type` rather than lose
# them. `technology` is intentionally also present in _FILE_TYPE_SYNONYMS (it is
# ambiguous with the code-corpus sense), so it is only recovered as biomedical
# when the node already shows a biomedical signal (an existing `entry_type`).
_BIOMEDICAL_CATEGORIES = {
"drug", "gene", "disease", "protein", "technology",
"peptide", "institution", "company",
}


# Hyperedge member lists are canonically keyed `nodes` (see graphify/llm.py
# extraction spec), but LLM/subagent drift and externally-supplied graph.json
Expand Down Expand Up @@ -428,8 +442,19 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat
if node.get("file_type") in (None, ""):
node["file_type"] = "concept"
ft = node.get("file_type", "")
if ft and ft not in {"code", "document", "paper", "image", "rationale", "concept"}:
node["file_type"] = _FILE_TYPE_SYNONYMS.get(ft, "concept")
if ft and ft not in VALID_FILE_TYPES:
# A leaked biomedical entity category (drug/gene/disease/...) carries
# real information, so preserve it in entry_type instead of
# collapsing it to `concept` and discarding it. `technology` is
# ambiguous with the code-corpus synonym, so only treat it as
# biomedical when the node already carries a biomedical signal.
if ft in _BIOMEDICAL_CATEGORIES and (
node.get("entry_type") or ft not in _FILE_TYPE_SYNONYMS
):
node.setdefault("entry_type", ft)
node["file_type"] = "document"
else:
node["file_type"] = _FILE_TYPE_SYNONYMS.get(ft, "concept")

# Canonicalize hyperedge member lists (#1561): producers sometimes key the
# member list `members`/`node_ids` instead of `nodes`. Fold aliases onto
Expand Down
52 changes: 52 additions & 0 deletions tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -994,3 +994,55 @@ def test_build_from_json_prunes_dangling_hyperedge_members(capsys):
assert set(hes) == {"he_partial"}, "an all-dangling hyperedge must be dropped"
assert hes["he_partial"]["nodes"] == ["alpha", "beta"]
assert "he_all_ghost" in capsys.readouterr().err


def test_leaked_biomedical_category_recovered_into_entry_type():
# #(new): biomedical semantic subagents are told to put the entity category
# (drug/gene/disease/...) in `entry_type`, but models sometimes also write
# it to `file_type`, which is not a valid file_type. Recover the category
# into entry_type and coerce file_type to a valid `document` — without
# emitting an invalid-file_type warning and without discarding the category.
ext = {
"nodes": [
# peptide is biomedical-exclusive: recovered even without entry_type
{"id": "phlip", "label": "pHLIP", "file_type": "peptide",
"source_file": "patents/records/x.md"},
# entry_type already correct; must not be overwritten
{"id": "mitocatch", "label": "MitoCatch", "file_type": "technology",
"entry_type": "technology", "source_file": "patents/records/y.md"},
],
"edges": [],
}
G = build_from_json(ext)
assert G.nodes["phlip"]["file_type"] == "document"
assert G.nodes["phlip"]["entry_type"] == "peptide"
assert G.nodes["mitocatch"]["file_type"] == "document"
assert G.nodes["mitocatch"]["entry_type"] == "technology"


def test_ambiguous_technology_without_signal_stays_code_synonym():
# `technology` is ambiguous with the code-corpus synonym (-> concept). With
# no biomedical signal (no entry_type), preserve the existing behavior so
# non-biomedical corpora are unaffected.
ext = {
"nodes": [
{"id": "n1", "label": "Some Tech", "file_type": "technology",
"source_file": "docs/a.md"},
],
"edges": [],
}
G = build_from_json(ext)
assert G.nodes["n1"]["file_type"] == "concept"
assert "entry_type" not in G.nodes["n1"]


def test_unknown_invalid_file_type_still_falls_back_to_concept():
ext = {
"nodes": [
{"id": "n1", "label": "Junk", "file_type": "gizmo",
"source_file": "docs/a.md"},
],
"edges": [],
}
G = build_from_json(ext)
assert G.nodes["n1"]["file_type"] == "concept"