Files
microsoft--semantic-kernel/python/semantic_kernel/prompt_template/kernel_prompt_template.py
T
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

152 lines
7.1 KiB
Python

# 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