chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -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,
)