chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from semantic_kernel.prompt_template.utils.handlebars_system_helpers import HANDLEBAR_SYSTEM_HELPERS
|
||||
from semantic_kernel.prompt_template.utils.jinja2_system_helpers import JINJA2_SYSTEM_HELPERS
|
||||
from semantic_kernel.prompt_template.utils.template_function_helpers import create_template_helper_from_function
|
||||
|
||||
__all__ = [
|
||||
"HANDLEBAR_SYSTEM_HELPERS",
|
||||
"JINJA2_SYSTEM_HELPERS",
|
||||
"create_template_helper_from_function",
|
||||
]
|
||||
@@ -0,0 +1,186 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from xml.etree.ElementTree import Element, SubElement, tostring # nosec B405
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _messages(this, options, *args, **kwargs):
|
||||
from semantic_kernel.contents.chat_history import ChatHistory
|
||||
|
||||
if not isinstance(this.context["chat_history"], ChatHistory):
|
||||
return ""
|
||||
return this.context["chat_history"].to_prompt()
|
||||
|
||||
|
||||
def _message_to_prompt(this, *args, **kwargs):
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
|
||||
if isinstance(this.context, ChatMessageContent):
|
||||
return str(this.context.to_prompt())
|
||||
return str(this.context)
|
||||
|
||||
|
||||
def _message(this, options, *args, **kwargs):
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
from semantic_kernel.contents.const import CHAT_MESSAGE_CONTENT_TAG
|
||||
|
||||
# When the context is a ChatMessageContent, delegate to to_element() so that
|
||||
# the XML contract is consistent with the Jinja2 path.
|
||||
if isinstance(this.context, ChatMessageContent):
|
||||
message = this.context.to_element()
|
||||
return tostring(message, encoding="unicode", short_empty_elements=False)
|
||||
|
||||
# Fallback: build the element manually from kwargs and block content.
|
||||
from semantic_kernel.contents.const import TEXT_CONTENT_TAG
|
||||
|
||||
message = Element(CHAT_MESSAGE_CONTENT_TAG)
|
||||
for key, value in kwargs.items():
|
||||
if isinstance(value, Enum):
|
||||
value = value.value
|
||||
if value is not None:
|
||||
message.set(key, str(value))
|
||||
try:
|
||||
content = str(options["fn"](this))
|
||||
except Exception: # pragma: no cover
|
||||
content = ""
|
||||
if content:
|
||||
text_elem = SubElement(message, TEXT_CONTENT_TAG)
|
||||
text_elem.text = content
|
||||
return tostring(message, encoding="unicode", short_empty_elements=False)
|
||||
|
||||
|
||||
def _set(this, *args, **kwargs):
|
||||
if "name" in kwargs and "value" in kwargs:
|
||||
this.context[kwargs["name"]] = kwargs["value"]
|
||||
if len(args) == 2 and isinstance(args[0], str):
|
||||
this.context[args[0]] = args[1]
|
||||
return ""
|
||||
|
||||
|
||||
def _get(this, *args, **kwargs):
|
||||
if len(args) == 0:
|
||||
return ""
|
||||
return this.context.get(args[0], "")
|
||||
|
||||
|
||||
def _array(this, *args, **kwargs):
|
||||
return list(args)
|
||||
|
||||
|
||||
def _range(this, *args, **kwargs):
|
||||
args = list(args)
|
||||
for index, arg in enumerate(args):
|
||||
if not isinstance(arg, int):
|
||||
try:
|
||||
args[index] = int(arg)
|
||||
except ValueError:
|
||||
args.pop(index)
|
||||
if len(args) == 1:
|
||||
return list(range(args[0]))
|
||||
if len(args) == 2:
|
||||
return list(range(args[0], args[1]))
|
||||
if len(args) == 3:
|
||||
return list(range(args[0], args[1], args[2]))
|
||||
return []
|
||||
|
||||
|
||||
def _concat(this, *args, **kwargs):
|
||||
return "".join([str(value) for value in args])
|
||||
|
||||
|
||||
def _or(this, *args, **kwargs):
|
||||
return any(args)
|
||||
|
||||
|
||||
def _add(this, *args, **kwargs):
|
||||
return sum([float(value) for value in args])
|
||||
|
||||
|
||||
def _subtract(this, *args, **kwargs):
|
||||
return float(args[0]) - sum([float(value) for value in args[1:]])
|
||||
|
||||
|
||||
def _equals(this, *args, **kwargs):
|
||||
return args[0] == args[1]
|
||||
|
||||
|
||||
def _less_than(this, *args, **kwargs):
|
||||
return float(args[0]) < float(args[1])
|
||||
|
||||
|
||||
def _greater_than(this, *args, **kwargs):
|
||||
return float(args[0]) > float(args[1])
|
||||
|
||||
|
||||
def _less_than_or_equal(this, *args, **kwargs):
|
||||
return float(args[0]) <= float(args[1])
|
||||
|
||||
|
||||
def _greater_than_or_equal(this, *args, **kwargs):
|
||||
return float(args[0]) >= float(args[1])
|
||||
|
||||
|
||||
def _json(this, *args, **kwargs):
|
||||
if not args:
|
||||
return ""
|
||||
return json.dumps(args[0])
|
||||
|
||||
|
||||
def _double_open(this, *args, **kwargs):
|
||||
return "{{"
|
||||
|
||||
|
||||
def _double_close(this, *args, **kwargs):
|
||||
return "}}"
|
||||
|
||||
|
||||
def _camel_case(this, *args, **kwargs):
|
||||
return "".join([word.capitalize() for word in args[0].split("_")])
|
||||
|
||||
|
||||
def _snake_case(this, *args, **kwargs):
|
||||
arg = args[0]
|
||||
arg = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", arg)
|
||||
arg = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", arg)
|
||||
arg = arg.replace("-", "_")
|
||||
return arg.lower()
|
||||
|
||||
|
||||
HANDLEBAR_SYSTEM_HELPERS: dict[str, Callable] = {
|
||||
"set": _set,
|
||||
"get": _get,
|
||||
"array": _array,
|
||||
"range": _range,
|
||||
"concat": _concat,
|
||||
"or": _or,
|
||||
"add": _add,
|
||||
"subtract": _subtract,
|
||||
"equals": _equals,
|
||||
"less_than": _less_than,
|
||||
"lessThan": _less_than,
|
||||
"greater_than": _greater_than,
|
||||
"greaterThan": _greater_than,
|
||||
"less_than_or_equal": _less_than_or_equal,
|
||||
"lessThanOrEqual": _less_than_or_equal,
|
||||
"greater_than_or_equal": _greater_than_or_equal,
|
||||
"greaterThanOrEqual": _greater_than_or_equal,
|
||||
"json": _json,
|
||||
"double_open": _double_open,
|
||||
"doubleOpen": _double_open,
|
||||
"double_close": _double_close,
|
||||
"doubleClose": _double_close,
|
||||
"camel_case": _camel_case,
|
||||
"camelCase": _camel_case,
|
||||
"snake_case": _snake_case,
|
||||
"snakeCase": _snake_case,
|
||||
"message": _message,
|
||||
"message_to_prompt": _message_to_prompt,
|
||||
"messageToPrompt": _message_to_prompt,
|
||||
"messages": _messages,
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from xml.etree.ElementTree import tostring # nosec B405
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _messages(chat_history):
|
||||
from semantic_kernel.contents.chat_history import ChatHistory
|
||||
|
||||
if not isinstance(chat_history, ChatHistory):
|
||||
return ""
|
||||
return chat_history.to_prompt()
|
||||
|
||||
|
||||
def _message_to_prompt(context):
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
|
||||
if isinstance(context, ChatMessageContent):
|
||||
return str(context.to_prompt())
|
||||
return str(context)
|
||||
|
||||
|
||||
def _message(item):
|
||||
message = item.to_element()
|
||||
return tostring(message, encoding="unicode", short_empty_elements=False)
|
||||
|
||||
|
||||
# Wrap the _get function to safely handle calls without arguments
|
||||
def _safe_get_wrapper(context=None, name=None, default=""):
|
||||
if context is None or name is None:
|
||||
return default
|
||||
return _get(context, name, default)
|
||||
|
||||
|
||||
def _get(context, name, default=""):
|
||||
"""Retrieves a value from the context, with a default if not found."""
|
||||
return context.get(name, default)
|
||||
|
||||
|
||||
def _double_open():
|
||||
"""Returns the string representing double open braces."""
|
||||
return "{{"
|
||||
|
||||
|
||||
def _double_close():
|
||||
"""Returns the string representing double close braces."""
|
||||
return "}}"
|
||||
|
||||
|
||||
def _array(*args, **kwargs):
|
||||
return list(args)
|
||||
|
||||
|
||||
def _camel_case(*args, **kwargs):
|
||||
return "".join([word.capitalize() for word in args[0].split("_")])
|
||||
|
||||
|
||||
def _snake_case(*args, **kwargs):
|
||||
arg = args[0]
|
||||
arg = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", arg)
|
||||
arg = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", arg)
|
||||
arg = arg.replace("-", "_")
|
||||
return arg.lower()
|
||||
|
||||
|
||||
JINJA2_SYSTEM_HELPERS: dict[str, Callable] = {
|
||||
"get": _safe_get_wrapper,
|
||||
"double_open": _double_open,
|
||||
"doubleOpen": _double_open,
|
||||
"double_close": _double_close,
|
||||
"doubleClose": _double_close,
|
||||
"message": _message,
|
||||
"message_to_prompt": _message_to_prompt,
|
||||
"messages": _messages,
|
||||
"messageToPrompt": _message_to_prompt,
|
||||
"array": _array,
|
||||
"camel_case": _camel_case,
|
||||
"camelCase": _camel_case,
|
||||
"snake_case": _snake_case,
|
||||
"snakeCase": _snake_case,
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from html import escape
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import nest_asyncio
|
||||
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
from semantic_kernel.prompt_template.const import (
|
||||
HANDLEBARS_TEMPLATE_FORMAT_NAME,
|
||||
JINJA2_TEMPLATE_FORMAT_NAME,
|
||||
TEMPLATE_FORMAT_TYPES,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.functions.kernel_function import KernelFunction
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_template_helper_from_function(
|
||||
function: "KernelFunction",
|
||||
kernel: "Kernel",
|
||||
base_arguments: "KernelArguments",
|
||||
template_format: TEMPLATE_FORMAT_TYPES,
|
||||
allow_dangerously_set_content: bool = False,
|
||||
enable_async: bool = False,
|
||||
) -> Callable[..., Any]:
|
||||
"""Create a helper function for both the Handlebars and Jinja2 templating engines from a kernel function.
|
||||
|
||||
Args:
|
||||
function (KernelFunction): The kernel function to create a helper for.
|
||||
kernel (Kernel): The kernel to use for invoking the function.
|
||||
base_arguments (KernelArguments): The base arguments to use when invoking the function.
|
||||
template_format (TEMPLATE_FORMAT_TYPES): The template format to create the helper for.
|
||||
allow_dangerously_set_content (bool, optional): Return the content of the function result
|
||||
without encoding it or not.
|
||||
enable_async (bool, optional): Enable async helper function. Defaults to False.
|
||||
Currently only works for Jinja2 templates.
|
||||
|
||||
Returns:
|
||||
The function with args that are callable by the different templates.
|
||||
|
||||
Raises:
|
||||
ValueError: If the template format is not supported.
|
||||
|
||||
"""
|
||||
if enable_async:
|
||||
return _create_async_template_helper_from_function(
|
||||
function=function,
|
||||
kernel=kernel,
|
||||
base_arguments=base_arguments,
|
||||
template_format=template_format,
|
||||
allow_dangerously_set_content=allow_dangerously_set_content,
|
||||
)
|
||||
return _create_sync_template_helper_from_function(
|
||||
function=function,
|
||||
kernel=kernel,
|
||||
base_arguments=base_arguments,
|
||||
template_format=template_format,
|
||||
allow_dangerously_set_content=allow_dangerously_set_content,
|
||||
)
|
||||
|
||||
|
||||
def _create_sync_template_helper_from_function(
|
||||
function: "KernelFunction",
|
||||
kernel: "Kernel",
|
||||
base_arguments: "KernelArguments",
|
||||
template_format: TEMPLATE_FORMAT_TYPES,
|
||||
allow_dangerously_set_content: bool = False,
|
||||
) -> Callable[..., Any]:
|
||||
"""Create a helper function for both the Handlebars and Jinja2 templating engines from a kernel function."""
|
||||
if template_format not in [JINJA2_TEMPLATE_FORMAT_NAME, HANDLEBARS_TEMPLATE_FORMAT_NAME]:
|
||||
raise ValueError(f"Invalid template format: {template_format}")
|
||||
|
||||
if not getattr(asyncio, "_nest_patched", False):
|
||||
nest_asyncio.apply()
|
||||
|
||||
def func(*args, **kwargs):
|
||||
arguments = KernelArguments()
|
||||
if base_arguments and base_arguments.execution_settings:
|
||||
arguments.execution_settings = base_arguments.execution_settings # pragma: no cover
|
||||
arguments.update(base_arguments)
|
||||
arguments.update(kwargs)
|
||||
|
||||
if len(args) > 0 and template_format == HANDLEBARS_TEMPLATE_FORMAT_NAME:
|
||||
this = args[0]
|
||||
actual_args = args[1:]
|
||||
else:
|
||||
this = None
|
||||
actual_args = args
|
||||
|
||||
if this is not None:
|
||||
logger.debug(f"Handlebars context with `this`: {this}")
|
||||
else:
|
||||
logger.debug("Jinja2 context or Handlebars context without `this`")
|
||||
|
||||
logger.debug(
|
||||
f"Invoking function {function.metadata.fully_qualified_name} "
|
||||
f"with args: {actual_args} and kwargs: {kwargs} and this: {this}."
|
||||
)
|
||||
|
||||
result = asyncio.run(function.invoke(kernel=kernel, arguments=arguments))
|
||||
if allow_dangerously_set_content:
|
||||
return result
|
||||
return escape(str(result))
|
||||
|
||||
return func
|
||||
|
||||
|
||||
def _create_async_template_helper_from_function(
|
||||
function: "KernelFunction",
|
||||
kernel: "Kernel",
|
||||
base_arguments: "KernelArguments",
|
||||
template_format: TEMPLATE_FORMAT_TYPES,
|
||||
allow_dangerously_set_content: bool = False,
|
||||
) -> Callable[..., Any]:
|
||||
"""Create a async helper function for Jinja2 templating engines from a kernel function."""
|
||||
if template_format not in [JINJA2_TEMPLATE_FORMAT_NAME]:
|
||||
raise ValueError(f"Invalid template format: {template_format}")
|
||||
|
||||
async def func(*args, **kwargs):
|
||||
arguments = KernelArguments()
|
||||
if base_arguments and base_arguments.execution_settings:
|
||||
arguments.execution_settings = base_arguments.execution_settings # pragma: no cover
|
||||
arguments.update(base_arguments)
|
||||
arguments.update(kwargs)
|
||||
logger.debug(
|
||||
f"Invoking function {function.metadata.fully_qualified_name} with args: {arguments} and kwargs: {kwargs}."
|
||||
)
|
||||
result = await function.invoke(kernel=kernel, arguments=arguments)
|
||||
if allow_dangerously_set_content:
|
||||
return result
|
||||
return escape(str(result))
|
||||
|
||||
return func
|
||||
Reference in New Issue
Block a user