chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.anthropic.prompt_execution_settings.anthropic_prompt_execution_settings import (
AnthropicChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.anthropic.services.anthropic_chat_completion import AnthropicChatCompletion
__all__ = [
"AnthropicChatCompletion",
"AnthropicChatPromptExecutionSettings",
]
@@ -0,0 +1,55 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from typing import Annotated, Any
from pydantic import Field, model_validator
from semantic_kernel.connectors.ai.function_choice_type import FunctionChoiceType
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions import ServiceInvalidExecutionSettingsError
logger = logging.getLogger(__name__)
class AnthropicPromptExecutionSettings(PromptExecutionSettings):
"""Common request settings for Anthropic services."""
ai_model_id: Annotated[str | None, Field(serialization_alias="model")] = None
class AnthropicChatPromptExecutionSettings(AnthropicPromptExecutionSettings):
"""Specific settings for the Chat Completion endpoint."""
messages: list[dict[str, Any]] | None = None
stream: bool | None = None
system: str | None = None
max_tokens: Annotated[int, Field(gt=0)] = 1024
temperature: Annotated[float | None, Field(ge=0.0, le=2.0)] = None
stop_sequences: list[str] | None = None
top_p: Annotated[float | None, Field(ge=0.0, le=1.0)] = None
top_k: Annotated[int | None, Field(ge=0)] = None
tools: Annotated[
list[dict[str, Any]] | None,
Field(
description=(
"Do not set this manually. It is set by the service based on the function choice configuration."
),
),
] = None
tool_choice: Annotated[
dict[str, str] | None,
Field(
description="Do not set this manually. It is set by the service based on the function choice configuration."
),
] = None
@model_validator(mode="after")
def validate_tool_choice(self) -> "AnthropicChatPromptExecutionSettings":
"""Validate tool choice. Anthropic doesn't support NONE tool choice."""
tool_choice = self.tool_choice
if tool_choice and tool_choice.get("type") == FunctionChoiceType.NONE.value:
raise ServiceInvalidExecutionSettingsError("Tool choice 'none' is not supported by Anthropic.")
return self
@@ -0,0 +1,393 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import logging
import sys
from collections.abc import AsyncGenerator, Callable
from typing import TYPE_CHECKING, Any, ClassVar
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from anthropic import AsyncAnthropic
from anthropic.lib.streaming._types import TextEvent
from anthropic.types import (
ContentBlockStopEvent,
Message,
RawMessageDeltaEvent,
RawMessageStartEvent,
TextBlock,
ToolUseBlock,
)
from pydantic import ValidationError
from semantic_kernel.connectors.ai.anthropic.prompt_execution_settings.anthropic_prompt_execution_settings import (
AnthropicChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.anthropic.services.utils import (
MESSAGE_CONVERTERS,
update_settings_from_function_call_configuration,
)
from semantic_kernel.connectors.ai.anthropic.settings.anthropic_settings import AnthropicSettings
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import CMC_ITEM_TYPES, ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.streaming_chat_message_content import STREAMING_CMC_ITEM_TYPES as STREAMING_ITEM_TYPES
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.contents.utils.finish_reason import FinishReason as SemanticKernelFinishReason
from semantic_kernel.exceptions.service_exceptions import (
ServiceInitializationError,
ServiceInvalidRequestError,
ServiceInvalidResponseError,
ServiceResponseException,
)
from semantic_kernel.utils.feature_stage_decorator import experimental
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_chat_completion,
trace_streaming_chat_completion,
)
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
# map finish reasons from Anthropic to Semantic Kernel
ANTHROPIC_TO_SEMANTIC_KERNEL_FINISH_REASON_MAP = {
"end_turn": SemanticKernelFinishReason.STOP,
"max_tokens": SemanticKernelFinishReason.LENGTH,
"tool_use": SemanticKernelFinishReason.TOOL_CALLS,
}
logger: logging.Logger = logging.getLogger(__name__)
@experimental
class AnthropicChatCompletion(ChatCompletionClientBase):
"""Anthropic ChatCompletion class."""
MODEL_PROVIDER_NAME: ClassVar[str] = "anthropic"
SUPPORTS_FUNCTION_CALLING: ClassVar[bool] = True
async_client: AsyncAnthropic
def __init__(
self,
ai_model_id: str | None = None,
service_id: str | None = None,
api_key: str | None = None,
async_client: AsyncAnthropic | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize an AnthropicChatCompletion service.
Args:
ai_model_id: Anthropic model name, see
https://docs.anthropic.com/en/docs/about-claude/models#model-names
service_id: Service ID tied to the execution settings.
api_key: The optional API key to use. If provided will override,
the env vars or .env file value.
async_client: An existing client to use.
env_file_path: Use the environment settings file as a fallback
to environment variables.
env_file_encoding: The encoding of the environment settings file.
"""
try:
anthropic_settings = AnthropicSettings(
api_key=api_key,
chat_model_id=ai_model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create Anthropic settings.", ex) from ex
if not anthropic_settings.chat_model_id:
raise ServiceInitializationError("The Anthropic chat model ID is required.")
if not async_client:
async_client = AsyncAnthropic(
api_key=anthropic_settings.api_key.get_secret_value(),
)
super().__init__(
async_client=async_client,
service_id=service_id or anthropic_settings.chat_model_id,
ai_model_id=anthropic_settings.chat_model_id,
)
# region Overriding base class methods
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return AnthropicChatPromptExecutionSettings
# Override from AIServiceClientBase
@override
def service_url(self) -> str | None:
return str(self.async_client.base_url)
@override
def _update_function_choice_settings_callback(
self,
) -> Callable[["FunctionCallChoiceConfiguration", "PromptExecutionSettings", FunctionChoiceType], None]:
return update_settings_from_function_call_configuration
@override
def _reset_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
if hasattr(settings, "tool_choice"):
settings.tool_choice = None
if hasattr(settings, "tools"):
settings.tools = None
@override
@trace_chat_completion(MODEL_PROVIDER_NAME)
async def _inner_get_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
) -> list["ChatMessageContent"]:
if not isinstance(settings, AnthropicChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, AnthropicChatPromptExecutionSettings) # nosec
settings.ai_model_id = settings.ai_model_id or self.ai_model_id
settings.messages, parsed_system_message = self._prepare_chat_history_for_request(chat_history)
if settings.system is None and parsed_system_message is not None:
settings.system = parsed_system_message
return await self._send_chat_request(settings)
@override
@trace_streaming_chat_completion(MODEL_PROVIDER_NAME)
async def _inner_get_streaming_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
function_invoke_attempt: int = 0,
) -> AsyncGenerator[list["StreamingChatMessageContent"], Any]:
if not isinstance(settings, AnthropicChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, AnthropicChatPromptExecutionSettings) # nosec
settings.messages, parsed_system_message = self._prepare_chat_history_for_request(chat_history, stream=True)
settings.ai_model_id = settings.ai_model_id or self.ai_model_id
if settings.system is None and parsed_system_message is not None:
settings.system = parsed_system_message
response = self._send_chat_stream_request(settings, function_invoke_attempt)
if not isinstance(response, AsyncGenerator):
raise ServiceInvalidResponseError("Expected an AsyncGenerator response.")
async for message in response:
yield message
@override
def _prepare_chat_history_for_request(
self,
chat_history: "ChatHistory",
role_key: str = "role",
content_key: str = "content",
stream: bool = False,
) -> tuple[list[dict[str, Any]], str | None]:
"""Prepare the chat history for an Anthropic request.
Allowing customization of the key names for role/author, and optionally overriding the role.
Args:
chat_history: The chat history to prepare.
role_key: The key name for the role/author.
content_key: The key name for the content/message.
stream: Whether the request is for a streaming chat.
Returns:
A tuple containing the prepared chat history and the first SYSTEM message content.
"""
system_message_content = None
system_message_count = 0
formatted_messages: list[dict[str, Any]] = []
for i in range(len(chat_history)):
prev_message = chat_history[i - 1] if i > 0 else None
curr_message = chat_history[i]
if curr_message.role == AuthorRole.SYSTEM:
# Skip system messages after the first one is found
if system_message_count == 0:
system_message_content = curr_message.content
system_message_count += 1
elif curr_message.role == AuthorRole.USER or curr_message.role == AuthorRole.ASSISTANT:
formatted_messages.append(MESSAGE_CONVERTERS[curr_message.role](curr_message))
elif curr_message.role == AuthorRole.TOOL:
if prev_message is None:
# Under no circumstances should a tool message be the first message in the chat history
raise ServiceInvalidRequestError("Tool message found without a preceding message.")
if prev_message.role == AuthorRole.USER or prev_message.role == AuthorRole.SYSTEM:
# A tool message should not be found after a user or system message
# Please NOTE that in SK there are the USER role and the TOOL role, but in Anthropic
# the tool messages are considered as USER messages. We are checking against the SK roles.
raise ServiceInvalidRequestError("Tool message found after a user or system message.")
formatted_message = MESSAGE_CONVERTERS[curr_message.role](curr_message)
if prev_message.role == AuthorRole.ASSISTANT:
# The first tool message after an assistant message should be a new message
formatted_messages.append(formatted_message)
else:
# Append the tool message to the previous tool message.
# This indicates that the assistant message requested multiple parallel tool calls.
# Anthropic requires that parallel Tool messages are grouped together in a single message.
formatted_messages[-1][content_key] += formatted_message[content_key]
else:
raise ServiceInvalidRequestError(f"Unsupported role in chat history: {curr_message.role}")
if system_message_count > 1:
logger.warning(
"Anthropic service only supports one system message, but %s system messages were found."
" Only the first system message will be included in the request.",
system_message_count,
)
return formatted_messages, system_message_content
# endregion
def _create_chat_message_content(
self, response: Message, response_metadata: dict[str, Any]
) -> "ChatMessageContent":
"""Create a chat message content object."""
items: list[CMC_ITEM_TYPES] = []
items += self._get_tool_calls_from_message(response)
for content_block in response.content:
if isinstance(content_block, TextBlock):
items.append(TextContent(text=content_block.text))
finish_reason = None
if response.stop_reason:
finish_reason = ANTHROPIC_TO_SEMANTIC_KERNEL_FINISH_REASON_MAP[response.stop_reason]
return ChatMessageContent(
inner_content=response,
ai_model_id=self.ai_model_id,
metadata=response_metadata,
role=AuthorRole.ASSISTANT,
items=items,
finish_reason=finish_reason,
)
def _create_streaming_chat_message_content(
self,
stream_event: TextEvent | ContentBlockStopEvent | RawMessageDeltaEvent,
metadata: dict[str, Any] | None = None,
function_invoke_attempt: int = 0,
) -> StreamingChatMessageContent:
"""Create a streaming chat message content object from a content block."""
items: list[STREAMING_ITEM_TYPES] = []
finish_reason = None
if isinstance(stream_event, TextEvent):
items.append(StreamingTextContent(choice_index=0, text=stream_event.text))
elif (
isinstance(stream_event, ContentBlockStopEvent)
and hasattr(stream_event, "content_block")
and stream_event.content_block.type == "tool_use"
):
tool_use_block = stream_event.content_block
items.append(
FunctionCallContent(
id=tool_use_block.id,
index=stream_event.index,
name=tool_use_block.name,
arguments=json.dumps(tool_use_block.input) if tool_use_block.input else None,
)
)
elif isinstance(stream_event, RawMessageDeltaEvent):
finish_reason = ANTHROPIC_TO_SEMANTIC_KERNEL_FINISH_REASON_MAP[str(stream_event.delta.stop_reason)]
output_tokens = stream_event.usage.output_tokens
if metadata is None:
metadata = {"usage": {"output_tokens": output_tokens}}
else:
metadata = metadata | {"usage": metadata.get("usage", {}) | {"output_tokens": output_tokens}}
return StreamingChatMessageContent(
choice_index=0,
inner_content=stream_event,
ai_model_id=self.ai_model_id,
metadata=metadata,
role=AuthorRole.ASSISTANT,
finish_reason=finish_reason,
items=items,
function_invoke_attempt=function_invoke_attempt,
)
async def _send_chat_request(self, settings: AnthropicChatPromptExecutionSettings) -> list["ChatMessageContent"]:
"""Send the chat request."""
try:
response = await self.async_client.messages.create(**settings.prepare_settings_dict())
except Exception as ex:
raise ServiceResponseException(
f"{type(self)} service failed to complete the request",
ex,
) from ex
response_metadata: dict[str, Any] = {"id": response.id}
if hasattr(response, "usage") and response.usage is not None:
response_metadata["usage"] = response.usage
return [self._create_chat_message_content(response, response_metadata)]
async def _send_chat_stream_request(
self,
settings: AnthropicChatPromptExecutionSettings,
function_invoke_attempt: int = 0,
) -> AsyncGenerator[list["StreamingChatMessageContent"], None]:
"""Send the chat stream request.
The stream yields a sequence of stream events, which are used to create streaming chat message content:
- RawMessageStartEvent is used to determine the message id and input tokens.
- RawMessageDeltaEvent is used to determine the finish reason.
- TextEvent is used to determine the text content and ContentBlockStopEvent is used to determine
the tool use content.
"""
try:
async with self.async_client.messages.stream(**settings.prepare_settings_dict()) as stream:
metadata: dict[str, Any] = {"usage": {}, "id": None}
async for stream_event in stream:
if isinstance(stream_event, RawMessageStartEvent):
metadata["usage"]["input_tokens"] = stream_event.message.usage.input_tokens
metadata["id"] = stream_event.message.id
elif isinstance(stream_event, (TextEvent, RawMessageDeltaEvent)) or (
isinstance(stream_event, ContentBlockStopEvent)
and stream_event.content_block.type == "tool_use"
):
yield [
self._create_streaming_chat_message_content(stream_event, metadata, function_invoke_attempt)
]
except Exception as ex:
raise ServiceResponseException(
f"{type(self)} service failed to complete the request",
ex,
) from ex
def _get_tool_calls_from_message(self, message: Message) -> list[FunctionCallContent]:
"""Get tool calls from a content blocks."""
tool_calls: list[FunctionCallContent] = []
for idx, content_block in enumerate(message.content):
if isinstance(content_block, ToolUseBlock):
tool_calls.append(
FunctionCallContent(
id=content_block.id,
index=idx,
name=content_block.name,
arguments=getattr(content_block, "input", None),
)
)
return tool_calls
@@ -0,0 +1,157 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import logging
from collections.abc import Callable, Mapping
from typing import TYPE_CHECKING, Any
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
logger: logging.Logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
def _format_user_message(message: ChatMessageContent) -> dict[str, Any]:
"""Format a user message to the expected object for the Anthropic client.
Args:
message: The user message.
Returns:
The formatted user message.
"""
return {
"role": "user",
"content": message.content,
}
def _format_assistant_message(message: ChatMessageContent) -> dict[str, Any]:
"""Format an assistant message to the expected object for the Anthropic client.
Args:
message: The assistant message.
Returns:
The formatted assistant message.
"""
tool_calls: list[dict[str, Any]] = []
for item in message.items:
if isinstance(item, TextContent):
# Assuming the assistant message will have only one text content item
# and we assign the content directly to the message content, which is a string.
continue
if isinstance(item, FunctionCallContent):
tool_calls.append({
"type": "tool_use",
"id": item.id or "",
"name": item.name or "",
"input": item.arguments
if isinstance(item.arguments, Mapping)
else json.loads(item.arguments)
if item.arguments
else {},
})
else:
logger.warning(
f"Unsupported item type in Assistant message while formatting chat history for Anthropic: {type(item)}"
)
formatted_message: dict[str, Any] = {"role": "assistant", "content": []}
if message.content:
# Only include the text content if it is not empty.
# Otherwise, the Anthropic client will throw an error.
formatted_message["content"].append({ # type: ignore
"type": "text",
"text": message.content,
})
if tool_calls:
# Only include the tool calls if there are any.
# Otherwise, the Anthropic client will throw an error.
formatted_message["content"].extend(tool_calls) # type: ignore
return formatted_message
def _format_tool_message(message: ChatMessageContent) -> dict[str, Any]:
"""Format a tool message to the expected object for the Anthropic client.
Args:
message: The tool message.
Returns:
The formatted tool message.
"""
function_result_contents: list[dict[str, Any]] = []
for item in message.items:
if not isinstance(item, FunctionResultContent):
logger.warning(
f"Unsupported item type in Tool message while formatting chat history for Anthropic: {type(item)}"
)
continue
function_result_contents.append({
"type": "tool_result",
"tool_use_id": item.id,
"content": str(item.result),
})
return {
"role": "user",
"content": function_result_contents,
}
MESSAGE_CONVERTERS: dict[AuthorRole, Callable[[ChatMessageContent], dict[str, Any]]] = {
AuthorRole.USER: _format_user_message,
AuthorRole.ASSISTANT: _format_assistant_message,
AuthorRole.TOOL: _format_tool_message,
}
def update_settings_from_function_call_configuration(
function_choice_configuration: "FunctionCallChoiceConfiguration",
settings: "PromptExecutionSettings",
type: FunctionChoiceType,
) -> None:
"""Update the settings from a FunctionChoiceConfiguration."""
if (
function_choice_configuration.available_functions
and hasattr(settings, "tools")
and hasattr(settings, "tool_choice")
):
settings.tools = [
kernel_function_metadata_to_function_call_format(f)
for f in function_choice_configuration.available_functions
]
if (
settings.function_choice_behavior and settings.function_choice_behavior.type_ == FunctionChoiceType.REQUIRED
) or type == FunctionChoiceType.REQUIRED:
settings.tool_choice = {"type": "any"}
else:
settings.tool_choice = {"type": type.value}
def kernel_function_metadata_to_function_call_format(metadata: KernelFunctionMetadata) -> dict[str, Any]:
"""Convert the kernel function metadata to function calling format."""
return {
"name": metadata.fully_qualified_name,
"description": metadata.description or "",
"input_schema": {
"type": "object",
"properties": {p.name: p.schema_data for p in metadata.parameters},
"required": [p.name for p in metadata.parameters if p.is_required],
},
}
@@ -0,0 +1,29 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from pydantic import SecretStr
from semantic_kernel.kernel_pydantic import KernelBaseSettings
class AnthropicSettings(KernelBaseSettings):
"""Anthropic model settings.
The settings are first loaded from environment variables with the prefix 'ANTHROPIC_'. If the
environment variables are not found, the settings can be loaded from a .env file with the
encoding 'utf-8'. If the settings are not found in the .env file, the settings are ignored;
however, validation will fail alerting that the settings are missing.
Optional settings for prefix 'ANTHROPIC_' are:
- api_key: ANTHROPIC API key, see https://console.anthropic.com/settings/keys
(Env var ANTHROPIC_API_KEY)
- chat_model_id: The Anthropic chat model ID to use see https://docs.anthropic.com/en/docs/about-claude/models.
(Env var ANTHROPIC_CHAT_MODEL_ID)
- env_file_path: if provided, the .env settings are read from this file path location
"""
env_prefix: ClassVar[str] = "ANTHROPIC_"
api_key: SecretStr
chat_model_id: str | None = None