chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,416 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import AsyncGenerator, Mapping, Sequence
|
||||
from html import unescape
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import yaml
|
||||
from pydantic import Field, ValidationError, model_validator
|
||||
|
||||
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
|
||||
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
|
||||
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
|
||||
from semantic_kernel.connectors.ai.text_to_audio_client_base import TextToAudioClientBase
|
||||
from semantic_kernel.connectors.ai.text_to_image_client_base import TextToImageClientBase
|
||||
from semantic_kernel.const import DEFAULT_SERVICE_NAME
|
||||
from semantic_kernel.contents.audio_content import AudioContent
|
||||
from semantic_kernel.contents.chat_history import ChatHistory
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
from semantic_kernel.contents.image_content import ImageContent
|
||||
from semantic_kernel.contents.text_content import TextContent
|
||||
from semantic_kernel.exceptions import FunctionExecutionException, FunctionInitializationError
|
||||
from semantic_kernel.exceptions.function_exceptions import PromptRenderingException
|
||||
from semantic_kernel.filters.filter_types import FilterTypes
|
||||
from semantic_kernel.filters.functions.function_invocation_context import FunctionInvocationContext
|
||||
from semantic_kernel.filters.kernel_filters_extension import _rebuild_prompt_render_context
|
||||
from semantic_kernel.filters.prompts.prompt_render_context import PromptRenderContext
|
||||
from semantic_kernel.functions.function_result import FunctionResult
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
from semantic_kernel.functions.kernel_function import TEMPLATE_FORMAT_MAP, KernelFunction
|
||||
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
|
||||
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
|
||||
from semantic_kernel.functions.prompt_rendering_result import PromptRenderingResult
|
||||
from semantic_kernel.prompt_template.const import KERNEL_TEMPLATE_FORMAT_NAME, TEMPLATE_FORMAT_TYPES
|
||||
from semantic_kernel.prompt_template.prompt_template_base import PromptTemplateBase
|
||||
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
PROMPT_FILE_NAME = "skprompt.txt"
|
||||
CONFIG_FILE_NAME = "config.json"
|
||||
PROMPT_RETURN_PARAM = KernelParameterMetadata(
|
||||
name="return",
|
||||
description="The completion result",
|
||||
default_value=None,
|
||||
type="FunctionResult", # type: ignore
|
||||
is_required=True,
|
||||
)
|
||||
|
||||
|
||||
class KernelFunctionFromPrompt(KernelFunction):
|
||||
"""Semantic Kernel Function from a prompt."""
|
||||
|
||||
prompt_template: PromptTemplateBase
|
||||
prompt_execution_settings: dict[str, PromptExecutionSettings] = Field(default_factory=dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
function_name: str,
|
||||
plugin_name: str | None = None,
|
||||
description: str | None = None,
|
||||
prompt: str | None = None,
|
||||
template_format: TEMPLATE_FORMAT_TYPES = KERNEL_TEMPLATE_FORMAT_NAME,
|
||||
prompt_template: PromptTemplateBase | None = None,
|
||||
prompt_template_config: PromptTemplateConfig | None = None,
|
||||
prompt_execution_settings: PromptExecutionSettings
|
||||
| Sequence[PromptExecutionSettings]
|
||||
| Mapping[str, PromptExecutionSettings]
|
||||
| None = None,
|
||||
) -> None:
|
||||
"""Initializes a new instance of the KernelFunctionFromPrompt class.
|
||||
|
||||
Args:
|
||||
function_name (str): The name of the function
|
||||
plugin_name (str): The name of the plugin
|
||||
description (str): The description for the function
|
||||
|
||||
prompt (Optional[str]): The prompt
|
||||
template_format (Optional[str]): The template format, default is "semantic-kernel"
|
||||
prompt_template (Optional[KernelPromptTemplate]): The prompt template
|
||||
prompt_template_config (Optional[PromptTemplateConfig]): The prompt template configuration
|
||||
prompt_execution_settings (Optional): instance, list or dict of PromptExecutionSettings to be used
|
||||
by the function, can also be supplied through prompt_template_config,
|
||||
but the supplied one is used if both are present.
|
||||
prompt_template_config (Optional[PromptTemplateConfig]): the prompt template config.
|
||||
"""
|
||||
if not prompt and not prompt_template_config and not prompt_template:
|
||||
raise FunctionInitializationError(
|
||||
"The prompt cannot be empty, must be supplied directly, \
|
||||
through prompt_template_config or in the prompt_template."
|
||||
)
|
||||
|
||||
if prompt and prompt_template_config and prompt_template_config.template != prompt:
|
||||
logger.warning(
|
||||
f"Prompt ({prompt}) and PromptTemplateConfig ({prompt_template_config.template}) both supplied, "
|
||||
"using the template in PromptTemplateConfig, ignoring prompt."
|
||||
)
|
||||
if template_format and prompt_template_config and prompt_template_config.template_format != template_format:
|
||||
logger.warning(
|
||||
f"Template ({template_format}) and PromptTemplateConfig ({prompt_template_config.template_format}) "
|
||||
"both supplied, using the template format in PromptTemplateConfig, ignoring template."
|
||||
)
|
||||
if not prompt_template:
|
||||
if not prompt_template_config:
|
||||
# prompt must be there if prompt_template and prompt_template_config is not supplied
|
||||
prompt_template_config = PromptTemplateConfig(
|
||||
name=function_name,
|
||||
description=description,
|
||||
template=prompt,
|
||||
template_format=template_format,
|
||||
)
|
||||
elif not prompt_template_config.template:
|
||||
prompt_template_config.template = prompt
|
||||
prompt_template = TEMPLATE_FORMAT_MAP[prompt_template_config.template_format](
|
||||
prompt_template_config=prompt_template_config
|
||||
) # type: ignore
|
||||
|
||||
try:
|
||||
metadata = KernelFunctionMetadata(
|
||||
name=function_name,
|
||||
plugin_name=plugin_name,
|
||||
description=description,
|
||||
parameters=prompt_template.prompt_template_config.get_kernel_parameter_metadata(), # type: ignore
|
||||
is_prompt=True,
|
||||
is_asynchronous=True,
|
||||
return_parameter=PROMPT_RETURN_PARAM,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
raise FunctionInitializationError("Failed to create KernelFunctionMetadata") from exc
|
||||
super().__init__(
|
||||
metadata=metadata,
|
||||
prompt_template=prompt_template, # type: ignore
|
||||
prompt_execution_settings=prompt_execution_settings or {}, # type: ignore
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def rewrite_execution_settings(
|
||||
cls,
|
||||
data: Any,
|
||||
) -> dict[str, PromptExecutionSettings]:
|
||||
"""Rewrite execution settings to a dictionary.
|
||||
|
||||
If the prompt_execution_settings is not a dictionary, it is converted to a dictionary.
|
||||
If it is not supplied, but prompt_template is, the prompt_template's execution settings are used.
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
prompt_execution_settings = data.get("prompt_execution_settings")
|
||||
prompt_template = data.get("prompt_template")
|
||||
if not prompt_execution_settings:
|
||||
if prompt_template:
|
||||
prompt_execution_settings = prompt_template.prompt_template_config.execution_settings
|
||||
data["prompt_execution_settings"] = prompt_execution_settings
|
||||
if not prompt_execution_settings:
|
||||
return data
|
||||
if isinstance(prompt_execution_settings, PromptExecutionSettings):
|
||||
data["prompt_execution_settings"] = {
|
||||
prompt_execution_settings.service_id or DEFAULT_SERVICE_NAME: prompt_execution_settings
|
||||
}
|
||||
if isinstance(prompt_execution_settings, Sequence):
|
||||
data["prompt_execution_settings"] = {
|
||||
s.service_id or DEFAULT_SERVICE_NAME: s for s in prompt_execution_settings
|
||||
}
|
||||
return data
|
||||
|
||||
async def _invoke_internal(self, context: FunctionInvocationContext) -> None:
|
||||
"""Invokes the function with the given arguments."""
|
||||
prompt_render_result = await self._render_prompt(context)
|
||||
if prompt_render_result.function_result is not None:
|
||||
context.result = prompt_render_result.function_result
|
||||
return
|
||||
|
||||
if isinstance(prompt_render_result.ai_service, ChatCompletionClientBase):
|
||||
chat_history = ChatHistory.from_rendered_prompt(prompt_render_result.rendered_prompt)
|
||||
try:
|
||||
chat_message_contents = await prompt_render_result.ai_service.get_chat_message_contents(
|
||||
chat_history=chat_history,
|
||||
settings=prompt_render_result.execution_settings,
|
||||
**{"kernel": context.kernel, "arguments": context.arguments},
|
||||
)
|
||||
except Exception as exc:
|
||||
raise FunctionExecutionException(f"Error occurred while invoking function {self.name}: {exc}") from exc
|
||||
|
||||
if not chat_message_contents:
|
||||
raise FunctionExecutionException(f"No completions returned while invoking function {self.name}")
|
||||
|
||||
context.result = self._create_function_result(
|
||||
completions=chat_message_contents,
|
||||
chat_history=chat_history,
|
||||
arguments=context.arguments,
|
||||
prompt=prompt_render_result.rendered_prompt,
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(prompt_render_result.ai_service, TextCompletionClientBase):
|
||||
try:
|
||||
texts = await prompt_render_result.ai_service.get_text_contents(
|
||||
prompt=unescape(prompt_render_result.rendered_prompt),
|
||||
settings=prompt_render_result.execution_settings,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise FunctionExecutionException(f"Error occurred while invoking function {self.name}: {exc}") from exc
|
||||
|
||||
context.result = self._create_function_result(
|
||||
completions=texts, arguments=context.arguments, prompt=prompt_render_result.rendered_prompt
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(prompt_render_result.ai_service, TextToImageClientBase):
|
||||
try:
|
||||
images = await prompt_render_result.ai_service.get_image_content(
|
||||
description=unescape(prompt_render_result.rendered_prompt),
|
||||
settings=prompt_render_result.execution_settings,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise FunctionExecutionException(f"Error occurred while invoking function {self.name}: {exc}") from exc
|
||||
|
||||
context.result = self._create_function_result(
|
||||
completions=[images], arguments=context.arguments, prompt=prompt_render_result.rendered_prompt
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(prompt_render_result.ai_service, TextToAudioClientBase):
|
||||
try:
|
||||
audio = await prompt_render_result.ai_service.get_audio_content(
|
||||
text=unescape(prompt_render_result.rendered_prompt),
|
||||
settings=prompt_render_result.execution_settings,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise FunctionExecutionException(f"Error occurred while invoking function {self.name}: {exc}") from exc
|
||||
|
||||
context.result = self._create_function_result(
|
||||
completions=[audio], arguments=context.arguments, prompt=prompt_render_result.rendered_prompt
|
||||
)
|
||||
return
|
||||
|
||||
raise ValueError(f"Service `{type(prompt_render_result.ai_service).__name__}` is not a valid AI service")
|
||||
|
||||
async def _invoke_internal_stream(self, context: FunctionInvocationContext) -> None:
|
||||
"""Invokes the function stream with the given arguments."""
|
||||
prompt_render_result = await self._render_prompt(context, is_streaming=True)
|
||||
if prompt_render_result.function_result is not None:
|
||||
context.result = prompt_render_result.function_result
|
||||
return
|
||||
|
||||
if isinstance(prompt_render_result.ai_service, ChatCompletionClientBase):
|
||||
chat_history = ChatHistory.from_rendered_prompt(prompt_render_result.rendered_prompt)
|
||||
value: AsyncGenerator = prompt_render_result.ai_service.get_streaming_chat_message_contents(
|
||||
chat_history=chat_history,
|
||||
settings=prompt_render_result.execution_settings,
|
||||
**{"kernel": context.kernel, "arguments": context.arguments},
|
||||
)
|
||||
elif isinstance(prompt_render_result.ai_service, TextCompletionClientBase):
|
||||
value = prompt_render_result.ai_service.get_streaming_text_contents(
|
||||
prompt=prompt_render_result.rendered_prompt, settings=prompt_render_result.execution_settings
|
||||
)
|
||||
else:
|
||||
raise FunctionExecutionException(
|
||||
f"Service `{type(prompt_render_result.ai_service)}` is not a valid AI service"
|
||||
)
|
||||
|
||||
context.result = FunctionResult(
|
||||
function=self.metadata, value=value, rendered_prompt=prompt_render_result.rendered_prompt
|
||||
)
|
||||
|
||||
async def _render_prompt(
|
||||
self, context: FunctionInvocationContext, is_streaming: bool = False
|
||||
) -> PromptRenderingResult:
|
||||
"""Render the prompt and apply the prompt rendering filters."""
|
||||
self.update_arguments_with_defaults(context.arguments)
|
||||
|
||||
_rebuild_prompt_render_context()
|
||||
prompt_render_context = PromptRenderContext(
|
||||
function=self, kernel=context.kernel, arguments=context.arguments, is_streaming=is_streaming
|
||||
)
|
||||
|
||||
stack = context.kernel.construct_call_stack(
|
||||
filter_type=FilterTypes.PROMPT_RENDERING,
|
||||
inner_function=self._inner_render_prompt,
|
||||
)
|
||||
await stack(prompt_render_context)
|
||||
|
||||
if prompt_render_context.rendered_prompt is None:
|
||||
raise PromptRenderingException("Prompt rendering failed, no rendered prompt was returned.")
|
||||
selected_service: tuple["AIServiceClientBase", PromptExecutionSettings] = context.kernel.select_ai_service(
|
||||
function=self,
|
||||
arguments=context.arguments,
|
||||
type=(TextCompletionClientBase, ChatCompletionClientBase) if prompt_render_context.is_streaming else None,
|
||||
)
|
||||
return PromptRenderingResult(
|
||||
rendered_prompt=prompt_render_context.rendered_prompt,
|
||||
ai_service=selected_service[0],
|
||||
execution_settings=selected_service[1],
|
||||
function_result=prompt_render_context.function_result,
|
||||
)
|
||||
|
||||
async def _inner_render_prompt(self, context: PromptRenderContext) -> None:
|
||||
"""Render the prompt using the prompt template."""
|
||||
context.rendered_prompt = await self.prompt_template.render(context.kernel, context.arguments)
|
||||
|
||||
def _create_function_result(
|
||||
self,
|
||||
completions: list[ChatMessageContent] | list[TextContent] | list[ImageContent] | list[AudioContent],
|
||||
arguments: KernelArguments,
|
||||
chat_history: ChatHistory | None = None,
|
||||
prompt: str | None = None,
|
||||
) -> FunctionResult:
|
||||
"""Creates a function result with the given completions."""
|
||||
metadata: dict[str, Any] = {
|
||||
"arguments": arguments,
|
||||
"metadata": [completion.metadata for completion in completions],
|
||||
}
|
||||
if chat_history:
|
||||
metadata["messages"] = chat_history
|
||||
if prompt:
|
||||
metadata["prompt"] = prompt
|
||||
return FunctionResult(
|
||||
function=self.metadata,
|
||||
value=completions,
|
||||
metadata=metadata,
|
||||
rendered_prompt=prompt,
|
||||
)
|
||||
|
||||
def update_arguments_with_defaults(self, arguments: KernelArguments) -> None:
|
||||
"""Update any missing values with their defaults."""
|
||||
for parameter in self.prompt_template.prompt_template_config.input_variables:
|
||||
if parameter.name not in arguments and parameter.default not in {None, "", False, 0}:
|
||||
arguments[parameter.name] = parameter.default
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, yaml_str: str, plugin_name: str | None = None) -> "KernelFunctionFromPrompt":
|
||||
"""Creates a new instance of the KernelFunctionFromPrompt class from a YAML string."""
|
||||
try:
|
||||
data = yaml.safe_load(yaml_str)
|
||||
except yaml.YAMLError as exc: # pragma: no cover
|
||||
raise FunctionInitializationError(f"Invalid YAML content: {yaml_str}, error: {exc}") from exc
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise FunctionInitializationError(f"The YAML content must represent a dictionary, got {yaml_str}")
|
||||
|
||||
try:
|
||||
prompt_template_config = PromptTemplateConfig(**data)
|
||||
except ValidationError as exc:
|
||||
raise FunctionInitializationError(
|
||||
f"Error initializing PromptTemplateConfig: {exc} from yaml data: {data}"
|
||||
) from exc
|
||||
return cls(
|
||||
function_name=prompt_template_config.name,
|
||||
plugin_name=plugin_name,
|
||||
description=prompt_template_config.description,
|
||||
prompt_template_config=prompt_template_config,
|
||||
template_format=prompt_template_config.template_format,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_directory(
|
||||
cls, path: str, plugin_name: str | None = None, encoding: str = "utf-8"
|
||||
) -> "KernelFunctionFromPrompt":
|
||||
"""Creates a new instance of the KernelFunctionFromPrompt class from a directory.
|
||||
|
||||
The directory needs to contain:
|
||||
- A prompt file named `skprompt.txt`
|
||||
- A config file named `config.json`
|
||||
|
||||
Args:
|
||||
path: The path to the directory containing the prompt and config files.
|
||||
plugin_name: The name of the plugin.
|
||||
encoding: The encoding to use when reading the files. Defaults to "utf-8".
|
||||
|
||||
Returns:
|
||||
KernelFunctionFromPrompt: The kernel function from prompt
|
||||
"""
|
||||
prompt_path = os.path.join(path, PROMPT_FILE_NAME)
|
||||
config_path = os.path.join(path, CONFIG_FILE_NAME)
|
||||
prompt_exists = os.path.exists(prompt_path)
|
||||
config_exists = os.path.exists(config_path)
|
||||
if not config_exists and not prompt_exists:
|
||||
raise FunctionInitializationError(
|
||||
f"{PROMPT_FILE_NAME} and {CONFIG_FILE_NAME} files are required to create a "
|
||||
f"function from a directory, path: {path!s}."
|
||||
)
|
||||
if not config_exists:
|
||||
raise FunctionInitializationError(
|
||||
f"{CONFIG_FILE_NAME} files are required to create a function from a directory, "
|
||||
f"path: {path!s}, prompt file is there."
|
||||
)
|
||||
if not prompt_exists:
|
||||
raise FunctionInitializationError(
|
||||
f"{PROMPT_FILE_NAME} files are required to create a function from a directory, "
|
||||
f"path: {path!s}, config file is there."
|
||||
)
|
||||
|
||||
function_name = os.path.basename(path)
|
||||
|
||||
with open(config_path, encoding=encoding) as config_file:
|
||||
prompt_template_config = PromptTemplateConfig.from_json(config_file.read())
|
||||
prompt_template_config.name = function_name
|
||||
|
||||
with open(prompt_path, encoding=encoding) as prompt_file:
|
||||
prompt_template_config.template = prompt_file.read()
|
||||
|
||||
prompt_template = TEMPLATE_FORMAT_MAP[prompt_template_config.template_format]( # type: ignore
|
||||
prompt_template_config=prompt_template_config
|
||||
)
|
||||
return cls(
|
||||
function_name=function_name,
|
||||
plugin_name=plugin_name,
|
||||
prompt_template=prompt_template,
|
||||
prompt_template_config=prompt_template_config,
|
||||
template_format=prompt_template_config.template_format,
|
||||
description=prompt_template_config.description,
|
||||
)
|
||||
Reference in New Issue
Block a user