$ 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