$ cat node-template.py

K

Knowledge Synthesizer

// Synthesizes an answer from multiple document chunks using an LLM. Connect one or more Document Chunk nodes to the context port.

Process
LLM
template.py
1import sys2import json3import traceback4from gais import Gais567def main():8    try:9        raw = json.loads(sys.stdin.read())10        inputs = raw.get("inputs", {})1112        query = inputs.get("query", "")13        context = inputs.get("context", [])14        model_id = inputs.get("llmModelId", "")15        # 'auto' (default) matches the question's language; locale codes map16        # to names; legacy full-name values from older canvases are accepted17        # via the allowlist. Anything else (wired upstream output, free-form18        # strings) falls back to auto: the value lands in the system prompt,19        # so unknown strings must never pass through.20        language = inputs.get("language") or "auto"21        language_names = {22            "de": "German", "en": "English", "es": "Spanish", "fr": "French",23            "it": "Italian", "ja": "Japanese", "ko": "Korean", "nl": "Dutch",24            "pt": "Portuguese", "ru": "Russian", "zh": "Chinese",25        }26        lang_name = None27        if isinstance(language, str) and language.strip() not in ("", "auto"):28            value = language.strip()29            if value in language_names:30                lang_name = language_names[value]31            elif value in language_names.values():32                lang_name = value33            else:34                print(f"[language] unrecognized value {value!r} ignored - using auto", file=sys.stderr)35        if lang_name:36            language_rule = f"**Always respond in {lang_name}** — Match the response language exactly."37        else:38            language_rule = (39                "**Respond in the language of the question** — Match the "40                "question's language, not the sources' language."41            )4243        # Normalize context to a flat list of strings. Fan-in delivers a raw44        # list whose elements may THEMSELVES be lists (a Text[] source arrives45        # as one nested list) — flatten one level so each chunk is its own46        # numbered Source instead of a python-repr blob.47        if isinstance(context, str):48            context = [context]49        flat_context = []50        for item in context:51            if isinstance(item, list):52                flat_context.extend(str(c) for c in item if c)53            elif item:54                flat_context.append(str(item))55        context = flat_context5657        if not query:58            print(json.dumps({"text": "No query provided."}))59            return6061        if not context or all(not c for c in context):62            print(json.dumps({"text": "No context chunks available. Connect Document Chunk nodes to provide source material."}))63            return6465        # Build context section from chunks66        context_text = ""67        for i, chunk in enumerate(context, 1):68            if chunk:69                context_text += f"--- Source {i} ---\n{chunk}\n\n"7071        # Build prompt (aligned with RAG agent citation and grounding rules)72        system_prompt = (73            f"You are a Knowledge Synthesis assistant. Your job: answer questions using ONLY the provided source materials.\n\n"74            f"## Rules\n"75            f"1. **Never use general knowledge** — Only use information from the sources below.\n"76            f"2. {language_rule}\n"77            f"3. **Never fabricate** — If the sources don't contain enough information, say so clearly.\n"78            f"4. **Cite every factual claim** using the format: [Source: <chunk_id>]\n"79            f"   - The Chunk ID is in each source's header: [Document Path: ... | Chunk ID: <id> | Relevance: ...]\n"80            f"   - Multiple sources: [Source: id1, id2]\n"81            f"   - Place citation IMMEDIATELY after each claim.\n"82            f"5. **Prioritize high-relevance sources** — Sources with higher Relevance scores are more likely to be relevant.\n"83            f"6. **Structure your answer** — Use headings, bullet points, or numbered lists when appropriate for clarity.\n"84            f"7. **Knowledge-graph sources are authoritative for counts and relationships** — A source starting with"85            f" 'Entity profile:' (or 'Fused ranking') comes from the organization's knowledge graph, not a document."86            f" Its per-link-type counts are EXACT (e.g. 'employed_by: 18 - <names>')."87            f" When the question asks how many people/entities hold a relationship (how many employees, who works"88            f" there, how many X does Y regulate), ANSWER WITH THE GRAPH COUNT and list the named entities,"89            f" citing [Source: Knowledge Graph]. State that the number is what the knowledge graph records;"90            f" you may add partial figures from document chunks as supporting detail, but never answer"91            f" 'cannot be determined' when an Entity profile block contains the relevant count.\n"92        )9394        user_prompt = f"Question: {query}\n\nSource Materials:\n\n{context_text}"9596        messages = [97            {"role": "system", "content": system_prompt},98            {"role": "user", "content": user_prompt},99        ]100101        # thinking=False: the reasoning on/off eval (qwen3.5-35b-a3b) showed102        # synthesis quality held with reasoning off — same grounded, cited103        # conclusions at ~8-25x lower latency — so skip the wasted CoT pass.104        result = Gais.llm.chat(messages, model_id=model_id, thinking=False)105        print(json.dumps({"text": result.text}))106107    except Exception as e:108        error_output = {109            "error": str(e),110            "errorType": type(e).__name__,111            "traceback": traceback.format_exc(),112        }113        print(json.dumps(error_output), file=sys.stderr)114        sys.exit(1)115116117if __name__ == "__main__":118    main()

$ git log --oneline

v1.7.0
HEAD
2026-08-18
v1.6.02026-07-19
v1.5.02026-06-27
v1.3.12026-05-22
v1.1.02026-04-09
v1.0.02026-04-08