chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
from typing import ClassVar
|
||||
|
||||
from pydantic import field_validator
|
||||
|
||||
from semantic_kernel.kernel_pydantic import KernelBaseModel
|
||||
from semantic_kernel.template_engine.blocks.block_types import BlockTypes
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Block(KernelBaseModel):
|
||||
"""A block."""
|
||||
|
||||
type: ClassVar[BlockTypes] = BlockTypes.UNDEFINED
|
||||
content: str
|
||||
|
||||
@field_validator("content", mode="before")
|
||||
@classmethod
|
||||
def content_strip(cls, content: str):
|
||||
"""Strip the content of the block."""
|
||||
return content.strip()
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from enum import Enum, auto
|
||||
|
||||
|
||||
class BlockTypes(Enum):
|
||||
"""Block types."""
|
||||
|
||||
UNDEFINED = auto()
|
||||
TEXT = auto()
|
||||
CODE = auto()
|
||||
VARIABLE = auto()
|
||||
VALUE = auto()
|
||||
FUNCTION_ID = auto()
|
||||
NAMED_ARG = auto()
|
||||
@@ -0,0 +1,173 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
from copy import copy
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
from pydantic import Field, field_validator, model_validator
|
||||
|
||||
from semantic_kernel.exceptions import CodeBlockRenderException, CodeBlockTokenError
|
||||
from semantic_kernel.exceptions.kernel_exceptions import KernelFunctionNotFoundError, KernelPluginNotFoundError
|
||||
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
|
||||
from semantic_kernel.template_engine.blocks.block import Block
|
||||
from semantic_kernel.template_engine.blocks.block_types import BlockTypes
|
||||
from semantic_kernel.template_engine.blocks.function_id_block import FunctionIdBlock
|
||||
from semantic_kernel.template_engine.blocks.named_arg_block import NamedArgBlock
|
||||
from semantic_kernel.template_engine.code_tokenizer import CodeTokenizer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
VALID_ARG_TYPES = [BlockTypes.VALUE, BlockTypes.VARIABLE, BlockTypes.NAMED_ARG]
|
||||
|
||||
|
||||
class CodeBlock(Block):
|
||||
"""Create a code block.
|
||||
|
||||
A code block is a block that usually contains functions to be executed by the kernel.
|
||||
It consists of a list of tokens that can be either a function_id, value, a variable or a named argument.
|
||||
|
||||
If the first token is not a function_id but a variable or value, the rest of the tokens will be ignored.
|
||||
Only the first argument for the function can be a variable or value, the rest of the tokens have be named arguments.
|
||||
|
||||
Args:
|
||||
content: The content of the code block.
|
||||
tokens: The list of tokens that compose the code block, if empty, will be created by the CodeTokenizer.
|
||||
|
||||
Raises:
|
||||
CodeBlockTokenError: If the content does not contain at least one token.
|
||||
CodeBlockTokenError: If the first token is a named argument.
|
||||
CodeBlockTokenError: If the second token is not a value or variable.
|
||||
CodeBlockTokenError: If a token is not a named argument after the second token.
|
||||
CodeBlockRenderError: If the plugin collection is not set in the kernel.
|
||||
CodeBlockRenderError: If the function is not found in the plugin collection.
|
||||
CodeBlockRenderError: If the function does not take any arguments, but it is being
|
||||
called in the template with arguments.
|
||||
"""
|
||||
|
||||
type: ClassVar[BlockTypes] = BlockTypes.CODE
|
||||
tokens: list[Block] = Field(default_factory=list)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def parse_content(cls, fields: Any) -> Any:
|
||||
"""Parse the content of the code block and tokenize it.
|
||||
|
||||
If tokens are already present, skip the tokenizing.
|
||||
"""
|
||||
if isinstance(fields, Block) or "tokens" in fields:
|
||||
return fields
|
||||
content = fields.get("content", "").strip()
|
||||
fields["tokens"] = CodeTokenizer.tokenize(content)
|
||||
return fields
|
||||
|
||||
@field_validator("tokens", mode="after")
|
||||
def check_tokens(cls, tokens: list[Block]) -> list[Block]:
|
||||
"""Check the tokens in the list.
|
||||
|
||||
If the first token is a value or variable, the rest of the tokens will be ignored.
|
||||
If the first token is a function_id, then the next token can be a value,
|
||||
variable or named_arg, the rest have to be named_args.
|
||||
|
||||
Raises:
|
||||
CodeBlockTokenError: If the content does not contain at least one token.
|
||||
CodeBlockTokenError: If the first token is a named argument.
|
||||
CodeBlockTokenError: If the second token is not a value or variable.
|
||||
CodeBlockTokenError: If a token is not a named argument after the second token.
|
||||
"""
|
||||
if not tokens:
|
||||
raise CodeBlockTokenError("The content should contain at least one token.")
|
||||
for index, token in enumerate(tokens):
|
||||
if index == 0 and token.type == BlockTypes.NAMED_ARG:
|
||||
raise CodeBlockTokenError(
|
||||
f"The first token needs to be a function_id, value or variable, got: {token.type}"
|
||||
)
|
||||
if index == 0 and token.type in [BlockTypes.VALUE, BlockTypes.VARIABLE]:
|
||||
if len(tokens) > 1:
|
||||
logger.warning(
|
||||
"The first token is a value or variable, but there are more tokens in the content, \
|
||||
these will be ignored."
|
||||
)
|
||||
return [token]
|
||||
if index == 1 and token.type not in VALID_ARG_TYPES:
|
||||
raise CodeBlockTokenError(
|
||||
f"Unexpected type for the second token type, should be variable, value or named_arg: {token.type}"
|
||||
)
|
||||
if index > 1 and token.type != BlockTypes.NAMED_ARG:
|
||||
raise CodeBlockTokenError(
|
||||
f"Every argument for the function after the first has to be a named arg, instead: {token.type}"
|
||||
)
|
||||
return tokens
|
||||
|
||||
async def render_code(self, kernel: "Kernel", arguments: "KernelArguments") -> str:
|
||||
"""Render the code block.
|
||||
|
||||
If the first token is a function_id, it will call the function from the plugin collection.
|
||||
Otherwise, it is a value or variable and those are then rendered directly.
|
||||
"""
|
||||
logger.debug(f"Rendering code: `{self.content}`")
|
||||
if isinstance(self.tokens[0], FunctionIdBlock):
|
||||
return await self._render_function_call(kernel, arguments)
|
||||
# validated that if the first token is not a function_id, it is a value or variable
|
||||
return self.tokens[0].render(kernel, arguments) # type: ignore
|
||||
|
||||
async def _render_function_call(self, kernel: "Kernel", arguments: "KernelArguments"):
|
||||
if not isinstance(self.tokens[0], FunctionIdBlock):
|
||||
raise CodeBlockRenderException("The first token should be a function_id")
|
||||
function_block: FunctionIdBlock = self.tokens[0]
|
||||
try:
|
||||
function = kernel.get_function(function_block.plugin_name, function_block.function_name)
|
||||
except (KernelFunctionNotFoundError, KernelPluginNotFoundError) as exc:
|
||||
error_msg = f"Function `{function_block.content}` not found"
|
||||
logger.error(error_msg)
|
||||
raise CodeBlockRenderException(error_msg) from exc
|
||||
|
||||
arguments_clone = copy(arguments)
|
||||
if len(self.tokens) > 1:
|
||||
arguments_clone = self._enrich_function_arguments(kernel, arguments_clone, function.metadata)
|
||||
try:
|
||||
result = await function.invoke(kernel, arguments_clone)
|
||||
except Exception as exc:
|
||||
error_msg = f"Error invoking function `{function_block.content}`"
|
||||
logger.error(error_msg)
|
||||
raise CodeBlockRenderException(error_msg) from exc
|
||||
return str(result) if result else ""
|
||||
|
||||
def _enrich_function_arguments(
|
||||
self,
|
||||
kernel: "Kernel",
|
||||
arguments: "KernelArguments",
|
||||
function_metadata: KernelFunctionMetadata,
|
||||
) -> "KernelArguments":
|
||||
if not function_metadata.parameters:
|
||||
raise CodeBlockRenderException(
|
||||
f"Function {function_metadata.plugin_name}.{function_metadata.name} does not take any arguments "
|
||||
f"but it is being called in the template with {len(self.tokens) - 1} arguments."
|
||||
)
|
||||
for index, token in enumerate(self.tokens[1:], start=1):
|
||||
logger.debug(f"Parsing variable/value: `{self.tokens[1].content}`")
|
||||
|
||||
# For VarBlock, get the raw value to preserve the original type
|
||||
# For other blocks (ValBlock, NamedArgBlock), render to string as usual
|
||||
from semantic_kernel.template_engine.blocks.var_block import VarBlock
|
||||
|
||||
if isinstance(token, VarBlock):
|
||||
rendered_value = token.get_value(arguments)
|
||||
elif isinstance(token, NamedArgBlock):
|
||||
# NamedArgBlock may contain a VarBlock, so check for that
|
||||
if token.variable:
|
||||
rendered_value = token.variable.get_value(arguments)
|
||||
else:
|
||||
rendered_value = token.render(kernel, arguments)
|
||||
else:
|
||||
rendered_value = token.render(kernel, arguments) # type: ignore
|
||||
|
||||
if not isinstance(token, NamedArgBlock) and index == 1:
|
||||
arguments[function_metadata.parameters[0].name] = rendered_value
|
||||
continue
|
||||
arguments[token.name] = rendered_value # type: ignore
|
||||
|
||||
return arguments
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
from re import compile
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
from pydantic import model_validator
|
||||
|
||||
from semantic_kernel.exceptions import FunctionIdBlockSyntaxError
|
||||
from semantic_kernel.template_engine.blocks.block import Block
|
||||
from semantic_kernel.template_engine.blocks.block_types import BlockTypes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
FUNCTION_ID_BLOCK_REGEX = r"^((?P<plugin>[0-9A-Za-z_]+)[.])?(?P<function>[0-9A-Za-z_]+)$"
|
||||
|
||||
FUNCTION_ID_BLOCK_MATCHER = compile(FUNCTION_ID_BLOCK_REGEX)
|
||||
|
||||
|
||||
class FunctionIdBlock(Block):
|
||||
"""Block to represent a function id. It can be used to call a function from a plugin.
|
||||
|
||||
The content is parsed using a regex, that returns either a plugin and
|
||||
function name or just a function name, depending on the content.
|
||||
|
||||
Anything other than that and a ValueError is raised.
|
||||
|
||||
Args:
|
||||
content (str): The content of the block.
|
||||
function_name (Optional[str], optional): The function name.
|
||||
plugin_name (Optional[str], optional): The plugin name.
|
||||
|
||||
Raises:
|
||||
ValueError: If the content does not have valid syntax.
|
||||
"""
|
||||
|
||||
type: ClassVar[BlockTypes] = BlockTypes.FUNCTION_ID
|
||||
function_name: str = ""
|
||||
plugin_name: str | None = None
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def parse_content(cls, fields: Any) -> dict[str, Any]:
|
||||
"""Parse the content of the function id block and extract the plugin and function name.
|
||||
|
||||
If both are present in the fields, return the fields as is.
|
||||
Otherwise, use the regex to extract the plugin and function name.
|
||||
"""
|
||||
if isinstance(fields, dict):
|
||||
if "plugin_name" in fields and "function_name" in fields:
|
||||
return fields
|
||||
content = fields.get("content", "").strip()
|
||||
matches = FUNCTION_ID_BLOCK_MATCHER.match(content)
|
||||
if not matches:
|
||||
raise FunctionIdBlockSyntaxError(content=content)
|
||||
if plugin := matches.groupdict().get("plugin"):
|
||||
fields["plugin_name"] = plugin
|
||||
fields["function_name"] = matches.group("function")
|
||||
return fields
|
||||
|
||||
def render(self, *_: "Kernel | KernelArguments | None") -> str:
|
||||
"""Render the function id block."""
|
||||
return self.content
|
||||
@@ -0,0 +1,98 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
from re import compile
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Optional
|
||||
|
||||
from pydantic import model_validator
|
||||
|
||||
from semantic_kernel.exceptions import NamedArgBlockSyntaxError
|
||||
from semantic_kernel.template_engine.blocks.block import Block
|
||||
from semantic_kernel.template_engine.blocks.block_types import BlockTypes
|
||||
from semantic_kernel.template_engine.blocks.val_block import ValBlock
|
||||
from semantic_kernel.template_engine.blocks.var_block import VarBlock
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
NAMED_ARG_REGEX = r"^(?P<name>[0-9A-Za-z_]+)[=]{1}(?P<value>[${1}](?P<var_name>[0-9A-Za-z_]+)|(?P<quote>[\"'])(?P<val>.[^\"^']*)(?P=quote))$" # noqa: E501
|
||||
|
||||
NAMED_ARG_MATCHER = compile(NAMED_ARG_REGEX)
|
||||
|
||||
|
||||
class NamedArgBlock(Block):
|
||||
"""Create a named argument block.
|
||||
|
||||
A named arg block is used to add arguments to a function call.
|
||||
It needs to be combined with a function_id block to be useful.
|
||||
Inside a code block, if the first block is a function_id block,
|
||||
the first block can be a variable or value block, anything else
|
||||
must be a named arg block.
|
||||
|
||||
The value inside the NamedArgBlock can be a ValBlock or a VarBlock.
|
||||
|
||||
Examples:
|
||||
{{ plugin.function arg1=$var }}
|
||||
{{ plugin.function arg1='value' }}
|
||||
{{ plugin.function 'value' arg2=$var }}
|
||||
{{ plugin.function $var arg2='value' }}
|
||||
{{ plugin_function arg1=$var1 arg2=$var2 arg3='value' }}
|
||||
|
||||
Args:
|
||||
content - str : The content of the named argument block,
|
||||
the name and value separated by an equal sign, for instance arg1=$var.
|
||||
name - str: The name of the argument.
|
||||
value - ValBlock: The value of the argument.
|
||||
variable - VarBlock: The variable of the argument.
|
||||
Either the value or variable field is used.
|
||||
|
||||
Raises:
|
||||
NamedArgBlockSyntaxError: If the content does not match the named argument syntax.
|
||||
|
||||
"""
|
||||
|
||||
type: ClassVar[BlockTypes] = BlockTypes.NAMED_ARG
|
||||
name: str | None = None
|
||||
value: ValBlock | None = None
|
||||
variable: VarBlock | None = None
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def parse_content(cls, fields: Any) -> Any:
|
||||
"""Parse the content of the named argument block and extract the name and value.
|
||||
|
||||
If the name and either value or variable is present the parsing is skipped.
|
||||
Otherwise, the content is parsed using a regex to extract the name and value.
|
||||
Those are then turned into Blocks.
|
||||
|
||||
Raises:
|
||||
NamedArgBlockSyntaxError: If the content does not match the named argument syntax.
|
||||
"""
|
||||
if isinstance(fields, Block) or ("name" in fields and ("value" in fields or "variable" in fields)):
|
||||
return fields
|
||||
content = fields.get("content", "").strip()
|
||||
matches = NAMED_ARG_MATCHER.match(content)
|
||||
if not matches:
|
||||
raise NamedArgBlockSyntaxError(content=content)
|
||||
matches_dict = matches.groupdict()
|
||||
if name := matches_dict.get("name"):
|
||||
fields["name"] = name
|
||||
if value := matches_dict.get("value"):
|
||||
if matches_dict.get("var_name"):
|
||||
fields["variable"] = VarBlock(content=value, name=matches_dict["var_name"])
|
||||
elif matches_dict.get("val"):
|
||||
fields["value"] = ValBlock(content=value, value=matches_dict["val"], quote=matches_dict["quote"])
|
||||
return fields
|
||||
|
||||
def render(self, kernel: "Kernel", arguments: Optional["KernelArguments"] = None) -> Any:
|
||||
"""Render the named argument block."""
|
||||
if self.value:
|
||||
return self.value.render()
|
||||
if arguments is None:
|
||||
return ""
|
||||
if self.variable:
|
||||
return self.variable.render(kernel, arguments)
|
||||
return None
|
||||
@@ -0,0 +1,22 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Symbols(str, Enum):
|
||||
"""Symbols used in the template engine."""
|
||||
|
||||
BLOCK_STARTER = "{"
|
||||
BLOCK_ENDER = "}"
|
||||
|
||||
VAR_PREFIX = "$"
|
||||
|
||||
DBL_QUOTE = '"'
|
||||
SGL_QUOTE = "'"
|
||||
ESCAPE_CHAR = "\\"
|
||||
|
||||
SPACE = " "
|
||||
TAB = "\t"
|
||||
NEW_LINE = "\n"
|
||||
CARRIAGE_RETURN = "\r"
|
||||
|
||||
NAMED_ARG_BLOCK_SEPARATOR = "="
|
||||
@@ -0,0 +1,59 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, ClassVar, Optional
|
||||
|
||||
from pydantic import field_validator
|
||||
|
||||
from semantic_kernel.template_engine.blocks.block import Block
|
||||
from semantic_kernel.template_engine.blocks.block_types import BlockTypes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TextBlock(Block):
|
||||
"""A block with text content."""
|
||||
|
||||
type: ClassVar[BlockTypes] = BlockTypes.TEXT
|
||||
|
||||
@field_validator("content", mode="before")
|
||||
@classmethod
|
||||
def content_strip(cls, content: str):
|
||||
"""Strip the content of the text block.
|
||||
|
||||
Overload strip method, text blocks are not stripped.
|
||||
"""
|
||||
return content
|
||||
|
||||
@classmethod
|
||||
def from_text(
|
||||
cls,
|
||||
text: str | None = None,
|
||||
start_index: int | None = None,
|
||||
stop_index: int | None = None,
|
||||
):
|
||||
"""Create a text block from a string."""
|
||||
if text is None:
|
||||
return cls(content="")
|
||||
if start_index is not None and stop_index is not None:
|
||||
if start_index > stop_index:
|
||||
raise ValueError(f"start_index ({start_index}) must be less than stop_index ({stop_index})")
|
||||
|
||||
if start_index < 0:
|
||||
raise ValueError(f"start_index ({start_index}) must be greater than 0")
|
||||
|
||||
text = text[start_index:stop_index]
|
||||
elif start_index is not None:
|
||||
text = text[start_index:]
|
||||
elif stop_index is not None:
|
||||
text = text[:stop_index]
|
||||
|
||||
return cls(content=text)
|
||||
|
||||
def render(self, *_: tuple[Optional["Kernel"], Optional["KernelArguments"]]) -> str:
|
||||
"""Render the text block."""
|
||||
return self.content
|
||||
@@ -0,0 +1,74 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
from re import S, compile
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
from pydantic import model_validator
|
||||
|
||||
from semantic_kernel.exceptions import ValBlockSyntaxError
|
||||
from semantic_kernel.template_engine.blocks.block import Block
|
||||
from semantic_kernel.template_engine.blocks.block_types import BlockTypes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
VAL_BLOCK_REGEX = r"^(?P<quote>[\"'])(?P<value>.*)(?P=quote)$"
|
||||
|
||||
VAL_BLOCK_MATCHER = compile(VAL_BLOCK_REGEX, flags=S)
|
||||
|
||||
|
||||
class ValBlock(Block):
|
||||
"""Create a value block.
|
||||
|
||||
A value block is used to represent a value in a template.
|
||||
It can be used to represent any characters.
|
||||
It needs to start and end with the same quote character,
|
||||
can be both single or double quotes, as long as they are not mixed.
|
||||
|
||||
Examples:
|
||||
'value'
|
||||
"value"
|
||||
'value with "quotes"'
|
||||
"value with 'quotes'"
|
||||
|
||||
Args:
|
||||
content - str : The content of the value block.
|
||||
value - str: The value of the block.
|
||||
quote - str: The quote used to wrap the value.
|
||||
|
||||
Raises:
|
||||
ValBlockSyntaxError: If the content does not match the value block syntax.
|
||||
|
||||
"""
|
||||
|
||||
type: ClassVar[BlockTypes] = BlockTypes.VALUE
|
||||
value: str | None = ""
|
||||
quote: str | None = "'"
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def parse_content(cls, fields: Any) -> Any:
|
||||
"""Parse the content and extract the value and quote.
|
||||
|
||||
The parsing is based on a regex that returns the value and quote.
|
||||
if the 'value' is already present then the parsing is skipped.
|
||||
"""
|
||||
if isinstance(fields, Block) or "value" in fields:
|
||||
return fields
|
||||
content = fields.get("content", "").strip()
|
||||
matches = VAL_BLOCK_MATCHER.match(content)
|
||||
if not matches:
|
||||
raise ValBlockSyntaxError(content=content)
|
||||
if value := matches.groupdict().get("value"):
|
||||
fields["value"] = value
|
||||
if quote := matches.groupdict().get("quote"):
|
||||
fields["quote"] = quote
|
||||
return fields
|
||||
|
||||
def render(self, *_: "Kernel | KernelArguments | None") -> str:
|
||||
"""Render the value block."""
|
||||
return self.value or ""
|
||||
@@ -0,0 +1,99 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
from re import compile
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Optional
|
||||
|
||||
from pydantic import model_validator
|
||||
|
||||
from semantic_kernel.exceptions import VarBlockRenderError, VarBlockSyntaxError
|
||||
from semantic_kernel.template_engine.blocks.block import Block
|
||||
from semantic_kernel.template_engine.blocks.block_types import BlockTypes
|
||||
from semantic_kernel.template_engine.blocks.symbols import Symbols
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
VAR_BLOCK_REGEX = r"^[${1}](?P<name>[0-9A-Za-z_]+)$"
|
||||
|
||||
VAR_BLOCK_MATCHER = compile(VAR_BLOCK_REGEX)
|
||||
|
||||
|
||||
class VarBlock(Block):
|
||||
"""Create a variable block.
|
||||
|
||||
A variable block is used to add a variable to a template.
|
||||
It gets rendered from KernelArguments, if the variable is not found
|
||||
a warning is logged and an empty string is returned.
|
||||
The variable must start with $ and be followed by a valid variable name.
|
||||
A valid variable name is a string of letters, numbers and underscores.
|
||||
|
||||
Examples:
|
||||
$var
|
||||
$test_var
|
||||
|
||||
Args:
|
||||
content - str : The content of the variable block, the name of the variable.
|
||||
name - str: The name of the variable.
|
||||
|
||||
Raises:
|
||||
VarBlockSyntaxError: If the content does not match the variable syntax.
|
||||
|
||||
"""
|
||||
|
||||
type: ClassVar[BlockTypes] = BlockTypes.VARIABLE
|
||||
name: str = ""
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def parse_content(cls, fields: Any) -> Any:
|
||||
"""Parse the content and extract the name.
|
||||
|
||||
The parsing is based on a regex that returns the name.
|
||||
if the 'name' is already present then the parsing is skipped.
|
||||
"""
|
||||
if isinstance(fields, Block) or "name" in fields:
|
||||
return fields
|
||||
content = fields.get("content", "").strip()
|
||||
matches = VAR_BLOCK_MATCHER.match(content)
|
||||
if not matches:
|
||||
raise VarBlockSyntaxError(content=content)
|
||||
if name := matches.groupdict().get("name"):
|
||||
fields["name"] = name
|
||||
return fields
|
||||
|
||||
def render(self, _: "Kernel", arguments: Optional["KernelArguments"] = None) -> str:
|
||||
"""Render the variable block with the given arguments.
|
||||
|
||||
If the variable is not found in the arguments, return an empty string.
|
||||
"""
|
||||
if arguments is None:
|
||||
return ""
|
||||
value = arguments.get(self.name, None)
|
||||
if value is None:
|
||||
logger.warning(f"Variable `{Symbols.VAR_PREFIX}: {self.name}` not found in the KernelArguments")
|
||||
return ""
|
||||
try:
|
||||
return str(value)
|
||||
except Exception as e:
|
||||
raise VarBlockRenderError(
|
||||
f"Block {self.name} failed to be parsed to a string, type is {type(value)}"
|
||||
) from e
|
||||
|
||||
def get_value(self, arguments: "KernelArguments | None" = None) -> Any:
|
||||
"""Get the raw value of the variable from arguments without converting to string.
|
||||
|
||||
This is used when passing arguments to functions to preserve their original types.
|
||||
|
||||
Args:
|
||||
arguments: The KernelArguments to get the value from.
|
||||
|
||||
Returns:
|
||||
The raw value from the arguments, or None if not found.
|
||||
"""
|
||||
if arguments is None:
|
||||
return None
|
||||
return arguments.get(self.name, None)
|
||||
Reference in New Issue
Block a user