Skip to content

Commit

Permalink
Merge branch 'main' into users/markwallace/transform_plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
markwallace-microsoft authored Jul 11, 2024
2 parents 367cce8 + bdf30a6 commit b453f79
Show file tree
Hide file tree
Showing 45 changed files with 2,252 additions and 794 deletions.
1 change: 1 addition & 0 deletions .github/workflows/python-test-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
python-tests-coverage:
name: Create Test Coverage Messages
runs-on: ${{ matrix.os }}
continue-on-error: true
permissions:
pull-requests: write
contents: read
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/python-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
permissions:
contents: write
defaults:
run:
working-directory: ./python
steps:
- uses: actions/checkout@v4
- name: Install poetry
Expand All @@ -27,9 +30,10 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install dependencies
run: cd python && poetry install --with unit-tests
run: poetry install --with unit-tests
- name: Test with pytest
run: cd python && poetry run pytest -q --junitxml=pytest-${{ matrix.os }}-${{ matrix.python-version }}.xml --cov=semantic_kernel --cov-report=term-missing:skip-covered ./tests/unit | tee python-coverage-${{ matrix.os }}-${{ matrix.python-version }}.txt
run: poetry run pytest -q --junitxml=pytest-${{ matrix.os }}-${{ matrix.python-version }}.xml --cov=semantic_kernel --cov-report=term-missing:skip-covered ./tests/unit | tee python-coverage-${{ matrix.os }}-${{ matrix.python-version }}.txt
continue-on-error: false
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
Expand Down
4 changes: 0 additions & 4 deletions python/mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ warn_untyped_fields = true
[mypy-semantic_kernel]
no_implicit_reexport = true

[mypy-semantic_kernel.connectors.ai.open_ai.*]
ignore_errors = true
# TODO (eavanvalkenburg): remove this: https://github.com/microsoft/semantic-kernel/issues/7131

[mypy-semantic_kernel.connectors.ai.azure_ai_inference.*]
ignore_errors = true
# TODO (eavanvalkenburg): remove this: https://github.com/microsoft/semantic-kernel/issues/7132
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,8 @@ async def get_chat_message_contents(
):
return await self._send_chat_request(chat_history, settings)

kernel: Kernel = kwargs.get("kernel")
arguments: KernelArguments = kwargs.get("arguments")
self._verify_function_choice_behavior(settings, kernel, arguments)
kernel = kwargs.get("kernel", None)
self._verify_function_choice_behavior(settings, kernel)
self._configure_function_choice_behavior(settings, kernel)

