382 lines
16 KiB
Python
382 lines
16 KiB
Python
# 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
|