Duibonduil commited on
Commit
114b169
·
verified ·
1 Parent(s): 701ab2a

Upload 3 files

Browse files
aworld/core/agent/agent_desc.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Copyright (c) 2025 inclusionAI.
3
+ from typing import Dict, Any
4
+
5
+ from aworld.core.agent.base import AgentFactory
6
+ from aworld.logs.util import logger
7
+
8
+
9
+ def get_agent_desc() -> Dict[str, dict]:
10
+ """Utility method of generate description of agents.
11
+
12
+ The agent can also serve as a tool to be called.
13
+ The standard protocol can be transformed based on the API of different llm.
14
+ Define as follows:
15
+ ```
16
+ {
17
+ "agent_name": {
18
+ "desc": "An agent description.",
19
+ "abilities": [
20
+ {
21
+ "name": "ability name",
22
+ "desc": "ability description.",
23
+ "params": {
24
+ "param_name": {
25
+ "desc": "param description.",
26
+ "type": "param type, such as int, str, etc.",
27
+ "required": True | False
28
+ }
29
+ }
30
+ }
31
+ ]
32
+ }
33
+ }
34
+ ```
35
+ """
36
+
37
+ descs = dict()
38
+ for agent in AgentFactory:
39
+ agent_val_dict = dict()
40
+ descs[agent] = agent_val_dict
41
+
42
+ agent_val_dict["desc"] = AgentFactory.desc(agent)
43
+ abilities = []
44
+ ability_dict = dict()
45
+ # all agent has only `policy` ability now
46
+ ability_dict["name"] = "policy"
47
+
48
+ # The same as agent description.
49
+ ability_dict["desc"] = AgentFactory.desc(agent)
50
+ ability_dict["params"] = dict()
51
+
52
+ # content in observation
53
+ ability_dict["params"]["content"] = {
54
+ "desc": "The status information of the agent making the decision, which may be sourced from the env tool or another agent or self.",
55
+ "type": "str",
56
+ "required": True
57
+ }
58
+ ability_dict["params"]["info"] = {
59
+ "desc": "Some extended information provided to the agent for decision-making.",
60
+ "type": "str",
61
+ "required": False
62
+ }
63
+ abilities.append(ability_dict)
64
+ agent_val_dict["abilities"] = abilities
65
+ return descs
66
+
67
+
68
+ def get_agent_desc_by_name(name: str) -> Dict[str, Any]:
69
+ return get_agent_desc().get(name, None)
70
+
71
+
72
+ def agent_handoffs_desc(agent: 'Agent', use_all: bool = False) -> Dict[str, dict]:
73
+ if not agent:
74
+ if use_all:
75
+ # use all agent description
76
+ return get_agent_desc()
77
+ logger.warning(f"no agent to gen description!")
78
+ return {}
79
+
80
+ desc = {}
81
+ # agent.handoffs never is None
82
+ for reachable in agent.handoffs:
83
+ res = get_agent_desc_by_name(reachable)
84
+ if not res:
85
+ logger.warning(f"{reachable} can not find in the agent factory, ignored it.")
86
+ continue
87
+ desc[reachable] = res
88
+
89
+ return desc
aworld/core/agent/base.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Copyright (c) 2025 inclusionAI.
3
+
4
+ import abc
5
+ import uuid
6
+
7
+ import aworld.trace as trace
8
+
9
+ from typing import Generic, TypeVar, Dict, Any, List, Tuple, Union
10
+
11
+ from pydantic import BaseModel
12
+
13
+ from aworld.config.conf import AgentConfig, load_config, ConfigDict
14
+ from aworld.core.common import Observation, ActionModel
15
+ from aworld.core.context.base import AgentContext, Context
16
+ from aworld.core.event import eventbus
17
+ from aworld.core.event.base import Message, Constants
18
+ from aworld.core.factory import Factory
19
+ from aworld.logs.util import logger
20
+ from aworld.output.base import StepOutput
21
+ from aworld.sandbox.base import Sandbox
22
+
23
+ from aworld.utils.common import convert_to_snake, replace_env_variables
24
+
25
+ INPUT = TypeVar('INPUT')
26
+ OUTPUT = TypeVar('OUTPUT')
27
+
28
+
29
+ def is_agent_by_name(name: str) -> bool:
30
+ return name in AgentFactory
31
+
32
+
33
+ def is_agent(policy: ActionModel) -> bool:
34
+ return is_agent_by_name(policy.tool_name) or (not policy.tool_name and not policy.action_name)
35
+
36
+
37
+ class AgentStatus:
38
+ # Init status
39
+ START = 0
40
+ # Agent is running for monitor or collection
41
+ RUNNING = 1
42
+ # Agent reject the task
43
+ REJECT = 2
44
+ # Agent is idle
45
+ IDLE = 3
46
+ # Agent meets exception
47
+ ERROR = 4
48
+ # End of one agent step
49
+ DONE = 5
50
+ # End of one task step
51
+ FINISHED = 6
52
+
53
+
54
+ class AgentResult(BaseModel):
55
+ current_state: Any
56
+ actions: List[ActionModel]
57
+ is_call_tool: bool = True
58
+
59
+
60
+ class MemoryModel(BaseModel):
61
+ # TODO: memory module
62
+ message: Dict = {}
63
+ tool_calls: Any = None
64
+ content: Any = None
65
+
66
+
67
+ class BaseAgent(Generic[INPUT, OUTPUT]):
68
+ __metaclass__ = abc.ABCMeta
69
+
70
+ def __init__(self,
71
+ conf: Union[Dict[str, Any], ConfigDict, AgentConfig],
72
+ name: str,
73
+ desc: str = None,
74
+ agent_id: str = None,
75
+ *,
76
+ tool_names: List[str] = [],
77
+ agent_names: List[str] = [],
78
+ mcp_servers: List[str] = [],
79
+ mcp_config: Dict[str, Any] = {},
80
+ feedback_tool_result: bool = False,
81
+ sandbox: Sandbox = None,
82
+ **kwargs):
83
+ """Base agent init.
84
+
85
+ Args:
86
+ conf: Agent config for internal processes.
87
+ name: Agent name as identifier.
88
+ desc: Agent description as tool description.
89
+ tool_names: Tool names of local that agents can use.
90
+ agent_names: Agents as tool name list.
91
+ mcp_servers: Mcp names that the agent can use.
92
+ mcp_config: Mcp config for mcp servers.
93
+ feedback_tool_result: Whether feedback on the results of the tool.
94
+ Agent1 uses tool1 when the value is True, it does not go to the other agent after obtaining the result of tool1.
95
+ Instead, Agent1 uses the tool's result and makes a decision again.
96
+ sandbox: Sandbox instance for tool execution, advanced usage.
97
+ """
98
+ self.conf = conf
99
+ if isinstance(conf, ConfigDict):
100
+ pass
101
+ elif isinstance(conf, Dict):
102
+ self.conf = ConfigDict(conf)
103
+ elif isinstance(conf, AgentConfig):
104
+ # To add flexibility
105
+ self.conf = ConfigDict(conf.model_dump())
106
+ else:
107
+ logger.warning(f"Unknown conf type: {type(conf)}")
108
+
109
+ self._name = name if name else convert_to_snake(self.__class__.__name__)
110
+ self._desc = desc if desc else self._name
111
+ # Unique flag based agent name
112
+ self._id = agent_id if agent_id else f"{self._name}---uuid{uuid.uuid1().hex[0:6]}uuid"
113
+ self.task = None
114
+ # An agent can use the tool list
115
+ self.tool_names: List[str] = tool_names
116
+ human_tools = self.conf.get("human_tools", [])
117
+ for tool in human_tools:
118
+ self.tool_names.append(tool)
119
+ # An agent can delegate tasks to other agent
120
+ self.handoffs: List[str] = agent_names
121
+ # Supported MCP server
122
+ self.mcp_servers: List[str] = mcp_servers
123
+ self.mcp_config: Dict[str, Any] = replace_env_variables(mcp_config)
124
+ self.trajectory: List[Tuple[INPUT, Dict[str, Any], AgentResult]] = []
125
+ # all tools that the agent can use. note: string name/id only
126
+ self.tools = []
127
+ self.context = None
128
+ self.agent_context = None
129
+ self.state = AgentStatus.START
130
+ self._finished = True
131
+ self.hooks: Dict[str, List[str]] = {}
132
+ self.feedback_tool_result = feedback_tool_result
133
+ self.sandbox = sandbox or Sandbox(
134
+ mcp_servers=self.mcp_servers, mcp_config=self.mcp_config)
135
+
136
+ def _init_context(self, context: Context):
137
+ self.context = context
138
+ self.agent_context = AgentContext(
139
+ agent_id=self.id(),
140
+ agent_name=self.name(),
141
+ agent_desc=self.desc(),
142
+ tool_names=self.tool_names,
143
+ context=self.context,
144
+ parent_state=self.context.state # Pass Context's state as parent state
145
+ )
146
+
147
+ def id(self) -> str:
148
+ return self._id
149
+
150
+ def name(self):
151
+ return self._name
152
+
153
+ def desc(self) -> str:
154
+ return self._desc
155
+
156
+ def run(self, message: Message, **kwargs) -> Message:
157
+ self._init_context(message.context)
158
+ observation = message.payload
159
+ with trace.span(self._name, run_type=trace.RunType.AGNET) as agent_span:
160
+ self.pre_run()
161
+ result = self.policy(observation, **kwargs)
162
+ final_result = self.post_run(result, observation)
163
+ if final_result:
164
+ final_result.context = self.context
165
+ final_result.session_id = self.context.session_id
166
+ return final_result
167
+
168
+ async def async_run(self, message: Message, **kwargs) -> Message:
169
+ self._init_context(message.context)
170
+ observation = message.payload
171
+ if eventbus is not None:
172
+ await eventbus.publish(Message(
173
+ category=Constants.OUTPUT,
174
+ payload=StepOutput.build_start_output(name=f"{self.id()}", alias_name=self.name(), step_num=0),
175
+ sender=self.id(),
176
+ session_id=self.context.session_id
177
+ ))
178
+ with trace.span(self._name, run_type=trace.RunType.AGNET) as agent_span:
179
+ await self.async_pre_run()
180
+ result = await self.async_policy(observation, **kwargs)
181
+ final_result = await self.async_post_run(result, observation)
182
+ if final_result:
183
+ final_result.context = self.context
184
+ final_result.session_id = self.context.session_id
185
+ return final_result
186
+
187
+ @abc.abstractmethod
188
+ def policy(self, observation: INPUT, info: Dict[str, Any] = None, **kwargs) -> OUTPUT:
189
+ """The strategy of an agent can be to decide which tools to use in the environment, or to delegate tasks to other agents.
190
+
191
+ Args:
192
+ observation: The state observed from tools in the environment.
193
+ info: Extended information is used to assist the agent to decide a policy.
194
+ """
195
+
196
+ @abc.abstractmethod
197
+ async def async_policy(self, observation: INPUT, info: Dict[str, Any] = None, **kwargs) -> OUTPUT:
198
+ """The strategy of an agent can be to decide which tools to use in the environment, or to delegate tasks to other agents.
199
+
200
+ Args:
201
+ observation: The state observed from tools in the environment.
202
+ info: Extended information is used to assist the agent to decide a policy.
203
+ """
204
+
205
+ def reset(self, options: Dict[str, Any]):
206
+ """Clean agent instance state and reset."""
207
+ if options is None:
208
+ options = {}
209
+ self.task = options.get("task")
210
+ self.tool_names = options.get("tool_names", [])
211
+ self.handoffs = options.get("agent_names", [])
212
+ self.mcp_servers = options.get("mcp_servers", [])
213
+ self.tools = []
214
+ self.trajectory = []
215
+ self._finished = True
216
+
217
+ async def async_reset(self, options: Dict[str, Any]):
218
+ """Clean agent instance state and reset."""
219
+ self.task = options.get("task")
220
+
221
+ @property
222
+ def finished(self) -> bool:
223
+ """Agent finished the thing, default is True."""
224
+ return self._finished
225
+
226
+ def pre_run(self):
227
+ pass
228
+
229
+ def post_run(self, policy_result: OUTPUT, input: INPUT) -> Message:
230
+ return policy_result
231
+
232
+ async def async_pre_run(self):
233
+ pass
234
+
235
+ async def async_post_run(self, policy_result: OUTPUT, input: INPUT) -> Message:
236
+ return policy_result
237
+
238
+
239
+ class AgentManager(Factory):
240
+ def __init__(self, type_name: str = None):
241
+ super(AgentManager, self).__init__(type_name)
242
+ self._agent_conf = {}
243
+ self._agent_instance = {}
244
+
245
+ def __call__(self, name: str = None, *args, **kwargs):
246
+ if name is None:
247
+ return self
248
+
249
+ conf = self._agent_conf.get(name)
250
+ if not conf:
251
+ logger.warning(f"{name} not find conf in agent factory")
252
+ conf = dict()
253
+ elif isinstance(conf, BaseModel):
254
+ conf = conf.model_dump()
255
+
256
+ user_conf = kwargs.pop('conf', None)
257
+ if user_conf:
258
+ if isinstance(user_conf, BaseModel):
259
+ conf.update(user_conf.model_dump())
260
+ elif isinstance(user_conf, dict):
261
+ conf.update(user_conf)
262
+ else:
263
+ logger.warning(
264
+ f"Unknown conf type: {type(user_conf)}, ignored!")
265
+
266
+ conf['name'] = name
267
+ conf = ConfigDict(conf)
268
+ if name in self._cls:
269
+ agent = self._cls[name](conf=conf, **kwargs)
270
+ self._agent_instance[name] = agent
271
+ else:
272
+ raise ValueError(f"Can not find {name} agent!")
273
+ return agent
274
+
275
+ def desc(self, name: str) -> str:
276
+ if self._agent_instance.get(name, None) and self._agent_instance[name].desc:
277
+ return self._agent_instance[name].desc
278
+ return self._desc.get(name, "")
279
+
280
+ def agent_instance(self, name: str) -> BaseAgent | None:
281
+ if self._agent_instance.get(name, None):
282
+ return self._agent_instance[name]
283
+ return None
284
+
285
+ def register(self, name: str, desc: str, conf_file_name: str = None, **kwargs):
286
+ """Register a tool to tool factory.
287
+
288
+ Args:
289
+ name: Tool name
290
+ desc: Tool description
291
+ supported_action: Tool abilities
292
+ conf_file_name: Default tool config
293
+ """
294
+ res = super(AgentManager, self).register(name, desc, **kwargs)
295
+ conf_file_name = conf_file_name if conf_file_name else f"{name}.yaml"
296
+ conf = load_config(conf_file_name, kwargs.get("dir"))
297
+ if not conf:
298
+ logger.warning(f"{conf_file_name} not find, will use default")
299
+ # use general tool config
300
+ conf = AgentConfig().model_dump()
301
+ self._agent_conf[name] = conf
302
+ return res
303
+
304
+
305
+ AgentFactory = AgentManager("agent_type")
aworld/core/agent/swarm.py ADDED
@@ -0,0 +1,746 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Copyright (c) 2025 inclusionAI.
3
+ import abc
4
+ import json
5
+ from enum import Enum
6
+ from typing import Dict, List, Any, Callable, Optional, Tuple
7
+
8
+ from aworld.agents.parallel_llm_agent import ParallelizableAgent
9
+ from aworld.agents.serial_llm_agent import SerialableAgent
10
+ from aworld.core.agent.agent_desc import agent_handoffs_desc
11
+ from aworld.core.agent.base import AgentFactory, BaseAgent
12
+ from aworld.core.common import ActionModel, Observation
13
+ from aworld.core.context.base import Context
14
+ from aworld.core.exceptions import AworldException
15
+ from aworld.logs.util import logger
16
+ from aworld.utils.common import new_instance, convert_to_subclass
17
+
18
+
19
+ class GraphBuildType(Enum):
20
+ WORKFLOW = "workflow"
21
+ HANDOFF = "handoff"
22
+ TEAM = "team"
23
+
24
+
25
+ class Swarm(object):
26
+ """Swarm is the multi-agent topology of AWorld, a collection of autonomous agents working together to
27
+ solve complex problems through collaboration or competition.
28
+
29
+ Swarm supports the key paradigms of workflow and handoff, and it satisfies the construction of various
30
+ agent graphs, including DAG and DCG, such as star, tree, mesh, ring, and hybrid topology.
31
+ """
32
+
33
+ def __init__(self,
34
+ *args, # agent
35
+ root_agent: BaseAgent = None,
36
+ max_steps: int = 0,
37
+ register_agents: List[BaseAgent] = [],
38
+ build_type: GraphBuildType = GraphBuildType.WORKFLOW,
39
+ builder_cls: str = None,
40
+ event_driven: bool = True):
41
+ self._communicate_agent = root_agent
42
+ if root_agent and root_agent not in args:
43
+ self.agent_list: List[BaseAgent] = [root_agent] + list(args)
44
+ else:
45
+ self.agent_list: List[BaseAgent] = list(args)
46
+
47
+ self.setting_build_type(build_type)
48
+ self.max_steps = max_steps
49
+ self._cur_step = 0
50
+ self._event_driven = event_driven
51
+ self.build_type = build_type.value
52
+ if builder_cls:
53
+ self.builder = new_instance(builder_cls, self)
54
+ else:
55
+ self.builder = BUILD_CLS.get(self.build_type)(self.agent_list, register_agents, max_steps)
56
+
57
+ self.agent_graph: AgentGraph = None
58
+
59
+ # global tools
60
+ self.tools = []
61
+ self.task = ''
62
+ self.initialized: bool = False
63
+ self._finished: bool = False
64
+
65
+ def setting_build_type(self, build_type: GraphBuildType):
66
+ all_pair = True
67
+ for agent in self.agent_list:
68
+ if isinstance(agent, (list, tuple)):
69
+ if len(agent) != 2:
70
+ all_pair = False
71
+ elif isinstance(agent, BaseAgent):
72
+ all_pair = False
73
+ else:
74
+ raise AworldException(f"Unknown type {type(agent)}, supported list, tuple, Agent only.")
75
+
76
+ # team and workflow support mixing individual agents and agent lists.
77
+ if build_type == GraphBuildType.HANDOFF and not all_pair:
78
+ raise AworldException('The type of `handoff` requires all pairs to appear.')
79
+
80
+ for agent in self.agent_list:
81
+ if isinstance(agent, BaseAgent):
82
+ agent = [agent]
83
+ for a in agent:
84
+ if a and a.event_driven:
85
+ self._event_driven = True
86
+ break
87
+
88
+ def reset(self, content: Any, context: Context = None, tools: List[str] = []):
89
+ """Resets the initial internal state, and init supported tools in agent in swarm.
90
+
91
+ Args:
92
+ tools: Tool names that all agents in the swarm can use.
93
+ """
94
+ # can use the tools in the agents in the swarm as a global
95
+ if self.initialized:
96
+ logger.warning(f"swarm {self} already init")
97
+ return
98
+
99
+ self.tools = tools
100
+ # origin task
101
+ self.task = content
102
+
103
+ # build graph
104
+ agent_graph: AgentGraph = self.builder.build()
105
+
106
+ if not agent_graph.agents:
107
+ logger.warning("No valid agent in swarm.")
108
+ return
109
+
110
+ agent_graph.topological_sequence()
111
+ if self.build_type == GraphBuildType.TEAM.value:
112
+ agent_graph.ordered_agents.clear()
113
+ agent_graph.ordered_agents.append(agent_graph.root_agent)
114
+
115
+ # Agent that communicate with the outside world, the default is the first if the root agent is None.
116
+ if not self._communicate_agent:
117
+ self._communicate_agent = agent_graph.ordered_agents[0]
118
+ self.cur_agent = self.communicate_agent
119
+ self.agent_graph = agent_graph
120
+
121
+ if context is None:
122
+ context = Context.instance()
123
+
124
+ for agent in agent_graph.agents.values():
125
+ agent.event_driven = self.event_driven
126
+ if hasattr(agent, 'need_reset') and agent.need_reset:
127
+ agent.context = context
128
+ agent.reset({"task": content,
129
+ "tool_names": agent.tool_names,
130
+ "agent_names": agent.handoffs,
131
+ "mcp_servers": agent.mcp_servers})
132
+ # global tools
133
+ agent.tool_names.extend(self.tools)
134
+
135
+ self.cur_step = 1
136
+ self.initialized = True
137
+
138
+ def find_agents_by_prefix(self, name, find_all=False):
139
+ """Fild the agent list by the prefix name.
140
+
141
+ Args:
142
+ name: The agent prefix name.
143
+ find_all: Find the total agents or the first match agent.
144
+ """
145
+ import re
146
+
147
+ res = []
148
+ for k, agent in self.agents.items():
149
+ val = re.split(r"---uuid\w{6}uuid", k)[0]
150
+ if name == val:
151
+ res.append(agent)
152
+ if not find_all:
153
+ return res
154
+ return res
155
+
156
+ def _check(self):
157
+ if not self.initialized:
158
+ self.reset('')
159
+
160
+ def handoffs_desc(self, agent_name: str = None, use_all: bool = False):
161
+ """Get agent description by name for handoffs.
162
+
163
+ Args:
164
+ agent_name: Agent unique name.
165
+ Returns:
166
+ Description of agent dict.
167
+ """
168
+ self._check()
169
+ agent: BaseAgent = self.agents.get(agent_name, None)
170
+ return agent_handoffs_desc(agent, use_all)
171
+
172
+ def action_to_observation(self, policy: List[ActionModel], observation: List[Observation], strategy: str = None):
173
+ """Based on the strategy, transform the agent's policy into an observation, the case of the agent as a tool.
174
+
175
+ Args:
176
+ policy: Agent policy based some messages.
177
+ observation: History of the current observable state in the environment.
178
+ strategy: Transform strategy, default is None. enum?
179
+ """
180
+ self._check()
181
+
182
+ if not policy:
183
+ logger.warning("no agent policy, will return origin observation.")
184
+ # get the latest one
185
+ if not observation:
186
+ raise RuntimeError("no observation and policy to transform in swarm, please check your params.")
187
+ return observation[-1]
188
+
189
+ if not strategy:
190
+ # default use the first policy
191
+ policy_info = policy[0].policy_info
192
+
193
+ if not observation:
194
+ res = Observation(content=policy_info)
195
+ else:
196
+ res = observation[-1]
197
+ if not res.content:
198
+ res.content = policy_info or ""
199
+
200
+ return res
201
+ else:
202
+ logger.warning(f"{strategy} not supported now.")
203
+
204
+ def supported_tools(self):
205
+ """Tool names that can be used by all agents in Swarm."""
206
+ self._check()
207
+ return self.tools
208
+
209
+ @property
210
+ def has_cycle(self):
211
+ self._check()
212
+ return self.agent_graph.has_cycle()
213
+
214
+ @property
215
+ def agents(self):
216
+ self._check()
217
+ return self.agent_graph.agents
218
+
219
+ @property
220
+ def ordered_agents(self):
221
+ self._check()
222
+ return self.agent_graph.ordered_agents
223
+
224
+ @property
225
+ def communicate_agent(self):
226
+ return self._communicate_agent
227
+
228
+ @communicate_agent.setter
229
+ def communicate_agent(self, agent: BaseAgent):
230
+ self._communicate_agent = agent
231
+
232
+ @property
233
+ def event_driven(self):
234
+ return self._event_driven
235
+
236
+ @event_driven.setter
237
+ def event_driven(self, event_driven):
238
+ self._event_driven = event_driven
239
+
240
+ @property
241
+ def cur_step(self) -> int:
242
+ return self._cur_step
243
+
244
+ @cur_step.setter
245
+ def cur_step(self, step):
246
+ self._cur_step = step
247
+
248
+ @property
249
+ def finished(self) -> bool:
250
+ """Need all agents in a finished state."""
251
+ self._check()
252
+ if not self._finished:
253
+ self._finished = all([agent.finished for _, agent in self.agents.items()])
254
+ return self._finished
255
+
256
+ @finished.setter
257
+ def finished(self, finished):
258
+ self._finished = finished
259
+
260
+
261
+ class WorkflowSwarm(Swarm):
262
+ def __init__(self,
263
+ *args, # agent
264
+ root_agent: BaseAgent = None,
265
+ max_steps: int = 0,
266
+ register_agents: List[BaseAgent] = [],
267
+ builder_cls: str = None,
268
+ event_driven: bool = True):
269
+ super().__init__(*args,
270
+ root_agent=root_agent,
271
+ max_steps=max_steps,
272
+ register_agents=register_agents,
273
+ build_type=GraphBuildType.WORKFLOW,
274
+ builder_cls=builder_cls,
275
+ event_driven=event_driven)
276
+
277
+
278
+ class TeamSwarm(Swarm):
279
+ def __init__(self,
280
+ *args, # agent
281
+ root_agent: BaseAgent = None,
282
+ max_steps: int = 0,
283
+ register_agents: List[BaseAgent] = [],
284
+ builder_cls: str = None,
285
+ event_driven: bool = True):
286
+ super().__init__(*args,
287
+ root_agent=root_agent,
288
+ max_steps=max_steps,
289
+ register_agents=register_agents,
290
+ build_type=GraphBuildType.TEAM,
291
+ builder_cls=builder_cls,
292
+ event_driven=event_driven)
293
+
294
+
295
+ class HandoffSwarm(Swarm):
296
+ def __init__(self,
297
+ *args, # agent
298
+ max_steps: int = 0,
299
+ register_agents: List[BaseAgent] = [],
300
+ builder_cls: str = None,
301
+ event_driven: bool = True):
302
+ super().__init__(*args,
303
+ max_steps=max_steps,
304
+ register_agents=register_agents,
305
+ build_type=GraphBuildType.HANDOFF,
306
+ builder_cls=builder_cls,
307
+ event_driven=event_driven)
308
+
309
+
310
+ class EdgeInfo:
311
+ def __init__(self,
312
+ clause: Optional[Callable[..., Any]] = None,
313
+ weight: float = 0.):
314
+ self.clause = clause
315
+ self.weight = weight
316
+
317
+
318
+ class AgentGraph:
319
+ """The agent's graph is a directed graph, and can update the topology at runtime."""
320
+
321
+ def __init__(self,
322
+ ordered_agents: List[BaseAgent] = [],
323
+ agents: Dict[str, BaseAgent] = {},
324
+ predecessor: Dict[str, Dict[str, EdgeInfo]] = {},
325
+ successor: Dict[str, Dict[str, EdgeInfo]] = {}):
326
+ """Agent graph init.
327
+
328
+ Args:
329
+ ordered_agents: Agents ordered.
330
+ agents: Agent nodes.
331
+ predecessor: The direct predecessor of the agent.
332
+ successor: The direct successor of the agent.
333
+ """
334
+ self.ordered_agents = ordered_agents
335
+ self.agents = agents
336
+ self.predecessor = predecessor
337
+ self.successor = successor
338
+ self.first = True
339
+ self.root_agent = None
340
+
341
+ def topological_sequence(self) -> Tuple[List[str], bool]:
342
+ """Obtain the agent sequence of topology, and be able to determine whether the topology has cycle during the process.
343
+
344
+ Returns:
345
+ Topological sequence and whether it is a cycle topology, False represents DAG, True represents DCG.
346
+ """
347
+ in_degree = dict(filter(lambda k: k[1] > 0, self.in_degree().items()))
348
+ zero_list = [v[0] for v in list(filter(lambda k: k[1] == 0, self.in_degree().items()))]
349
+
350
+ res = []
351
+ while zero_list:
352
+ tmp = zero_list
353
+ zero_list = []
354
+ for agent_id in tmp:
355
+ if agent_id not in self.agents:
356
+ raise RuntimeError("Agent topology changed during iteration")
357
+
358
+ for key, _ in self.successor.get(agent_id).items():
359
+ try:
360
+ in_degree[key] -= 1
361
+ except KeyError as err:
362
+ raise RuntimeError("Agent topology changed during iteration")
363
+
364
+ if in_degree[key] == 0:
365
+ zero_list.append(key)
366
+ del in_degree[key]
367
+ res.append(tmp)
368
+
369
+ dcg = False
370
+ if in_degree:
371
+ logger.info("Agent topology contains cycle!")
372
+ # sequence may be incomplete
373
+ res.clear()
374
+ dcg = True
375
+
376
+ if not self.ordered_agents:
377
+ for agent_ids in res:
378
+ for agent_id in agent_ids:
379
+ self.ordered_agents.append(self.agents[agent_id])
380
+ return res, dcg
381
+
382
+ def has_cycle(self):
383
+ res, is_dcg = self.topological_sequence()
384
+ return is_dcg
385
+
386
+ def add_node(self, agent: BaseAgent):
387
+ if not agent:
388
+ raise AworldException("agent is None, can not build the graph.")
389
+
390
+ if self.first:
391
+ self.root_agent = agent
392
+ self.first = False
393
+
394
+ if agent.id() not in self.agents:
395
+ self.agents[agent.id()] = agent
396
+ self.successor[agent.id()] = {}
397
+ self.predecessor[agent.id()] = {}
398
+ else:
399
+ logger.info(f"{agent.id()} already in agent graph.")
400
+
401
+ def del_node(self, agent: BaseAgent):
402
+ if not agent or agent.id() not in self.agents:
403
+ return
404
+
405
+ self.ordered_agents.remove(agent)
406
+ del self.agents[agent.id()]
407
+
408
+ successor = self.successor.get(agent.id(), {})
409
+ for key, _ in successor.items():
410
+ del self.predecessor[key][agent.id()]
411
+ del self.successor[agent.id()]
412
+
413
+ for key, _ in self.predecessor.get(agent.id(), {}):
414
+ del self.successor[key][agent.id()]
415
+ del self.predecessor[agent.id()]
416
+
417
+ def add_edge(self, left_agent: BaseAgent, right_agent: BaseAgent, edge_info: EdgeInfo = EdgeInfo()):
418
+ """Adding an edge between the left and the right agent means establishing the relationship
419
+ between these two agents.
420
+
421
+ Args:
422
+ left_agent: As the agent node of the predecessor node.
423
+ right_agent: As the agent node of the successor node.
424
+ edge_info: Edge info between the agents.
425
+ """
426
+ if left_agent and left_agent.id() not in self.agents:
427
+ raise RuntimeError(f"{left_agent.id()} not in agents node.")
428
+ if right_agent and right_agent.id() not in self.agents:
429
+ raise RuntimeError(f"{right_agent.id()} not in agents node.")
430
+
431
+ if left_agent.id() not in self.successor:
432
+ self.successor[left_agent.id()] = {}
433
+ self.predecessor[left_agent.id()] = {}
434
+
435
+ if right_agent.id() not in self.successor:
436
+ self.successor[right_agent.id()] = {}
437
+ self.predecessor[right_agent.id()] = {}
438
+
439
+ self.successor[left_agent.id()][right_agent.id()] = edge_info
440
+ self.predecessor[right_agent.id()][left_agent.id()] = edge_info
441
+
442
+ def remove_edge(self, left_agent: BaseAgent, right_agent: BaseAgent):
443
+ """Removing an edge between the left and the right agent means removing the relationship
444
+ between these two agents.
445
+
446
+ Args:
447
+ left_agent: As the agent node of the predecessor node.
448
+ right_agent: As the agent node of the successor node.
449
+ """
450
+ if left_agent.id() in self.successor and right_agent.id() in self.successor[left_agent.id()]:
451
+ del self.successor[left_agent.id()][right_agent.id()]
452
+ if right_agent.id() in self.predecessor and left_agent.id() in self.successor[right_agent.id()]:
453
+ del self.predecessor[right_agent.id()][left_agent.id()]
454
+
455
+ def in_degree(self) -> Dict[str, int]:
456
+ """In degree of the agent is the number of agents pointing to the agent."""
457
+ in_degree = {}
458
+ for k, _ in self.agents.items():
459
+ agents = self.predecessor[k]
460
+ in_degree[k] = len(agents.values())
461
+ return in_degree
462
+
463
+ def out_degree(self) -> Dict[str, int]:
464
+ """Out degree of the agent is the number of agents pointing out of the agent."""
465
+ out_degree = {}
466
+ for k, _ in self.agents.items():
467
+ agents = self.successor[k]
468
+ out_degree[k] = len(agents.values())
469
+ return out_degree
470
+
471
+ def loop_agent(self,
472
+ agent: BaseAgent,
473
+ max_run_times: int,
474
+ loop_point: str = None,
475
+ loop_point_finder: Callable[..., Any] = None,
476
+ stop_func: Callable[..., Any] = None):
477
+ """Loop execution of the flow.
478
+
479
+ Args:
480
+ agent: The agent.
481
+ max_run_times: Maximum number of loops.
482
+ loop_point: Loop point of the desired execution.
483
+ loop_point_finder: Strategy function for obtaining execution loop point.
484
+ stop_func: Termination function.
485
+ """
486
+ from aworld.agents.loop_llm_agent import LoopableAgent
487
+
488
+ if agent not in self.ordered_agents:
489
+ raise RuntimeError(f"{agent.id()} not in swarm, agent instance {agent}.")
490
+
491
+ loop_agent: LoopableAgent = convert_to_subclass(agent, LoopableAgent)
492
+ # loop_agent: LoopableAgent = type(LoopableAgent)(agent)
493
+ loop_agent.max_run_times = max_run_times
494
+ loop_agent.loop_point = loop_point
495
+ loop_agent.loop_point_finder = loop_point_finder
496
+ loop_agent.stop_func = stop_func
497
+
498
+ idx = self.ordered_agents.index(agent)
499
+ self.ordered_agents[idx] = loop_agent
500
+
501
+ def parallel_agent(self,
502
+ agent: BaseAgent,
503
+ agents: List[BaseAgent],
504
+ aggregate_func: Callable[..., Any] = None):
505
+ """Parallel execution of agents.
506
+
507
+ Args:
508
+ agent: The agent.
509
+ agents: Agents that require parallel execution.
510
+ aggregate_func: Aggregate strategy function.
511
+ """
512
+ from aworld.agents.parallel_llm_agent import ParallelizableAgent
513
+
514
+ if agent not in self.ordered_agents:
515
+ raise RuntimeError(f"{agent.id()} not in swarm, agent instance {agent}.")
516
+ for agent in agents:
517
+ if agent not in self.ordered_agents:
518
+ raise RuntimeError(f"{agent.id()} not in swarm, agent instance {agent}.")
519
+
520
+ parallel_agent: ParallelizableAgent = convert_to_subclass(agent, ParallelizableAgent)
521
+ parallel_agent.agents = agents
522
+ parallel_agent.aggregate_func = aggregate_func
523
+
524
+ idx = self.ordered_agents.index(agent)
525
+ self.ordered_agents[idx] = parallel_agent
526
+
527
+ def save(self, filepath: str):
528
+ vals = {"agents": self.agents, "successor": self.successor, "predecessor": self.predecessor}
529
+ json.dumps(vals)
530
+
531
+ def load(self, filepath: str):
532
+ pass
533
+
534
+
535
+ class TopologyBuilder:
536
+ """Multi-agent topology base builder."""
537
+ __metaclass__ = abc.ABCMeta
538
+
539
+ def __init__(self, agent_list: List[BaseAgent], register_agents: List[BaseAgent] = [], max_steps: int = 0):
540
+ self.agent_list = agent_list
541
+ self.max_steps = max_steps
542
+
543
+ for agent in register_agents:
544
+ TopologyBuilder.register_agent(agent)
545
+
546
+ @abc.abstractmethod
547
+ def build(self):
548
+ """Build a multi-agent topology diagram using custom build strategies or syntax."""
549
+
550
+ @staticmethod
551
+ def register_agent(agent: BaseAgent):
552
+ if agent.id() not in AgentFactory:
553
+ AgentFactory._cls[agent.id()] = agent.__class__
554
+ AgentFactory._desc[agent.id()] = agent.desc()
555
+ AgentFactory._agent_conf[agent.id()] = agent.conf
556
+ AgentFactory._agent_instance[agent.id()] = agent
557
+ else:
558
+ if agent.id() not in AgentFactory._agent_instance:
559
+ AgentFactory._agent_instance[agent.id()] = agent
560
+ if agent.desc():
561
+ AgentFactory._desc[agent.id()] = agent.desc()
562
+
563
+
564
+ class WorkflowBuilder(TopologyBuilder):
565
+ """Workflow mechanism, workflow is a deterministic process orchestration where each node must execute.
566
+
567
+ There are three forms supported by the workflow builder: single agent, tuple of paired agents, and agent list.
568
+ Examples:
569
+ >>> agent1 = Agent(name='agent1'); agent2 = Agent(name='agent2'); agent3 = Agent(name='agent3')
570
+ >>> agent4 = Agent(name='agent4'); agent5 = Agent(name='agent5'); agent6 = Agent(name='agent6')
571
+ >>> Swarm(agent1, [agent2, agent3], (agent2, agent4), (agent3, agent5), agent6)
572
+
573
+ The meaning of the topology is that after agent1 completes execution, agent2 and agent3 are executed in parallel,
574
+ then agent4 and agent5 are executed sequentially after agent2 and agent3, and agent6 is executed after completion.
575
+ """
576
+
577
+ def build(self):
578
+ """Built as workflow, different forms will be internally constructed as different agents,
579
+ such as ParallelizableAgent, SerialableAgent or LoopableAgent.
580
+
581
+ # TODO: Complete Graph Definition Capability
582
+ Returns:
583
+ Direct topology diagram (AgentGraph) of the agents.
584
+ """
585
+ agent_graph = AgentGraph(ordered_agents=[], agents={}, successor={}, predecessor={})
586
+ valid_agents = []
587
+ for agent in self.agent_list:
588
+ if isinstance(agent, BaseAgent):
589
+ valid_agents.append(agent)
590
+ elif isinstance(agent, tuple):
591
+ serial_agent = SerialableAgent(name="_".join(agent), conf=agent[0].conf, agents=list(agent))
592
+ valid_agents.append(serial_agent)
593
+ elif isinstance(agent, list):
594
+ # list
595
+ parallel_agent = ParallelizableAgent(name="_".join(agent), conf=agent[0].conf, agents=agent)
596
+ valid_agents.append(parallel_agent)
597
+ else:
598
+ raise RuntimeError(f"agent in {agent} is not a agent or agent list, please check it.")
599
+
600
+ if not valid_agents:
601
+ raise RuntimeError(f"no valid agent in swarm to build execution graph.")
602
+
603
+ last_agent = None
604
+ for agent in valid_agents:
605
+ TopologyBuilder.register_agent(agent)
606
+
607
+ agent_graph.add_node(agent)
608
+ if last_agent:
609
+ agent_graph.add_edge(last_agent, agent)
610
+ last_agent = agent
611
+ return agent_graph
612
+
613
+
614
+ class HandoffBuilder(TopologyBuilder):
615
+ """Handoff mechanism using agents as tools, but during the runtime,
616
+ the agent remains an independent entity with a state.
617
+
618
+ Handoffs builder only supports tuple of paired agents forms.
619
+ Examples:
620
+ >>> agent1 = Agent(name='agent1'); agent2 = Agent(name='agent2'); agent3 = Agent(name='agent3')
621
+ >>> agent4 = Agent(name='agent4'); agent5 = Agent(name='agent5'); agent6 = Agent(name='agent6')
622
+ >>> Swarm((agent1, agent2), (agent1, agent3), (agent2, agent3), build_type=GraphBuildType.HANDOFF)
623
+ """
624
+
625
+ def build(self):
626
+ """Build a graph in pairs, with the right agent serving as the tool on the left.
627
+
628
+ Using pure AI to drive the flow of the entire topology diagram, one agent's decision
629
+ hands off control to another. Agents may be fully connected or circular, depending
630
+ on the defined pairs of agents.
631
+
632
+ Returns:
633
+ Direct topology diagram (AgentGraph) of the agents.
634
+ """
635
+ valid_agent_pair = []
636
+ for pair in self.agent_list:
637
+ if not isinstance(pair, (list, tuple)):
638
+ raise RuntimeError(f"{pair} is not a tuple or list value, please check it.")
639
+ elif len(pair) != 2:
640
+ raise RuntimeError(f"{pair} is not a pair, please check it.")
641
+
642
+ valid_agent_pair.append(pair)
643
+
644
+ if not valid_agent_pair:
645
+ raise RuntimeError(f"no valid agent pair to build execution graph.")
646
+
647
+ # agent handoffs graph build.
648
+ agent_graph = AgentGraph(ordered_agents=[], agents={}, successor={}, predecessor={})
649
+ for pair in valid_agent_pair:
650
+ TopologyBuilder.register_agent(pair[0])
651
+ TopologyBuilder.register_agent(pair[1])
652
+
653
+ # need feedback
654
+ pair[0].feedback_tool_result = True
655
+ pair[1].feedback_tool_result = True
656
+
657
+ agent_graph.add_node(pair[0])
658
+ agent_graph.add_node(pair[1])
659
+ agent_graph.add_edge(pair[0], pair[1])
660
+
661
+ # explicitly set handoffs in the agent
662
+ pair[0].handoffs.append(pair[1].id())
663
+ pair[1].handoffs.remove(pair[1].id())
664
+ return agent_graph
665
+
666
+
667
+ class TeamBuilder(TopologyBuilder):
668
+ """Team mechanism requires a leadership agent, and other agents follow its command.
669
+ If there is interaction between agents other than the leadership agent, they need to explicitly
670
+ set `agent_names` themselves or use a tuple with two agents.
671
+
672
+ Team builder supported form of single agent, tuple of paired agents, and agent list, similar to workflow.
673
+ Examples:
674
+ >>> agent1 = Agent(name='agent1'); agent2 = Agent(name='agent2'); agent3 = Agent(name='agent3')
675
+ >>> agent4 = Agent(name='agent4'); agent5 = Agent(name='agent5'); agent6 = Agent(name='agent6')
676
+ >>> Swarm(agent1, agent2, agent3, (agent4, agent5), agent6, build_type=GraphBuildType.TEAM)
677
+
678
+ The topology means that agent1 is the leader agent, agent5 as a tool of agent4,
679
+ and agent2, agent3, agent6, agent4 are executors of agent1.
680
+
681
+ Using the `root_agent` parameter, will obtain the same topology as above.
682
+ >>> Swarm(agent2, agent3, (agent4, agent5), agent6, root_agent=agent1, build_type=GraphBuildType.TEAM)
683
+ >>> Swarm(agent1, agent2, agent3, (agent4, agent5), agent6, root_agent=agent1, build_type=GraphBuildType.TEAM)
684
+ """
685
+
686
+ def build(self):
687
+ agent_graph = AgentGraph(ordered_agents=[], agents={}, successor={}, predecessor={})
688
+ valid_agents = []
689
+ root_agent = self.agent_list[0]
690
+ if isinstance(root_agent, tuple):
691
+ valid_agents.append(root_agent)
692
+ root_agent = root_agent[0]
693
+ agent_graph.add_node(root_agent)
694
+ root_agent.feedback_tool_result = True
695
+
696
+ single_agents = []
697
+ for agent in self.agent_list[1:]:
698
+ if isinstance(agent, BaseAgent):
699
+ single_agents.append(agent)
700
+ elif isinstance(agent, tuple):
701
+ valid_agents.append(agent)
702
+ elif isinstance(agent, list):
703
+ # list of agent can parallel
704
+ parallel_agent = ParallelizableAgent(name="_".join(agent), conf=agent[0].conf, agents=agent)
705
+ single_agents.append(parallel_agent)
706
+ else:
707
+ raise RuntimeError(f"agent in {agent} is not a agent or agent list, please check it.")
708
+
709
+ if not valid_agents and not single_agents:
710
+ raise RuntimeError(f"no valid agent in swarm to build execution graph.")
711
+
712
+ for agent in single_agents:
713
+ TopologyBuilder.register_agent(agent)
714
+
715
+ agent.feedback_tool_result = True
716
+ agent_graph.add_node(agent)
717
+ agent_graph.add_edge(root_agent, agent)
718
+
719
+ root_agent.handoffs.append(agent.id())
720
+ agent.handoffs.remove(agent.id())
721
+
722
+ for pair in valid_agents:
723
+ TopologyBuilder.register_agent(pair[0])
724
+ pair[0].feedback_tool_result = True
725
+ if len(pair) > 1:
726
+ TopologyBuilder.register_agent(pair[1])
727
+ pair[1].feedback_tool_result = True
728
+
729
+ agent_graph.add_node(pair[0])
730
+ agent_graph.add_node(pair[1])
731
+ if pair[0] != root_agent:
732
+ agent_graph.add_edge(root_agent, pair[0])
733
+ root_agent.handoffs.append(pair[0].id())
734
+ pair[0].handoffs.remove(pair[0].id())
735
+ else:
736
+ agent_graph.add_edge(root_agent, pair[1])
737
+ root_agent.handoffs.append(pair[1].id())
738
+ pair[1].handoffs.remove(pair[1].id())
739
+ return agent_graph
740
+
741
+
742
+ BUILD_CLS = {
743
+ GraphBuildType.WORKFLOW.value: WorkflowBuilder,
744
+ GraphBuildType.HANDOFF.value: HandoffBuilder,
745
+ GraphBuildType.TEAM.value: TeamBuilder,
746
+ }