for request_index in range(settings.function_choice_behavior.maximum_auto_invoke_attempts):
Expand All @@ -146,7 +145,7 @@ async def get_chat_message_contents(
function_calls=function_calls,
chat_history=chat_history,
kernel=kernel,
arguments=arguments,
arguments=kwargs.get("arguments", None),
function_call_count=fc_count,
request_index=request_index,
function_behavior=settings.function_choice_behavior,
Expand Down Expand Up @@ -250,9 +249,8 @@ async def _get_streaming_chat_message_contents_auto_invoke(
**kwargs: Any,
) -> AsyncGenerator[list[StreamingChatMessageContent], Any]:
"""Get streaming chat message contents from the Azure AI Inference service with auto invoking functions."""
kernel: Kernel = kwargs.get("kernel")
arguments: KernelArguments = kwargs.get("arguments")
self._verify_function_choice_behavior(settings, kernel, arguments)
kernel: Kernel = kwargs.get("kernel", None)
self._verify_function_choice_behavior(settings, kernel)
self._configure_function_choice_behavior(settings, kernel)
request_attempts = settings.function_choice_behavior.maximum_auto_invoke_attempts

Expand All @@ -279,7 +277,7 @@ async def _get_streaming_chat_message_contents_auto_invoke(
function_calls=function_calls,
chat_history=chat_history,
kernel=kernel,
arguments=arguments,
arguments=kwargs.get("arguments", None),
function_call_count=len(function_calls),
request_index=request_index,
function_behavior=settings.function_choice_behavior,
Expand Down Expand Up @@ -396,14 +394,11 @@ def _verify_function_choice_behavior(
self,
settings: AzureAIInferenceChatPromptExecutionSettings,
kernel: Kernel,
arguments: KernelArguments,
):
"""Verify the function choice behavior."""
if settings.function_choice_behavior is not None:
if kernel is None:
raise ServiceInvalidExecutionSettingsError("Kernel is required for tool calls.")
if arguments is None and settings.function_choice_behavior.auto_invoke_kernel_functions:
raise ServiceInvalidExecutionSettingsError("Kernel arguments are required for auto tool calls.")
if settings.extra_parameters is not None and settings.extra_parameters.get("n", 1) > 1:
# Currently only OpenAI models allow multiple completions but the Azure AI Inference service
# does not expose the functionality directly. If users want to have more than 1 responses, they
Expand All @@ -425,7 +420,7 @@ async def _invoke_function_calls(
function_calls: list[FunctionCallContent],
chat_history: ChatHistory,
kernel: Kernel,
arguments: KernelArguments,
arguments: KernelArguments | None,
function_call_count: int,
request_index: int,
function_behavior: FunctionChoiceBehavior,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,25 @@


class ChatCompletionClientBase(AIServiceClientBase, ABC):
"""Base class for chat completion AI services."""

@abstractmethod
async def get_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
**kwargs: Any,
) -> list["ChatMessageContent"]:
"""This is the method that is called from the kernel to get a response from a chat-optimized LLM.
"""Create chat message contents, in the number specified by the settings.
Args:
chat_history (ChatHistory): A list of chats in a chat_history object, that can be
rendered into messages from system, user, assistant and tools.
settings (PromptExecutionSettings): Settings for the request.
kwargs (Dict[str, Any]): The optional arguments.
**kwargs (Any): The optional arguments.
Returns:
Union[str, List[str]]: A string or list of strings representing the response(s) from the LLM.
A list of chat message contents representing the response(s) from the LLM.
"""
pass

Expand All @@ -41,7 +43,7 @@ def get_streaming_chat_message_contents(
settings: "PromptExecutionSettings",
**kwargs: Any,
) -> AsyncGenerator[list["StreamingChatMessageContent"], Any]:
"""This is the method that is called from the kernel to get a stream response from a chat-optimized LLM.
"""Create streaming chat message contents, in the number specified by the settings.
Args:
chat_history (ChatHistory): A list of chat chat_history, that can be rendered into a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

@experimental_class
class EmbeddingGeneratorBase(AIServiceClientBase, ABC):
"""Base class for embedding generators."""

@abstractmethod
async def generate_embeddings(self, texts: list[str], **kwargs: Any) -> "ndarray":
"""Returns embeddings for the given texts as ndarray.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class ContentFilterAIException(ServiceContentFilterException):
"""AI exception for an error from Azure OpenAI's content filter."""

# The parameter that caused the error.
param: str
param: str | None

# The error code specific to the content filter.
content_filter_code: ContentFilterCodes
Expand All @@ -72,12 +72,12 @@ def __init__(
super().__init__(message)

self.param = inner_exception.param

inner_error = inner_exception.body.get("innererror", {})
self.content_filter_code = ContentFilterCodes(
inner_error.get("code", ContentFilterCodes.RESPONSIBLE_AI_POLICY_VIOLATION.value)
)
self.content_filter_result = {
key: ContentFilterResult.from_inner_error_result(values)
for key, values in inner_error.get("content_filter_result", {}).items()
}
if inner_exception.body is not None and isinstance(inner_exception.body, dict):
inner_error = inner_exception.body.get("innererror", {})
self.content_filter_code = ContentFilterCodes(
inner_error.get("code", ContentFilterCodes.RESPONSIBLE_AI_POLICY_VIOLATION.value)
)
self.content_filter_result = {
key: ContentFilterResult.from_inner_error_result(values)
for key, values in inner_error.get("content_filter_result", {}).items()
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def validate_function_calling_behaviors(cls, data) -> Any:

if isinstance(data, dict) and "function_call_behavior" in data.get("extension_data", {}):
data["function_choice_behavior"] = FunctionChoiceBehavior.from_function_call_behavior(
data.get("extension_data").get("function_call_behavior")
data.get("extension_data", {}).get("function_call_behavior")
)
return data

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
from collections.abc import Mapping
from copy import deepcopy
from typing import Any
from typing import Any, TypeVar
from uuid import uuid4

from openai import AsyncAzureOpenAI
Expand All @@ -29,10 +29,11 @@
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.finish_reason import FinishReason
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
from semantic_kernel.kernel_pydantic import HttpsUrl

logger: logging.Logger = logging.getLogger(__name__)

TChatMessageContent = TypeVar("TChatMessageContent", ChatMessageContent, StreamingChatMessageContent)


class AzureChatCompletion(AzureOpenAIConfigBase, OpenAIChatCompletionBase, OpenAITextCompletionBase):
"""Azure Chat completion class."""
Expand Down Expand Up @@ -93,13 +94,6 @@ def __init__(
if not azure_openai_settings.api_key and not ad_token and not ad_token_provider:
raise ServiceInitializationError("Please provide either api_key, ad_token or ad_token_provider")

if not azure_openai_settings.base_url and not azure_openai_settings.endpoint:
raise ServiceInitializationError("At least one of base_url or endpoint must be provided.")

if azure_openai_settings.endpoint and azure_openai_settings.chat_deployment_name:
azure_openai_settings.base_url = HttpsUrl(
f"{str(azure_openai_settings.endpoint).rstrip('/')}/openai/deployments/{azure_openai_settings.chat_deployment_name}"
)
super().__init__(
deployment_name=azure_openai_settings.chat_deployment_name,
endpoint=azure_openai_settings.endpoint,
Expand All @@ -111,11 +105,11 @@ def __init__(
ad_token_provider=ad_token_provider,
default_headers=default_headers,
ai_model_type=OpenAIModelTypes.CHAT,
async_client=async_client,
client=async_client,
)

@classmethod
def from_dict(cls, settings: dict[str, str]) -> "AzureChatCompletion":
def from_dict(cls, settings: dict[str, Any]) -> "AzureChatCompletion":
"""Initialize an Azure OpenAI service from a dictionary of settings.
Args:
Expand All @@ -136,7 +130,7 @@ def from_dict(cls, settings: dict[str, str]) -> "AzureChatCompletion":
env_file_path=settings.get("env_file_path"),
)

def get_prompt_execution_settings_class(self) -> "PromptExecutionSettings":
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
"""Create a request settings object."""
return AzureChatPromptExecutionSettings

Expand All @@ -155,37 +149,41 @@ def _create_streaming_chat_message_content(
) -> "StreamingChatMessageContent":
"""Create an Azure streaming chat message content object from a choice."""
content = super()._create_streaming_chat_message_content(chunk, choice, chunk_metadata)
assert isinstance(content, StreamingChatMessageContent) and isinstance(choice, ChunkChoice) # nosec
return self._add_tool_message_to_chat_message_content(content, choice)

def _add_tool_message_to_chat_message_content(
self, content: ChatMessageContent | StreamingChatMessageContent, choice: Choice
) -> "ChatMessageContent | StreamingChatMessageContent":
self,
content: TChatMessageContent,
choice: Choice | ChunkChoice,
) -> TChatMessageContent:
if tool_message := self._get_tool_message_from_chat_choice(choice=choice):
try:
tool_message_dict = json.loads(tool_message)
except json.JSONDecodeError:
logger.error("Failed to parse tool message JSON: %s", tool_message)
tool_message_dict = {"citations": tool_message}

if not isinstance(tool_message, dict):
# try to json, to ensure it is a dictionary
try:
tool_message = json.loads(tool_message)
except json.JSONDecodeError:
logger.warning("Tool message is not a dictionary, ignore context.")
return content
function_call = FunctionCallContent(
id=str(uuid4()),
name="Azure-OnYourData",
arguments=json.dumps({"query": tool_message_dict.get("intent", [])}),
arguments=json.dumps({"query": tool_message.get("intent", [])}),
)
result = FunctionResultContent.from_function_call_content_and_result(
result=tool_message_dict["citations"], function_call_content=function_call
result=tool_message["citations"], function_call_content=function_call
)
content.items.insert(0, function_call)
content.items.insert(1, result)
return content

def _get_tool_message_from_chat_choice(self, choice: Choice | ChunkChoice) -> str | None:
def _get_tool_message_from_chat_choice(self, choice: Choice | ChunkChoice) -> dict[str, Any] | None:
"""Get the tool message from a choice."""
content = choice.message if isinstance(choice, Choice) else choice.delta
if content.model_extra is not None and "context" in content.model_extra:
return json.dumps(content.model_extra["context"])

return None
if content.model_extra is not None:
return content.model_extra.get("context", None)
# openai allows extra content, so model_extra will be a dict, but we need to check anyway, but no way to test.
return None # pragma: no cover

@staticmethod
def split_message(message: "ChatMessageContent") -> list["ChatMessageContent"]:
Expand Down
Loading

0 comments on commit b453f79

Please sign in to comment.