$ cat node-template.py
O
Ontology Traverse
// Walk typed links out of, into, or both directions from an ontology object and return the connected sub-graph.
Process
Data
#ontology#knowledge-graph#traverse#graph#read
template.py
1"""ontology-traverse — walk typed links out of (or into) an ontology object.23Given a starting object id and a link type, returns the connected sub-graph via4the gais.ontology SDK (``Gais.ontology.traverse``), which carries the5per-execution USER_TOKEN and talks to the permissioned www-emblema v2 Ontology6API — this node imports no HTTP client. The object id is UUID-validated here7(defense in depth) so an id wired from an upstream node can't rewrite the8request route.9"""1011from __future__ import annotations1213import json14import re15import sys16import traceback17from typing import Any1819from gais import Gais2021_UUID_RE = re.compile(22 r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",23 re.IGNORECASE,24)25_DIRECTIONS = ("out", "in", "both")262728def _normalize_uuid(value: Any, field: str) -> str:29 """Accept a bare UUID or a ``mention://.../<uuid>`` URI; validate as UUID."""30 if not isinstance(value, str):31 raise ValueError(f"{field} must be a string, got {type(value).__name__}")32 s = value.strip()33 if "://" in s:34 s = s.rsplit("/", 1)[-1]35 if not _UUID_RE.match(s):36 raise ValueError(f"{field} is not a valid UUID: {value!r}")37 return s383940def _summarize(result: dict[str, Any], link_type: str, direction: str) -> str:41 """Render the traversal as synthesizer-ready TEXT.4243 The node previously emitted only ``result`` (Json), which cannot wire into a44 Knowledge Synthesizer's ``context`` port (Text[]). Mirrors the pattern45 ontology-entity-360 already uses with its ``summaryText`` output — that is46 precisely why entity-360 was composable in a macro and this node was not.47 """48 endpoints = result.get("endpoints")49 if not isinstance(endpoints, list) or not endpoints:50 scope = link_type or "any relation"51 return f"No connected entities found ({scope}, direction={direction})."5253 lines = [54 f"Connected entities ({link_type or 'all relations'}, direction={direction}): "55 f"{len(endpoints)}"56 ]57 for ep in endpoints[:25]:58 if not isinstance(ep, dict):59 continue60 name = ep.get("displayName") or ep.get("name") or ep.get("id") or "?"61 rel = ep.get("linkType") or ep.get("relation") or ""62 etype = ep.get("typeName") or ep.get("type") or ""63 bits = [b for b in (str(etype), str(rel)) if b]64 lines.append(f"- {name}" + (f" ({', '.join(bits)})" if bits else ""))65 if len(endpoints) > 25:66 lines.append(f"... and {len(endpoints) - 25} more")67 return "\n".join(lines)686970def process(inputs: dict[str, Any]) -> dict[str, Any]:71 object_id = _normalize_uuid(inputs.get("objectId"), "objectId")7273 # linkType is OPTIONAL: empty means "walk every relation". Making it74 # mandatory is what stopped this node being composable inside a macro,75 # where the relation is not known ahead of time (E1 surface race).76 link_type_raw = inputs.get("linkType")77 link_type = link_type_raw.strip() if isinstance(link_type_raw, str) else ""7879 direction = (inputs.get("direction") or "out").strip().lower()80 if direction not in _DIRECTIONS:81 raise ValueError(f"direction must be one of {list(_DIRECTIONS)}")8283 try:84 depth = int(inputs.get("depth") or 1)85 except (TypeError, ValueError):86 depth = 187 depth = max(1, depth)8889 result = Gais.ontology.traverse(90 object_id=object_id,91 link_type=link_type or None,92 direction=direction,93 depth=depth,94 )95 if not isinstance(result, dict):96 result = {}9798 summary_text = _summarize(result, link_type, direction)99100 print(101 f"[ontology-traverse] object={object_id} "102 f"link_type={link_type or '<all>'} direction={direction} depth={depth} "103 f"endpoints={len(result.get('endpoints') or [])}",104 file=sys.stderr,105 )106107 return {"result": result, "summaryText": summary_text}108109110def main() -> None:111 try:112 envelope = json.loads(sys.stdin.read() or "{}")113 if not isinstance(envelope, dict):114 envelope = {}115 inputs = envelope.get("inputs", {}) or {}116 json.dump(process(inputs), sys.stdout)117 except Exception as e:118 print(119 json.dumps({120 "error": str(e),121 "errorType": type(e).__name__,122 "traceback": traceback.format_exc(),123 }),124 file=sys.stderr,125 )126 sys.exit(1)127128129if __name__ == "__main__":130 main()$ git log --oneline
v1.1.0
HEAD
2026-08-18v1.0.02026-07-16