$ cat node-template.py
O
Ontology Aggregate
// Count ontology objects, grouped by object type. Returns EXACT totals over everything the caller may see — a real census, not a sample or a ranked page. Leave every input empty for a whole-graph breakdown, or set a type for a single count.
Process
Data
#ontology#knowledge-graph#aggregate#count#statistics#read
template.py
1"""ontology-aggregate — EXACT counts of ontology objects, grouped by object type.23Given no inputs at all this returns a census of the whole graph the caller may4see; given a ``type`` it returns the count for that type. Counts come from the5gais.ontology SDK (``Gais.ontology.aggregate``), which carries the per-execution6USER_TOKEN and talks to the permissioned www-emblema v2 Ontology API — this node7imports no HTTP client.89WHY THIS NODE EXISTS. A macro had no way to count, so when asked "which entity10types are most frequent?" the synthesizer answered from the composition of its11own 25-item retrieval list — a relevance ranking presented as a corpus census12(measured 2026-07-28, E1). That is the worst failure shape available: not a13visible refusal, but a confident wrong number with no tell. Real counts remove14the incentive to infer them.1516The counts are EXACT, not sampled: the contract collects every accessible object17and folds it in memory, so the number is the caller's true total rather than a18page. It is trust-gated per object, so two users can legitimately see different19totals for the same graph.2021GROUPING IS BY OBJECT TYPE ONLY — the single grouping the contract supports.22A ``groupBy`` knob is deliberately NOT exposed: the underlying API echoes it23without honoring it, and a control that silently does nothing is worse than an24absent one.25"""2627from __future__ import annotations2829import json30import sys31import traceback32from typing import Any3334from gais import Gais3536# Rendered census lines. Beyond this the tail is summarised rather than listed —37# the total and the leading types are what a synthesizer reasons over, and an38# unbounded list would crowd out the rest of its context.39_MAX_BUCKET_LINES = 40404142def _buckets(result: dict[str, Any]) -> list[dict[str, Any]]:43 raw = result.get("byType")44 if not isinstance(raw, list):45 return []46 return [b for b in raw if isinstance(b, dict)]474849def _summarize(result: dict[str, Any], type_filter: str, query: str) -> str:50 """Render the census as synthesizer-ready TEXT (wires into context: Text[]).5152 States EXACT explicitly. The synthesizer is simultaneously handed a fused53 relevance ranking, and without the distinction it has been observed reading54 frequency off that ranking; saying which number is a real count is the point55 of the node.56 """57 total = result.get("total")58 total = total if isinstance(total, int) else 059 buckets = _buckets(result)6061 scope = []62 if type_filter:63 scope.append(f"type={type_filter}")64 if query:65 scope.append(f"name contains {query!r}")66 scope_note = f" ({'; '.join(scope)})" if scope else " (whole accessible graph)"6768 if not total:69 return (70 f"Ontology census{scope_note}: EXACT count = 0. "71 "No objects match — do not infer a count from any ranked list."72 )7374 lines = [75 f"Ontology census{scope_note}: EXACT total = {total} objects, "76 f"grouped by object type ({len(buckets)} types). "77 "These are true counts over everything you may see, NOT a sample and "78 "NOT derived from any ranked result list — use them verbatim for "79 "how-many / most-frequent questions."80 ]81 for bucket in buckets[:_MAX_BUCKET_LINES]:82 name = bucket.get("typeName") or bucket.get("typeId") or "?"83 count = bucket.get("count")84 lines.append(f"- {name}: {count if isinstance(count, int) else 0}")85 if len(buckets) > _MAX_BUCKET_LINES:86 rest = buckets[_MAX_BUCKET_LINES:]87 tail = sum(b.get("count") or 0 for b in rest if isinstance(b.get("count"), int))88 lines.append(f"... and {len(rest)} further types totalling {tail} objects")89 return "\n".join(lines)909192def process(inputs: dict[str, Any]) -> dict[str, Any]:93 # Every input is OPTIONAL by design: with none set the node is a pure corpus94 # census and needs no wiring, which is what lets it sit in a macro whose95 # input mapping is strictly one-to-one.96 raw_type = inputs.get("type")97 type_filter = raw_type.strip() if isinstance(raw_type, str) else ""9899 raw_query = inputs.get("query")100 query = raw_query.strip() if isinstance(raw_query, str) else ""101102 # The contract's `filter` accepts only `q` today (everything else is echoed),103 # so `query` is passed through it rather than inventing a richer surface the104 # backend would silently drop.105 result = Gais.ontology.aggregate(106 type=type_filter or None,107 filter={"q": query} if query else None,108 )109 if not isinstance(result, dict):110 result = {}111112 total = result.get("total")113 total = total if isinstance(total, int) else 0114115 print(116 f"[ontology-aggregate] type={type_filter or '<all>'} "117 f"query={query or '<none>'} total={total} "118 f"types={len(_buckets(result))}",119 file=sys.stderr,120 )121122 return {123 "result": result,124 "total": total,125 "summaryText": _summarize(result, type_filter, query),126 }127128129def main() -> None:130 try:131 envelope = json.loads(sys.stdin.read() or "{}")132 if not isinstance(envelope, dict):133 envelope = {}134 inputs = envelope.get("inputs", {}) or {}135 json.dump(process(inputs), sys.stdout)136 except Exception as e:137 print(138 json.dumps({139 "error": str(e),140 "errorType": type(e).__name__,141 "traceback": traceback.format_exc(),142 }),143 file=sys.stderr,144 )145 sys.exit(1)146147148if __name__ == "__main__":149 main()$ git log --oneline
v1.0.0
HEAD
2026-08-18