chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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"
|
||||
Reference in New Issue
Block a user