chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run

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,27 @@
# Amazon Bedrock AI Agents in Semantic Kernel
## Overview
AWS Bedrock Agents is a managed service that allows users to stand up and run AI agents in the AWS cloud quickly.
## Tools/Functions
Bedrock Agents allow the use of tools via [action groups](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-action-create.html).
The integration of Bedrock Agents with Semantic Kernel allows users to register kernel functions as tools in Bedrock Agents.
## Enable code interpretation
Bedrock Agents can write and execute code via a feature known as [code interpretation](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-code-interpretation.html) similar to what OpenAI also offers.
## Enable user input
Bedrock Agents can [request user input](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-user-input.html) in case of missing information to invoke a tool. When this is enabled, the agent will prompt the user for the missing information. When this is disabled, the agent will guess the missing information.
## Knowledge base
Bedrock Agents can leverage data saved on AWS to perform RAG tasks, this is referred to as the [knowledge base](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-kb-add.html) in AWS.
## Multi-agent
Bedrock Agents support [multi-agent workflows](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-multi-agent-collaboration.html) for more complex tasks. However, it employs a different pattern than what we have in Semantic Kernel, thus this is not supported in the current integration.
@@ -0,0 +1,117 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
def kernel_function_to_bedrock_function_schema(
function_choice_configuration: FunctionCallChoiceConfiguration,
) -> dict[str, Any]:
"""Convert the kernel function to bedrock function schema."""
return {
"functions": [
kernel_function_metadata_to_bedrock_function_schema(function_metadata)
for function_metadata in function_choice_configuration.available_functions or []
]
}
def kernel_function_metadata_to_bedrock_function_schema(function_metadata: KernelFunctionMetadata) -> dict[str, Any]:
"""Convert the kernel function metadata to bedrock function schema."""
schema = {
"description": function_metadata.description,
"name": function_metadata.fully_qualified_name,
"parameters": {
parameter.name: kernel_function_parameter_to_bedrock_function_parameter(parameter)
for parameter in function_metadata.parameters
},
# This field controls whether user confirmation is required to invoke the function.
# If this is set to "ENABLED", the user will be prompted to confirm the function invocation.
# Only after the user confirms, the function call request will be issued by the agent.
# If the user denies the confirmation, the agent will act as if the function does not exist.
# Currently, we do not support this feature, so we set it to "DISABLED".
"requireConfirmation": "DISABLED",
}
# Remove None values from the schema
return {key: value for key, value in schema.items() if value is not None}
def kernel_function_parameter_to_bedrock_function_parameter(parameter: KernelParameterMetadata):
"""Convert the kernel function parameters to bedrock function parameters."""
schema = {
"description": parameter.description,
"type": kernel_function_parameter_type_to_bedrock_function_parameter_type(parameter.schema_data),
"required": parameter.is_required,
}
# Remove None values from the schema
return {key: value for key, value in schema.items() if value is not None}
# These are the allowed parameter types in bedrock function.
# https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent-runtime_ParameterDetail.html
BEDROCK_FUNCTION_ALLOWED_PARAMETER_TYPES = {
"string",
"number",
"integer",
"boolean",
"array",
}
def kernel_function_parameter_type_to_bedrock_function_parameter_type(schema_data: dict[str, Any] | None) -> str:
"""Convert the kernel function parameter type to bedrock function parameter type."""
if schema_data is None:
raise ValueError(
"Schema data is required to convert the kernel function parameter type to bedrock function parameter type."
)
type_ = schema_data.get("type")
if type_ is None:
raise ValueError(
"Type is required to convert the kernel function parameter type to bedrock function parameter type."
)
if type_ not in BEDROCK_FUNCTION_ALLOWED_PARAMETER_TYPES:
raise ValueError(
f"Type {type_} is not allowed in bedrock function parameter type. "
f"Allowed types are {BEDROCK_FUNCTION_ALLOWED_PARAMETER_TYPES}."
)
return type_
def parse_return_control_payload(return_control_payload: dict[str, Any]) -> list[FunctionCallContent]:
"""Parse the return control payload to a list of function call contents for the kernel."""
return [
FunctionCallContent(
id=return_control_payload["invocationId"],
name=invocation_input["functionInvocationInput"]["function"],
arguments={
parameter["name"]: parameter["value"]
for parameter in invocation_input["functionInvocationInput"]["parameters"]
},
metadata=invocation_input,
)
for invocation_input in return_control_payload.get("invocationInputs", [])
]
def parse_function_result_contents(function_result_contents: list[FunctionResultContent]) -> list[dict[str, Any]]:
"""Parse the function result contents to be returned to the agent in the session state."""
return [
{
"functionResult": {
"actionGroup": function_result_content.metadata["functionInvocationInput"]["actionGroup"],
"function": function_result_content.name,
"responseBody": {"TEXT": {"body": str(function_result_content.result)}},
}
}
for function_result_content in function_result_contents
]
@@ -0,0 +1,746 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
import os
import sys
from collections.abc import AsyncIterable, Awaitable, Callable
from functools import partial, reduce
from typing import Any, ClassVar
from pydantic import ValidationError
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from semantic_kernel.agents import AgentResponseItem, AgentThread
from semantic_kernel.agents.bedrock.action_group_utils import (
parse_function_result_contents,
parse_return_control_payload,
)
from semantic_kernel.agents.bedrock.bedrock_agent_base import BedrockAgentBase
from semantic_kernel.agents.bedrock.bedrock_agent_settings import BedrockAgentSettings
from semantic_kernel.agents.bedrock.models.bedrock_agent_event_type import BedrockAgentEventType
from semantic_kernel.agents.bedrock.models.bedrock_agent_model import BedrockAgentModel
from semantic_kernel.agents.bedrock.models.bedrock_agent_status import BedrockAgentStatus
from semantic_kernel.agents.channels.agent_channel import AgentChannel
from semantic_kernel.agents.channels.bedrock_agent_channel import BedrockAgentChannel
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.contents.binary_content import BinaryContent
from semantic_kernel.contents.chat_history import ChatHistory
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.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.agent_exceptions import AgentInitializationException, AgentInvokeException
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_plugin import KernelPlugin
from semantic_kernel.kernel import Kernel
from semantic_kernel.utils.async_utils import run_in_executor
from semantic_kernel.utils.feature_stage_decorator import experimental
from semantic_kernel.utils.telemetry.agent_diagnostics.decorators import (
trace_agent_get_response,
trace_agent_invocation,
trace_agent_streaming_invocation,
)
logger = logging.getLogger(__name__)
@experimental
class BedrockAgentThread(AgentThread):
"""Bedrock Agent Thread class."""
def __init__(
self,
bedrock_runtime_client: Any,
session_id: str | None = None,
) -> None:
"""Initialize the Bedrock Agent Thread.
The underlying Bedrock session of the thread is created when the thread is started.
https://docs.aws.amazon.com/bedrock/latest/userguide/sessions.html
Args:
bedrock_runtime_client: The Bedrock Runtime Client.
session_id: The session ID.
"""
super().__init__()
self._bedrock_runtime_client = bedrock_runtime_client
self._id = session_id
@override
async def _create(self) -> str:
"""Starts the thread and returns the underlying Bedrock session ID."""
response = await run_in_executor(
None,
partial(
self._bedrock_runtime_client.create_session,
),
)
self._id = response["sessionId"]
return self._id # type: ignore
@override
async def _delete(self) -> None:
"""Ends the current thread.
This will only end the underlying Bedrock session but not delete it.
"""
# Must end the session before deleting it.
await run_in_executor(
None,
partial(
self._bedrock_runtime_client.end_session,
sessionIdentifier=self._id,
),
)
@override
async def _on_new_message(self, new_message: str | ChatMessageContent) -> None:
"""Called when a new message has been contributed to the chat."""
raise NotImplementedError(
"This method is not implemented for BedrockAgentThread. "
"Messages and responses are automatically handled by the Bedrock service."
)
@experimental
class BedrockAgent(BedrockAgentBase):
"""Bedrock Agent.
Manages the interaction with Amazon Bedrock Agent Service.
"""
channel_type: ClassVar[type[AgentChannel]] = BedrockAgentChannel
def __init__(
self,
agent_model: BedrockAgentModel | dict[str, Any],
*,
function_choice_behavior: FunctionChoiceBehavior | None = None,
kernel: Kernel | None = None,
plugins: list[KernelPlugin | object] | dict[str, KernelPlugin | object] | None = None,
arguments: KernelArguments | None = None,
bedrock_runtime_client: Any | None = None,
bedrock_client: Any | None = None,
**kwargs,
) -> None:
"""Initialize the Bedrock Agent.
Note that this only creates the agent object and does not create the agent in the service.
Args:
agent_model (BedrockAgentModel | dict[str, Any]): The agent model.
function_choice_behavior (FunctionChoiceBehavior, optional): The function choice behavior for accessing
the kernel functions and filters.
kernel (Kernel, optional): The kernel to use.
plugins (list[KernelPlugin | object] | dict[str, KernelPlugin | object], optional): The plugins to use.
arguments (KernelArguments, optional): The kernel arguments.
Invoke method arguments take precedence over the arguments provided here.
bedrock_runtime_client: The Bedrock Runtime Client.
bedrock_client: The Bedrock Client.
**kwargs: Additional keyword arguments.
"""
args: dict[str, Any] = {
"agent_model": agent_model,
**kwargs,
}
if function_choice_behavior:
args["function_choice_behavior"] = function_choice_behavior
if kernel:
args["kernel"] = kernel
if plugins:
args["plugins"] = plugins
if arguments:
args["arguments"] = arguments
if bedrock_runtime_client:
args["bedrock_runtime_client"] = bedrock_runtime_client
if bedrock_client:
args["bedrock_client"] = bedrock_client
super().__init__(**args)
# region convenience class methods
@classmethod
async def create_and_prepare_agent(
cls,
name: str,
instructions: str,
*,
agent_resource_role_arn: str | None = None,
foundation_model: str | None = None,
bedrock_runtime_client: Any | None = None,
bedrock_client: Any | None = None,
kernel: Kernel | None = None,
plugins: list[KernelPlugin | object] | dict[str, KernelPlugin | object] | None = None,
function_choice_behavior: FunctionChoiceBehavior | None = None,
arguments: KernelArguments | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> "BedrockAgent":
"""Create a new agent asynchronously.
This is a convenience method that creates an instance of BedrockAgent and then creates the agent on the service.
Args:
name (str): The name of the agent.
instructions (str, optional): The instructions for the agent.
agent_resource_role_arn (str, optional): The ARN of the agent resource role.
foundation_model (str, optional): The foundation model.
bedrock_runtime_client (Any, optional): The Bedrock Runtime Client.
bedrock_client (Any, optional): The Bedrock Client.
kernel (Kernel, optional): The kernel to use.
plugins (list[KernelPlugin | object] | dict[str, KernelPlugin | object], optional): The plugins to use.
function_choice_behavior (FunctionChoiceBehavior, optional): The function choice behavior for accessing
the kernel functions and filters. Only FunctionChoiceType.AUTO is supported.
arguments (KernelArguments, optional): The kernel arguments.
prompt_template_config (PromptTemplateConfig, optional): The prompt template configuration.
env_file_path (str, optional): The path to the environment file.
env_file_encoding (str, optional): The encoding of the environment file.
Returns:
An instance of BedrockAgent with the created agent.
"""
try:
bedrock_agent_settings = BedrockAgentSettings(
agent_resource_role_arn=agent_resource_role_arn,
foundation_model=foundation_model,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise AgentInitializationException(f"Failed to initialize the Amazon Bedrock Agent settings: {e}") from e
import boto3
from botocore.exceptions import ClientError
bedrock_runtime_client = bedrock_runtime_client or boto3.client("bedrock-agent-runtime")
bedrock_client = bedrock_client or boto3.client("bedrock-agent")
try:
response = await run_in_executor(
None,
partial(
bedrock_client.create_agent,
agentName=name,
foundationModel=bedrock_agent_settings.foundation_model,
agentResourceRoleArn=bedrock_agent_settings.agent_resource_role_arn,
instruction=instructions,
),
)
except ClientError as e:
logger.error(f"Failed to create agent {name}.")
raise AgentInitializationException(f"Failed to create the Amazon Bedrock Agent: {e}") from e
bedrock_agent = cls(
response["agent"],
function_choice_behavior=function_choice_behavior,
kernel=kernel,
plugins=plugins,
arguments=arguments,
bedrock_runtime_client=bedrock_runtime_client,
bedrock_client=bedrock_client,
)
# The agent will first enter the CREATING status.
# When the operation finishes, it will enter the NOT_PREPARED status.
# We need to wait for the agent to reach the NOT_PREPARED status before we can prepare it.
await bedrock_agent._wait_for_agent_status(BedrockAgentStatus.NOT_PREPARED)
await bedrock_agent.prepare_agent_and_wait_until_prepared()
return bedrock_agent
# endregion
@trace_agent_get_response
@override
async def get_response(
self,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
*,
thread: AgentThread | None = None,
agent_alias: str | None = None,
arguments: KernelArguments | None = None,
kernel: "Kernel | None" = None,
**kwargs,
) -> AgentResponseItem[ChatMessageContent]:
"""Get a response from the agent.
Args:
messages (str | ChatMessageContent | list[str | ChatMessageContent]): The messages.
thread (AgentThread, optional): The thread. This is used to maintain the session state in the service.
agent_alias (str, optional): The agent alias.
arguments (KernelArguments, optional): The kernel arguments to override the current arguments.
kernel (Kernel, optional): The kernel to override the current kernel.
**kwargs: Additional keyword arguments.
Returns:
A chat message content with the response.
"""
if not isinstance(messages, str) and not isinstance(messages, ChatMessageContent):
raise AgentInvokeException("Messages must be a string or a ChatMessageContent for BedrockAgent.")
thread = await self._ensure_thread_exists_with_messages(
messages=messages,
thread=thread,
construct_thread=lambda: BedrockAgentThread(bedrock_runtime_client=self.bedrock_runtime_client),
expected_type=BedrockAgentThread,
)
assert thread.id is not None # nosec
if arguments is None:
arguments = KernelArguments(**kwargs)
else:
arguments.update(kwargs)
kernel = kernel or self.kernel
arguments = self._merge_arguments(arguments)
kwargs.setdefault("streamingConfigurations", {})["streamFinalResponse"] = False
kwargs.setdefault("sessionState", {})
for _ in range(self.function_choice_behavior.maximum_auto_invoke_attempts):
response = await self._invoke_agent(thread.id, messages, agent_alias, **kwargs)
events: list[dict[str, Any]] = []
for event in response.get("completion", []):
events.append(event)
if any(BedrockAgentEventType.RETURN_CONTROL in event for event in events):
# Check if there is function call requests. If there are function calls,
# parse and invoke them and return the results back to the agent.
# Not yielding the function call results back to the user.
kwargs["sessionState"].update(
await self._handle_return_control_event(
next(event for event in events if BedrockAgentEventType.RETURN_CONTROL in event),
kernel,
arguments,
)
)
else:
# For the rest of the events, the chunk will become the chat message content.
# If there are files or trace, they will be added to the chat message content.
file_items: list[BinaryContent] | None = None
trace_metadata: dict[str, Any] | None = None
chat_message_content: ChatMessageContent | None = None
for event in events:
if BedrockAgentEventType.CHUNK in event:
chat_message_content = self._handle_chunk_event(event)
elif BedrockAgentEventType.FILES in event:
file_items = self._handle_files_event(event)
elif BedrockAgentEventType.TRACE in event:
trace_metadata = self._handle_trace_event(event)
if not chat_message_content or not chat_message_content.content:
raise AgentInvokeException("Chat message content is expected but not found in the response.")
if file_items:
chat_message_content.items.extend(file_items)
if trace_metadata:
chat_message_content.metadata.update({"trace": trace_metadata})
if not chat_message_content:
raise AgentInvokeException("No response from the agent.")
chat_message_content.metadata["thread_id"] = thread.id
return AgentResponseItem(message=chat_message_content, thread=thread)
raise AgentInvokeException(
"Failed to get a response from the agent. Please consider increasing the auto invoke attempts."
)
@trace_agent_invocation
@override
async def invoke(
self,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
*,
thread: AgentThread | None = None,
on_new_message: Callable[[ChatMessageContent], Awaitable[None]] | None = None,
agent_alias: str | None = None,
arguments: KernelArguments | None = None,
kernel: "Kernel | None" = None,
**kwargs,
) -> AsyncIterable[AgentResponseItem[ChatMessageContent]]:
"""Invoke an agent.
Args:
messages (str | ChatMessageContent | list[str | ChatMessageContent]): The messages.
thread (AgentThread, optional): The thread. This is used to maintain the session state in the service.
on_new_message: A callback function to handle intermediate steps of the agent's execution.
agent_alias (str, optional): The agent alias.
arguments (KernelArguments, optional): The kernel arguments to override the current arguments.
kernel (Kernel, optional): The kernel to override the current kernel.
**kwargs: Additional keyword arguments.
Returns:
An async iterable of chat message content.
"""
if not isinstance(messages, str) and not isinstance(messages, ChatMessageContent):
raise AgentInvokeException("Messages must be a string or a ChatMessageContent for BedrockAgent.")
if on_new_message:
logger.warning("The on_new_message callback is not supported for BedrockAgent.")
thread = await self._ensure_thread_exists_with_messages(
messages=messages,
thread=thread,
construct_thread=lambda: BedrockAgentThread(bedrock_runtime_client=self.bedrock_runtime_client),
expected_type=BedrockAgentThread,
)
assert thread.id is not None # nosec
if arguments is None:
arguments = KernelArguments(**kwargs)
else:
arguments.update(kwargs)
kernel = kernel or self.kernel
arguments = self._merge_arguments(arguments)
kwargs.setdefault("streamingConfigurations", {})["streamFinalResponse"] = False
kwargs.setdefault("sessionState", {})
for _ in range(self.function_choice_behavior.maximum_auto_invoke_attempts):
response = await self._invoke_agent(thread.id, messages, agent_alias, **kwargs)
events: list[dict[str, Any]] = []
for event in response.get("completion", []):
events.append(event)
if any(BedrockAgentEventType.RETURN_CONTROL in event for event in events):
# Check if there is function call requests. If there are function calls,
# parse and invoke them and return the results back to the agent.
# Not yielding the function call results back to the user.
kwargs["sessionState"].update(
await self._handle_return_control_event(
next(event for event in events if BedrockAgentEventType.RETURN_CONTROL in event),
kernel,
arguments,
)
)
else:
for event in events:
if BedrockAgentEventType.CHUNK in event:
cmc = self._handle_chunk_event(event)
cmc.metadata["thread_id"] = thread.id
yield AgentResponseItem(message=cmc, thread=thread)
elif BedrockAgentEventType.FILES in event:
cmc = ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=self._handle_files_event(event), # type: ignore
name=self.name,
inner_content=event,
ai_model_id=self.agent_model.foundation_model,
)
cmc.metadata["thread_id"] = thread.id
yield AgentResponseItem(message=cmc, thread=thread)
elif BedrockAgentEventType.TRACE in event:
cmc = ChatMessageContent(
role=AuthorRole.ASSISTANT,
name=self.name,
content="",
inner_content=event,
ai_model_id=self.agent_model.foundation_model,
metadata=self._handle_trace_event(event),
)
cmc.metadata["thread_id"] = thread.id
yield AgentResponseItem(message=cmc, thread=thread)
return
raise AgentInvokeException(
"Failed to get a response from the agent. Please consider increasing the auto invoke attempts."
)
@trace_agent_streaming_invocation
@override
async def invoke_stream(
self,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
*,
thread: AgentThread | None = None,
on_new_message: Callable[[ChatMessageContent], Awaitable[None]] | None = None,
agent_alias: str | None = None,
arguments: KernelArguments | None = None,
kernel: "Kernel | None" = None,
**kwargs,
) -> AsyncIterable[AgentResponseItem[StreamingChatMessageContent]]:
"""Invoke an agent with streaming.
Args:
messages (str | ChatMessageContent | list[str | ChatMessageContent]): The messages.
thread (AgentThread, optional): The thread. This is used to maintain the session state in the service.
on_new_message: A callback function to handle intermediate steps of the
agent's execution as fully formed messages.
agent_alias (str, optional): The agent alias.
arguments (KernelArguments, optional): The kernel arguments to override the current arguments.
kernel (Kernel, optional): The kernel to override the current kernel.
**kwargs: Additional keyword arguments.
Returns:
An async iterable of streaming chat message content
"""
if not isinstance(messages, str) and not isinstance(messages, ChatMessageContent):
raise AgentInvokeException("Messages must be a string or a ChatMessageContent for BedrockAgent.")
if on_new_message:
logger.warning("The on_new_message callback is not supported for BedrockAgent.")
thread = await self._ensure_thread_exists_with_messages(
messages=messages,
thread=thread,
construct_thread=lambda: BedrockAgentThread(bedrock_runtime_client=self.bedrock_runtime_client),
expected_type=BedrockAgentThread,
)
assert thread.id is not None # nosec
if arguments is None:
arguments = KernelArguments(**kwargs)
else:
arguments.update(kwargs)
kernel = kernel or self.kernel
arguments = self._merge_arguments(arguments)
kwargs.setdefault("streamingConfigurations", {})["streamFinalResponse"] = True
kwargs.setdefault("sessionState", {})
for request_index in range(self.function_choice_behavior.maximum_auto_invoke_attempts):
response = await self._invoke_agent(thread.id, messages, agent_alias, **kwargs)
all_function_call_messages: list[StreamingChatMessageContent] = []
for event in response.get("completion", []):
if BedrockAgentEventType.CHUNK in event:
scmc = self._handle_streaming_chunk_event(event)
scmc.metadata["thread_id"] = thread.id
yield AgentResponseItem(message=scmc, thread=thread)
continue
if BedrockAgentEventType.FILES in event:
scmc = self._handle_streaming_files_event(event)
scmc.metadata["thread_id"] = thread.id
yield AgentResponseItem(message=scmc, thread=thread)
continue
if BedrockAgentEventType.TRACE in event:
scmc = self._handle_streaming_trace_event(event)
scmc.metadata["thread_id"] = thread.id
yield AgentResponseItem(message=scmc, thread=thread)
continue
if BedrockAgentEventType.RETURN_CONTROL in event:
all_function_call_messages.append(self._handle_streaming_return_control_event(event))
continue
if not all_function_call_messages:
return
full_message: StreamingChatMessageContent = reduce(lambda x, y: x + y, all_function_call_messages)
function_calls = [item for item in full_message.items if isinstance(item, FunctionCallContent)]
function_result_contents = await self._handle_function_call_contents(function_calls)
kwargs["sessionState"].update({
"invocationId": function_calls[0].id,
"returnControlInvocationResults": parse_function_result_contents(function_result_contents),
})
# region non streaming Event Handlers
def _handle_chunk_event(self, event: dict[str, Any]) -> ChatMessageContent:
"""Create a chat message content."""
chunk = event[BedrockAgentEventType.CHUNK]
completion = chunk["bytes"].decode()
return ChatMessageContent(
role=AuthorRole.ASSISTANT,
content=completion,
name=self.name,
inner_content=event,
ai_model_id=self.agent_model.foundation_model,
metadata=chunk,
)
async def _handle_return_control_event(
self,
event: dict[str, Any],
kernel: Kernel,
kernel_arguments: KernelArguments,
) -> dict[str, Any]:
"""Handle return control event."""
return_control_payload = event[BedrockAgentEventType.RETURN_CONTROL]
function_calls = parse_return_control_payload(return_control_payload)
if not function_calls:
raise AgentInvokeException("Function call is expected but not found in the response.")
function_result_contents = await self._handle_function_call_contents(function_calls)
return {
"invocationId": function_calls[0].id,
"returnControlInvocationResults": parse_function_result_contents(function_result_contents),
}
def _handle_files_event(self, event: dict[str, Any]) -> list[BinaryContent]:
"""Handle file event."""
files_event = event[BedrockAgentEventType.FILES]
return [
BinaryContent(
data=file["bytes"],
data_format="base64",
mime_type=file["type"],
metadata={"name": self._sanitize_filename(file["name"])},
)
for file in files_event["files"]
]
def _handle_trace_event(self, event: dict[str, Any]) -> dict[str, Any]:
"""Handle trace event."""
return event[BedrockAgentEventType.TRACE]
# endregion
# region streaming Event Handlers
def _handle_streaming_chunk_event(self, event: dict[str, Any]) -> StreamingChatMessageContent:
"""Handle streaming chunk event."""
chunk = event[BedrockAgentEventType.CHUNK]
completion = chunk["bytes"].decode()
return StreamingChatMessageContent(
role=AuthorRole.ASSISTANT,
choice_index=0,
content=completion,
name=self.name,
inner_content=event,
ai_model_id=self.agent_model.foundation_model,
)
def _handle_streaming_return_control_event(self, event: dict[str, Any]) -> StreamingChatMessageContent:
"""Handle streaming return control event."""
return_control_payload = event[BedrockAgentEventType.RETURN_CONTROL]
function_calls = parse_return_control_payload(return_control_payload)
return StreamingChatMessageContent(
role=AuthorRole.ASSISTANT,
choice_index=0,
items=function_calls, # type: ignore
name=self.name,
inner_content=event,
ai_model_id=self.agent_model.foundation_model,
)
def _handle_streaming_files_event(self, event: dict[str, Any]) -> StreamingChatMessageContent:
"""Handle streaming file event."""
files_event = event[BedrockAgentEventType.FILES]
items: list[BinaryContent] = [
BinaryContent(
data=file["bytes"],
data_format="base64",
mime_type=file["type"],
metadata={"name": self._sanitize_filename(file["name"])},
)
for file in files_event["files"]
]
return StreamingChatMessageContent(
role=AuthorRole.ASSISTANT,
choice_index=0,
items=items, # type: ignore
name=self.name,
inner_content=event,
ai_model_id=self.agent_model.foundation_model,
)
def _handle_streaming_trace_event(self, event: dict[str, Any]) -> StreamingChatMessageContent:
"""Handle streaming trace event."""
return StreamingChatMessageContent(
role=AuthorRole.ASSISTANT,
choice_index=0,
items=[],
name=self.name,
inner_content=event,
ai_model_id=self.agent_model.foundation_model,
metadata=event[BedrockAgentEventType.TRACE],
)
# endregion
async def _handle_function_call_contents(
self,
function_call_contents: list[FunctionCallContent],
) -> list[FunctionResultContent]:
"""Handle function call contents."""
chat_history = ChatHistory()
await asyncio.gather(
*[
self.kernel.invoke_function_call(
function_call=function_call,
chat_history=chat_history,
arguments=self.arguments,
function_call_count=len(function_call_contents),
function_behavior=self.function_choice_behavior,
)
for function_call in function_call_contents
],
)
return [
item
for chat_message in chat_history.messages
for item in chat_message.items
if isinstance(item, FunctionResultContent)
]
async def create_channel(self, thread_id: str | None = None) -> AgentChannel:
"""Create a ChatHistoryChannel.
Args:
chat_history: The chat history for the channel. If None, a new ChatHistory instance will be created.
thread_id: The ID of the thread. If None, a new thread will be created.
Returns:
An instance of AgentChannel.
"""
from semantic_kernel.agents.bedrock.bedrock_agent import BedrockAgentThread
BedrockAgentChannel.model_rebuild()
thread = BedrockAgentThread(bedrock_runtime_client=self.bedrock_runtime_client, session_id=thread_id)
if thread.id is None:
await thread.create()
return BedrockAgentChannel(thread=thread)
@override
async def _notify_thread_of_new_message(self, thread, new_message):
"""Bedrock agent doesn't need to notify the thread of new messages.
The new message is passed to the agent when invoking the agent.
"""
pass
@staticmethod
def _sanitize_filename(filename: str) -> str:
"""Sanitize filename to prevent directory traversal attacks.
Args:
filename: The filename to sanitize.
Returns:
The sanitized filename with directory components removed.
"""
# Extract basename to remove any directory traversal attempts
# Handle both Unix and Windows path separators
sanitized = os.path.basename(filename.replace("\\", "/"))
# Remove any remaining path separators or null bytes
result = sanitized.replace("/", "").replace("\\", "").replace("\x00", "")
if result != filename:
logger.warning(
f"Filename contained potentially malicious path components and was sanitized: "
f"'{filename}' -> '{result}'"
)
return result
@@ -0,0 +1,381 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
from functools import partial
from typing import Any, ClassVar
import boto3
from botocore.exceptions import ClientError
from pydantic import Field, field_validator
from semantic_kernel.agents.agent import Agent
from semantic_kernel.agents.bedrock.action_group_utils import kernel_function_to_bedrock_function_schema
from semantic_kernel.agents.bedrock.models.bedrock_action_group_model import BedrockActionGroupModel
from semantic_kernel.agents.bedrock.models.bedrock_agent_model import BedrockAgentModel
from semantic_kernel.agents.bedrock.models.bedrock_agent_status import BedrockAgentStatus
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior, FunctionChoiceType
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.utils.async_utils import run_in_executor
from semantic_kernel.utils.feature_stage_decorator import experimental
logger = logging.getLogger(__name__)
@experimental
class BedrockAgentBase(Agent):
"""Bedrock Agent Base Class to provide common functionalities for Bedrock Agents."""
# There is a default alias created by Bedrock for the working draft version of the agent.
# https://docs.aws.amazon.com/bedrock/latest/userguide/agents-deploy.html
WORKING_DRAFT_AGENT_ALIAS: ClassVar[str] = "TSTALIASID"
# Amazon Bedrock Clients
# Runtime Client: Use for inference
bedrock_runtime_client: Any
# Client: Use for model management
bedrock_client: Any
# Function Choice Behavior: this is primarily used to control the behavior of the kernel when
# the agent requests functions, and to configure the kernel function action group (i.e. via filters).
# When this is None, users won't be able to create a kernel function action groups.
function_choice_behavior: FunctionChoiceBehavior = Field(default=FunctionChoiceBehavior.Auto())
# Agent Model: stores the agent information
agent_model: BedrockAgentModel
def __init__(
self,
agent_model: BedrockAgentModel | dict[str, Any],
*,
function_choice_behavior: FunctionChoiceBehavior | None = None,
bedrock_runtime_client: Any | None = None,
bedrock_client: Any | None = None,
**kwargs,
) -> None:
"""Initialize the Bedrock Agent Base.
Args:
agent_model: The Bedrock Agent Model.
function_choice_behavior: The function choice behavior.
bedrock_client: The Bedrock Client.
bedrock_runtime_client: The Bedrock Runtime Client.
kwargs: Additional keyword arguments.
"""
agent_model = (
agent_model if isinstance(agent_model, BedrockAgentModel) else BedrockAgentModel.model_validate(agent_model)
)
args = {
"agent_model": agent_model,
"id": agent_model.agent_id,
"name": agent_model.agent_name,
"bedrock_runtime_client": bedrock_runtime_client or boto3.client("bedrock-agent-runtime"),
"bedrock_client": bedrock_client or boto3.client("bedrock-agent"),
**kwargs,
}
if function_choice_behavior:
args["function_choice_behavior"] = function_choice_behavior
super().__init__(**args)
@field_validator("function_choice_behavior", mode="after")
@classmethod
def validate_function_choice_behavior(
cls, function_choice_behavior: FunctionChoiceBehavior | None
) -> FunctionChoiceBehavior | None:
"""Validate the function choice behavior."""
if function_choice_behavior and function_choice_behavior.type_ != FunctionChoiceType.AUTO:
# Users cannot specify REQUIRED or NONE for the Bedrock agents.
# Please note that the function choice behavior only control if the kernel will automatically
# execute the functions the agent requests. It does not control the behavior of the agent.
raise ValueError("Only FunctionChoiceType.AUTO is supported.")
return function_choice_behavior
def __repr__(self):
"""Return the string representation of the Bedrock Agent."""
return f"{self.agent_model}"
# region Agent Management
async def prepare_agent_and_wait_until_prepared(self) -> None:
"""Prepare the agent for use."""
if not self.agent_model.agent_id:
raise ValueError("Agent does not exist. Please create the agent before preparing it.")
try:
await run_in_executor(
None,
partial(
self.bedrock_client.prepare_agent,
agentId=self.agent_model.agent_id,
),
)
# The agent will take some time to enter the PREPARING status after the prepare operation is called.
# We need to wait for the agent to reach the PREPARING status before we can proceed, otherwise we
# will return immediately if the agent is already in PREPARED status.
await self._wait_for_agent_status(BedrockAgentStatus.PREPARING)
# The agent will enter the PREPARED status when the preparation is complete.
await self._wait_for_agent_status(BedrockAgentStatus.PREPARED)
except ClientError as e:
logger.error(f"Failed to prepare agent {self.agent_model.agent_id}.")
raise e
async def delete_agent(self, **kwargs) -> None:
"""Delete an agent asynchronously."""
if not self.agent_model.agent_id:
raise ValueError("Agent does not exist. Please create the agent before deleting it.")
try:
await run_in_executor(
None,
partial(
self.bedrock_client.delete_agent,
agentId=self.agent_model.agent_id,
**kwargs,
),
)
self.agent_model.agent_id = None
except ClientError as e:
logger.error(f"Failed to delete agent {self.agent_model.agent_id}.")
raise e
async def _get_agent(self) -> None:
"""Get an agent."""
if not self.agent_model.agent_id:
raise ValueError("Agent does not exist. Please create the agent before getting it.")
try:
response = await run_in_executor(
None,
partial(
self.bedrock_client.get_agent,
agentId=self.agent_model.agent_id,
),
)
# Update the agent model
self.agent_model = BedrockAgentModel(**response["agent"])
except ClientError as e:
logger.error(f"Failed to get agent {self.agent_model.agent_id}.")
raise e
async def _wait_for_agent_status(
self,
status: BedrockAgentStatus,
interval: int = 2,
max_attempts: int = 5,
) -> None:
"""Wait for the agent to reach a specific status."""
for _ in range(max_attempts):
await self._get_agent()
if self.agent_model.agent_status == status:
return
await asyncio.sleep(interval)
raise TimeoutError(
f"Agent did not reach status {status} within the specified time."
f" Current status: {self.agent_model.agent_status}"
)
# endregion Agent Management
# region Action Group Management
async def create_code_interpreter_action_group(self, **kwargs) -> BedrockActionGroupModel:
"""Create a code interpreter action group."""
if not self.agent_model.agent_id:
raise ValueError("Agent does not exist. Please create the agent before creating an action group for it.")
try:
response = await run_in_executor(
None,
partial(
self.bedrock_client.create_agent_action_group,
agentId=self.agent_model.agent_id,
agentVersion=self.agent_model.agent_version or "DRAFT",
actionGroupName=f"{self.agent_model.agent_name}_code_interpreter",
actionGroupState="ENABLED",
parentActionGroupSignature="AMAZON.CodeInterpreter",
**kwargs,
),
)
await self.prepare_agent_and_wait_until_prepared()
return BedrockActionGroupModel(**response["agentActionGroup"])
except ClientError as e:
logger.error(f"Failed to create code interpreter action group for agent {self.agent_model.agent_id}.")
raise e
async def create_user_input_action_group(self, **kwargs) -> BedrockActionGroupModel:
"""Create a user input action group."""
if not self.agent_model.agent_id:
raise ValueError("Agent does not exist. Please create the agent before creating an action group for it.")
try:
response = await run_in_executor(
None,
partial(
self.bedrock_client.create_agent_action_group,
agentId=self.agent_model.agent_id,
agentVersion=self.agent_model.agent_version or "DRAFT",
actionGroupName=f"{self.agent_model.agent_name}_user_input",
actionGroupState="ENABLED",
parentActionGroupSignature="AMAZON.UserInput",
**kwargs,
),
)
await self.prepare_agent_and_wait_until_prepared()
return BedrockActionGroupModel(**response["agentActionGroup"])
except ClientError as e:
logger.error(f"Failed to create user input action group for agent {self.agent_model.agent_id}.")
raise e
async def create_kernel_function_action_group(self, **kwargs) -> BedrockActionGroupModel | None:
"""Create a kernel function action group."""
if not self.agent_model.agent_id:
raise ValueError("Agent does not exist. Please create the agent before creating an action group for it.")
function_call_choice_config = self.function_choice_behavior.get_config(self.kernel)
if not function_call_choice_config.available_functions:
logger.warning("No available functions. Skipping kernel function action group creation.")
return None
try:
response = await run_in_executor(
None,
partial(
self.bedrock_client.create_agent_action_group,
agentId=self.agent_model.agent_id,
agentVersion=self.agent_model.agent_version or "DRAFT",
actionGroupName=f"{self.agent_model.agent_name}_kernel_function",
actionGroupState="ENABLED",
actionGroupExecutor={"customControl": "RETURN_CONTROL"},
functionSchema=kernel_function_to_bedrock_function_schema(function_call_choice_config),
**kwargs,
),
)
await self.prepare_agent_and_wait_until_prepared()
return BedrockActionGroupModel(**response["agentActionGroup"])
except ClientError as e:
logger.error(f"Failed to create kernel function action group for agent {self.agent_model.agent_id}.")
raise e
# endregion Action Group Management
# region Knowledge Base Management
async def associate_agent_knowledge_base(self, knowledge_base_id: str, **kwargs) -> dict[str, Any]:
"""Associate an agent with a knowledge base."""
if not self.agent_model.agent_id:
raise ValueError(
"Agent does not exist. Please create the agent before associating it with a knowledge base."
)
try:
response = await run_in_executor(
None,
partial(
self.bedrock_client.associate_agent_knowledge_base,
agentId=self.agent_model.agent_id,
agentVersion=self.agent_model.agent_version,
knowledgeBaseId=knowledge_base_id,
**kwargs,
),
)
await self.prepare_agent_and_wait_until_prepared()
return response
except ClientError as e:
logger.error(
f"Failed to associate agent {self.agent_model.agent_id} with knowledge base {knowledge_base_id}."
)
raise e
async def disassociate_agent_knowledge_base(self, knowledge_base_id: str, **kwargs) -> None:
"""Disassociate an agent with a knowledge base."""
if not self.agent_model.agent_id:
raise ValueError(
"Agent does not exist. Please create the agent before disassociating it with a knowledge base."
)
try:
response = await run_in_executor(
None,
partial(
self.bedrock_client.disassociate_agent_knowledge_base,
agentId=self.agent_model.agent_id,
agentVersion=self.agent_model.agent_version,
knowledgeBaseId=knowledge_base_id,
**kwargs,
),
)
await self.prepare_agent_and_wait_until_prepared()
return response
except ClientError as e:
logger.error(
f"Failed to disassociate agent {self.agent_model.agent_id} with knowledge base {knowledge_base_id}."
)
raise e
async def list_associated_agent_knowledge_bases(self, **kwargs) -> dict[str, Any]:
"""List associated knowledge bases with an agent."""
if not self.agent_model.agent_id:
raise ValueError("Agent does not exist. Please create the agent before listing associated knowledge bases.")
try:
return await run_in_executor(
None,
partial(
self.bedrock_client.list_agent_knowledge_bases,
agentId=self.agent_model.agent_id,
agentVersion=self.agent_model.agent_version,
**kwargs,
),
)
except ClientError as e:
logger.error(f"Failed to list associated knowledge bases for agent {self.agent_model.agent_id}.")
raise e
# endregion Knowledge Base Management
async def _invoke_agent(
self,
thread_id: str,
message: str | ChatMessageContent,
agent_alias: str | None = None,
**kwargs,
) -> dict[str, Any]:
"""Invoke an agent."""
if not self.agent_model.agent_id:
raise ValueError("Agent does not exist. Please create the agent before invoking it.")
if isinstance(message, ChatMessageContent) and message.role != AuthorRole.USER:
raise ValueError("Only user messages are supported for invoking a Bedrock agent.")
agent_alias = agent_alias or self.WORKING_DRAFT_AGENT_ALIAS
try:
return await run_in_executor(
None,
partial(
self.bedrock_runtime_client.invoke_agent,
agentAliasId=agent_alias,
agentId=self.agent_model.agent_id,
sessionId=thread_id,
inputText=message if isinstance(message, str) else message.content,
**kwargs,
),
)
except ClientError as e:
logger.error(f"Failed to invoke agent {self.agent_model.agent_id}.")
raise e
@@ -0,0 +1,32 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from semantic_kernel.kernel_pydantic import KernelBaseSettings
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
class BedrockAgentSettings(KernelBaseSettings):
"""Amazon Bedrock Agent service settings.
The settings are first loaded from environment variables with
the prefix 'BEDROCK_AGENT_'.
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 'BEDROCK_' are:
- agent_resource_role_arn: str - The Amazon Bedrock agent resource role ARN.
https://docs.aws.amazon.com/bedrock/latest/userguide/getting-started.html
(Env var BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN)
- foundation_model: str - The Amazon Bedrock foundation model ID to use.
(Env var BEDROCK_AGENT_FOUNDATION_MODEL)
"""
env_prefix: ClassVar[str] = "BEDROCK_AGENT_"
agent_resource_role_arn: str
foundation_model: str
@@ -0,0 +1,21 @@
# Copyright (c) Microsoft. All rights reserved.
from pydantic import ConfigDict, Field
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
class BedrockActionGroupModel(KernelBaseModel):
"""Bedrock Action Group Model.
Model field definitions for the Amazon Bedrock Action Group Service:
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-agent/client/create_agent_action_group.html
"""
# This model_config will merge with the KernelBaseModel.model_config
model_config = ConfigDict(extra="allow")
action_group_id: str = Field(..., alias="actionGroupId", description="The unique identifier of the action group.")
action_group_name: str = Field(..., alias="actionGroupName", description="The name of the action group.")
@@ -0,0 +1,19 @@
# Copyright (c) Microsoft. All rights reserved.
from enum import Enum
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
class BedrockAgentEventType(str, Enum):
"""Bedrock Agent Event Type."""
# Contains the text response from the agent.
CHUNK = "chunk"
# Contains the trace information (reasoning process) from the agent.
TRACE = "trace"
# Contains the function call requests from the agent.
RETURN_CONTROL = "returnControl"
# Contains the files generated by the agent using the code interpreter.
FILES = "files"
@@ -0,0 +1,24 @@
# Copyright (c) Microsoft. All rights reserved.
from pydantic import ConfigDict, Field
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
class BedrockAgentModel(KernelBaseModel):
"""Bedrock Agent Model.
Model field definitions for the Amazon Bedrock Agent Service:
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-agent/client/create_agent.html
"""
# This model_config will merge with the KernelBaseModel.model_config
model_config = ConfigDict(extra="allow")
agent_id: str | None = Field(default=None, alias="agentId", description="The unique identifier of the agent.")
agent_name: str | None = Field(default=None, alias="agentName", description="The name of the agent.")
agent_version: str | None = Field(default=None, alias="agentVersion", description="The version of the agent.")
foundation_model: str | None = Field(default=None, alias="foundationModel", description="The foundation model.")
agent_status: str | None = Field(default=None, alias="agentStatus", description="The status of the agent.")
@@ -0,0 +1,23 @@
# Copyright (c) Microsoft. All rights reserved.
from enum import Enum
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
class BedrockAgentStatus(str, Enum):
"""Bedrock Agent Status.
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_PrepareAgent.html#API_agent_PrepareAgent_ResponseElements
"""
CREATING = "CREATING"
PREPARING = "PREPARING"
PREPARED = "PREPARED"
NOT_PREPARED = "NOT_PREPARED"
DELETING = "DELETING"
FAILED = "FAILED"
VERSIONING = "VERSIONING"
UPDATING = "UPDATING"