chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user