/ src / solace_agent_mesh / common / utils / templates / template_resolver.py
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