template_resolver.py
1 """ 2 Resolves template blocks within artifact content. 3 """ 4 5 import logging 6 import re 7 from typing import Any 8 9 from .liquid_renderer import render_liquid_template 10 11 log = logging.getLogger(__name__) 12 13 # Regex to match template blocks: «««template: params\ncontent\n»»» or «««template_liquid: params\ncontent\n»»» 14 # Supports both 'template:' (legacy) and 'template_liquid:' (new) 15 TEMPLATE_BLOCK_REGEX = re.compile( 16 r'«««template(?:_liquid)?:\s*([^\n]+)\n((?:(?!»»»).)*?)»»»', 17 re.DOTALL 18 ) 19 20 # Regex to parse parameters from template block header 21 TEMPLATE_PARAMS_REGEX = re.compile(r'(\w+)\s*=\s*"([^"]*)"') 22 23 24 async def resolve_template_blocks_in_string( 25 text: str, 26 artifact_service: Any, 27 session_context: dict[str, str], 28 log_identifier: str = "[TemplateResolver]", 29 ) -> str: 30 """ 31 Finds and resolves all template blocks in the given text. 32 33 Template blocks have the format: 34 «««template: data="filename.ext" jsonpath="$.path" limit="10" 35 ...template content... 36 »»» 37 38 Args: 39 text: The text containing potential template blocks 40 artifact_service: Service to load data artifacts 41 session_context: Dict with app_name, user_id, session_id 42 log_identifier: Identifier for logging 43 44 Returns: 45 Text with all template blocks resolved to their rendered output 46 """ 47 # Import here to avoid circular dependency 48 from ....agent.utils.artifact_helpers import load_artifact_content_or_metadata 49 50 # Find all template blocks 51 matches = list(TEMPLATE_BLOCK_REGEX.finditer(text)) 52 53 if not matches: 54 return text 55 56 log.info( 57 "%s Found %d template block(s) to resolve", 58 log_identifier, 59 len(matches), 60 ) 61 62 # Process each match and collect replacements 63 replacements = [] 64 for match in matches: 65 params_str = match.group(1) 66 template_content = match.group(2) 67 68 # Parse parameters 69 params = dict(TEMPLATE_PARAMS_REGEX.findall(params_str)) 70 71 log.info( 72 "%s Resolving template block with params: %s", 73 log_identifier, 74 params, 75 ) 76 77 data_artifact_spec = params.get("data") 78 if not data_artifact_spec: 79 error_msg = "[Template Error: Missing 'data' parameter]" 80 log.error("%s %s", log_identifier, error_msg) 81 replacements.append((match.start(), match.end(), error_msg)) 82 continue 83 84 # Parse data artifact spec (filename or filename:version) 85 artifact_parts = data_artifact_spec.split(":", 1) 86 filename = artifact_parts[0] 87 version = int(artifact_parts[1]) if len(artifact_parts) > 1 else "latest" 88 89 try: 90 # Load the data artifact with a large max_content_length (2MB) 91 # to ensure full JSON/YAML content is loaded for template rendering 92 artifact_data = await load_artifact_content_or_metadata( 93 artifact_service, 94 **session_context, 95 filename=filename, 96 version=version, 97 load_metadata_only=False, 98 max_content_length=2_000_000, # 2MB limit for template data 99 ) 100 101 if artifact_data.get("status") != "success": 102 error_msg = f"[Template Error: Failed to load data artifact '{filename}']" 103 log.error("%s %s", log_identifier, error_msg) 104 replacements.append((match.start(), match.end(), error_msg)) 105 continue 106 107 # Get artifact content and MIME type 108 artifact_content = artifact_data.get("content") 109 artifact_mime = artifact_data.get("mime_type", "text/plain") 110 111 # Parse optional parameters 112 jsonpath = params.get("jsonpath") 113 limit_str = params.get("limit") 114 limit = int(limit_str) if limit_str else None 115 116 # Render the template 117 rendered_output, render_error = render_liquid_template( 118 template_content=template_content, 119 data_artifact_content=artifact_content, 120 data_mime_type=artifact_mime, 121 jsonpath=jsonpath, 122 limit=limit, 123 log_identifier=log_identifier, 124 ) 125 126 if render_error: 127 log.error( 128 "%s Template rendering failed: %s", 129 log_identifier, 130 render_error, 131 ) 132 replacements.append((match.start(), match.end(), rendered_output)) 133 else: 134 log.info( 135 "%s Template rendered successfully. Output length: %d", 136 log_identifier, 137 len(rendered_output), 138 ) 139 replacements.append((match.start(), match.end(), rendered_output)) 140 141 except Exception as e: 142 error_msg = f"[Template Error: {str(e)}]" 143 log.exception( 144 "%s Exception during template resolution: %s", 145 log_identifier, 146 e, 147 ) 148 replacements.append((match.start(), match.end(), error_msg)) 149 150 # Apply all replacements from end to start to maintain positions 151 result = text 152 for start, end, replacement in reversed(replacements): 153 result = result[:start] + replacement + result[end:] 154 155 log.info( 156 "%s Resolved %d template block(s)", 157 log_identifier, 158 len(replacements), 159 ) 160 161 return result