From 08794d3ce1846737d000920ddeb53900bca3f66a Mon Sep 17 00:00:00 2001 From: edvan_microsoft Date: Tue, 14 May 2024 14:25:47 +0200 Subject: [PATCH 1/6] mypy coverage for prompt templates --- python/mypy.ini | 3 --- .../prompt_template/jinja2_prompt_template.py | 3 ++- .../prompt_template/kernel_prompt_template.py | 20 +++++++++++-------- .../utils/template_function_helpers.py | 13 +++++++++--- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/python/mypy.ini b/python/mypy.ini index 8bf962bd2dce..79daae107003 100644 --- a/python/mypy.ini +++ b/python/mypy.ini @@ -28,9 +28,6 @@ ignore_errors = true [mypy-semantic_kernel.planners.*] ignore_errors = true -[mypy-semantic_kernel.prompt_template.*] -ignore_errors = true - [mypy-semantic_kernel.reliability.*] ignore_errors = true diff --git a/python/semantic_kernel/prompt_template/jinja2_prompt_template.py b/python/semantic_kernel/prompt_template/jinja2_prompt_template.py index cd9e31fe227a..59073f196422 100644 --- a/python/semantic_kernel/prompt_template/jinja2_prompt_template.py +++ b/python/semantic_kernel/prompt_template/jinja2_prompt_template.py @@ -44,7 +44,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 @@ -95,6 +95,7 @@ async def render(self, kernel: "Kernel", arguments: Optional["KernelArguments"] } ) try: + assert self.prompt_template_config.template is not None template = self._env.from_string(self.prompt_template_config.template, globals=helpers) return template.render(**arguments) except TemplateError as exc: diff --git a/python/semantic_kernel/prompt_template/kernel_prompt_template.py b/python/semantic_kernel/prompt_template/kernel_prompt_template.py index 70e49540467e..1116a8985c5e 100644 --- a/python/semantic_kernel/prompt_template/kernel_prompt_template.py +++ b/python/semantic_kernel/prompt_template/kernel_prompt_template.py @@ -11,7 +11,9 @@ 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: @@ -39,25 +41,27 @@ def model_post_init(self, __context: Any) -> None: # 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 # is a named arg block. self._add_if_missing(sub_block.variable.name, seen) - def _add_if_missing(self, variable_name: str, seen: Optional[set] = None): + def _add_if_missing(self, variable_name: str | None, seen: Optional[set] = 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)) @@ -141,7 +145,7 @@ def render_variables( rendered_blocks: List[Block] = [] for block in blocks: - if block.type == BlockTypes.VARIABLE: + if isinstance(block, VarBlock): rendered_blocks.append(TextBlock.from_text(block.render(kernel, arguments))) continue rendered_blocks.append(block) @@ -164,7 +168,7 @@ async def render_code(self, blocks: List[Block], kernel: "Kernel", arguments: "K rendered_blocks: List[Block] = [] for block in blocks: - if block.type == BlockTypes.CODE: + if isinstance(block, CodeBlock): rendered_blocks.append(TextBlock.from_text(await block.render_code(kernel, arguments))) continue rendered_blocks.append(block) diff --git a/python/semantic_kernel/prompt_template/utils/template_function_helpers.py b/python/semantic_kernel/prompt_template/utils/template_function_helpers.py index 8e02968a46af..eb1e92bb0db8 100644 --- a/python/semantic_kernel/prompt_template/utils/template_function_helpers.py +++ b/python/semantic_kernel/prompt_template/utils/template_function_helpers.py @@ -2,11 +2,15 @@ import asyncio import logging -from typing import TYPE_CHECKING, Callable, Literal +from typing import TYPE_CHECKING, Callable import nest_asyncio -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_arguments import KernelArguments @@ -21,9 +25,12 @@ def create_template_helper_from_function( function: "KernelFunction", kernel: "Kernel", base_arguments: "KernelArguments", - template_format: Literal["handlebars", "jinja2"], + template_format: TEMPLATE_FORMAT_TYPES, ) -> Callable: """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() From 66ed8fdae64aaae55a026c053d15b5206171c3c8 Mon Sep 17 00:00:00 2001 From: edvan_microsoft Date: Tue, 14 May 2024 14:57:51 +0200 Subject: [PATCH 2/6] mypy coverage for template engine folder --- python/mypy.ini | 3 --- .../template_engine/blocks/code_block.py | 27 ++++++++++++------- .../blocks/function_id_block.py | 10 +++---- .../template_engine/blocks/named_arg_block.py | 8 +++--- .../template_engine/blocks/text_block.py | 10 +++---- .../template_engine/blocks/val_block.py | 10 +++---- .../template_engine/blocks/var_block.py | 2 +- .../template_engine/code_tokenizer.py | 7 +++-- .../template_engine/template_tokenizer.py | 9 +++---- 9 files changed, 44 insertions(+), 42 deletions(-) diff --git a/python/mypy.ini b/python/mypy.ini index 8bf962bd2dce..76a9f882c188 100644 --- a/python/mypy.ini +++ b/python/mypy.ini @@ -37,9 +37,6 @@ ignore_errors = true [mypy-semantic_kernel.services.*] ignore_errors = true -[mypy-semantic_kernel.template_engine.*] -ignore_errors = true - [mypy-semantic_kernel.text.*] ignore_errors = true diff --git a/python/semantic_kernel/template_engine/blocks/code_block.py b/python/semantic_kernel/template_engine/blocks/code_block.py index 061f9f577a9d..0c9f7b006e87 100644 --- a/python/semantic_kernel/template_engine/blocks/code_block.py +++ b/python/semantic_kernel/template_engine/blocks/code_block.py @@ -2,7 +2,7 @@ import logging from copy import copy -from typing import TYPE_CHECKING, Any, ClassVar, List +from typing import TYPE_CHECKING, Any, ClassVar from pydantic import Field, field_validator, model_validator @@ -16,6 +16,10 @@ if TYPE_CHECKING: from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.kernel import Kernel + 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.blocks.val_block import ValBlock + from semantic_kernel.template_engine.blocks.var_block import VarBlock logger: logging.Logger = logging.getLogger(__name__) @@ -47,7 +51,7 @@ class CodeBlock(Block): """ type: ClassVar[BlockTypes] = BlockTypes.CODE - tokens: List[Block] = Field(default_factory=list) + tokens: list["VarBlock | ValBlock | NamedArgBlock | FunctionIdBlock"] = Field(default_factory=list) @model_validator(mode="before") @classmethod @@ -63,7 +67,7 @@ def parse_content(cls, fields: Any) -> Any: return fields @field_validator("tokens", mode="after") - def check_tokens(cls, tokens: List[Block]) -> List[Block]: + 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. @@ -107,13 +111,14 @@ async def render_code(self, kernel: "Kernel", arguments: "KernelArguments") -> s 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] + assert isinstance(self.tokens[0], FunctionIdBlock) + function_block: FunctionIdBlock = self.tokens[0] try: function = kernel.get_function(function_block.plugin_name, function_block.function_name) except (KernelFunctionNotFoundError, KernelPluginNotFoundError) as exc: @@ -126,8 +131,10 @@ async def _render_function_call(self, kernel: "Kernel", arguments: "KernelArgume arguments_clone = self._enrich_function_arguments(kernel, arguments_clone, function.metadata) result = await function.invoke(kernel, arguments_clone) - if exc := result.metadata.get("error", None): - raise CodeBlockRenderException(f"Error rendering function: {function.metadata} with error: {exc}") from exc + if func_exc := result.metadata.get("exception", None): + raise CodeBlockRenderException( + f"Error rendering function: {function.metadata} with error: {func_exc}" + ) from func_exc return str(result) if result else "" @@ -145,9 +152,9 @@ def _enrich_function_arguments( 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: + 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 diff --git a/python/semantic_kernel/template_engine/blocks/function_id_block.py b/python/semantic_kernel/template_engine/blocks/function_id_block.py index d031295acafd..f8a76667cbeb 100644 --- a/python/semantic_kernel/template_engine/blocks/function_id_block.py +++ b/python/semantic_kernel/template_engine/blocks/function_id_block.py @@ -2,7 +2,7 @@ import logging from re import compile -from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Tuple +from typing import TYPE_CHECKING, Any, ClassVar from pydantic import model_validator @@ -39,12 +39,12 @@ class FunctionIdBlock(Block): """ type: ClassVar[BlockTypes] = BlockTypes.FUNCTION_ID - function_name: Optional[str] = "" - plugin_name: Optional[str] = None + function_name: str = "" + plugin_name: str | None = None @model_validator(mode="before") @classmethod - def parse_content(cls, fields: Dict[str, Any]) -> Dict[str, Any]: + def parse_content(cls, fields: dict[str, 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. @@ -61,5 +61,5 @@ def parse_content(cls, fields: Dict[str, Any]) -> Dict[str, Any]: fields["function_name"] = matches.group("function") return fields - def render(self, *_: Tuple["Kernel", Optional["KernelArguments"]]) -> str: + def render(self, *_: "Kernel | KernelArguments | None") -> str: return self.content diff --git a/python/semantic_kernel/template_engine/blocks/named_arg_block.py b/python/semantic_kernel/template_engine/blocks/named_arg_block.py index f276791624ad..e7a54142beca 100644 --- a/python/semantic_kernel/template_engine/blocks/named_arg_block.py +++ b/python/semantic_kernel/template_engine/blocks/named_arg_block.py @@ -55,9 +55,9 @@ class NamedArgBlock(Block): """ type: ClassVar[BlockTypes] = BlockTypes.NAMED_ARG - name: Optional[str] = None - value: Optional[ValBlock] = None - variable: Optional[VarBlock] = None + name: str | None = None + value: ValBlock | None = None + variable: VarBlock | None = None @model_validator(mode="before") @classmethod @@ -89,7 +89,7 @@ def parse_content(cls, fields: Any) -> Any: def render(self, kernel: "Kernel", arguments: Optional["KernelArguments"] = None) -> Any: if self.value: - return self.value.render(kernel, arguments) + return self.value.render() if arguments is None: return "" if self.variable: diff --git a/python/semantic_kernel/template_engine/blocks/text_block.py b/python/semantic_kernel/template_engine/blocks/text_block.py index 0e27d40037bd..20bd2cbb8b6f 100644 --- a/python/semantic_kernel/template_engine/blocks/text_block.py +++ b/python/semantic_kernel/template_engine/blocks/text_block.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. import logging -from typing import TYPE_CHECKING, ClassVar, Optional, Tuple +from typing import TYPE_CHECKING, ClassVar, Optional from pydantic import field_validator @@ -27,9 +27,9 @@ def content_strip(cls, content: str): @classmethod def from_text( cls, - text: Optional[str] = None, - start_index: Optional[int] = None, - stop_index: Optional[int] = None, + text: str | None = None, + start_index: int | None = None, + stop_index: int | None = None, ): if text is None: return cls(content="") @@ -48,5 +48,5 @@ def from_text( return cls(content=text) - def render(self, *_: Tuple[Optional["Kernel"], Optional["KernelArguments"]]) -> str: + def render(self, *_: tuple[Optional["Kernel"], Optional["KernelArguments"]]) -> str: return self.content diff --git a/python/semantic_kernel/template_engine/blocks/val_block.py b/python/semantic_kernel/template_engine/blocks/val_block.py index 87133d5e7624..ed832892b423 100644 --- a/python/semantic_kernel/template_engine/blocks/val_block.py +++ b/python/semantic_kernel/template_engine/blocks/val_block.py @@ -2,7 +2,7 @@ import logging from re import S, compile -from typing import TYPE_CHECKING, Any, ClassVar, Optional, Tuple +from typing import TYPE_CHECKING, Any, ClassVar from pydantic import model_validator @@ -46,8 +46,8 @@ class ValBlock(Block): """ type: ClassVar[BlockTypes] = BlockTypes.VALUE - value: Optional[str] = "" - quote: Optional[str] = "'" + value: str | None = "" + quote: str | None = "'" @model_validator(mode="before") @classmethod @@ -69,5 +69,5 @@ def parse_content(cls, fields: Any) -> Any: fields["quote"] = quote return fields - def render(self, *_: Tuple["Kernel", Optional["KernelArguments"]]) -> str: - return self.value + def render(self, *_: "Kernel | KernelArguments | None") -> str: + return self.value or "" diff --git a/python/semantic_kernel/template_engine/blocks/var_block.py b/python/semantic_kernel/template_engine/blocks/var_block.py index 2f05def84960..e67b5dbaf1f1 100644 --- a/python/semantic_kernel/template_engine/blocks/var_block.py +++ b/python/semantic_kernel/template_engine/blocks/var_block.py @@ -45,7 +45,7 @@ class VarBlock(Block): """ type: ClassVar[BlockTypes] = BlockTypes.VARIABLE - name: Optional[str] = "" + name: str | None = "" @model_validator(mode="before") @classmethod diff --git a/python/semantic_kernel/template_engine/code_tokenizer.py b/python/semantic_kernel/template_engine/code_tokenizer.py index 8ccd64d2bfbb..697bb0c33b47 100644 --- a/python/semantic_kernel/template_engine/code_tokenizer.py +++ b/python/semantic_kernel/template_engine/code_tokenizer.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. import logging -from typing import List from semantic_kernel.exceptions import CodeBlockSyntaxError from semantic_kernel.template_engine.blocks.block import Block @@ -25,7 +24,7 @@ # [parameter] ::= [variable] | [value] class CodeTokenizer: @staticmethod - def tokenize(text: str) -> List[Block]: + def tokenize(text: str) -> list[Block]: # Remove spaces, which are ignored anyway text = text.strip() if text else "" # Render None/empty to [] @@ -39,14 +38,14 @@ def tokenize(text: str) -> List[Block]: current_token_type = None # Track the content of the current token - current_token_content: List[str] = [] + current_token_content: list[str] = [] # Other state we need to track text_value_delimiter = None space_separator_found = False skip_next_char = False next_char = "" - blocks: List[Block] = [] + blocks: list[Block] = [] for index, current_char in enumerate(text[:-1]): next_char = text[index + 1] diff --git a/python/semantic_kernel/template_engine/template_tokenizer.py b/python/semantic_kernel/template_engine/template_tokenizer.py index a21f9b924535..2b0c8c59df99 100644 --- a/python/semantic_kernel/template_engine/template_tokenizer.py +++ b/python/semantic_kernel/template_engine/template_tokenizer.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. import logging -from typing import List from semantic_kernel.exceptions import ( BlockSyntaxError, @@ -28,7 +27,7 @@ # [any-char] ::= any char class TemplateTokenizer: @staticmethod - def tokenize(text: str) -> List[Block]: + def tokenize(text: str) -> list[Block]: code_tokenizer = CodeTokenizer() # An empty block consists of 4 chars: "{{}}" EMPTY_CODE_BLOCK_LENGTH = 4 @@ -46,7 +45,7 @@ def tokenize(text: str) -> List[Block]: if len(text) < MIN_CODE_BLOCK_LENGTH: return [TextBlock.from_text(text)] - blocks: List[Block] = [] + blocks: list[Block] = [] end_of_last_block = 0 block_start_pos = 0 block_start_found = False @@ -111,7 +110,7 @@ def tokenize(text: str) -> List[Block]: @staticmethod def _extract_blocks( text: str, code_tokenizer: CodeTokenizer, block_start_pos: int, end_of_last_block: int, next_char_pos: int - ) -> List[Block]: + ) -> list[Block]: """Extract the blocks from the found code. If there is text before the current block, create a TextBlock from that. @@ -122,7 +121,7 @@ def _extract_blocks( If there is only a variable or value in the code block, return just that, instead of the CodeBlock. """ - new_blocks: List[Block] = [] + new_blocks: list[Block] = [] if block_start_pos > end_of_last_block: new_blocks.append( TextBlock.from_text( From 5d9d6701caa29529fbb9e73c558260e07d27f1f3 Mon Sep 17 00:00:00 2001 From: edvan_microsoft Date: Tue, 14 May 2024 16:20:10 +0200 Subject: [PATCH 3/6] mypy coverage for the smaller folders --- python/mypy.ini | 9 ----- python/semantic_kernel/kernel.py | 29 +++++++------- .../reliability/pass_through_without_retry.py | 2 +- .../services/ai_service_client_base.py | 4 +- .../services/ai_service_selector.py | 8 ++-- .../text/function_extension.py | 3 +- python/semantic_kernel/text/text_chunker.py | 40 +++++++++---------- 7 files changed, 42 insertions(+), 53 deletions(-) diff --git a/python/mypy.ini b/python/mypy.ini index 8bf962bd2dce..19b12e35cda8 100644 --- a/python/mypy.ini +++ b/python/mypy.ini @@ -31,18 +31,9 @@ ignore_errors = true [mypy-semantic_kernel.prompt_template.*] ignore_errors = true -[mypy-semantic_kernel.reliability.*] -ignore_errors = true - -[mypy-semantic_kernel.services.*] -ignore_errors = true - [mypy-semantic_kernel.template_engine.*] ignore_errors = true -[mypy-semantic_kernel.text.*] -ignore_errors = true - [mypy-semantic_kernel.utils.*] ignore_errors = true diff --git a/python/semantic_kernel/kernel.py b/python/semantic_kernel/kernel.py index e9b56e0867cd..28083b988bc3 100644 --- a/python/semantic_kernel/kernel.py +++ b/python/semantic_kernel/kernel.py @@ -49,6 +49,7 @@ T = TypeVar("T") ALL_SERVICE_TYPES = Union["TextCompletionClientBase", "ChatCompletionClientBase", "EmbeddingGeneratorBase"] +AI_SERVICE_TYPES = TypeVar("AI_SERVICE_TYPES", bound=AIServiceClientBase) logger: logging.Logger = logging.getLogger(__name__) @@ -847,8 +848,8 @@ def select_ai_service( def get_service( self, service_id: str | None = None, - type: Type[ALL_SERVICE_TYPES] | None = None, - ) -> "AIServiceClientBase": + type: Type[AI_SERVICE_TYPES] | tuple[Type[AI_SERVICE_TYPES], ...] | None = None, + ) -> AI_SERVICE_TYPES: """Get a service by service_id and type. Type is optional and when not supplied, no checks are done. @@ -866,32 +867,30 @@ def get_service( type (Type[ALL_SERVICE_TYPES] | None): The type of the service, if None, no checks are done. Returns: - ALL_SERVICE_TYPES: The service. + AI_SERVICE_TYPES: The service. Raises: ValueError: If no service is found that matches the type. """ - service: "AIServiceClientBase | None" = None + service: AI_SERVICE_TYPES | None = None if not service_id or service_id == "default": if not type: - if default_service := self.services.get("default"): - return default_service - return list(self.services.values())[0] + type = AIServiceClientBase # type: ignore if default_service := self.services.get("default"): - if isinstance(default_service, type): - return default_service - for service in self.services.values(): - if isinstance(service, type): - return service + if isinstance(default_service, type): # type: ignore + return default_service # type: ignore + for service in self.services.values(): # type: ignore + if isinstance(service, type): # type: ignore + return service # type: ignore raise KernelServiceNotFoundError(f"No service found of type {type}") - if not (service := self.services.get(service_id)): + if not (service := self.services.get(service_id)): # type: ignore 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 + return service # type: ignore - def get_services_by_type(self, type: Type[ALL_SERVICE_TYPES]) -> dict[str, "AIServiceClientBase"]: + def get_services_by_type(self, type: Type[AI_SERVICE_TYPES]) -> dict[str, AI_SERVICE_TYPES]: return {service.service_id: service for service in self.services.values() if isinstance(service, type)} def get_prompt_execution_settings_from_service_id( diff --git a/python/semantic_kernel/reliability/pass_through_without_retry.py b/python/semantic_kernel/reliability/pass_through_without_retry.py index c568497480ea..bcf41634408f 100644 --- a/python/semantic_kernel/reliability/pass_through_without_retry.py +++ b/python/semantic_kernel/reliability/pass_through_without_retry.py @@ -24,7 +24,7 @@ async def execute_with_retry(self, action: Callable[[], Awaitable[T]]) -> Awaita 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 diff --git a/python/semantic_kernel/services/ai_service_client_base.py b/python/semantic_kernel/services/ai_service_client_base.py index 2c3100565bcf..5f05695ee69f 100644 --- a/python/semantic_kernel/services/ai_service_client_base.py +++ b/python/semantic_kernel/services/ai_service_client_base.py @@ -2,7 +2,7 @@ import sys from abc import ABC -from typing import Optional +from typing import Optional, Type if sys.version_info >= (3, 9): from typing import Annotated @@ -34,7 +34,7 @@ def model_post_init(self, __context: Optional[object] = None): 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 diff --git a/python/semantic_kernel/services/ai_service_selector.py b/python/semantic_kernel/services/ai_service_selector.py index e16faa2a7b9b..ba469b07e033 100644 --- a/python/semantic_kernel/services/ai_service_selector.py +++ b/python/semantic_kernel/services/ai_service_selector.py @@ -24,7 +24,7 @@ class AIServiceSelector: def select_ai_service( self, kernel: "Kernel", function: "KernelFunction", arguments: KernelArguments - ) -> Tuple["ALL_COMPLETION_SERVICE_TYPES", PromptExecutionSettings]: + ) -> Tuple["ALL_COMPLETION_SERVICE_TYPES", "PromptExecutionSettings"]: """Select a AI Service on a first come, first served basis, starting with execution settings in the arguments, followed by the execution settings from the function. @@ -42,10 +42,10 @@ def select_ai_service( execution_settings_dict = {"default": PromptExecutionSettings()} for service_id, settings in execution_settings_dict.items(): try: - service = kernel.get_service(service_id, type=(TextCompletionClientBase, ChatCompletionClientBase)) + service = kernel.get_service(service_id, type=(TextCompletionClientBase, ChatCompletionClientBase)) # type: ignore except KernelServiceNotFoundError: continue - if service: + if service is not None: service_settings = service.get_prompt_execution_settings_from_settings(settings) - return service, service_settings + return service, service_settings # type: ignore raise KernelServiceNotFoundError("No service found.") diff --git a/python/semantic_kernel/text/function_extension.py b/python/semantic_kernel/text/function_extension.py index d9ad06c52376..d5ee00923b0d 100644 --- a/python/semantic_kernel/text/function_extension.py +++ b/python/semantic_kernel/text/function_extension.py @@ -1,6 +1,5 @@ # Copyright (c) Microsoft. All rights reserved. -from typing import List from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.functions.kernel_function import KernelFunction @@ -8,7 +7,7 @@ async def aggregate_chunked_results( - func: KernelFunction, chunked_results: List[str], kernel: Kernel, arguments: KernelArguments + func: KernelFunction, chunked_results: list[str], kernel: Kernel, arguments: KernelArguments ) -> str: """ Aggregate the results from the chunked results. diff --git a/python/semantic_kernel/text/text_chunker.py b/python/semantic_kernel/text/text_chunker.py index b83e867a170b..ba25f919455c 100644 --- a/python/semantic_kernel/text/text_chunker.py +++ b/python/semantic_kernel/text/text_chunker.py @@ -7,11 +7,11 @@ import os import re -from typing import Callable, List, Tuple +from collections.abc import Callable NEWLINE = os.linesep -TEXT_SPLIT_OPTIONS = [ +TEXT_SPLIT_OPTIONS: list[list[str] | None] = [ ["\n", "\r"], ["."], ["?", "!"], @@ -24,7 +24,7 @@ None, ] -MD_SPLIT_OPTIONS = [ +MD_SPLIT_OPTIONS: list[list[str] | None] = [ ["."], ["?", "!"], [";"], @@ -49,7 +49,7 @@ def _token_counter(text: str) -> int: return len(text) // 4 -def split_plaintext_lines(text: str, max_token_per_line: int, token_counter: Callable = _token_counter) -> List[str]: +def split_plaintext_lines(text: str, max_token_per_line: int, token_counter: Callable = _token_counter) -> list[str]: """ Split plain text into lines. it will split on new lines first, and then on punctuation. @@ -62,7 +62,7 @@ def split_plaintext_lines(text: str, max_token_per_line: int, token_counter: Cal ) -def split_markdown_lines(text: str, max_token_per_line: int, token_counter: Callable = _token_counter) -> List[str]: +def split_markdown_lines(text: str, max_token_per_line: int, token_counter: Callable = _token_counter) -> list[str]: """ Split markdown into lines. It will split on punctuation first, and then on space and new lines. @@ -75,7 +75,7 @@ def split_markdown_lines(text: str, max_token_per_line: int, token_counter: Call ) -def split_plaintext_paragraph(text: List[str], max_tokens: int, token_counter: Callable = _token_counter) -> List[str]: +def split_plaintext_paragraph(text: list[str], max_tokens: int, token_counter: Callable = _token_counter) -> list[str]: """ Split plain text into paragraphs. """ @@ -94,7 +94,7 @@ def split_plaintext_paragraph(text: List[str], max_tokens: int, token_counter: C return _split_text_paragraph(text=split_lines, max_tokens=max_tokens, token_counter=token_counter) -def split_markdown_paragraph(text: List[str], max_tokens: int, token_counter: Callable = _token_counter) -> List[str]: +def split_markdown_paragraph(text: list[str], max_tokens: int, token_counter: Callable = _token_counter) -> list[str]: """ Split markdown into paragraphs. """ @@ -112,15 +112,15 @@ def split_markdown_paragraph(text: List[str], max_tokens: int, token_counter: Ca return _split_text_paragraph(text=split_lines, max_tokens=max_tokens, token_counter=token_counter) -def _split_text_paragraph(text: List[str], max_tokens: int, token_counter: Callable = _token_counter) -> List[str]: +def _split_text_paragraph(text: list[str], max_tokens: int, token_counter: Callable = _token_counter) -> list[str]: """ Split text into paragraphs. """ if not text: return [] - paragraphs = [] - current_paragraph = [] + paragraphs: list[str] = [] + current_paragraph: list[str] = [] for line in text: num_tokens_line = token_counter(line) @@ -164,7 +164,7 @@ def _split_markdown_lines( max_token_per_line: int, trim: bool, token_counter: Callable = _token_counter, -) -> List[str]: +) -> list[str]: """ Split markdown into lines. """ @@ -183,7 +183,7 @@ def _split_text_lines( max_token_per_line: int, trim: bool, token_counter: Callable = _token_counter, -) -> List[str]: +) -> list[str]: """ Split text into lines. """ @@ -200,15 +200,15 @@ 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]: +) -> list[str]: if not text: return [] text = text.replace("\r\n", "\n") - lines = [] + lines: list[str] = [] was_split = False for split_option in separators: if not lines: @@ -236,10 +236,10 @@ 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]: +) -> tuple[list[str], bool]: """ Split text into lines. """ @@ -295,12 +295,12 @@ def _split_str( def _split_list( - text: List[str], + text: list[str], max_tokens: int, - separators: List[str], + separators: list[str] | None, trim: bool, token_counter: Callable = _token_counter, -) -> Tuple[List[str], bool]: +) -> tuple[list[str], bool]: """ Split list of string into lines. """ From bc173657e18283c3bfa64d8bb59a09e9bc344b68 Mon Sep 17 00:00:00 2001 From: edvan_microsoft Date: Tue, 14 May 2024 16:26:25 +0200 Subject: [PATCH 4/6] updated codeblock --- .../template_engine/blocks/code_block.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/python/semantic_kernel/template_engine/blocks/code_block.py b/python/semantic_kernel/template_engine/blocks/code_block.py index 0c9f7b006e87..6fbf189ddbc7 100644 --- a/python/semantic_kernel/template_engine/blocks/code_block.py +++ b/python/semantic_kernel/template_engine/blocks/code_block.py @@ -11,15 +11,13 @@ 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 - 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.blocks.val_block import ValBlock - from semantic_kernel.template_engine.blocks.var_block import VarBlock logger: logging.Logger = logging.getLogger(__name__) @@ -51,7 +49,7 @@ class CodeBlock(Block): """ type: ClassVar[BlockTypes] = BlockTypes.CODE - tokens: list["VarBlock | ValBlock | NamedArgBlock | FunctionIdBlock"] = Field(default_factory=list) + tokens: list[Block] = Field(default_factory=list) @model_validator(mode="before") @classmethod @@ -151,7 +149,7 @@ def _enrich_function_arguments( ) 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) + 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 From 4740d58d0fec682b30a92582d6e0b2625e0678c1 Mon Sep 17 00:00:00 2001 From: edvan_microsoft Date: Fri, 31 May 2024 15:17:41 +0200 Subject: [PATCH 5/6] added core_plugins --- python/mypy.ini | 9 ------ .../conversation_summary_plugin.py | 28 ++++++++++++------- .../sessions_python_plugin.py | 26 +++++++++-------- .../core_plugins/text_memory_plugin.py | 2 +- .../core_plugins/web_search_engine_plugin.py | 4 +-- .../utils/experimental_decorator.py | 7 ++--- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/python/mypy.ini b/python/mypy.ini index b33ff19c34c4..6dc03eb3ccbe 100644 --- a/python/mypy.ini +++ b/python/mypy.ini @@ -16,18 +16,9 @@ no_implicit_reexport = true [mypy-semantic_kernel.connectors.*] ignore_errors = true -[mypy-semantic_kernel.core_plugins.*] -ignore_errors = true - -[mypy-semantic_kernel.events.*] -ignore_errors = true - [mypy-semantic_kernel.memory.*] ignore_errors = true [mypy-semantic_kernel.planners.*] ignore_errors = true -[mypy-semantic_kernel.utils.*] -ignore_errors = true - diff --git a/python/semantic_kernel/core_plugins/conversation_summary_plugin.py b/python/semantic_kernel/core_plugins/conversation_summary_plugin.py index 081dee2571e4..b648670772de 100644 --- a/python/semantic_kernel/core_plugins/conversation_summary_plugin.py +++ b/python/semantic_kernel/core_plugins/conversation_summary_plugin.py @@ -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 @@ 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 @@ def __init__( ) -> 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 @@ async def summarize_conversation( ]: """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 diff --git a/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_plugin.py b/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_plugin.py index b1fcd1a68577..536d20209733 100644 --- a/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_plugin.py +++ b/python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_plugin.py @@ -18,7 +18,7 @@ SessionsPythonSettings, ) from semantic_kernel.core_plugins.sessions_python_tool.sessions_remote_file_metadata import SessionsRemoteFileMetadata -from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException +from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException, FunctionInitializationError from semantic_kernel.functions.kernel_function_decorator import kernel_function from semantic_kernel.kernel_pydantic import KernelBaseModel @@ -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, @@ -58,14 +58,12 @@ def __init__( ) except ValidationError as e: logger.error(f"Failed to load the ACASessionsSettings with message: {str(e)}") - raise FunctionExecutionException(f"Failed to load the ACASessionsSettings with message: {str(e)}") from e - - endpoint = pool_management_endpoint or aca_settings.pool_management_endpoint + raise FunctionInitializationError(f"Failed to load the ACASessionsSettings with message: {str(e)}") from e 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 @@ def _sanitize_input(self, code: str) -> str: 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 """ @@ -166,7 +164,11 @@ async def execute_code(self, code: Annotated[str, "The valid Python code to exec @kernel_function(name="upload_file", description="Uploads a file for the current Session ID") async def upload_file( - self, *, data: BufferedReader = None, remote_file_path: str = None, local_file_path: str = None + self, + *, + data: BufferedReader | None = None, + remote_file_path: str | None = None, + local_file_path: str | None = None, ) -> SessionsRemoteFileMetadata: """Upload a file to the session pool. @@ -198,7 +200,7 @@ async def upload_file( 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() @@ -229,7 +231,7 @@ async def list_files(self) -> list[SessionsRemoteFileMetadata]: 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) -> 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: diff --git a/python/semantic_kernel/core_plugins/text_memory_plugin.py b/python/semantic_kernel/core_plugins/text_memory_plugin.py index 64479c3a4f5b..04b8e60f2f06 100644 --- a/python/semantic_kernel/core_plugins/text_memory_plugin.py +++ b/python/semantic_kernel/core_plugins/text_memory_plugin.py @@ -68,7 +68,7 @@ async def recall( 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", diff --git a/python/semantic_kernel/core_plugins/web_search_engine_plugin.py b/python/semantic_kernel/core_plugins/web_search_engine_plugin.py index fd695493ff88..65341a51759f 100644 --- a/python/semantic_kernel/core_plugins/web_search_engine_plugin.py +++ b/python/semantic_kernel/core_plugins/web_search_engine_plugin.py @@ -31,8 +31,8 @@ def __init__(self, connector: "ConnectorBase") -> None: 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) diff --git a/python/semantic_kernel/utils/experimental_decorator.py b/python/semantic_kernel/utils/experimental_decorator.py index ffd6c136d16c..2f472b0d68f2 100644 --- a/python/semantic_kernel/utils/experimental_decorator.py +++ b/python/semantic_kernel/utils/experimental_decorator.py @@ -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 From ba8cf5a558f874fd1d27775624496c4f95521ca0 Mon Sep 17 00:00:00 2001 From: edvan_microsoft Date: Fri, 31 May 2024 15:24:13 +0200 Subject: [PATCH 6/6] add todos --- python/mypy.ini | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/python/mypy.ini b/python/mypy.ini index 6dc03eb3ccbe..3c9d7b974110 100644 --- a/python/mypy.ini +++ b/python/mypy.ini @@ -15,10 +15,16 @@ no_implicit_reexport = true [mypy-semantic_kernel.connectors.*] ignore_errors = true +# TODO (eavanvalkenburg): remove this +# https://github.com/microsoft/semantic-kernel/issues/6462 [mypy-semantic_kernel.memory.*] ignore_errors = true +# TODO (eavanvalkenburg): remove this +# https://github.com/microsoft/semantic-kernel/issues/6463 [mypy-semantic_kernel.planners.*] ignore_errors = true +# TODO (eavanvalkenburg): remove this +# https://github.com/microsoft/semantic-kernel/issues/6465