$ cat node-template.py

T

Text Summarizer

// Summarizes large text content using an LLM. Automatically splits long documents into chunks and summarizes them hierarchically to produce a final summary.

Process
LLM
template.py
1import sys2import json3import traceback4import asyncio5import time6from gais import Gais789# Output-language directive (language-persistence workstream). Values are10# platform locale codes; legacy full names are accepted via the allowlist.11# Anything else (wired upstream output, free-form strings) falls back to12# auto: the value is interpolated into the system prompt, so unknown13# strings must never pass through.14LANGUAGE_NAMES = {15    "de": "German", "en": "English", "es": "Spanish", "fr": "French",16    "it": "Italian", "ja": "Japanese", "ko": "Korean", "nl": "Dutch",17    "pt": "Portuguese", "ru": "Russian", "zh": "Chinese",18}192021def resolve_language_name(language):22    """Full display name for an allowlisted language value, else None."""23    if not isinstance(language, str):24        return None25    value = language.strip()26    if not value or value == "auto":27        return None28    if value in LANGUAGE_NAMES:29        return LANGUAGE_NAMES[value]30    if value in LANGUAGE_NAMES.values():31        return value32    print(f"[language] unrecognized value {value!r} ignored - using auto", file=sys.stderr)33    return None343536def language_directive(language):37    """System-prompt suffix forcing the output language; '' when auto."""38    name = resolve_language_name(language)39    if not name:40        return ""41    return (42        f"\n\nOUTPUT LANGUAGE: {name}. Write the summary in {name} regardless "43        "of the source material's language or any language implied by the "44        "instructions. Keep proper nouns and verbatim quotes in their "45        "original language."46    )474849def split_content_into_chunks(content: str, num_chunks: int, model: str) -> list:50    """51    Split content into roughly equal chunks by character count.5253    Tries to split on paragraph boundaries when possible.5455    Args:56        content: Text to split57        num_chunks: Number of chunks to create58        model: Model name for char limit calculation5960    Returns:61        List of text chunks62    """63    safe_chars = Gais.llm.safe_input_chars(model_id=model)64    chunk_size = min(len(content) // num_chunks, safe_chars)6566    chunks = []67    current_pos = 06869    while current_pos < len(content):70        # Calculate end position71        end_pos = min(current_pos + chunk_size, len(content))7273        # Try to find a good break point (paragraph, sentence, space)74        if end_pos < len(content):75            # Look for paragraph break76            para_break = content.rfind("\n\n", current_pos, end_pos)77            if para_break > current_pos + chunk_size // 2:78                end_pos = para_break + 279            else:80                # Look for sentence break81                sent_break = content.rfind(". ", current_pos, end_pos)82                if sent_break > current_pos + chunk_size // 2:83                    end_pos = sent_break + 284                else:85                    # Look for any whitespace86                    space_break = content.rfind(" ", current_pos, end_pos)87                    if space_break > current_pos + chunk_size // 2:88                        end_pos = space_break + 18990        chunk = content[current_pos:end_pos].strip()91        if chunk:92            chunks.append(chunk)9394        current_pos = end_pos9596    return chunks979899async def recursive_summarize(100    content: str,101    user_prompt: str,102    model: str,103    level: int = 0,104    max_levels: int = 10,105    max_concurrent: int = 5,106    lang_directive: str = ""107) -> str:108    """109    Recursively summarize content using hierarchical reduction with parallel chunk processing.110111    Algorithm:112    1. Estimate tokens in content113    2. If fits in context → summarize directly114    3. If exceeds → split into chunks, summarize in parallel, combine, recurse115    4. Safety: max 10 levels, then truncate116117    Args:118        content: Text to summarize119        user_prompt: User's summarization instructions120        model: LLM model name121        level: Current recursion level (0-indexed)122        max_levels: Maximum recursion depth123        max_concurrent: Maximum concurrent LLM requests (default: 5)124125    Returns:126        Summary text127    """128    estimated_tokens = Gais.llm.estimate_tokens(content, model_id=model)129    safe_chars = Gais.llm.safe_input_chars(model_id=model)130131    # Log progress to stderr132    print(f"[Level {level}] Estimated tokens: {estimated_tokens:,}, Safe char limit: {safe_chars:,}", file=sys.stderr)133134    # Base case: content fits in context135    if len(content) <= safe_chars:136        print(f"[Level {level}] Content fits in context, generating summary...", file=sys.stderr)137138        system_prompt = (139            "You are an expert summarizer. Extract all key information, "140            "main points, and important details. Preserve critical facts and context."141        ) + lang_directive142143        user_message = (144            f"<user_prompt>{user_prompt}</user_prompt>\n\n"145            f"Content to summarize:\n\n"146            f"<content>{content}</content>"147        )148149        messages = [150            {"role": "system", "content": system_prompt},151            {"role": "user", "content": user_message},152        ]153154        return (await Gais.llm.chat_async(messages, model_id=model, temperature=0, thinking=False)).text155156    # Safety limit: max recursion depth157    if level >= max_levels:158        print(f"[Level {level}] ⚠️  Max recursion depth reached, truncating content...", file=sys.stderr)159160        # Truncate to safe char count161        truncated = content[:safe_chars]162163        system_prompt = (164            "You are an expert summarizer. Extract all key information, "165            "main points, and important details. Preserve critical facts and context. "166            "Note: Content was truncated due to length."167        ) + lang_directive168169        user_message = (170            f"<user_prompt>{user_prompt}</user_prompt>\n\n"171            f"Content to summarize (truncated):\n\n"172            f"<content>{truncated}</content>"173        )174175        messages = [176            {"role": "system", "content": system_prompt},177            {"role": "user", "content": user_message},178        ]179180        return (await Gais.llm.chat_async(messages, model_id=model, temperature=0, thinking=False)).text181182    # Recursive case: split and reduce with parallel processing183    reduction_needed = len(content) / safe_chars184185    # Adaptive group size (from background-task tool.py pattern)186    if reduction_needed > 8:187        num_chunks = 5188    elif reduction_needed > 4:189        num_chunks = 4190    else:191        num_chunks = 3192193    print(f"[Level {level}] Content exceeds limit ({reduction_needed:.2f}x), splitting into {num_chunks} chunks...", file=sys.stderr)194195    # Split content196    chunks = split_content_into_chunks(content, num_chunks, model)197    print(f"[Level {level}] Split into {len(chunks)} chunks", file=sys.stderr)198199    # Create semaphore for rate limiting200    semaphore = asyncio.Semaphore(max_concurrent)201202    # Summarize each chunk in parallel203    async def summarize_chunk(i: int, chunk: str) -> str:204        """Summarize a single chunk with error handling"""205        try:206            start_time = time.time()207            timestamp = time.strftime("%H:%M:%S", time.localtime(start_time))208            print(f"[Level {level}] [{timestamp}] 🚀 STARTING chunk {i+1}/{len(chunks)} (task launched)", file=sys.stderr)209210            summary_prompt = "Create a comprehensive summary that preserves all key information and important details."211            system_prompt = "You are an expert summarizer." + lang_directive212213            user_message = (214                f"<summary_prompt>{summary_prompt}</summary_prompt>\n\n"215                f"Content:\n\n"216                f"<content>{chunk}</content>"217            )218219            messages = [220                {"role": "system", "content": system_prompt},221                {"role": "user", "content": user_message},222            ]223224            llm_start = time.time()225            llm_timestamp = time.strftime("%H:%M:%S", time.localtime(llm_start))226            print(f"[Level {level}] [{llm_timestamp}] 📡 Sending LLM request for chunk {i+1}/{len(chunks)}", file=sys.stderr)227228            async with semaphore:229                summary = (await Gais.llm.chat_async(messages, model_id=model, temperature=0, thinking=False)).text230231            end_time = time.time()232            end_timestamp = time.strftime("%H:%M:%S", time.localtime(end_time))233            duration = end_time - start_time234            print(f"[Level {level}] [{end_timestamp}] ✅ COMPLETED chunk {i+1}/{len(chunks)} (took {duration:.2f}s)", file=sys.stderr)235            return summary236237        except Exception as e:238            error_time = time.time()239            error_timestamp = time.strftime("%H:%M:%S", time.localtime(error_time))240            print(f"[Level {level}] [{error_timestamp}] ⚠️  Error in chunk {i+1}: {e}", file=sys.stderr)241            # Return truncated chunk as fallback242            return f"[Partial] {chunk[:500]}..."243244    batch_start = time.time()245    batch_timestamp = time.strftime("%H:%M:%S", time.localtime(batch_start))246    print(f"[Level {level}] [{batch_timestamp}] ⚡ LAUNCHING {len(chunks)} chunks in PARALLEL (max {max_concurrent} concurrent)...", file=sys.stderr)247248    # Execute all chunk summaries in parallel249    chunk_summaries = await asyncio.gather(250        *[summarize_chunk(i, chunk) for i, chunk in enumerate(chunks)],251        return_exceptions=False  # Errors are handled in summarize_chunk252    )253254    batch_end = time.time()255    batch_end_timestamp = time.strftime("%H:%M:%S", time.localtime(batch_end))256    batch_duration = batch_end - batch_start257    print(f"[Level {level}] [{batch_end_timestamp}] 🎉 ALL {len(chunks)} chunks completed in {batch_duration:.2f}s (speedup: {len(chunks)}x vs sequential)", file=sys.stderr)258259    # Combine summaries and recurse260    combined = "\n\n".join(chunk_summaries)261    combined_tokens = Gais.llm.estimate_tokens(combined, model_id=model)262263    print(f"[Level {level}] Combined {len(chunk_summaries)} summaries ({combined_tokens:,} tokens), recursing to level {level+1}...", file=sys.stderr)264265    return await recursive_summarize(266        combined,267        user_prompt,268        model,269        level=level + 1,270        max_levels=max_levels,271        max_concurrent=max_concurrent,272        lang_directive=lang_directive273    )274275276def main():277    """Main execution function"""278    try:279        # Read execution input from stdin280        input_json = sys.stdin.read()281        execution_input = json.loads(input_json)282283        # Extract inputs284        inputs = execution_input.get("inputs", {})285        content = inputs.get("content")286        llm_model_id = inputs.get("llmModelId")287        prompt = inputs.get("prompt")288        language = inputs.get("language", "auto")289290        # Validate inputs291        if not content:292            raise ValueError("Required input 'content' not provided")293        if not llm_model_id:294            raise ValueError("Required input 'llmModelId' not provided")295        if not prompt:296            raise ValueError("Required input 'prompt' not provided")297298        print("="*80, file=sys.stderr)299        print(f"🚀 RECURSIVE SUMMARIZER v12 - Starting (GAIS LLM SDK)", file=sys.stderr)300        print("="*80, file=sys.stderr)301        print(f"📋 Selected Model ID: '{llm_model_id}'", file=sys.stderr)302        print(f"📄 Content length: {len(content):,} characters", file=sys.stderr)303        print(f"🔢 Estimated tokens: {Gais.llm.estimate_tokens(content, model_id=llm_model_id):,}", file=sys.stderr)304        print(f"⚡ Max concurrent requests: 5", file=sys.stderr)305        print("="*80, file=sys.stderr)306307        # Output-language directive rides in the SYSTEM prompt of every308        # recursion level (chunk summaries AND the combine pass), so it309        # cannot be overridden by language cues inside the source content.310        # '' / 'auto' = today's behavior; unknown values fall back to auto.311        lang_directive = language_directive(language)312313        # Perform recursive summarization with async execution314        summary = asyncio.run(recursive_summarize(content, prompt, llm_model_id, max_concurrent=5, lang_directive=lang_directive))315316        print(f"✓ Summarization complete! Summary length: {len(summary):,} characters", file=sys.stderr)317318        # Prepare output matching OUTPUT_SCHEMA319        output = {320            "summary": summary,321        }322323        # Write output to stdout324        print(json.dumps(output, indent=2))325326    except Exception as e:327        # Other errors328        error_output = {329            "error": str(e),330            "errorType": type(e).__name__,331            "traceback": traceback.format_exc(),332        }333        print(json.dumps(error_output), file=sys.stderr)334        sys.exit(1)335336337if __name__ == "__main__":338    main()

$ git log --oneline

v1.6.0
HEAD
2026-08-18
v1.5.12026-06-27
v1.5.02026-06-24
v1.3.02026-05-22
v1.0.02026-04-09