Python: mypy coverage enhancement (#6250)

### 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.
-->
extend mypy coverage for prompt_templates, template_engine and some
other smaller folders.

still todo: connectors, memory and planner folders.

### 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 😄
This commit is contained in:
Eduard van Valkenburg
2024-05-31 16:37:19 +00:00
committed by GitHub
parent 4508e8d966
commit ba9af0e07b
28 changed files with 182 additions and 165 deletions
+2
View File
@@ -1,4 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Final
METADATA_EXCEPTION_KEY: Final[str] = "exception"
DEFAULT_SERVICE_NAME: Final[str] = "default"
@@ -1,8 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import TYPE_CHECKING, Annotated
from semantic_kernel.functions.kernel_function_decorator import kernel_function
if TYPE_CHECKING:
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_function import KernelFunction
from semantic_kernel.kernel import Kernel
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
@@ -10,8 +13,6 @@ if TYPE_CHECKING:
class ConversationSummaryPlugin:
"""Semantic plugin that enables conversations summarization."""
from semantic_kernel.functions.kernel_function_decorator import kernel_function
# The max tokens to process in a single semantic function call.
_max_tokens = 1024
@@ -30,17 +31,21 @@ class ConversationSummaryPlugin:
) -> None:
"""Initializes a new instance of the ConversationSummaryPlugin class.
:param kernel: The kernel instance.
:param prompt_template_config: The prompt template configuration.
:param return_key: The key to use for the return value.
Args:
kernel (Kernel): The kernel instance to use for the function.
prompt_template_config (PromptTemplateConfig): The configuration to use for functions.
return_key (str): The key to use for the return value.
"""
self.return_key = return_key
self._summarizeConversationFunction = kernel.add_function(
kernel.add_function(
prompt=ConversationSummaryPlugin._summarize_conversation_prompt_template,
plugin_name=ConversationSummaryPlugin.__name__,
function_name="SummarizeConversation",
prompt_template_config=prompt_template_config,
)
self._summarizeConversationFunction: "KernelFunction" = kernel.get_function(
plugin_name=ConversationSummaryPlugin.__name__, function_name="SummarizeConversation"
)
@kernel_function(
description="Given a long conversation transcript, summarize the conversation.",
@@ -56,10 +61,13 @@ class ConversationSummaryPlugin:
]:
"""Given a long conversation transcript, summarize the conversation.
:param input: A long conversation transcript.
:param kernel: The kernel for function execution.
:param arguments: Arguments used by the kernel.
:return: KernelArguments with the summarized conversation result in key self.return_key.
Args:
input (str): A long conversation transcript.
kernel (Kernel): The kernel for function execution.
arguments (KernelArguments): Arguments used by the kernel.
Returns:
KernelArguments with the summarized conversation result in key self.return_key.
"""
from semantic_kernel.text import text_chunker
from semantic_kernel.text.function_extension import aggregate_chunked_results
@@ -32,9 +32,9 @@ class SessionsPythonTool(KernelBaseModel):
"""A plugin for running Python code in an Azure Container Apps dynamic sessions code interpreter."""
pool_management_endpoint: str
settings: SessionsPythonSettings | None = None
settings: SessionsPythonSettings
auth_callback: Callable[..., Awaitable[Any]]
http_client: httpx.AsyncClient | None = None
http_client: httpx.AsyncClient
def __init__(
self,
@@ -60,12 +60,10 @@ class SessionsPythonTool(KernelBaseModel):
logger.error(f"Failed to load the ACASessionsSettings with message: {e!s}")
raise FunctionExecutionException(f"Failed to load the ACASessionsSettings with message: {e!s}") from e
endpoint = pool_management_endpoint or aca_settings.pool_management_endpoint
super().__init__(
pool_management_endpoint=endpoint,
auth_callback=auth_callback,
pool_management_endpoint=aca_settings.pool_management_endpoint,
settings=settings,
auth_callback=auth_callback,
http_client=http_client,
**kwargs,
)
@@ -100,7 +98,7 @@ class SessionsPythonTool(KernelBaseModel):
Remove whitespace, backtick & python (if llm mistakes python console as terminal).
Args:
code: The query to sanitize
code (str): The query to sanitize
Returns:
str: The sanitized query
"""
@@ -201,7 +199,7 @@ class SessionsPythonTool(KernelBaseModel):
response = await self.http_client.post(
url=f"{self.pool_management_endpoint}python/uploadFile?identifier={self.settings.session_id}",
json={},
files=files,
files=files, # type: ignore
)
response.raise_for_status()
@@ -232,9 +230,7 @@ class SessionsPythonTool(KernelBaseModel):
response_json = response.json()
return [SessionsRemoteFileMetadata.from_dict(entry) for entry in response_json["$values"]]
async def download_file(
self, *, remote_file_path: str, local_file_path: str | None = None
) -> BufferedReader | None:
async def download_file(self, *, remote_file_path: str, local_file_path: str | None = None) -> BytesIO | None:
"""Download a file from the session pool.
Args:
@@ -68,7 +68,7 @@ class TextMemoryPlugin(KernelBaseModel):
logger.warning(f"Memory not found in collection: {collection}")
return ""
return results[0].text if limit == 1 else json.dumps([r.text for r in results])
return results[0].text if limit == 1 else json.dumps([r.text for r in results]) # type: ignore
@kernel_function(
description="Save information to semantic memory",
@@ -31,8 +31,8 @@ class WebSearchEnginePlugin:
async def search(
self,
query: Annotated[str, "The search query"],
num_results: Annotated[int | None, "The number of search results to return"] = 1,
offset: Annotated[int | None, "The number of search results to skip"] = 0,
num_results: Annotated[int, "The number of search results to return"] = 1,
offset: Annotated[int, "The number of search results to skip"] = 0,
) -> list[str]:
"""Returns the search results of the query provided."""
return await self._connector.search(query, num_results, offset)
@@ -2,6 +2,8 @@
from typing import TYPE_CHECKING, Any
from semantic_kernel.const import DEFAULT_SERVICE_NAME
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
@@ -35,7 +37,7 @@ class KernelArguments(dict):
if isinstance(settings, dict):
settings_dict = settings
elif isinstance(settings, list):
settings_dict = {s.service_id or "default": s for s in settings}
settings_dict = {s.service_id or DEFAULT_SERVICE_NAME: s for s in settings}
else:
settings_dict = {settings.service_id or "default": settings}
settings_dict = {settings.service_id or DEFAULT_SERVICE_NAME: settings}
self.execution_settings: dict[str, "PromptExecutionSettings"] | None = settings_dict
@@ -4,7 +4,7 @@ import logging
import os
from collections.abc import AsyncGenerator
from html import unescape
from typing import Any
from typing import TYPE_CHECKING, Any
import yaml
from pydantic import Field, ValidationError, model_validator
@@ -12,6 +12,7 @@ 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.const import DEFAULT_SERVICE_NAME
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.text_content import TextContent
@@ -31,6 +32,9 @@ from semantic_kernel.prompt_template.const import KERNEL_TEMPLATE_FORMAT_NAME, T
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"
@@ -146,10 +150,12 @@ through prompt_template_config or in the prompt_template."
return data
if isinstance(prompt_execution_settings, PromptExecutionSettings):
data["prompt_execution_settings"] = {
prompt_execution_settings.service_id or "default": prompt_execution_settings
prompt_execution_settings.service_id or DEFAULT_SERVICE_NAME: prompt_execution_settings
}
if isinstance(prompt_execution_settings, list):
data["prompt_execution_settings"] = {s.service_id or "default": s for s in prompt_execution_settings}
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:
@@ -232,7 +238,6 @@ through prompt_template_config or in the prompt_template."
async def _render_prompt(self, context: FunctionInvocationContext) -> PromptRenderingResult:
"""Render the prompt and apply the prompt rendering filters."""
self.update_arguments_with_defaults(context.arguments)
service, execution_settings = context.kernel.select_ai_service(self, context.arguments)
_rebuild_prompt_render_context()
prompt_render_context = PromptRenderContext(function=self, kernel=context.kernel, arguments=context.arguments)
@@ -245,10 +250,13 @@ through prompt_template_config or in the prompt_template."
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
)
return PromptRenderingResult(
rendered_prompt=prompt_render_context.rendered_prompt,
ai_service=service,
execution_settings=execution_settings,
ai_service=selected_service[0],
execution_settings=selected_service[1],
)
async def _inner_render_prompt(self, context: PromptRenderContext) -> None:
+5 -2
View File
@@ -3,7 +3,7 @@
import logging
from collections.abc import AsyncGenerator, AsyncIterable
from copy import copy
from typing import TYPE_CHECKING, Any, Literal
from typing import TYPE_CHECKING, Any, Literal, TypeVar
from semantic_kernel.const import METADATA_EXCEPTION_KEY
from semantic_kernel.contents.streaming_content_mixin import StreamingContentMixin
@@ -19,14 +19,17 @@ from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_function_extension import KernelFunctionExtension
from semantic_kernel.functions.kernel_function_from_prompt import KernelFunctionFromPrompt
from semantic_kernel.functions.kernel_plugin import KernelPlugin
from semantic_kernel.kernel_types import AI_SERVICE_CLIENT_TYPE
from semantic_kernel.prompt_template.const import KERNEL_TEMPLATE_FORMAT_NAME
from semantic_kernel.reliability.kernel_reliability_extension import KernelReliabilityExtension
from semantic_kernel.services.ai_service_selector import AIServiceSelector
from semantic_kernel.services.kernel_services_extension import AI_SERVICE_CLIENT_TYPE, KernelServicesExtension
from semantic_kernel.services.kernel_services_extension import KernelServicesExtension
if TYPE_CHECKING:
from semantic_kernel.functions.kernel_function import KernelFunction
T = TypeVar("T")
logger: logging.Logger = logging.getLogger(__name__)
+7
View File
@@ -0,0 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import TypeVar
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
AI_SERVICE_CLIENT_TYPE = TypeVar("AI_SERVICE_CLIENT_TYPE", bound=AIServiceClientBase)
@@ -15,6 +15,7 @@ from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_pro
from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion import AzureChatCompletion
from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion
from semantic_kernel.connectors.ai.open_ai.services.utils import kernel_function_metadata_to_openai_tool_format
from semantic_kernel.const import DEFAULT_SERVICE_NAME
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
@@ -256,8 +257,8 @@ class FunctionCallingStepwisePlanner(KernelBaseModel):
"""
data = yaml.safe_load(self.generate_plan_yaml)
prompt_template_config = PromptTemplateConfig(**data)
if "default" in prompt_template_config.execution_settings:
settings = prompt_template_config.execution_settings.pop("default")
if DEFAULT_SERVICE_NAME in prompt_template_config.execution_settings:
settings = prompt_template_config.execution_settings.pop(DEFAULT_SERVICE_NAME)
prompt_template_config.execution_settings[self.service_id] = settings
return kernel.add_function(
function_name="create_plan",
@@ -3,7 +3,7 @@
import os
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.const import METADATA_EXCEPTION_KEY
from semantic_kernel.const import DEFAULT_SERVICE_NAME, METADATA_EXCEPTION_KEY
from semantic_kernel.exceptions import PlannerCreatePlanError, PlannerException, PlannerInvalidGoalError
from semantic_kernel.functions.function_result import FunctionResult
from semantic_kernel.functions.kernel_arguments import KernelArguments
@@ -67,8 +67,8 @@ class SequentialPlanner:
prompt_template = prompt or read_file(PROMPT_TEMPLATE_FILE_PATH)
if service_id in prompt_config.execution_settings:
prompt_config.execution_settings[service_id].extension_data["max_tokens"] = self.config.max_tokens
elif "default" in prompt_config.execution_settings:
prompt_config.execution_settings["default"].extension_data["max_tokens"] = self.config.max_tokens
elif DEFAULT_SERVICE_NAME in prompt_config.execution_settings:
prompt_config.execution_settings[DEFAULT_SERVICE_NAME].extension_data["max_tokens"] = self.config.max_tokens
else:
prompt_config.execution_settings[service_id] = PromptExecutionSettings(
service_id=service_id, max_tokens=self.config.max_tokens
@@ -79,9 +79,9 @@ class SequentialPlanner:
if (
service_id
and service_id not in prompt_config.execution_settings
and "default" in prompt_config.execution_settings
and DEFAULT_SERVICE_NAME in prompt_config.execution_settings
):
settings = prompt_config.execution_settings.pop("default")
settings = prompt_config.execution_settings.pop(DEFAULT_SERVICE_NAME)
prompt_config.execution_settings[service_id] = settings
return self._kernel.add_function(
@@ -9,6 +9,7 @@ from jinja2.sandbox import ImmutableSandboxedEnvironment
from pydantic import PrivateAttr, field_validator
from semantic_kernel.exceptions import Jinja2TemplateRenderException
from semantic_kernel.exceptions.template_engine_exceptions import TemplateRenderException
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.prompt_template.const import JINJA2_TEMPLATE_FORMAT_NAME
from semantic_kernel.prompt_template.prompt_template_base import PromptTemplateBase
@@ -47,7 +48,7 @@ class Jinja2PromptTemplate(PromptTemplateBase):
Jinja2TemplateSyntaxError: If there is a syntax error in the Jinja2 template.
"""
_env: ImmutableSandboxedEnvironment = PrivateAttr()
_env: ImmutableSandboxedEnvironment | None = PrivateAttr()
@field_validator("prompt_template_config")
@classmethod
@@ -101,6 +102,8 @@ class Jinja2PromptTemplate(PromptTemplateBase):
}
)
try:
if self.prompt_template_config.template is None:
raise TemplateRenderException("Template is None")
template = self._env.from_string(self.prompt_template_config.template, globals=helpers)
return template.render(**arguments)
@@ -12,7 +12,9 @@ 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.block_types import BlockTypes
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:
@@ -56,17 +58,17 @@ class KernelPromptTemplate(PromptTemplateBase):
# Enumerate every block in the template, adding any variables that are referenced.
for block in self._blocks:
if block.type == BlockTypes.VARIABLE:
if isinstance(block, VarBlock):
# Add all variables from variable blocks, e.g. "{{$a}}".
self._add_if_missing(block.name, seen)
continue
if block.type == BlockTypes.CODE:
if isinstance(block, CodeBlock):
for sub_block in block.tokens:
if sub_block.type == BlockTypes.VARIABLE:
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 sub_block.type == BlockTypes.NAMED_ARG and sub_block.variable:
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
@@ -75,6 +77,8 @@ class KernelPromptTemplate(PromptTemplateBase):
def _add_if_missing(self, variable_name: str, seen: set | None = None):
# Convert variable_name to lower case to handle case-insensitivity
if not seen:
seen = set()
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))
@@ -5,6 +5,7 @@ from typing import TypeVar
from pydantic import Field, field_validator, model_validator
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.const import DEFAULT_SERVICE_NAME
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.prompt_template.const import KERNEL_TEMPLATE_FORMAT_NAME, TEMPLATE_FORMAT_TYPES
@@ -57,16 +58,16 @@ class PromptTemplateConfig(KernelBaseModel):
if not settings:
return {}
if isinstance(settings, PromptExecutionSettings):
return {settings.service_id or "default": settings}
return {settings.service_id or DEFAULT_SERVICE_NAME: settings}
if isinstance(settings, list):
return {s.service_id or "default": s for s in settings}
return {s.service_id or DEFAULT_SERVICE_NAME: s for s in settings}
return settings
def add_execution_settings(self, settings: PromptExecutionSettings, overwrite: bool = True) -> None:
"""Add execution settings to the prompt template."""
if settings.service_id in self.execution_settings and not overwrite:
return
self.execution_settings[settings.service_id or "default"] = settings
self.execution_settings[settings.service_id or DEFAULT_SERVICE_NAME] = settings
logger.warning("Execution settings already exist and overwrite is set to False")
def get_kernel_parameter_metadata(self) -> list[KernelParameterMetadata]:
@@ -4,12 +4,16 @@ import asyncio
import logging
from collections.abc import Callable
from html import escape
from typing import TYPE_CHECKING, Any, Literal
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
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
@@ -23,10 +27,13 @@ def create_template_helper_from_function(
function: "KernelFunction",
kernel: "Kernel",
base_arguments: "KernelArguments",
template_format: Literal["handlebars", "jinja2"],
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()
@@ -25,7 +25,7 @@ class PassThroughWithoutRetry(RetryMechanismBase, KernelBaseModel):
Awaitable[T]: An awaitable that will return the result of the action.
"""
try:
await action()
return action()
except Exception as e:
logger.warning(e, "Error executing action, not retrying")
raise e
@@ -28,7 +28,7 @@ class AIServiceClientBase(KernelBaseModel, ABC):
if not self.service_id:
self.service_id = self.ai_model_id
def get_prompt_execution_settings_class(self) -> "PromptExecutionSettings":
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
"""Get the request settings class."""
return PromptExecutionSettings # pragma: no cover
@@ -2,13 +2,16 @@
from typing import TYPE_CHECKING
from semantic_kernel.const import DEFAULT_SERVICE_NAME
from semantic_kernel.exceptions import KernelServiceNotFoundError
from semantic_kernel.kernel_types import AI_SERVICE_CLIENT_TYPE
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_function import KernelFunction
from semantic_kernel.kernel import AI_SERVICE_CLIENT_TYPE, Kernel
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
from semantic_kernel.services.kernel_services_extension import KernelServicesExtension
class AIServiceSelector:
@@ -20,11 +23,11 @@ class AIServiceSelector:
def select_ai_service(
self,
kernel: "Kernel",
kernel: "KernelServicesExtension",
function: "KernelFunction",
arguments: "KernelArguments",
type_: type["AI_SERVICE_CLIENT_TYPE"] | None = None,
) -> tuple["AI_SERVICE_CLIENT_TYPE", "PromptExecutionSettings"]:
type_: type[AI_SERVICE_CLIENT_TYPE] | tuple[type[AI_SERVICE_CLIENT_TYPE], ...] | None = None,
) -> tuple["AIServiceClientBase", "PromptExecutionSettings"]:
"""Select an AI Service on a first come, first served basis.
Starts with execution settings in the arguments,
@@ -35,7 +38,7 @@ class AIServiceSelector:
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
type_ = (TextCompletionClientBase, ChatCompletionClientBase)
type_ = (TextCompletionClientBase, ChatCompletionClientBase) # type: ignore
execution_settings_dict = arguments.execution_settings or {}
if func_exec_settings := getattr(function, "prompt_execution_settings", None):
@@ -45,13 +48,13 @@ class AIServiceSelector:
if not execution_settings_dict:
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
execution_settings_dict = {"default": PromptExecutionSettings()}
execution_settings_dict = {DEFAULT_SERVICE_NAME: PromptExecutionSettings()}
for service_id, settings in execution_settings_dict.items():
try:
service = kernel.get_service(service_id, type=type_)
except KernelServiceNotFoundError:
continue
if service:
if service is not None:
service_settings = service.get_prompt_execution_settings_from_settings(settings)
return service, service_settings
raise KernelServiceNotFoundError("No service found.")
@@ -2,32 +2,22 @@
import logging
from abc import ABC
from typing import TYPE_CHECKING, TypeVar, Union
from typing import TYPE_CHECKING
from pydantic import Field, field_validator
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions import (
KernelFunctionAlreadyExistsError,
KernelServiceNotFoundError,
ServiceInvalidTypeError,
)
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.const import DEFAULT_SERVICE_NAME
from semantic_kernel.exceptions import KernelFunctionAlreadyExistsError, KernelServiceNotFoundError
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.kernel_types import AI_SERVICE_CLIENT_TYPE
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
from semantic_kernel.services.ai_service_selector import AIServiceSelector
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.embeddings.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_function import KernelFunction
T = TypeVar("T")
AI_SERVICE_CLIENT_TYPE = TypeVar("AI_SERVICE_CLIENT_TYPE", bound=AIServiceClientBase)
ALL_SERVICE_TYPES = Union["TextCompletionClientBase", "ChatCompletionClientBase", "EmbeddingGeneratorBase"]
logger: logging.Logger = logging.getLogger(__name__)
@@ -48,22 +38,22 @@ class KernelServicesExtension(KernelBaseModel, ABC):
if not services:
return {}
if isinstance(services, AIServiceClientBase):
return {services.service_id if services.service_id else "default": services} # type: ignore
return {services.service_id if services.service_id else DEFAULT_SERVICE_NAME: services} # type: ignore
if isinstance(services, list):
return {s.service_id if s.service_id else "default": s for s in services}
return {s.service_id if s.service_id else DEFAULT_SERVICE_NAME: s for s in services}
return services
def select_ai_service(
self, function: "KernelFunction", arguments: KernelArguments
) -> tuple[ALL_SERVICE_TYPES, PromptExecutionSettings]:
self, function: "KernelFunction", arguments: "KernelArguments"
) -> tuple[AIServiceClientBase, PromptExecutionSettings]:
"""Uses the AI service selector to select a service for the function."""
return self.ai_service_selector.select_ai_service(self, function, arguments)
def get_service(
self,
service_id: str | None = None,
type: type[ALL_SERVICE_TYPES] | None = None,
) -> "AIServiceClientBase":
type: type[AI_SERVICE_CLIENT_TYPE] | tuple[type[AI_SERVICE_CLIENT_TYPE], ...] | None = None,
) -> AIServiceClientBase:
"""Get a service by service_id and type.
Type is optional and when not supplied, no checks are done.
@@ -71,46 +61,47 @@ class KernelServicesExtension(KernelBaseModel, ABC):
TextCompletionClientBase, ChatCompletionClientBase, EmbeddingGeneratorBase
or a subclass of one.
You can also check for multiple types in one go,
by using TextCompletionClientBase | ChatCompletionClientBase.
by using a tuple: (TextCompletionClientBase, ChatCompletionClientBase).
If type and service_id are both None, the first service is returned.
Args:
service_id (str | None): The service id,
if None, the default service is returned or the first service is returned.
type (Type[ALL_SERVICE_TYPES] | None): The type of the service, if None, no checks are done.
type (Type[AI_SERVICE_CLIENT_TYPE] | tuple[type[AI_SERVICE_CLIENT_TYPE], ...] | None):
The type of the service, if None, no checks are done on service type.
Returns:
ALL_SERVICE_TYPES: The service.
AIServiceClientBase: The service, should be a class derived from AIServiceClientBase.
Raises:
ValueError: If no service is found that matches the type.
KernelServiceNotFoundError: If no service is found that matches the type or id.
"""
service: "AIServiceClientBase | None" = None
if not service_id or service_id == "default":
if not type:
if default_service := self.services.get("default"):
return default_service
return next(iter(self.services.values()))
if (default_service := self.services.get("default")) and isinstance(default_service, type):
return default_service
for service in self.services.values():
if isinstance(service, type):
return service
raise KernelServiceNotFoundError(f"No service found of type {type}")
if not (service := self.services.get(service_id)):
raise KernelServiceNotFoundError(f"Service with service_id '{service_id}' does not exist")
if type and not isinstance(service, type):
raise ServiceInvalidTypeError(f"Service with service_id '{service_id}' is not of type {type}")
return service
services = self.get_services_by_type(type)
if not services:
raise KernelServiceNotFoundError(f"No services found of type {type}.")
if not service_id:
service_id = DEFAULT_SERVICE_NAME
def get_services_by_type(self, type: type[ALL_SERVICE_TYPES]) -> dict[str, ALL_SERVICE_TYPES]:
if service_id not in services:
if service_id == DEFAULT_SERVICE_NAME:
return next(iter(services.values()))
raise KernelServiceNotFoundError(
f"Service with service_id '{service_id}' does not exist or has a different type."
)
return services[service_id]
def get_services_by_type(
self, type: type[AI_SERVICE_CLIENT_TYPE] | tuple[type[AI_SERVICE_CLIENT_TYPE], ...] | None
) -> dict[str, AIServiceClientBase]:
"""Get all services of a specific type."""
return {service.service_id: service for service in self.services.values() if isinstance(service, type)} # type: ignore
if type is None:
return self.services
return {service.service_id: service for service in self.services.values() if isinstance(service, type)}
def get_prompt_execution_settings_from_service_id(
self, service_id: str, type: type[ALL_SERVICE_TYPES] | None = None
self, service_id: str, type: type[AI_SERVICE_CLIENT_TYPE] | None = None
) -> PromptExecutionSettings:
"""Get the specific request settings from the service, instantiated with the service_id and ai_model_id."""
service = self.get_service(service_id, type=type)
@@ -128,8 +119,8 @@ class KernelServicesExtension(KernelBaseModel, ABC):
"""
if service.service_id not in self.services or overwrite:
self.services[service.service_id] = service
else:
raise KernelFunctionAlreadyExistsError(f"Service with service_id '{service.service_id}' already exists")
return
raise KernelFunctionAlreadyExistsError(f"Service with service_id '{service.service_id}' already exists")
def remove_service(self, service_id: str) -> None:
"""Delete a single service from the Kernel."""
@@ -11,6 +11,8 @@ from semantic_kernel.exceptions.kernel_exceptions import KernelFunctionNotFoundE
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:
@@ -107,13 +109,15 @@ these will be ignored."
Otherwise, it is a value or variable and those are then rendered directly.
"""
logger.debug(f"Rendering code: `{self.content}`")
if self.tokens[0].type == BlockTypes.FUNCTION_ID:
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)
return self.tokens[0].render(kernel, arguments) # type: ignore
async def _render_function_call(self, kernel: "Kernel", arguments: "KernelArguments"):
function_block = self.tokens[0]
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:
@@ -145,10 +149,10 @@ these will be ignored."
)
for index, token in enumerate(self.tokens[1:], start=1):
logger.debug(f"Parsing variable/value: `{self.tokens[1].content}`")
rendered_value = token.render(kernel, arguments)
if token.type != BlockTypes.NAMED_ARG and index == 1:
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
arguments[token.name] = rendered_value # type: ignore
return arguments
@@ -2,7 +2,7 @@
import logging
from re import compile
from typing import TYPE_CHECKING, Any, ClassVar, Optional
from typing import TYPE_CHECKING, Any, ClassVar
from pydantic import model_validator
@@ -39,7 +39,7 @@ class FunctionIdBlock(Block):
"""
type: ClassVar[BlockTypes] = BlockTypes.FUNCTION_ID
function_name: str | None = ""
function_name: str = ""
plugin_name: str | None = None
@model_validator(mode="before")
@@ -61,6 +61,6 @@ class FunctionIdBlock(Block):
fields["function_name"] = matches.group("function")
return fields
def render(self, *_: tuple["Kernel", Optional["KernelArguments"]]) -> str:
def render(self, *_: "Kernel | KernelArguments | None") -> str:
"""Render the function id block."""
return self.content
@@ -90,7 +90,7 @@ class NamedArgBlock(Block):
def render(self, kernel: "Kernel", arguments: Optional["KernelArguments"] = None) -> Any:
"""Render the named argument block."""
if self.value:
return self.value.render(kernel, arguments)
return self.value.render()
if arguments is None:
return ""
if self.variable:
@@ -2,7 +2,7 @@
import logging
from re import S, compile
from typing import TYPE_CHECKING, Any, ClassVar, Optional
from typing import TYPE_CHECKING, Any, ClassVar
from pydantic import model_validator
@@ -69,6 +69,6 @@ class ValBlock(Block):
fields["quote"] = quote
return fields
def render(self, *_: tuple["Kernel", Optional["KernelArguments"]]) -> str:
def render(self, *_: "Kernel | KernelArguments | None") -> str:
"""Render the value block."""
return self.value
return self.value or ""
@@ -45,7 +45,7 @@ class VarBlock(Block):
"""
type: ClassVar[BlockTypes] = BlockTypes.VARIABLE
name: str | None = ""
name: str = ""
@model_validator(mode="before")
@classmethod
+8 -8
View File
@@ -12,7 +12,7 @@ from collections.abc import Callable
NEWLINE = os.linesep
TEXT_SPLIT_OPTIONS = [
TEXT_SPLIT_OPTIONS: list[list[str] | None] = [
["\n", "\r"],
["."],
["?", "!"],
@@ -25,7 +25,7 @@ TEXT_SPLIT_OPTIONS = [
None,
]
MD_SPLIT_OPTIONS = [
MD_SPLIT_OPTIONS: list[list[str] | None] = [
["."],
["?", "!"],
[";"],
@@ -112,8 +112,8 @@ def _split_text_paragraph(text: list[str], max_tokens: int, token_counter: Calla
if not text:
return []
paragraphs = []
current_paragraph = []
paragraphs: list[str] = []
current_paragraph: list[str] = []
for line in text:
num_tokens_line = token_counter(line)
@@ -187,7 +187,7 @@ def _split_text_lines(
def _split_str_lines(
text: str,
max_tokens: int,
separators: list[list[str]],
separators: list[list[str] | None],
trim: bool,
token_counter: Callable = _token_counter,
) -> list[str]:
@@ -196,7 +196,7 @@ def _split_str_lines(
return []
text = text.replace("\r\n", "\n")
lines = []
lines: list[str] = []
was_split = False
for split_option in separators:
if not lines:
@@ -224,7 +224,7 @@ def _split_str_lines(
def _split_str(
text: str,
max_tokens: int,
separators: list[str],
separators: list[str] | None,
trim: bool,
token_counter: Callable = _token_counter,
) -> tuple[list[str], bool]:
@@ -283,7 +283,7 @@ def _split_str(
def _split_list(
text: list[str],
max_tokens: int,
separators: list[str],
separators: list[str] | None,
trim: bool,
token_counter: Callable = _token_counter,
) -> tuple[list[str], bool]:
@@ -1,18 +1,17 @@
# Copyright (c) Microsoft. All rights reserved.
import types
from collections.abc import Callable
def experimental_function(func: Callable) -> Callable:
"""Decorator to mark a function as experimental."""
if isinstance(func, types.FunctionType):
if callable(func):
if func.__doc__:
func.__doc__ += "\n\nNote: This function is experimental and may change in the future."
else:
func.__doc__ = "Note: This function is experimental and may change in the future."
func.is_experimental = True
setattr(func, "is_experimental", True)
return func
@@ -25,6 +24,6 @@ def experimental_class(cls: type) -> type:
else:
cls.__doc__ = "Note: This class is experimental and may change in the future."
cls.is_experimental = True
setattr(cls, "is_experimental", True)
return cls