# Copyright (c) Microsoft. All rights reserved. import logging from html import escape from typing import TYPE_CHECKING, Any from pydantic import PrivateAttr, field_validator from semantic_kernel.exceptions import TemplateRenderException from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.prompt_template.const import KERNEL_TEMPLATE_FORMAT_NAME from semantic_kernel.prompt_template.input_variable import InputVariable from semantic_kernel.prompt_template.prompt_template_base import PromptTemplateBase from semantic_kernel.template_engine.blocks.block import Block from semantic_kernel.template_engine.blocks.code_block import CodeBlock from semantic_kernel.template_engine.blocks.named_arg_block import NamedArgBlock from semantic_kernel.template_engine.blocks.var_block import VarBlock from semantic_kernel.template_engine.template_tokenizer import TemplateTokenizer if TYPE_CHECKING: from semantic_kernel.kernel import Kernel from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig logger: logging.Logger = logging.getLogger(__name__) class KernelPromptTemplate(PromptTemplateBase): """Create a Kernel prompt template.""" _blocks: list[Block] = PrivateAttr(default_factory=list) @field_validator("prompt_template_config") @classmethod def validate_template_format(cls, v: "PromptTemplateConfig") -> "PromptTemplateConfig": """Validate the template format.""" if v.template_format != KERNEL_TEMPLATE_FORMAT_NAME: raise ValueError(f"Invalid prompt template format: {v.template_format}. Expected: semantic-kernel") return v def model_post_init(self, _: Any) -> None: """Post init model.""" self._blocks = self.extract_blocks() # Add all of the existing input variables to our known set. We'll avoid adding any # dynamically discovered input variables with the same name. seen = {iv.name.lower() for iv in self.prompt_template_config.input_variables} # Enumerate every block in the template, adding any variables that are referenced. for block in self._blocks: if isinstance(block, VarBlock): # Add all variables from variable blocks, e.g. "{{$a}}". self._add_if_missing(block.name, seen) continue if isinstance(block, CodeBlock): for sub_block in block.tokens: if isinstance(sub_block, VarBlock): # Add all variables from code blocks, e.g. "{{p.bar $b}}". self._add_if_missing(sub_block.name, seen) continue if isinstance(sub_block, NamedArgBlock) and sub_block.variable: # Add all variables from named arguments, e.g. "{{p.bar b = $b}}". # represents a named argument for a function call. # For example, in the template {{ MyPlugin.MyFunction var1=$boo }}, var1=$boo # is a named arg block. self._add_if_missing(sub_block.variable.name, seen) def extract_blocks(self) -> list[Block]: """Given the prompt template, extract all the blocks (text, variables, function calls).""" logger.debug(f"Extracting blocks from template: {self.prompt_template_config.template}") if not self.prompt_template_config.template: return [] return TemplateTokenizer.tokenize(self.prompt_template_config.template) def _add_if_missing(self, variable_name: str, seen: set): # Convert variable_name to lower case to handle case-insensitivity if variable_name and variable_name.lower() not in seen: seen.add(variable_name.lower()) self.prompt_template_config.input_variables.append(InputVariable(name=variable_name)) async def render(self, kernel: "Kernel", arguments: "KernelArguments | None" = None) -> str: """Render the prompt template. Using the prompt template, replace the variables with their values and execute the functions replacing their reference with the function result. Args: kernel ("Kernel"): The kernel to use for functions. arguments ("KernelArguments | None"): The arguments to use for rendering. (Default value = None) Returns: str: The prompt template ready to be used for an AI request """ return await self.render_blocks(self._blocks, kernel, arguments) async def render_blocks( self, blocks: list[Block], kernel: "Kernel", arguments: "KernelArguments | None" = None ) -> str: """Given a list of blocks render each block and compose the final result. Args: blocks (list[Block]): Template blocks generated by ExtractBlocks kernel ("Kernel"): The kernel to use for functions arguments ("KernelArguments | None"): The arguments to use for rendering (Default value = None) Returns: str: The prompt template ready to be used for an AI request """ from semantic_kernel.template_engine.protocols.code_renderer import CodeRenderer from semantic_kernel.template_engine.protocols.text_renderer import TextRenderer logger.debug(f"Rendering list of {len(blocks)} blocks") rendered_blocks: list[str] = [] arguments = self._get_trusted_arguments(arguments or KernelArguments()) allow_unsafe_function_output = self._get_allow_dangerously_set_function_output() for block in blocks: if isinstance(block, TextRenderer): rendered_blocks.append(block.render(kernel, arguments)) continue if isinstance(block, CodeRenderer): try: rendered = await block.render_code(kernel, arguments) except Exception as exc: logger.error(f"Error rendering code block: {exc}") raise TemplateRenderException(f"Error rendering code block: {exc}") from exc rendered_blocks.append(rendered if allow_unsafe_function_output else escape(rendered)) prompt = "".join(rendered_blocks) logger.debug(f"Rendered prompt: {prompt}") return prompt @staticmethod def quick_render(template: str, arguments: dict[str, Any]) -> str: """Quick render a Kernel prompt template, only supports text and variable blocks. Args: template: The template to render arguments: The arguments to use for rendering Returns: str: The prompt template ready to be used for an AI request """ from semantic_kernel import Kernel from semantic_kernel.template_engine.protocols.code_renderer import CodeRenderer blocks = TemplateTokenizer.tokenize(template) if any(isinstance(block, CodeRenderer) for block in blocks): raise ValueError("Quick render does not support code blocks.") kernel = Kernel() return "".join([block.render(kernel, arguments) for block in blocks]) # type: ignore