$ 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-18v1.5.12026-06-27
v1.5.02026-06-24
v1.3.02026-05-22
v1.0.02026-04-09