Python: mypy coverage for contents folder (#5904)

### 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.
-->

Adds mypy typing coverage for the contents folder, includes some small
changes to the types in it.

### 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-04-17 13:15:38 +00:00
committed by GitHub
parent 6eebbfc25d
commit 15691eb931
12 changed files with 78 additions and 61 deletions
-3
View File
@@ -16,9 +16,6 @@ no_implicit_reexport = true
[mypy-semantic_kernel.connectors.*]
ignore_errors = true
[mypy-semantic_kernel.contents.*]
ignore_errors = true
[mypy-semantic_kernel.core_plugins.*]
ignore_errors = true
@@ -2,7 +2,7 @@
from typing import Literal, Optional
from semantic_kernel.connectors.ai.open_ai.contents.open_ai_chat_message_content import OpenAIChatMessageContent
from semantic_kernel.contents.const import AZURE_CHAT_MESSAGE_CONTENT
from semantic_kernel.contents.types import AZURE_CHAT_MESSAGE_CONTENT
class AzureChatMessageContent(OpenAIChatMessageContent):
@@ -6,7 +6,7 @@ from pydantic import field_validator
from semantic_kernel.connectors.ai.open_ai.contents.function_call import FunctionCall
from semantic_kernel.connectors.ai.open_ai.contents.tool_calls import ToolCall
from semantic_kernel.contents import ChatMessageContent
from semantic_kernel.contents.const import OPENAI_CHAT_MESSAGE_CONTENT
from semantic_kernel.contents.types import OPENAI_CHAT_MESSAGE_CONTENT
class OpenAIChatMessageContent(ChatMessageContent):
+19 -17
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
import logging
from typing import Any, Iterator, List
from typing import Any, Generator
from xml.etree.ElementTree import Element, tostring
from defusedxml.ElementTree import XML, ParseError
@@ -12,10 +12,12 @@ from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.chat_message_content_base import ChatMessageContentBase
from semantic_kernel.contents.chat_role import ChatRole
from semantic_kernel.contents.const import (
CHAT_MESSAGE_CONTENT,
ROOT_KEY_HISTORY,
ROOT_KEY_MESSAGE,
TYPES_CHAT_MESSAGE_CONTENT,
)
from semantic_kernel.contents.types import (
CHAT_MESSAGE_CONTENT,
CHAT_MESSAGE_CONTENT_TYPE_NAMES,
)
from semantic_kernel.exceptions import ContentInitializationError, ContentSerializationError
from semantic_kernel.kernel_pydantic import KernelBaseModel
@@ -36,7 +38,7 @@ class ChatHistory(KernelBaseModel):
"""
messages: list[ChatMessageContent]
message_type: TYPES_CHAT_MESSAGE_CONTENT = CHAT_MESSAGE_CONTENT
message_type: CHAT_MESSAGE_CONTENT_TYPE_NAMES = "ChatMessageContent"
def __init__(self, **data: Any):
"""
@@ -78,10 +80,10 @@ class ChatHistory(KernelBaseModel):
@field_validator("messages", mode="before")
@classmethod
def _validate_messages(cls, messages: List[ChatMessageContent]) -> List[ChatMessageContent]:
def _validate_messages(cls, messages: list[ChatMessageContent]) -> list[ChatMessageContent]:
if not messages:
return messages
out_msgs: List[ChatMessageContent] = []
out_msgs: list[ChatMessageContent] = []
for message in messages:
if isinstance(message, dict):
out_msgs.append(ChatMessageContentBase.from_dict(message))
@@ -109,7 +111,7 @@ class ChatHistory(KernelBaseModel):
def add_message(
self,
message: "ChatMessageContent" | dict[str, Any],
message: ChatMessageContent | dict[str, Any],
encoding: str | None = None,
metadata: dict[str, Any] | None = None,
) -> None:
@@ -145,7 +147,7 @@ class ChatHistory(KernelBaseModel):
kwargs["content"] = content
return kwargs
def remove_message(self, message: "ChatMessageContent") -> bool:
def remove_message(self, message: ChatMessageContent) -> bool:
"""Remove a message from the history.
Args:
@@ -164,7 +166,7 @@ class ChatHistory(KernelBaseModel):
"""Return the number of messages in the history."""
return len(self.messages)
def __getitem__(self, index: int) -> "ChatMessageContent":
def __getitem__(self, index: int) -> ChatMessageContent:
"""Get a message from the history using the [] operator.
Args:
@@ -175,7 +177,7 @@ class ChatHistory(KernelBaseModel):
"""
return self.messages[index]
def __contains__(self, item: "ChatMessageContent") -> bool:
def __contains__(self, item: ChatMessageContent) -> bool:
"""Check if a message is in the history.
Args:
@@ -193,9 +195,9 @@ class ChatHistory(KernelBaseModel):
chat_history_xml.append(message.to_element(root_key=ROOT_KEY_MESSAGE))
return tostring(chat_history_xml, encoding="unicode", short_empty_elements=True)
def __iter__(self) -> Iterator["ChatMessageContent"]:
def __iter__(self) -> Generator[ChatMessageContent, None, None]: # type: ignore
"""Return an iterator over the messages in the history."""
return iter(self.messages)
yield from self.messages
def __eq__(self, other: Any) -> bool:
"""Check if two ChatHistory instances are equal."""
@@ -205,7 +207,7 @@ class ChatHistory(KernelBaseModel):
return self.messages == other.messages
@classmethod
def from_rendered_prompt(cls, rendered_prompt: str, message_type: str = CHAT_MESSAGE_CONTENT) -> "ChatHistory":
def from_rendered_prompt(cls, rendered_prompt: str, message_type: str = CHAT_MESSAGE_CONTENT) -> ChatHistory:
"""
Create a ChatHistory instance from a rendered prompt.
@@ -215,7 +217,7 @@ class ChatHistory(KernelBaseModel):
Returns:
ChatHistory: The ChatHistory instance created from the rendered prompt.
"""
messages: List[ChatMessageContent] = []
messages: list[ChatMessageContent] = []
prompt = rendered_prompt.strip()
try:
xml_prompt = XML(text=f"<prompt>{prompt}</prompt>")
@@ -260,7 +262,7 @@ class ChatHistory(KernelBaseModel):
raise ContentSerializationError(f"Unable to serialize ChatHistory to JSON: {e}") from e
@classmethod
def restore_chat_history(cls, chat_history_json: str) -> "ChatHistory":
def restore_chat_history(cls, chat_history_json: str) -> ChatHistory:
"""
Restores a ChatHistory instance from a JSON string.
@@ -292,7 +294,7 @@ class ChatHistory(KernelBaseModel):
file.write(json_str)
@classmethod
def load_chat_history_from_file(cls, file_path: str) -> "ChatHistory":
def load_chat_history_from_file(cls, file_path: str) -> ChatHistory:
"""
Loads the ChatHistory from a file.
@@ -302,6 +304,6 @@ class ChatHistory(KernelBaseModel):
Returns:
ChatHistory: The deserialized ChatHistory instance.
"""
with open(file_path, "r") as file:
with open(file_path) as file:
json_str = file.read()
return cls.restore_chat_history(json_str)
@@ -1,15 +1,17 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
from enum import Enum
from typing import Literal, Optional
from typing import Literal
from xml.etree.ElementTree import Element
from defusedxml import ElementTree
from semantic_kernel.contents.chat_message_content_base import DISCRIMINATOR_FIELD
from semantic_kernel.contents.chat_role import ChatRole
from semantic_kernel.contents.const import CHAT_MESSAGE_CONTENT
from semantic_kernel.contents.const import DISCRIMINATOR_FIELD
from semantic_kernel.contents.finish_reason import FinishReason
from semantic_kernel.contents.kernel_content import KernelContent
from semantic_kernel.contents.types import CHAT_MESSAGE_CONTENT
from semantic_kernel.kernel_pydantic import KernelBaseModel
@@ -35,9 +37,9 @@ class ChatMessageContent(KernelContent):
type: Literal[CHAT_MESSAGE_CONTENT] = CHAT_MESSAGE_CONTENT # type: ignore
role: ChatRole
content: Optional[str] = None
encoding: Optional[str] = None
finish_reason: Optional[FinishReason] = None
content: str | None = None
encoding: str | None = None
finish_reason: FinishReason | None = None
def __str__(self) -> str:
return self.content or ""
@@ -1,8 +1,8 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from typing import TYPE_CHECKING, Any, Dict, Final
from __future__ import annotations
from semantic_kernel.contents.const import ALL_CHAT_MESSAGE_CONTENTS, CHAT_MESSAGE_CONTENT
import sys
from typing import TYPE_CHECKING, Any, Union
if sys.version_info >= (3, 9):
from typing import Annotated
@@ -13,10 +13,14 @@ from xml.etree.ElementTree import Element
from pydantic import Field, RootModel
from semantic_kernel.contents.const import DISCRIMINATOR_FIELD
from semantic_kernel.contents.types import CHAT_MESSAGE_CONTENT
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.open_ai.contents import AzureChatMessageContent, OpenAIChatMessageContent
from semantic_kernel.contents.chat_message_content import ChatMessageContent
DISCRIMINATOR_FIELD: Final[str] = "type"
CHAT_MESSAGE_CONTENT_TYPES = Union["ChatMessageContent", "OpenAIChatMessageContent", "AzureChatMessageContent"]
class ChatMessageContentBase(RootModel):
@@ -33,10 +37,10 @@ class ChatMessageContentBase(RootModel):
which is a instance of ChatMessageContent or the requested subclass.
"""
root: Annotated[ALL_CHAT_MESSAGE_CONTENTS, Field(discriminator=DISCRIMINATOR_FIELD)]
root: Annotated[CHAT_MESSAGE_CONTENT_TYPES, Field(discriminator=DISCRIMINATOR_FIELD)]
@classmethod
def from_fields(cls, **kwargs: Any) -> "ChatMessageContent":
def from_fields(cls, **kwargs: Any) -> ChatMessageContent:
"""Create a new instance of ChatMessageContent from fields.
Args:
@@ -54,10 +58,10 @@ class ChatMessageContentBase(RootModel):
cls.model_rebuild()
if DISCRIMINATOR_FIELD not in kwargs:
kwargs[DISCRIMINATOR_FIELD] = CHAT_MESSAGE_CONTENT
return cls(**kwargs).root
return cls(**kwargs).root # type: ignore
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "ChatMessageContent":
def from_dict(cls, data: dict[str, Any]) -> ChatMessageContent:
"""Create a new instance of ChatMessageContent from a dictionary.
Args:
@@ -69,7 +73,7 @@ class ChatMessageContentBase(RootModel):
return cls.from_fields(**data)
@classmethod
def from_element(cls, element: Element) -> "ChatMessageContent":
def from_element(cls, element: Element) -> ChatMessageContent:
"""Create a new instance of ChatMessageContent from a XML element.
Args:
@@ -78,7 +82,7 @@ class ChatMessageContentBase(RootModel):
Returns:
ChatMessageContent - The new instance of ChatMessageContent or a subclass.
"""
kwargs: Dict[str, Any] = {"content": element.text}
kwargs: dict[str, Any] = {"content": element.text}
for key, value in element.items():
kwargs[key] = value
return cls.from_fields(**kwargs)
+2 -8
View File
@@ -1,12 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Final, Literal, Union
from typing import Final
ROOT_KEY_MESSAGE: Final[str] = "message"
ROOT_KEY_HISTORY: Final[str] = "chat_history"
AZURE_CHAT_MESSAGE_CONTENT: Final[str] = "AzureChatMessageContent"
OPENAI_CHAT_MESSAGE_CONTENT: Final[str] = "OpenAIChatMessageContent"
CHAT_MESSAGE_CONTENT: Final[str] = "ChatMessageContent"
ALL_CHAT_MESSAGE_CONTENTS = Union[CHAT_MESSAGE_CONTENT, OPENAI_CHAT_MESSAGE_CONTENT, AZURE_CHAT_MESSAGE_CONTENT]
TYPES_CHAT_MESSAGE_CONTENT = Literal[CHAT_MESSAGE_CONTENT, OPENAI_CHAT_MESSAGE_CONTENT, AZURE_CHAT_MESSAGE_CONTENT]
DISCRIMINATOR_FIELD: Final[str] = "type"
@@ -1,6 +1,8 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional
from typing import Any
from pydantic import Field
@@ -10,9 +12,9 @@ from semantic_kernel.kernel_pydantic import KernelBaseModel
class KernelContent(KernelBaseModel, ABC):
"""Base class for all kernel contents."""
inner_content: Optional[Any] = None
ai_model_id: Optional[str] = None
metadata: Optional[Dict[str, Any]] = Field(default_factory=dict)
inner_content: Any | None = None
ai_model_id: str | None = None
metadata: dict[str, Any] | None = Field(default_factory=dict)
@abstractmethod
def __str__(self) -> str:
@@ -1,5 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.streaming_content_mixin import StreamingContentMixin
@@ -34,7 +34,7 @@ class StreamingChatMessageContent(StreamingContentMixin, ChatMessageContent):
def __bytes__(self) -> bytes:
return self.content.encode(self.encoding if self.encoding else "utf-8") if self.content else b""
def __add__(self, other: "StreamingChatMessageContent") -> "StreamingChatMessageContent":
def __add__(self, other: StreamingChatMessageContent) -> StreamingChatMessageContent:
"""When combining two StreamingChatMessageContent instances, the content fields are combined.
The inner_content of the first one is used, ai_model_id and encoding should be the same,
@@ -1,13 +1,14 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from abc import ABC, abstractmethod
from typing import Any
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self
from typing import Any
from semantic_kernel.kernel_pydantic import KernelBaseModel
@@ -1,5 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Optional
from __future__ import annotations
from semantic_kernel.contents.kernel_content import KernelContent
@@ -11,20 +11,20 @@ class TextContent(KernelContent):
Or they can implement their own subclass of this class and return an instance.
Args:
inner_content: Optional[Any] - The inner content of the response,
inner_content: Any - The inner content of the response,
this should hold all the information from the response so even
when not creating a subclass a developer can leverage the full thing.
ai_model_id: Optional[str] - The id of the AI model that generated this response.
metadata: Dict[str, Any] - Any metadata that should be attached to the response.
text: Optional[str] - The text of the response.
encoding: Optional[str] - The encoding of the text.
ai_model_id: str | None - The id of the AI model that generated this response.
metadata: dict[str, Any] - Any metadata that should be attached to the response.
text: str | None - The text of the response.
encoding: str | None - The encoding of the text.
Methods:
__str__: Returns the text of the response.
"""
text: Optional[str] = None
encoding: Optional[str] = None
text: str | None = None
encoding: str | None = None
def __str__(self) -> str:
return self.text or ""
+15
View File
@@ -0,0 +1,15 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Literal, Union, get_args
AZURE_CHAT_MESSAGE_CONTENT_TYPE = Literal["AzureChatMessageContent"]
OPENAI_CHAT_MESSAGE_CONTENT_TYPE = Literal["OpenAIChatMessageContent"]
CHAT_MESSAGE_CONTENT_TYPE = Literal["ChatMessageContent"]
AZURE_CHAT_MESSAGE_CONTENT: AZURE_CHAT_MESSAGE_CONTENT_TYPE = get_args(AZURE_CHAT_MESSAGE_CONTENT_TYPE)[0]
OPENAI_CHAT_MESSAGE_CONTENT: OPENAI_CHAT_MESSAGE_CONTENT_TYPE = get_args(OPENAI_CHAT_MESSAGE_CONTENT_TYPE)[0]
CHAT_MESSAGE_CONTENT: CHAT_MESSAGE_CONTENT_TYPE = get_args(CHAT_MESSAGE_CONTENT_TYPE)[0]
CHAT_MESSAGE_CONTENT_TYPE_NAMES = Union[
CHAT_MESSAGE_CONTENT_TYPE, OPENAI_CHAT_MESSAGE_CONTENT_TYPE, AZURE_CHAT_MESSAGE_CONTENT_TYPE
]