refactor(agent): Agent modular refactoring (#1487)

This commit is contained in:
Fangyin Cheng
2024-05-07 09:45:26 +08:00
committed by GitHub
parent 2a418f91e8
commit 863b5404dd
86 changed files with 4513 additions and 967 deletions

View File

@@ -0,0 +1,4 @@
"""External planner.
Use AWEL as the external planner.
"""

View File

@@ -0,0 +1,311 @@
"""Agent Operator for AWEL."""
from abc import ABC
from typing import List, Optional, Type
from dbgpt.core.awel import MapOperator
from dbgpt.core.awel.flow import (
IOField,
OperatorCategory,
OperatorType,
Parameter,
ViewMetadata,
)
from dbgpt.core.awel.trigger.base import Trigger
from dbgpt.core.interface.message import ModelMessageRoleType
# TODO: Don't dependent on MixinLLMOperator
from dbgpt.model.operators.llm_operator import MixinLLMOperator
from ....util.llm.llm import LLMConfig
from ...agent import Agent, AgentGenerateContext, AgentMessage
from ...agent_manage import get_agent_manager
from ...base_agent import ConversableAgent
from .agent_operator_resource import AWELAgent
class BaseAgentOperator:
"""The abstract operator for an Agent."""
SHARE_DATA_KEY_MODEL_NAME = "share_data_key_agent_name"
def __init__(self, agent: Optional[Agent] = None):
"""Create an AgentOperator."""
self._agent = agent
@property
def agent(self) -> Agent:
"""Return the Agent."""
if not self._agent:
raise ValueError("agent is not set")
return self._agent
class WrappedAgentOperator(
BaseAgentOperator, MapOperator[AgentGenerateContext, AgentGenerateContext], ABC
):
"""The Agent operator.
Wrap the agent and trigger the agent to generate a reply.
"""
def __init__(self, agent: Agent, **kwargs):
"""Create an WrappedAgentOperator."""
super().__init__(agent=agent)
MapOperator.__init__(self, **kwargs)
async def map(self, input_value: AgentGenerateContext) -> AgentGenerateContext:
"""Trigger agent to generate a reply."""
now_rely_messages: List[AgentMessage] = []
if not input_value.message:
raise ValueError("The message is empty.")
input_message = input_value.message.copy()
# Isolate the message delivery mechanism and pass it to the operator
_goal = self.agent.name if self.agent.name else self.agent.role
current_goal = f"[{_goal}]:"
if input_message.content:
current_goal += input_message.content
input_message.current_goal = current_goal
# What was received was the User message
human_message = input_message.copy()
human_message.role = ModelMessageRoleType.HUMAN
now_rely_messages.append(human_message)
# Send a message (no reply required) and pass the message content
now_message = input_message
if input_value.rely_messages and len(input_value.rely_messages) > 0:
now_message = input_value.rely_messages[-1]
if not input_value.sender:
raise ValueError("The sender is empty.")
await input_value.sender.send(
now_message, self.agent, input_value.reviewer, False
)
agent_reply_message = await self.agent.generate_reply(
received_message=input_message,
sender=input_value.sender,
reviewer=input_value.reviewer,
rely_messages=input_value.rely_messages,
)
is_success = agent_reply_message.success
if not is_success:
raise ValueError(
f"The task failed at step {self.agent.role} and the attempt "
f"to repair it failed. The final reason for "
f"failure:{agent_reply_message.content}!"
)
# What is sent is an AI message
ai_message = agent_reply_message.copy()
ai_message.role = ModelMessageRoleType.AI
now_rely_messages.append(ai_message)
# Handle user goals and outcome dependencies
return AgentGenerateContext(
message=input_message,
sender=self.agent,
reviewer=input_value.reviewer,
# Default single step transfer of information
rely_messages=now_rely_messages,
silent=input_value.silent,
)
class AWELAgentOperator(
MixinLLMOperator, MapOperator[AgentGenerateContext, AgentGenerateContext]
):
"""The Agent operator for AWEL."""
metadata = ViewMetadata(
label="AWEL Agent Operator",
name="agent_operator",
category=OperatorCategory.AGENT,
description="The Agent operator.",
parameters=[
Parameter.build_from(
"Agent",
"awel_agent",
AWELAgent,
description="The dbgpt agent.",
),
],
inputs=[
IOField.build_from(
"Agent Operator Request",
"agent_operator_request",
AgentGenerateContext,
"The Agent Operator request.",
)
],
outputs=[
IOField.build_from(
"Agent Operator Output",
"agent_operator_output",
AgentGenerateContext,
description="The Agent Operator output.",
)
],
)
def __init__(self, awel_agent: AWELAgent, **kwargs):
"""Create an AgentOperator."""
MixinLLMOperator.__init__(self)
MapOperator.__init__(self, **kwargs)
self.awel_agent = awel_agent
async def map(
self,
input_value: AgentGenerateContext,
) -> AgentGenerateContext:
"""Trigger agent to generate a reply."""
if not input_value.message:
raise ValueError("The message is empty.")
input_message = input_value.message.copy()
agent = await self.get_agent(input_value)
if agent.fixed_subgoal and len(agent.fixed_subgoal) > 0:
# Isolate the message delivery mechanism and pass it to the operator
current_goal = f"[{agent.name if agent.name else agent.role}]:"
if agent.fixed_subgoal:
current_goal += agent.fixed_subgoal
input_message.current_goal = current_goal
input_message.content = agent.fixed_subgoal
else:
# Isolate the message delivery mechanism and pass it to the operator
current_goal = f"[{agent.name if agent.name else agent.role}]:"
if input_message.content:
current_goal += input_message.content
input_message.current_goal = current_goal
now_rely_messages: List[AgentMessage] = []
# What was received was the User message
human_message = input_message.copy()
human_message.role = ModelMessageRoleType.HUMAN
now_rely_messages.append(human_message)
# Send a message (no reply required) and pass the message content
now_message = input_message
if input_value.rely_messages and len(input_value.rely_messages) > 0:
now_message = input_value.rely_messages[-1]
sender = input_value.sender
if not sender:
raise ValueError("The sender is empty.")
await sender.send(now_message, agent, input_value.reviewer, False)
agent_reply_message = await agent.generate_reply(
received_message=input_message,
sender=sender,
reviewer=input_value.reviewer,
rely_messages=input_value.rely_messages,
)
is_success = agent_reply_message.success
if not is_success:
raise ValueError(
f"The task failed at step {agent.role} and the attempt to "
f"repair it failed. The final reason for "
f"failure:{agent_reply_message.content}!"
)
# What is sent is an AI message
ai_message: AgentMessage = agent_reply_message.copy()
ai_message.role = ModelMessageRoleType.AI
now_rely_messages.append(ai_message)
# Handle user goals and outcome dependencies
return AgentGenerateContext(
message=input_message,
sender=agent,
reviewer=input_value.reviewer,
# Default single step transfer of information
rely_messages=now_rely_messages,
silent=input_value.silent,
memory=input_value.memory.structure_clone() if input_value.memory else None,
agent_context=input_value.agent_context,
resource_loader=input_value.resource_loader,
llm_client=input_value.llm_client,
round_index=agent.consecutive_auto_reply_counter,
)
async def get_agent(
self,
input_value: AgentGenerateContext,
) -> ConversableAgent:
"""Build the agent."""
# agent build
agent_cls: Type[ConversableAgent] = get_agent_manager().get_by_name(
self.awel_agent.agent_profile
)
llm_config = self.awel_agent.llm_config
if not llm_config:
if input_value.llm_client:
llm_config = LLMConfig(llm_client=input_value.llm_client)
else:
llm_config = LLMConfig(llm_client=self.llm_client)
else:
if not llm_config.llm_client:
if input_value.llm_client:
llm_config.llm_client = input_value.llm_client
else:
llm_config.llm_client = self.llm_client
kwargs = {}
if self.awel_agent.role_name:
kwargs["name"] = self.awel_agent.role_name
if self.awel_agent.fixed_subgoal:
kwargs["fixed_subgoal"] = self.awel_agent.fixed_subgoal
agent = (
await agent_cls(**kwargs)
.bind(input_value.memory)
.bind(llm_config)
.bind(input_value.agent_context)
.bind(self.awel_agent.resources)
.bind(input_value.resource_loader)
.build()
)
return agent
class AgentDummyTrigger(Trigger):
"""Http trigger for AWEL.
Http trigger is used to trigger a DAG by http request.
"""
metadata = ViewMetadata(
label="Agent Trigger",
name="agent_trigger",
category=OperatorCategory.AGENT,
operator_type=OperatorType.INPUT,
description="Trigger your workflow by agent",
inputs=[],
parameters=[],
outputs=[
IOField.build_from(
"Agent Operator Context",
"agent_operator_context",
AgentGenerateContext,
description="The Agent Operator output.",
)
],
)
def __init__(
self,
**kwargs,
) -> None:
"""Initialize a HttpTrigger."""
super().__init__(**kwargs)
async def trigger(self, **kwargs) -> None:
"""Trigger the DAG. Not used in HttpTrigger."""
raise NotImplementedError("Dummy trigger does not support trigger.")

View File

@@ -0,0 +1,209 @@
"""The AWEL Agent Operator Resource."""
from typing import Any, Dict, List, Optional
from dbgpt._private.pydantic import BaseModel, ConfigDict, Field, model_validator
from dbgpt.core import LLMClient
from dbgpt.core.awel.flow import (
FunctionDynamicOptions,
OptionValue,
Parameter,
ResourceCategory,
register_resource,
)
from ....resource.resource_api import AgentResource, ResourceType
from ....util.llm.llm import LLMConfig, LLMStrategyType
from ...agent_manage import get_agent_manager
@register_resource(
label="AWEL Agent Resource",
name="agent_operator_resource",
description="The Agent Resource.",
category=ResourceCategory.AGENT,
parameters=[
Parameter.build_from(
label="Agent Resource Type",
name="agent_resource_type",
type=str,
optional=True,
default=None,
options=[
OptionValue(label=item.name, name=item.value, value=item.value)
for item in ResourceType
],
),
Parameter.build_from(
label="Agent Resource Name",
name="agent_resource_name",
type=str,
optional=True,
default=None,
description="The agent resource name.",
),
Parameter.build_from(
label="Agent Resource Value",
name="agent_resource_value",
type=str,
optional=True,
default=None,
description="The agent resource value.",
),
],
alias=[
"dbgpt.serve.agent.team.layout.agent_operator_resource.AwelAgentResource",
"dbgpt.agent.plan.awel.agent_operator_resource.AWELAgentResource",
],
)
class AWELAgentResource(AgentResource):
"""AWEL Agent Resource."""
@model_validator(mode="before")
@classmethod
def pre_fill(cls, values: Dict[str, Any]) -> Dict[str, Any]:
"""Pre fill the agent ResourceType."""
if not isinstance(values, dict):
return values
name = values.pop("agent_resource_name")
type = values.pop("agent_resource_type")
value = values.pop("agent_resource_value")
values["name"] = name
values["type"] = ResourceType(type)
values["value"] = value
return values
@register_resource(
label="AWEL Agent LLM Config",
name="agent_operator_llm_config",
description="The Agent LLM Config.",
category=ResourceCategory.AGENT,
parameters=[
Parameter.build_from(
"LLM Client",
"llm_client",
LLMClient,
optional=True,
default=None,
description="The LLM Client.",
),
Parameter.build_from(
label="Agent LLM Strategy",
name="llm_strategy",
type=str,
optional=True,
default=None,
options=[
OptionValue(label=item.name, name=item.value, value=item.value)
for item in LLMStrategyType
],
description="The Agent LLM Strategy.",
),
Parameter.build_from(
label="Agent LLM Strategy Value",
name="strategy_context",
type=str,
optional=True,
default=None,
description="The agent LLM Strategy Value.",
),
],
alias=[
"dbgpt.serve.agent.team.layout.agent_operator_resource.AwelAgentConfig",
"dbgpt.agent.plan.awel.agent_operator_resource.AWELAgentConfig",
],
)
class AWELAgentConfig(LLMConfig):
"""AWEL Agent Config."""
pass
def _agent_resource_option_values() -> List[OptionValue]:
return [
OptionValue(label=item["name"], name=item["name"], value=item["name"])
for item in get_agent_manager().list_agents()
]
@register_resource(
label="AWEL Layout Agent",
name="agent_operator_agent",
description="The Agent to build the Agent Operator.",
category=ResourceCategory.AGENT,
parameters=[
Parameter.build_from(
label="Agent Profile",
name="agent_profile",
type=str,
description="Which agent want use.",
options=FunctionDynamicOptions(func=_agent_resource_option_values),
),
Parameter.build_from(
label="Role Name",
name="role_name",
type=str,
optional=True,
default=None,
description="The agent role name.",
),
Parameter.build_from(
label="Fixed Gogal",
name="fixed_subgoal",
type=str,
optional=True,
default=None,
description="The agent fixed gogal.",
),
Parameter.build_from(
label="Agent Resource",
name="agent_resource",
type=AWELAgentResource,
optional=True,
default=None,
description="The agent resource.",
),
Parameter.build_from(
label="Agent LLM Config",
name="agent_llm_Config",
type=AWELAgentConfig,
optional=True,
default=None,
description="The agent llm config.",
),
],
alias=[
"dbgpt.serve.agent.team.layout.agent_operator_resource.AwelAgent",
"dbgpt.agent.plan.awel.agent_operator_resource.AWELAgent",
],
)
class AWELAgent(BaseModel):
"""AWEL Agent."""
model_config = ConfigDict(arbitrary_types_allowed=True)
agent_profile: str
role_name: Optional[str] = None
llm_config: Optional[LLMConfig] = None
resources: List[AgentResource] = Field(default_factory=list)
fixed_subgoal: Optional[str] = None
@model_validator(mode="before")
@classmethod
def pre_fill(cls, values: Dict[str, Any]) -> Dict[str, Any]:
"""Pre fill the agent ResourceType."""
if not isinstance(values, dict):
return values
resource = values.pop("agent_resource")
llm_config = values.pop("agent_llm_Config")
if resource is not None:
values["resources"] = [resource]
if llm_config is not None:
values["llm_config"] = llm_config
return values

View File

@@ -0,0 +1,268 @@
"""The manager of the team for the AWEL layout."""
import logging
from abc import ABC, abstractmethod
from typing import Optional, cast
from dbgpt._private.config import Config
from dbgpt._private.pydantic import (
BaseModel,
ConfigDict,
Field,
model_to_dict,
validator,
)
from dbgpt.core.awel import DAG
from dbgpt.core.awel.dag.dag_manager import DAGManager
from ...action.base import ActionOutput
from ...agent import Agent, AgentGenerateContext, AgentMessage
from ...base_team import ManagerAgent
from ...profile import DynConfig, ProfileConfig
from .agent_operator import AWELAgentOperator, WrappedAgentOperator
logger = logging.getLogger(__name__)
class AWELTeamContext(BaseModel):
"""The context of the team for the AWEL layout."""
dag_id: str = Field(
...,
description="The unique id of dag",
examples=["flow_dag_testflow_66d8e9d6-f32e-4540-a5bd-ea0648145d0e"],
)
uid: str = Field(
default=None,
description="The unique id of flow",
examples=["66d8e9d6-f32e-4540-a5bd-ea0648145d0e"],
)
name: Optional[str] = Field(
default=None,
description="The name of dag",
)
label: Optional[str] = Field(
default=None,
description="The label of dag",
)
version: Optional[str] = Field(
default=None,
description="The version of dag",
)
description: Optional[str] = Field(
default=None,
description="The description of dag",
)
editable: bool = Field(
default=False,
description="is the dag is editable",
examples=[True, False],
)
state: Optional[str] = Field(
default=None,
description="The state of dag",
)
user_name: Optional[str] = Field(
default=None,
description="The owner of current dag",
)
sys_code: Optional[str] = Field(
default=None,
description="The system code of current dag",
)
flow_category: Optional[str] = Field(
default="common",
description="The flow category of current dag",
)
def to_dict(self):
"""Convert the object to a dictionary."""
return model_to_dict(self)
class AWELBaseManager(ManagerAgent, ABC):
"""AWEL base manager."""
model_config = ConfigDict(arbitrary_types_allowed=True)
profile: ProfileConfig = ProfileConfig(
name="AWELBaseManager",
role=DynConfig(
"PlanManager", category="agent", key="dbgpt_agent_plan_awel_profile_name"
),
goal=DynConfig(
"Promote and solve user problems according to the process arranged "
"by AWEL.",
category="agent",
key="dbgpt_agent_plan_awel_profile_goal",
),
desc=DynConfig(
"Promote and solve user problems according to the process arranged "
"by AWEL.",
category="agent",
key="dbgpt_agent_plan_awel_profile_desc",
),
)
async def _a_process_received_message(self, message: AgentMessage, sender: Agent):
"""Process the received message."""
pass
@abstractmethod
def get_dag(self) -> DAG:
"""Get the DAG of the manager."""
async def act(
self,
message: Optional[str],
sender: Optional[Agent] = None,
reviewer: Optional[Agent] = None,
**kwargs,
) -> Optional[ActionOutput]:
"""Perform the action."""
try:
agent_dag = self.get_dag()
last_node: AWELAgentOperator = cast(
AWELAgentOperator, agent_dag.leaf_nodes[0]
)
start_message_context: AgentGenerateContext = AgentGenerateContext(
message=AgentMessage(content=message, current_goal=message),
sender=sender,
reviewer=reviewer,
memory=self.memory.structure_clone(),
agent_context=self.agent_context,
resource_loader=self.resource_loader,
llm_client=self.not_null_llm_config.llm_client,
)
final_generate_context: AgentGenerateContext = await last_node.call(
call_data=start_message_context
)
last_message = final_generate_context.rely_messages[-1]
last_agent = await last_node.get_agent(final_generate_context)
if final_generate_context.round_index is not None:
last_agent.consecutive_auto_reply_counter = (
final_generate_context.round_index
)
if not sender:
raise ValueError("sender is required!")
await last_agent.send(
last_message, sender, start_message_context.reviewer, False
)
view_message: Optional[str] = None
if last_message.action_report:
view_message = last_message.action_report.get("view", None)
return ActionOutput(
content=last_message.content,
view=view_message,
)
except Exception as e:
logger.exception(f"DAG run failed!{str(e)}")
return ActionOutput(
is_exe_success=False,
content=f"Failed to complete goal! {str(e)}",
)
class WrappedAWELLayoutManager(AWELBaseManager):
"""The manager of the team for the AWEL layout.
Receives a DAG or builds a DAG from the agents.
"""
model_config = ConfigDict(arbitrary_types_allowed=True)
dag: Optional[DAG] = Field(None, description="The DAG of the manager")
def get_dag(self) -> DAG:
"""Get the DAG of the manager."""
if self.dag:
return self.dag
conv_id = self.not_null_agent_context.conv_id
last_node: Optional[WrappedAgentOperator] = None
with DAG(
f"layout_agents_{self.not_null_agent_context.gpts_app_name}_{conv_id}"
) as dag:
for agent in self.agents:
now_node = WrappedAgentOperator(agent=agent)
if not last_node:
last_node = now_node
else:
last_node >> now_node
last_node = now_node
self.dag = dag
return dag
async def act(
self,
message: Optional[str],
sender: Optional[Agent] = None,
reviewer: Optional[Agent] = None,
**kwargs,
) -> Optional[ActionOutput]:
"""Perform the action."""
try:
dag = self.get_dag()
last_node: WrappedAgentOperator = cast(
WrappedAgentOperator, dag.leaf_nodes[0]
)
start_message_context: AgentGenerateContext = AgentGenerateContext(
message=AgentMessage(content=message, current_goal=message),
sender=self,
reviewer=reviewer,
)
final_generate_context: AgentGenerateContext = await last_node.call(
call_data=start_message_context
)
last_message = final_generate_context.rely_messages[-1]
last_agent = last_node.agent
await last_agent.send(
last_message,
self,
start_message_context.reviewer,
False,
)
view_message: Optional[str] = None
if last_message.action_report:
view_message = last_message.action_report.get("view", None)
return ActionOutput(
content=last_message.content,
view=view_message,
)
except Exception as e:
logger.exception(f"DAG run failed!{str(e)}")
return ActionOutput(
is_exe_success=False,
content=f"Failed to complete goal! {str(e)}",
)
class DefaultAWELLayoutManager(AWELBaseManager):
"""The manager of the team for the AWEL layout."""
model_config = ConfigDict(arbitrary_types_allowed=True)
dag: AWELTeamContext = Field(...)
@validator("dag")
def check_dag(cls, value):
"""Check the DAG of the manager."""
assert value is not None and value != "", "dag must not be empty"
return value
def get_dag(self) -> DAG:
"""Get the DAG of the manager."""
cfg = Config()
_dag_manager = DAGManager.get_instance(cfg.SYSTEM_APP) # type: ignore
agent_dag: Optional[DAG] = _dag_manager.get_dag(alias_name=self.dag.uid)
if agent_dag is None:
raise ValueError(f"The configured flow cannot be found![{self.dag.name}]")
return agent_dag