$ cat node-template.py

O

Ontology Search

// Search the organization ontology (knowledge graph) for matching objects plus a match count. Supports entity mode (search by NAME), schema mode (search by RELATIONSHIP/THEME via matching types), and hybrid.

Input
Data
#ontology#knowledge-graph#search#read
template.py
1"""ontology-search — full-text search the organization ontology.23Queries the knowledge graph for objects matching a free-text ``query`` via the4gais.ontology SDK (``Gais.ontology.search``), which carries the per-execution5USER_TOKEN and talks to the permissioned www-emblema v2 Ontology API — this node6imports no HTTP client. Returns the raw match list plus a convenience ``count``7so a downstream node can branch on "did we find anything?".8"""910from __future__ import annotations1112import json13import sys14import traceback15from typing import Any1617from gais import Gais1819MAX_LIMIT = 10020MAX_QUERY_VARIANTS = 4212223def _norm(value: Any) -> str:24    return " ".join(str(value or "").lower().split())252627def _lexical_text(row: dict[str, Any]) -> str:28    """The NAME-bearing text a hit is scored on lexically: display name +29    keywords_en. Deliberately EXCLUDES description_en — that is the vector arm's30    field, and a hit whose description merely MENTIONS the query is not a lexical31    entity match (see `_is_lexical`)."""32    props = row.get("properties") if isinstance(row.get("properties"), dict) else {}33    parts = [34        row.get("displayName"),35        props.get("name"),36        props.get("keywords_en"),37        props.get("nameAlt"),38    ]39    return _norm(" ".join(str(p) for p in parts if p))404142def _is_lexical(row: dict[str, Any], variant_norms: list[str]) -> bool:43    """True when the query terms appear in the hit's NAME/keywords — i.e. it44    matched the exact/BM25 arms, not just the vector arm.4546    B-prime side-effect: `profile_vector` embeds `description_en`, so a document47    that only MENTIONS the entity (e.g. a certification "issued at the Ministry of48    Foreign Affairs") surfaces via kNN and can rank #1 of the entity search — but49    it is NOT the entity the question names. Tagging lets `merge-ranked-lists`50    anchor the profiled entity (topId) to a real lexical hit, skipping mentions.51    """52    text = _lexical_text(row)53    if not text:54        return False55    for variant in variant_norms:56        if not variant:57            continue58        if variant in text:59            return True60        tokens = [t for t in variant.split() if len(t) > 2]61        if tokens and all(t in text for t in tokens):62            return True63    return False646566def process(inputs: dict[str, Any]) -> dict[str, Any]:67    query = inputs.get("query")68    if not isinstance(query, str) or not query.strip():69        raise ValueError("Required input 'query' not provided")70    query = query.strip()7172    # Multi-variant queries (1.2.0): "term italiano | english term" runs one73    # search per variant and unions the results by id, keeping the FIRST74    # variant's ranking priority. Observed 2026-07-19: agents fill a single75    # language and miss the corpus-language names; the pipe pattern makes76    # supplying Italian + English variants deterministic instead of a prompt77    # dice roll. Capped to keep the fan-out bounded.78    variants: list[str] = []79    for part in query.split("|"):80        trimmed = part.strip()81        if trimmed and trimmed not in variants:82            variants.append(trimmed)83    variants = variants[:MAX_QUERY_VARIANTS] or [query]8485    # Optional object-type filter (id or name); blank -> search every type.86    raw_type = inputs.get("type")87    object_type = (88        raw_type.strip() if isinstance(raw_type, str) and raw_type.strip() else None89    )9091    # Optional N2 retrieval mode (entity | schema | hybrid). Blank/unknown ->92    # None, and the API defaults to entity search. `schema`/`hybrid` reach the93    # graph through matching TYPES (relationship/theme questions) instead of94    # entity NAMES — see the input description for when the agent picks each.95    raw_mode = inputs.get("mode")96    mode = raw_mode.strip().lower() if isinstance(raw_mode, str) and raw_mode.strip() else None97    if mode not in {"entity", "schema", "hybrid"}:98        mode = None99100    # Clamp the limit to a sane range (SDK default is 10).101    try:102        limit = int(inputs.get("limit") or 10)103    except (TypeError, ValueError):104        limit = 10105    limit = max(1, min(limit, MAX_LIMIT))106107    # Normalized variants for the lexical-vs-mention tag (1.3.0). A hit is108    # LEXICAL when the query terms are in its name/keywords; a vector-only109    # description-mention is not, so merge-ranked-lists won't anchor topId to it.110    variant_norms = [_norm(v) for v in variants]111112    results: list[Any] = []113    seen_ids: set[str] = set()114    for variant in variants:115        batch = Gais.ontology.search(116            query=variant, type=object_type, limit=limit, mode=mode117        )118        if not isinstance(batch, list):119            continue120        for row in batch:121            row_id = row.get("id") if isinstance(row, dict) else None122            if isinstance(row_id, str):123                if row_id in seen_ids:124                    continue125                seen_ids.add(row_id)126            if isinstance(row, dict):127                row["lexical"] = _is_lexical(row, variant_norms)128            results.append(row)129            if len(results) >= limit:130                break131        if len(results) >= limit:132            break133134    print(135        f"[ontology-search] variants={variants!r} type={object_type} "136        f"mode={mode or 'entity'} limit={limit} found={len(results)}",137        file=sys.stderr,138    )139140    return {"results": results, "count": len(results)}141142143def main() -> None:144    try:145        envelope = json.loads(sys.stdin.read() or "{}")146        if not isinstance(envelope, dict):147            envelope = {}148        inputs = envelope.get("inputs", {}) or {}149        json.dump(process(inputs), sys.stdout)150    except Exception as e:151        print(152            json.dumps({153                "error": str(e),154                "errorType": type(e).__name__,155                "traceback": traceback.format_exc(),156            }),157            file=sys.stderr,158        )159        sys.exit(1)160161162if __name__ == "__main__":163    main()

$ git log --oneline

v1.4.0
HEAD
2026-08-18
v1.3.0

Ontology Search 1.3.0 (B-prime lexical anchor): tags each hit `lexical` when the query terms are in the object name/keywords_en (not just a vector description-mention), so Merge Ranked Lists can anchor topId to a real name match and skip vector-only mentions.

v1.2.02026-07-19
v1.1.02026-07-19

1.1.0: query-crafting guidance in input descriptions (corpus-language translation, keywords not questions, topic preservation)

v1.0.02026-07-16