mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> Fixes the mypy pre-commit job, and subsequent fixes from there. Moved all python tests to use the uv setup task instead of manual cache, python install and uv install. ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone 😄
152 lines
7.1 KiB
Python
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
|