ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import sys
|
||||
|
||||
from aworld.core.factory import Factory
|
||||
from aworld.logs.util import logger
|
||||
|
||||
|
||||
class HandlerManager(Factory):
|
||||
def __init__(self, type_name: str = None):
|
||||
super(HandlerManager, self).__init__(type_name)
|
||||
|
||||
def __call__(self, name: str, asyn: bool = False, runner: 'TaskRunner' = None, **kwargs):
|
||||
if name is None or runner is None:
|
||||
raise ValueError("handler name or runner instance is None")
|
||||
|
||||
try:
|
||||
if name in self._cls:
|
||||
act = self._cls[name](runner)
|
||||
else:
|
||||
raise RuntimeError("The handler was not registered.\nPlease confirm the package has been imported.")
|
||||
except Exception:
|
||||
err = sys.exc_info()
|
||||
logger.warning(f"Failed to create handler with name {name}:\n{err[1]}")
|
||||
act = None
|
||||
return act
|
||||
|
||||
|
||||
HandlerFactory = HandlerManager("hook_type")
|
||||
@@ -0,0 +1,856 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import aworld.trace as trace
|
||||
|
||||
from typing import List, Dict, Any, Tuple
|
||||
|
||||
from aworld.config.conf import ToolConfig
|
||||
from aworld.core.agent.base import is_agent
|
||||
from aworld.agents.llm_agent import Agent
|
||||
from aworld.core.common import Observation, ActionModel, ActionResult
|
||||
from aworld.core.context.base import Context
|
||||
from aworld.core.event.base import Message, ToolMessage, AgentMessage
|
||||
from aworld.core.tool.base import ToolFactory, Tool, AsyncTool
|
||||
from aworld.core.tool.tool_desc import is_tool_by_name
|
||||
from aworld.core.task import Task, TaskResponse
|
||||
from aworld.logs.util import logger, color_log, Color, trace_logger
|
||||
from aworld.models.model_response import ToolCall
|
||||
from aworld.output.base import StepOutput, ToolResultOutput
|
||||
from aworld.runners.task_runner import TaskRunner
|
||||
from aworld.runners.utils import endless_detect
|
||||
from aworld.sandbox import Sandbox
|
||||
from aworld.tools.utils import build_observation
|
||||
from aworld.utils.common import override_in_subclass
|
||||
from aworld.utils.serialized_util import NumpyEncoder
|
||||
|
||||
|
||||
def action_result_transform(message: Message, sandbox: Sandbox) -> Tuple[Observation, float, bool, bool, dict]:
|
||||
action_results = message.payload
|
||||
result: ActionResult = action_results[-1]
|
||||
# ignore image, dom_tree attribute, need to process them from action_results in the agent.
|
||||
return build_observation(container_id=sandbox.sandbox_id,
|
||||
observer=result.tool_name,
|
||||
ability=result.action_name,
|
||||
content=result.content,
|
||||
action_result=action_results), 1.0, result.is_done, result.is_done, {}
|
||||
|
||||
|
||||
class WorkflowRunner(TaskRunner):
|
||||
def __init__(self, task: Task, *args, **kwargs):
|
||||
super().__init__(task=task, *args, **kwargs)
|
||||
|
||||
async def do_run(self, context: Context = None) -> TaskResponse:
|
||||
self.max_steps = self.conf.get("max_steps", 100)
|
||||
resp = await self._do_run(context)
|
||||
self._task_response = resp
|
||||
return resp
|
||||
|
||||
async def _do_run(self, context: Context = None) -> TaskResponse:
|
||||
"""Multi-agent sequence general process workflow.
|
||||
|
||||
NOTE: Use the agent's finished state(no tool calls) to control the inner loop.
|
||||
Args:
|
||||
observation: Observation based on env
|
||||
info: Extend info by env
|
||||
"""
|
||||
observation = self.observation
|
||||
if not observation:
|
||||
raise RuntimeError("no observation, check run process")
|
||||
|
||||
start = time.time()
|
||||
msg = None
|
||||
response = None
|
||||
|
||||
# Use trace.span to record the entire task execution process
|
||||
with trace.span(f"task_execution_{self.task.id}", attributes={
|
||||
"task_id": self.task.id,
|
||||
"task_name": self.task.name,
|
||||
"start_time": start
|
||||
}) as task_span:
|
||||
try:
|
||||
response = await self._common_process(task_span)
|
||||
except Exception as err:
|
||||
logger.error(f"Runner run failed, err is {traceback.format_exc()}")
|
||||
finally:
|
||||
if not self.task.is_sub_task:
|
||||
logger.info(f"FINISHED|call_driven_runner|mark_completed|{self.task.id}")
|
||||
await self.outputs.mark_completed()
|
||||
color_log(f"task token usage: {self.context.token_usage}",
|
||||
color=Color.pink,
|
||||
logger_=trace_logger)
|
||||
for _, tool in self.tools.items():
|
||||
if isinstance(tool, AsyncTool):
|
||||
await tool.close()
|
||||
else:
|
||||
tool.close()
|
||||
task_span.set_attributes({
|
||||
"end_time": time.time(),
|
||||
"duration": time.time() - start,
|
||||
"error": msg
|
||||
})
|
||||
# todo sandbox cleanup
|
||||
if self.swarm and hasattr(self.swarm, 'agents') and self.swarm.agents:
|
||||
for agent_name, agent in self.swarm.agents.items():
|
||||
try:
|
||||
if hasattr(agent, 'sandbox') and agent.sandbox:
|
||||
await agent.sandbox.cleanup()
|
||||
except Exception as e:
|
||||
logger.warning(f"call_driven_runner Failed to cleanup sandbox for agent {agent_name}: {e}")
|
||||
return response
|
||||
|
||||
async def _common_process(self, task_span):
|
||||
start = time.time()
|
||||
step = 1
|
||||
pre_agent_name = None
|
||||
observation = self.observation
|
||||
|
||||
for idx, agent in enumerate(self.swarm.ordered_agents):
|
||||
observation.from_agent_name = agent.id()
|
||||
observations = [observation]
|
||||
policy = None
|
||||
cur_agent = agent
|
||||
while step <= self.max_steps:
|
||||
await self.outputs.add_output(
|
||||
StepOutput.build_start_output(name=f"Step{step}", step_num=step, task_id=self.task.id))
|
||||
|
||||
terminated = False
|
||||
|
||||
observation = self.swarm.action_to_observation(policy, observations)
|
||||
observation.from_agent_name = observation.from_agent_name or cur_agent.id()
|
||||
|
||||
if observation.to_agent_name and observation.to_agent_name != cur_agent.id():
|
||||
cur_agent = self.swarm.agents.get(observation.to_agent_name)
|
||||
|
||||
exp_id = self._get_step_span_id(step, cur_agent.id())
|
||||
with trace.span(f"step_execution_{exp_id}") as step_span:
|
||||
try:
|
||||
step_span.set_attributes({
|
||||
"exp_id": exp_id,
|
||||
"task_id": self.task.id,
|
||||
"task_name": self.task.name,
|
||||
"trace_id": trace.get_current_span().get_trace_id(),
|
||||
"step": step,
|
||||
"agent_id": cur_agent.id(),
|
||||
"pre_agent": pre_agent_name,
|
||||
"observation": json.dumps(observation.model_dump(exclude_none=True),
|
||||
ensure_ascii=False,
|
||||
cls=NumpyEncoder)
|
||||
})
|
||||
except:
|
||||
pass
|
||||
pre_agent_name = cur_agent.id()
|
||||
agent_message = AgentMessage(
|
||||
payload=observation,
|
||||
session_id=self.context.session_id,
|
||||
headers={"context": self.context}
|
||||
)
|
||||
|
||||
if not override_in_subclass('async_policy', cur_agent.__class__, Agent):
|
||||
message = cur_agent.run(agent_message,
|
||||
step=step,
|
||||
outputs=self.outputs,
|
||||
stream=self.conf.get("stream", False),
|
||||
exp_id=exp_id)
|
||||
else:
|
||||
message = await cur_agent.async_run(agent_message,
|
||||
step=step,
|
||||
outputs=self.outputs,
|
||||
stream=self.conf.get("stream",
|
||||
False),
|
||||
exp_id=exp_id)
|
||||
policy = message.payload
|
||||
step_span.set_attribute("actions",
|
||||
json.dumps([action.model_dump() for action in policy],
|
||||
ensure_ascii=False))
|
||||
observation.content = None
|
||||
color_log(f"{cur_agent.id()} policy: {policy}")
|
||||
if not policy:
|
||||
logger.warning(f"current agent {cur_agent.id()} no policy to use.")
|
||||
await self.outputs.add_output(
|
||||
StepOutput.build_failed_output(name=f"Step{step}",
|
||||
step_num=step,
|
||||
data=f"current agent {cur_agent.id()} no policy to use.",
|
||||
task_id=self.context.task_id)
|
||||
)
|
||||
if not self.task.is_sub_task:
|
||||
await self.outputs.mark_completed()
|
||||
task_span.set_attributes({
|
||||
"end_time": time.time(),
|
||||
"duration": time.time() - start,
|
||||
"status": "failed",
|
||||
"error": f"current agent {cur_agent.id()} no policy to use."
|
||||
})
|
||||
return TaskResponse(msg=f"current agent {cur_agent.id()} no policy to use.",
|
||||
answer="",
|
||||
success=False,
|
||||
id=self.task.id,
|
||||
time_cost=(time.time() - start),
|
||||
usage=self.context.token_usage)
|
||||
|
||||
if is_agent(policy[0]):
|
||||
status, info = await self._agent(agent, observation, policy, step)
|
||||
if status == 'normal':
|
||||
if info:
|
||||
observations.append(observation)
|
||||
elif status == 'break':
|
||||
observation = self.swarm.action_to_observation(policy, observations)
|
||||
if idx == len(self.swarm.ordered_agents) - 1:
|
||||
return TaskResponse(
|
||||
answer=observation.content,
|
||||
success=True,
|
||||
id=self.task.id,
|
||||
time_cost=(time.time() - start),
|
||||
usage=self.context.token_usage
|
||||
)
|
||||
break
|
||||
elif status == 'return':
|
||||
await self.outputs.add_output(
|
||||
StepOutput.build_finished_output(name=f"Step{step}",
|
||||
step_num=step,
|
||||
task_id=self.context.task_id)
|
||||
)
|
||||
info.time_cost = (time.time() - start)
|
||||
task_span.set_attributes({
|
||||
"end_time": time.time(),
|
||||
"duration": info.time_cost,
|
||||
"status": "success"
|
||||
})
|
||||
return info
|
||||
elif is_tool_by_name(policy[0].tool_name):
|
||||
# todo sandbox
|
||||
msg, reward, terminated = await self._tool_call(policy, observations, step,
|
||||
cur_agent)
|
||||
step_span.set_attribute("reward", reward)
|
||||
|
||||
else:
|
||||
logger.warning(f"Unrecognized policy: {policy[0]}")
|
||||
await self.outputs.add_output(
|
||||
StepOutput.build_failed_output(
|
||||
name=f"Step{step}",
|
||||
step_num=step,
|
||||
data=f"Unrecognized policy: {policy[0]}, need to check prompt or agent / tool.",
|
||||
task_id=self.context.task_id
|
||||
)
|
||||
)
|
||||
if not self.task.is_sub_task:
|
||||
logger.info(f"FINISHED|WorkflowRunner|outputs|{self.task.id} {self.task.is_sub_task}")
|
||||
await self.outputs.mark_completed()
|
||||
task_span.set_attributes({
|
||||
"end_time": time.time(),
|
||||
"duration": time.time() - start,
|
||||
"status": "failed",
|
||||
"error": f"Unrecognized policy: {policy[0]}, need to check prompt or agent / tool."
|
||||
})
|
||||
return TaskResponse(
|
||||
msg=f"Unrecognized policy: {policy[0]}, need to check prompt or agent / tool.",
|
||||
answer="",
|
||||
success=False,
|
||||
id=self.task.id,
|
||||
time_cost=(time.time() - start),
|
||||
usage=self.context.token_usage
|
||||
)
|
||||
await self.outputs.add_output(
|
||||
StepOutput.build_finished_output(name=f"Step{step}",
|
||||
step_num=step,
|
||||
task_id=self.context.task_id)
|
||||
)
|
||||
step += 1
|
||||
if terminated and agent.finished:
|
||||
logger.info(f"{agent.id()} finished")
|
||||
if idx == len(self.swarm.ordered_agents) - 1:
|
||||
return TaskResponse(
|
||||
answer=observations[-1].content,
|
||||
success=True,
|
||||
id=self.task.id,
|
||||
time_cost=(time.time() - start),
|
||||
usage=self.context.token_usage
|
||||
)
|
||||
break
|
||||
|
||||
async def _agent(self, agent: Agent, observation: Observation, policy: List[ActionModel], step: int):
|
||||
# only one agent, and get agent from policy
|
||||
policy_for_agent = policy[0]
|
||||
agent_name = policy_for_agent.tool_name
|
||||
if not agent_name:
|
||||
agent_name = policy_for_agent.agent_name
|
||||
cur_agent: Agent = self.swarm.agents.get(agent_name)
|
||||
if not cur_agent:
|
||||
raise RuntimeError(f"Can not find {agent_name} agent in swarm.")
|
||||
|
||||
status = "normal"
|
||||
if cur_agent.id() == agent.id():
|
||||
# Current agent is entrance agent, means need to exit to the outer loop
|
||||
logger.info(f"{cur_agent.id()} exit the loop")
|
||||
status = "break"
|
||||
return status, None
|
||||
|
||||
if agent.handoffs and agent_name not in agent.handoffs:
|
||||
# Unable to hand off, exit to the outer loop
|
||||
status = "return"
|
||||
return status, TaskResponse(msg=f"Can not handoffs {agent_name} agent ",
|
||||
answer=observation.content,
|
||||
success=False,
|
||||
id=self.task.id,
|
||||
usage=self.context.token_usage)
|
||||
# Check if current agent done
|
||||
if cur_agent.finished:
|
||||
cur_agent._finished = False
|
||||
logger.info(f"{cur_agent.id()} agent be be handed off, so finished state reset to False.")
|
||||
|
||||
con = policy_for_agent.policy_info
|
||||
if policy_for_agent.params and 'content' in policy_for_agent.params:
|
||||
con = policy_for_agent.params['content']
|
||||
if observation:
|
||||
observation.content = con
|
||||
else:
|
||||
observation = Observation(content=con)
|
||||
return status, observation
|
||||
return status, None
|
||||
|
||||
# todo sandbox
|
||||
async def _tool_call(self, policy: List[ActionModel], observations: List[Observation], step: int, agent: Agent):
|
||||
msg = None
|
||||
terminated = False
|
||||
# group action by tool name
|
||||
tool_mapping = dict()
|
||||
reward = 0.0
|
||||
# Directly use or use tools after creation.
|
||||
for act in policy:
|
||||
if not self.tools or (self.tools and act.tool_name not in self.tools):
|
||||
# dynamic only use default config in module.
|
||||
conf = self.tools_conf.get(act.tool_name)
|
||||
tool = ToolFactory(act.tool_name, conf=conf, asyn=conf.use_async if conf else False)
|
||||
if isinstance(tool, Tool):
|
||||
tool.reset()
|
||||
elif isinstance(tool, AsyncTool):
|
||||
await tool.reset()
|
||||
tool_mapping[act.tool_name] = []
|
||||
self.tools[act.tool_name] = tool
|
||||
if act.tool_name not in tool_mapping:
|
||||
tool_mapping[act.tool_name] = []
|
||||
tool_mapping[act.tool_name].append(act)
|
||||
|
||||
for tool_name, action in tool_mapping.items():
|
||||
tool_message = ToolMessage(
|
||||
payload=action,
|
||||
session_id=self.context.session_id,
|
||||
headers={"context": self.context}
|
||||
)
|
||||
# Execute action using browser tool and unpack all return values
|
||||
if isinstance(self.tools[tool_name], Tool):
|
||||
message = self.tools[tool_name].step(tool_message)
|
||||
elif isinstance(self.tools[tool_name], AsyncTool):
|
||||
# todo sandbox
|
||||
message = await self.tools[tool_name].step(tool_message, agent=agent)
|
||||
else:
|
||||
logger.warning(f"Unsupported tool type: {self.tools[tool_name]}")
|
||||
continue
|
||||
|
||||
observation, reward, terminated, _, info = message.payload
|
||||
# observation, reward, terminated, _, info = action_result_transform(message, sandbox=None)
|
||||
observations.append(observation)
|
||||
for i, item in enumerate(action):
|
||||
tool_output = ToolResultOutput(
|
||||
tool_type=tool_name,
|
||||
tool_name=item.tool_name,
|
||||
data=observation.content,
|
||||
origin_tool_call=ToolCall.from_dict({
|
||||
"function": {
|
||||
"name": item.action_name,
|
||||
"arguments": item.params,
|
||||
}
|
||||
})
|
||||
)
|
||||
await self.outputs.add_output(tool_output)
|
||||
|
||||
# Check if there's an exception in info
|
||||
if info.get("exception"):
|
||||
color_log(f"Step {step} failed with exception: {info['exception']}", color=Color.red)
|
||||
msg = f"Step {step} failed with exception: {info['exception']}"
|
||||
logger.info(f"step: {step} finished by tool action: {action}.")
|
||||
log_ob = Observation(content='' if observation.content is None else observation.content,
|
||||
action_result=observation.action_result)
|
||||
trace_logger.info(f"{tool_name} observation: {log_ob}", color=Color.green)
|
||||
return msg, reward, terminated
|
||||
|
||||
def _get_step_span_id(self, step, cur_agent_name):
|
||||
key = (step, cur_agent_name)
|
||||
if key not in self.step_agent_counter:
|
||||
self.step_agent_counter[key] = 0
|
||||
else:
|
||||
self.step_agent_counter[key] += 1
|
||||
exp_index = self.step_agent_counter[key]
|
||||
|
||||
return f"{self.task.id}_{step}_{cur_agent_name}_{exp_index}"
|
||||
|
||||
|
||||
class LoopWorkflowRunner(WorkflowRunner):
|
||||
|
||||
async def _do_run(self, context: Context = None) -> TaskResponse:
|
||||
observation = self.observation
|
||||
if not observation:
|
||||
raise RuntimeError("no observation, check run process")
|
||||
|
||||
start = time.time()
|
||||
step = 1
|
||||
msg = None
|
||||
|
||||
# Use trace.span to record the entire task execution process
|
||||
with trace.span(f"task_execution_{self.task.id}", attributes={
|
||||
"task_id": self.task.id,
|
||||
"task_name": self.task.name,
|
||||
"start_time": start
|
||||
}) as task_span:
|
||||
try:
|
||||
for i in range(self.max_steps):
|
||||
await self._common_process(task_span)
|
||||
step += 1
|
||||
except Exception as err:
|
||||
logger.error(f"Runner run failed, err is {traceback.format_exc()}")
|
||||
finally:
|
||||
if not self.task.is_sub_task:
|
||||
logger.info(f"FINISHED|LoopWorkflowRunner|outputs|{self.task.id} {self.task.is_sub_task}")
|
||||
await self.outputs.mark_completed()
|
||||
color_log(f"task token usage: {self.context.token_usage}",
|
||||
color=Color.pink,
|
||||
logger_=trace_logger)
|
||||
for _, tool in self.tools.items():
|
||||
if isinstance(tool, AsyncTool):
|
||||
await tool.close()
|
||||
else:
|
||||
tool.close()
|
||||
task_span.set_attributes({
|
||||
"end_time": time.time(),
|
||||
"duration": time.time() - start,
|
||||
"error": msg
|
||||
})
|
||||
return TaskResponse(msg=msg,
|
||||
answer=observation.content,
|
||||
success=True if not msg else False,
|
||||
id=self.task.id,
|
||||
time_cost=(time.time() - start),
|
||||
usage=self.context.token_usage)
|
||||
|
||||
|
||||
class HandoffRunner(TaskRunner):
|
||||
def __init__(self, task: Task, *args, **kwargs):
|
||||
super().__init__(task=task, *args, **kwargs)
|
||||
|
||||
async def do_run(self, context: Context = None) -> TaskResponse:
|
||||
resp = await self._do_run(context)
|
||||
self._task_response = resp
|
||||
return resp
|
||||
|
||||
async def _do_run(self, context: Context = None) -> TaskResponse:
|
||||
"""Multi-agent general process based on handoff.
|
||||
|
||||
NOTE: Use the agent's finished state to control the loop, so the agent must carefully set finished state.
|
||||
|
||||
Args:
|
||||
context: Context of runner.
|
||||
"""
|
||||
start = time.time()
|
||||
|
||||
observation = self.observation
|
||||
info = dict()
|
||||
step = 0
|
||||
max_steps = self.conf.get("max_steps", 100)
|
||||
results = []
|
||||
swarm_resp = None
|
||||
self.loop_detect = []
|
||||
# Use trace.span to record the entire task execution process
|
||||
with trace.span(f"task_execution_{self.task.id}", attributes={
|
||||
"task_id": self.task.id,
|
||||
"task_name": self.task.name,
|
||||
"start_time": start
|
||||
}) as task_span:
|
||||
try:
|
||||
while step < max_steps:
|
||||
# Loose protocol
|
||||
result_dict = await self._process(observation=observation, info=info)
|
||||
results.append(result_dict)
|
||||
|
||||
swarm_resp = result_dict.get("response")
|
||||
logger.info(f"Step: {step} response:\n {result_dict}")
|
||||
|
||||
step += 1
|
||||
if self.swarm.finished or endless_detect(self.loop_detect,
|
||||
self.endless_threshold,
|
||||
self.swarm.communicate_agent.id()):
|
||||
logger.info("task done!")
|
||||
break
|
||||
|
||||
if not swarm_resp:
|
||||
logger.warning(f"Step: {step} swarm no valid response")
|
||||
break
|
||||
|
||||
observation = result_dict.get("observation")
|
||||
if not observation:
|
||||
observation = Observation(content=swarm_resp)
|
||||
else:
|
||||
observation.content = swarm_resp
|
||||
|
||||
time_cost = time.time() - start
|
||||
if not results:
|
||||
logger.warning("task no result!")
|
||||
task_span.set_attributes({
|
||||
"status": "failed",
|
||||
"error": f"task no result!"
|
||||
})
|
||||
return TaskResponse(msg=traceback.format_exc(),
|
||||
answer='',
|
||||
success=False,
|
||||
id=self.task.id,
|
||||
time_cost=time_cost,
|
||||
usage=self.context.token_usage)
|
||||
|
||||
answer = results[-1].get('observation').content if results[-1].get('observation') else swarm_resp
|
||||
return TaskResponse(answer=answer,
|
||||
success=True,
|
||||
id=self.task.id,
|
||||
time_cost=(time.time() - start),
|
||||
usage=self.context.token_usage)
|
||||
except Exception as e:
|
||||
logger.error(f"Task execution failed with error: {str(e)}\n{traceback.format_exc()}")
|
||||
task_span.set_attributes({
|
||||
"status": "failed",
|
||||
"error": f"Task execution failed with error: {str(e)}\n{traceback.format_exc()}"
|
||||
})
|
||||
return TaskResponse(msg=traceback.format_exc(),
|
||||
answer='',
|
||||
success=False,
|
||||
id=self.task.id,
|
||||
time_cost=(time.time() - start),
|
||||
usage=self.context.token_usage)
|
||||
finally:
|
||||
color_log(f"task token usage: {self.context.token_usage}",
|
||||
color=Color.pink,
|
||||
logger_=trace_logger)
|
||||
for _, tool in self.tools.items():
|
||||
if isinstance(tool, AsyncTool):
|
||||
await tool.close()
|
||||
else:
|
||||
tool.close()
|
||||
task_span.set_attributes({
|
||||
"end_time": time.time(),
|
||||
"duration": time.time() - start,
|
||||
})
|
||||
|
||||
async def _process(self, observation, info) -> Dict[str, Any]:
|
||||
if not self.swarm.initialized:
|
||||
raise RuntimeError("swarm needs to use `reset` to init first.")
|
||||
|
||||
start = time.time()
|
||||
step = 0
|
||||
max_steps = self.conf.get("max_steps", 100)
|
||||
self.swarm.cur_agent = self.swarm.communicate_agent
|
||||
pre_agent_name = None
|
||||
# use communicate agent every time
|
||||
agent_message = AgentMessage(
|
||||
payload=observation,
|
||||
session_id=self.context.session_id,
|
||||
headers={"context": self.context}
|
||||
)
|
||||
if override_in_subclass('async_policy', self.swarm.cur_agent.__class__, Agent):
|
||||
message = self.swarm.cur_agent.run(agent_message,
|
||||
step=step,
|
||||
outputs=self.outputs,
|
||||
stream=self.conf.get("stream", False))
|
||||
else:
|
||||
message = await self.swarm.cur_agent.async_run(agent_message,
|
||||
step=step,
|
||||
outputs=self.outputs,
|
||||
stream=self.conf.get("stream", False))
|
||||
self.loop_detect.append(self.swarm.cur_agent.id())
|
||||
policy = message.payload
|
||||
if not policy:
|
||||
logger.warning(f"current agent {self.swarm.cur_agent.id()} no policy to use.")
|
||||
exp_id = self._get_step_span_id(step, self.swarm.cur_agent.id())
|
||||
with trace.span(f"step_execution_{exp_id}") as step_span:
|
||||
step_span.set_attributes({
|
||||
"exp_id": exp_id,
|
||||
"task_id": self.task.id,
|
||||
"task_name": self.task.name,
|
||||
"trace_id": trace.get_current_span().get_trace_id(),
|
||||
"step": step,
|
||||
"agent_id": self.swarm.cur_agent.id(),
|
||||
"pre_agent": pre_agent_name,
|
||||
"observation": json.dumps(observation.model_dump(exclude_none=True),
|
||||
ensure_ascii=False,
|
||||
cls=NumpyEncoder),
|
||||
"actions": json.dumps([action.model_dump() for action in policy], ensure_ascii=False)
|
||||
})
|
||||
return {"msg": f"current agent {self.swarm.cur_agent.id()} no policy to use.",
|
||||
"steps": step,
|
||||
"success": False,
|
||||
"time_cost": (time.time() - start)}
|
||||
color_log(f"{self.swarm.cur_agent.id()} policy: {policy}")
|
||||
|
||||
msg = None
|
||||
response = None
|
||||
return_entry = False
|
||||
cur_agent = None
|
||||
cur_observation = observation
|
||||
finished = False
|
||||
try:
|
||||
while step < max_steps:
|
||||
terminated = False
|
||||
exp_id = self._get_step_span_id(step, self.swarm.cur_agent.id())
|
||||
with trace.span(f"step_execution_{exp_id}") as step_span:
|
||||
try:
|
||||
step_span.set_attributes({
|
||||
"exp_id": exp_id,
|
||||
"task_id": self.task.id,
|
||||
"task_name": self.task.name,
|
||||
"trace_id": trace.get_current_span().get_trace_id(),
|
||||
"step": step,
|
||||
"agent_id": self.swarm.cur_agent.id(),
|
||||
"pre_agent": pre_agent_name,
|
||||
"observation": json.dumps(cur_observation.model_dump(exclude_none=True),
|
||||
ensure_ascii=False,
|
||||
cls=NumpyEncoder),
|
||||
"actions": json.dumps([action.model_dump() for action in policy], ensure_ascii=False)
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
if is_agent(policy[0]):
|
||||
status, info, ob = await self._social_agent(policy, step)
|
||||
if status == 'normal':
|
||||
self.swarm.cur_agent = self.swarm.agents.get(policy[0].agent_name)
|
||||
policy = info
|
||||
|
||||
cur_observation = ob
|
||||
# clear observation
|
||||
observation = None
|
||||
elif is_tool_by_name(policy[0].tool_name):
|
||||
status, terminated, info = await self._social_tool_call(policy, step)
|
||||
if status == 'normal':
|
||||
observation = info
|
||||
cur_observation = observation
|
||||
else:
|
||||
logger.warning(f"Unrecognized policy: {policy[0]}")
|
||||
return {"msg": f"Unrecognized policy: {policy[0]}, need to check prompt or agent / tool.",
|
||||
"response": "",
|
||||
"steps": step,
|
||||
"success": False}
|
||||
|
||||
if status == 'break':
|
||||
return_entry = info
|
||||
break
|
||||
elif status == 'return':
|
||||
return info
|
||||
|
||||
step += 1
|
||||
pre_agent_name = self.swarm.cur_agent.id()
|
||||
if terminated and self.swarm.cur_agent.finished:
|
||||
logger.info(f"{self.swarm.cur_agent.id()} finished")
|
||||
break
|
||||
|
||||
if observation:
|
||||
if cur_agent is None:
|
||||
cur_agent = self.swarm.cur_agent
|
||||
agent_message = AgentMessage(
|
||||
payload=observation,
|
||||
session_id=self.context.session_id,
|
||||
headers={"context": self.context}
|
||||
)
|
||||
if not override_in_subclass('async_policy', cur_agent.__class__, Agent):
|
||||
message = cur_agent.run(agent_message,
|
||||
step=step,
|
||||
outputs=self.outputs,
|
||||
stream=self.conf.get("stream", False))
|
||||
else:
|
||||
message = await cur_agent.async_run(agent_message,
|
||||
step=step,
|
||||
outputs=self.outputs,
|
||||
stream=self.conf.get("stream", False))
|
||||
policy = message.payload
|
||||
color_log(f"{cur_agent.id()} policy: {policy}")
|
||||
|
||||
if policy:
|
||||
response = policy[0].policy_info if policy[0].policy_info else policy[0].action_name
|
||||
|
||||
# All agents or tools have completed their tasks
|
||||
if all(agent.finished for _, agent in self.swarm.agents.items()) or (all(
|
||||
tool.finished for _, tool in self.tools.items()) and len(self.swarm.agents) == 1):
|
||||
logger.info("entry agent finished, swarm process finished.")
|
||||
finished = True
|
||||
|
||||
if return_entry and not finished:
|
||||
# Return to the entrance, reset current agent finished state
|
||||
self.swarm.cur_agent._finished = False
|
||||
return {"steps": step,
|
||||
"response": response,
|
||||
"observation": observation,
|
||||
"msg": msg,
|
||||
"success": True if not msg else False}
|
||||
except Exception as e:
|
||||
logger.error(f"Task execution failed with error: {str(e)}\n{traceback.format_exc()}")
|
||||
return {
|
||||
"msg": str(e),
|
||||
"response": "",
|
||||
"traceback": traceback.format_exc(),
|
||||
"steps": step,
|
||||
"success": False
|
||||
}
|
||||
|
||||
async def _social_agent(self, policy: List[ActionModel], step):
|
||||
# only one agent, and get agent from policy
|
||||
policy_for_agent = policy[0]
|
||||
agent_name = policy_for_agent.tool_name
|
||||
if not agent_name:
|
||||
agent_name = policy_for_agent.agent_name
|
||||
|
||||
cur_agent: Agent = self.swarm.agents.get(agent_name)
|
||||
if not cur_agent:
|
||||
raise RuntimeError(f"Can not find {agent_name} agent in swarm.")
|
||||
|
||||
if cur_agent.id() == self.swarm.communicate_agent.id() or cur_agent.id() == self.swarm.cur_agent.id():
|
||||
# Current agent is entrance agent, means need to exit to the outer loop
|
||||
logger.info(f"{cur_agent.id()} exit to the outer loop")
|
||||
return 'break', True, None
|
||||
|
||||
if self.swarm.cur_agent.handoffs and agent_name not in self.swarm.cur_agent.handoffs:
|
||||
# Unable to hand off, exit to the outer loop
|
||||
return "return", {"msg": f"Can not handoffs {agent_name} agent "
|
||||
f"by {cur_agent.id()} agent.",
|
||||
"response": policy[0].policy_info if policy else "",
|
||||
"steps": step,
|
||||
"success": False}, None
|
||||
# Check if current agent done
|
||||
if cur_agent.finished:
|
||||
cur_agent._finished = False
|
||||
logger.info(f"{cur_agent.id()} agent be be handed off, so finished state reset to False.")
|
||||
|
||||
observation = Observation(content=policy_for_agent.policy_info)
|
||||
self.loop_detect.append(cur_agent.id())
|
||||
if cur_agent.step_reset:
|
||||
cur_agent.reset()
|
||||
|
||||
agent_message = AgentMessage(
|
||||
payload=observation,
|
||||
session_id=self.context.session_id,
|
||||
headers={"context": self.context}
|
||||
)
|
||||
|
||||
if not override_in_subclass('async_policy', cur_agent.__class__, Agent):
|
||||
message = cur_agent.run(agent_message,
|
||||
step=step,
|
||||
outputs=self.outputs,
|
||||
stream=self.conf.get("stream", False))
|
||||
else:
|
||||
message = await cur_agent.async_run(agent_message,
|
||||
step=step,
|
||||
outputs=self.outputs,
|
||||
stream=self.conf.get("stream", False))
|
||||
|
||||
agent_policy = message.payload
|
||||
if not agent_policy:
|
||||
logger.warning(
|
||||
f"{observation} can not get the valid policy in {policy_for_agent.agent_name}, exit task!")
|
||||
return "return", {"msg": f"{policy_for_agent.agent_name} invalid policy",
|
||||
"response": "",
|
||||
"steps": step,
|
||||
"success": False}, None
|
||||
color_log(f"{cur_agent.id()} policy: {agent_policy}")
|
||||
return 'normal', agent_policy, observation
|
||||
|
||||
async def _social_tool_call(self, policy: List[ActionModel], step: int):
|
||||
observation = None
|
||||
terminated = False
|
||||
# group action by tool name
|
||||
tool_mapping = dict()
|
||||
# Directly use or use tools after creation.
|
||||
for act in policy:
|
||||
if not self.tools or (self.tools and act.tool_name not in self.tools):
|
||||
# dynamic only use default config in module.
|
||||
conf: ToolConfig = self.tools_conf.get(act.tool_name)
|
||||
tool = ToolFactory(act.tool_name, conf=conf, asyn=conf.use_async if conf else False)
|
||||
if isinstance(tool, Tool):
|
||||
tool.reset()
|
||||
elif isinstance(tool, AsyncTool):
|
||||
await tool.reset()
|
||||
|
||||
tool_mapping[act.tool_name] = []
|
||||
self.tools[act.tool_name] = tool
|
||||
if act.tool_name not in tool_mapping:
|
||||
tool_mapping[act.tool_name] = []
|
||||
tool_mapping[act.tool_name].append(act)
|
||||
|
||||
for tool_name, action in tool_mapping.items():
|
||||
tool_message = ToolMessage(
|
||||
payload=action,
|
||||
session_id=self.context.session_id,
|
||||
headers={"context": self.context}
|
||||
)
|
||||
# Execute action using browser tool and unpack all return values
|
||||
if isinstance(self.tools[tool_name], Tool):
|
||||
message = self.tools[tool_name].step(tool_message)
|
||||
elif isinstance(self.tools[tool_name], AsyncTool):
|
||||
message = await self.tools[tool_name].step(tool_message)
|
||||
else:
|
||||
logger.warning(f"Unsupported tool type: {self.tools[tool_name]}")
|
||||
continue
|
||||
|
||||
observation, reward, terminated, _, info = message.payload
|
||||
for i, item in enumerate(action):
|
||||
tool_output = ToolResultOutput(
|
||||
data=observation.content,
|
||||
origin_tool_call=ToolCall.from_dict({
|
||||
"function": {
|
||||
"name": item.action_name,
|
||||
"arguments": item.params,
|
||||
}
|
||||
}),
|
||||
task_id=self.task.id
|
||||
)
|
||||
await self.outputs.add_output(tool_output)
|
||||
|
||||
# Check if there's an exception in info
|
||||
if info.get("exception"):
|
||||
color_log(f"Step {step} failed with exception: {info['exception']}", color=Color.red)
|
||||
logger.info(f"step: {step} finished by tool action {action}.")
|
||||
log_ob = Observation(content='' if observation.content is None else observation.content,
|
||||
action_result=observation.action_result)
|
||||
color_log(f"{tool_name} observation: {log_ob}", color=Color.green)
|
||||
|
||||
# The tool results give itself, exit; give to other agents, continue
|
||||
tmp_name = policy[0].agent_name
|
||||
if self.swarm.cur_agent.id() == self.swarm.communicate_agent.id() and (
|
||||
len(self.swarm.agents) == 1 or tmp_name is None or self.swarm.cur_agent.id() == tmp_name):
|
||||
return "break", terminated, True
|
||||
elif policy[0].agent_name:
|
||||
policy_for_agent = policy[0]
|
||||
agent_name = policy_for_agent.agent_name
|
||||
if not agent_name:
|
||||
agent_name = policy_for_agent.tool_name
|
||||
cur_agent: Agent = self.swarm.agents.get(agent_name)
|
||||
if not cur_agent:
|
||||
raise RuntimeError(f"Can not find {agent_name} agent in swarm.")
|
||||
if self.swarm.cur_agent.handoffs and agent_name not in self.swarm.cur_agent.handoffs:
|
||||
# Unable to hand off, exit to the outer loop
|
||||
return "return", {"msg": f"Can not handoffs {agent_name} agent "
|
||||
f"by {cur_agent.id()} agent.",
|
||||
"response": policy[0].policy_info if policy else "",
|
||||
"steps": step,
|
||||
"success": False}
|
||||
# Check if current agent done
|
||||
if cur_agent.finished:
|
||||
cur_agent._finished = False
|
||||
logger.info(f"{cur_agent.id()} agent be be handed off, so finished state reset to False.")
|
||||
return "normal", terminated, observation
|
||||
|
||||
def _get_step_span_id(self, step, cur_agent_name):
|
||||
key = (step, cur_agent_name)
|
||||
if key not in self.step_agent_counter:
|
||||
self.step_agent_counter[key] = 0
|
||||
else:
|
||||
self.step_agent_counter[key] += 1
|
||||
exp_index = self.step_agent_counter[key]
|
||||
|
||||
return f"{self.task.id}_{step}_{cur_agent_name}_{exp_index}"
|
||||
@@ -0,0 +1,170 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
from typing import Dict, Any, Callable, Optional, Union, Awaitable
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger("callback_registry")
|
||||
|
||||
|
||||
class CallbackRegistry:
|
||||
"""Callback function registry, used to manage and execute callback functions"""
|
||||
|
||||
# Registry for storing decorated callback functions
|
||||
_registry: Dict[str, Callable] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, key_name: str, func: Callable) -> Callable:
|
||||
"""Register callback function to the registry
|
||||
|
||||
Args:
|
||||
key_name: Unique identifier for the callback function
|
||||
func: Callback function to register
|
||||
|
||||
Returns:
|
||||
Registered callback function
|
||||
"""
|
||||
# Check if a callback function with the same key_name already exists
|
||||
if key_name in cls._registry:
|
||||
existing_func = cls._registry[key_name]
|
||||
logger.warning(
|
||||
f"Callback function '{key_name}' already exists and will be overwritten! "
|
||||
f"Original function: {existing_func.__name__ if hasattr(existing_func, '__name__') else str(existing_func)}, "
|
||||
f"New function: {func.__name__ if hasattr(func, '__name__') else str(func)}"
|
||||
)
|
||||
|
||||
cls._registry[key_name] = func
|
||||
return func
|
||||
|
||||
@classmethod
|
||||
def get(cls, key_name: str) -> Optional[Callable]:
|
||||
"""Get registered callback function by key_name
|
||||
|
||||
Args:
|
||||
key_name: Unique identifier for the callback function
|
||||
|
||||
Returns:
|
||||
Registered callback function, or None if not found
|
||||
"""
|
||||
return cls._registry.get(key_name)
|
||||
|
||||
@classmethod
|
||||
async def execute(
|
||||
cls,
|
||||
key_name: str,
|
||||
tool: Any,
|
||||
args: Dict[str, Any],
|
||||
tool_context: Any,
|
||||
tool_response: Optional[Dict[str, Any]] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Execute registered callback function
|
||||
|
||||
Args:
|
||||
key_name: Unique identifier for the callback function
|
||||
tool: Tool object
|
||||
args: Tool arguments
|
||||
tool_context: Tool context
|
||||
tool_response: Tool response (for post-callbacks)
|
||||
|
||||
Returns:
|
||||
Return value of the callback function, or None if the callback function doesn't exist
|
||||
"""
|
||||
callback = cls.get(key_name)
|
||||
if not callback:
|
||||
return None
|
||||
|
||||
# Determine parameters based on callback type
|
||||
if tool_response is not None:
|
||||
# Post-callback
|
||||
result = callback(tool, args, tool_context, tool_response)
|
||||
else:
|
||||
# Pre-callback
|
||||
result = callback(tool, args, tool_context)
|
||||
|
||||
# Handle asynchronous callbacks
|
||||
if inspect.isawaitable(result):
|
||||
result = await result
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def list(cls) -> Dict[str, str]:
|
||||
"""List all registered callback functions
|
||||
|
||||
Returns:
|
||||
Dictionary containing callback function names and descriptions
|
||||
"""
|
||||
return {
|
||||
key: func.__name__ if hasattr(func, '__name__') else str(func)
|
||||
for key, func in cls._registry.items()
|
||||
}
|
||||
|
||||
|
||||
def reg_callback(key_name: str):
|
||||
"""Decorator for registering callback functions
|
||||
|
||||
Args:
|
||||
key_name: Unique identifier for the callback function
|
||||
|
||||
Returns:
|
||||
Decorator function
|
||||
"""
|
||||
def decorator(func):
|
||||
# Register function to the global registry
|
||||
CallbackRegistry.register(key_name, func)
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
# For backward compatibility, keep these functions
|
||||
def get_callback(key_name: str) -> Optional[Callable]:
|
||||
"""Get registered callback function by key_name
|
||||
|
||||
Args:
|
||||
key_name: Unique identifier for the callback function
|
||||
|
||||
Returns:
|
||||
Registered callback function, or None if not found
|
||||
"""
|
||||
return CallbackRegistry.get(key_name)
|
||||
|
||||
|
||||
async def execute_callback(
|
||||
key_name: str,
|
||||
tool: Any,
|
||||
args: Dict[str, Any],
|
||||
tool_context: Any,
|
||||
tool_response: Optional[Dict[str, Any]] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Execute registered callback function
|
||||
|
||||
Args:
|
||||
key_name: Unique identifier for the callback function
|
||||
tool: Tool object
|
||||
args: Tool arguments
|
||||
tool_context: Tool context
|
||||
tool_response: Tool response (for post-callbacks)
|
||||
|
||||
Returns:
|
||||
Return value of the callback function, or None if the callback function doesn't exist
|
||||
"""
|
||||
return await CallbackRegistry.execute(key_name, tool, args, tool_context, tool_response)
|
||||
|
||||
|
||||
def list_callbacks() -> Dict[str, str]:
|
||||
"""List all registered callback functions
|
||||
|
||||
Returns:
|
||||
Dictionary containing callback function names and descriptions
|
||||
"""
|
||||
return CallbackRegistry.list()
|
||||
@@ -0,0 +1,84 @@
|
||||
from typing import Tuple
|
||||
|
||||
from aworld.runners.callback.decorator import CallbackRegistry
|
||||
from aworld.runners.handler.base import DefaultHandler
|
||||
from aworld.core.common import Observation, CallbackItem
|
||||
from aworld.core.event.base import Message, Constants
|
||||
from aworld.logs.util import logger
|
||||
from aworld.runners.state_manager import RuntimeStateManager, HandleResult, RunNodeStatus
|
||||
|
||||
|
||||
class ToolCallbackHandler(DefaultHandler):
|
||||
def __init__(self, runner):
|
||||
self.runner = runner
|
||||
|
||||
async def handle(self, message):
|
||||
if message.category != Constants.TOOL_CALLBACK:
|
||||
return
|
||||
logger.info(f"-------ToolCallbackHandler start handle message----: {message}")
|
||||
self.context = message.context
|
||||
observation = None
|
||||
state_mng = RuntimeStateManager.instance()
|
||||
if not state_mng:
|
||||
logger.eror("-------ToolCallbackHandler state_mng is None----")
|
||||
return
|
||||
try:
|
||||
payload = message.payload
|
||||
if not payload:
|
||||
state_mng.run_failed(message.id, "callback failed", [])
|
||||
return
|
||||
if isinstance(payload, CallbackItem):
|
||||
observation = payload.data[0] if isinstance(payload.data, Tuple) else payload.data
|
||||
elif isinstance(payload, Tuple) and isinstance(payload[0], Observation):
|
||||
observation=payload[0]
|
||||
if not isinstance(observation, Observation):
|
||||
state_mng.run_failed(message.id, "callback failed", [])
|
||||
return
|
||||
if not observation.action_result:
|
||||
state_mng.run_failed(message.id, "callback failed", [])
|
||||
return
|
||||
|
||||
results = []
|
||||
for res in observation.action_result:
|
||||
success = False
|
||||
result = HandleResult(
|
||||
result=Message(payload=None,
|
||||
category=Constants.TOOL_CALLBACK,
|
||||
sender=self.name(),
|
||||
session_id=message.context.session_id,
|
||||
headers={"context": message.context}),
|
||||
status=RunNodeStatus.FAILED
|
||||
)
|
||||
if not res or not res.content or not res.tool_name or not res.action_name:
|
||||
results.append(result)
|
||||
continue
|
||||
callback_func = CallbackRegistry.get(res.tool_name + "__" + res.action_name)
|
||||
if not callback_func:
|
||||
result.status = RunNodeStatus.SUCCESS
|
||||
results.append(result)
|
||||
continue
|
||||
callback_res = callback_func(res)
|
||||
if not callback_res or callback_res.success is False:
|
||||
results.append(result)
|
||||
continue
|
||||
result.status = RunNodeStatus.SUCCESS
|
||||
result.result.payload = callback_res
|
||||
results.append(result)
|
||||
|
||||
state_mng.run_succeed(message.id, "test callback succ", results)
|
||||
except Exception as e:
|
||||
# todo
|
||||
logger.warning(f"ToolCallbackHandler Failed to parse payload: {e}")
|
||||
state_mng.run_failed(message.id, "callback failed", [])
|
||||
finally:
|
||||
yield Message(
|
||||
category=Constants.OUTPUT,
|
||||
payload=None,
|
||||
sender=self.name(),
|
||||
session_id=message.session_id,
|
||||
headers={"context": self.context}
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import asyncio
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from aworld.core.agent.base import BaseAgent
|
||||
|
||||
from aworld.core.exceptions import AWorldRuntimeException
|
||||
|
||||
import aworld.trace as trace
|
||||
from typing import List, Callable, Any
|
||||
|
||||
from aworld.core.common import TaskItem, ActionModel
|
||||
from aworld.core.context.base import Context
|
||||
|
||||
from aworld.agents.llm_agent import Agent
|
||||
from aworld.core.event.base import Message, Constants, TopicType, ToolMessage, AgentMessage
|
||||
from aworld.core.task import Task, TaskResponse
|
||||
from aworld.events.manager import EventManager
|
||||
from aworld.logs.util import logger
|
||||
from aworld.replay_buffer import EventReplayBuffer
|
||||
from aworld.runners import HandlerFactory
|
||||
from aworld.runners.handler.base import DefaultHandler
|
||||
|
||||
from aworld.runners.task_runner import TaskRunner
|
||||
from aworld.utils.common import override_in_subclass, new_instance
|
||||
from aworld.runners.state_manager import EventRuntimeStateManager
|
||||
|
||||
|
||||
class TaskEventRunner(TaskRunner):
|
||||
"""Event driven task runner."""
|
||||
|
||||
def __init__(self, task: Task, *args, **kwargs):
|
||||
super().__init__(task, *args, **kwargs)
|
||||
self._task_response = None
|
||||
self.event_mng = EventManager(self.context)
|
||||
self.hooks = {}
|
||||
self.handlers = []
|
||||
self.init_messages = []
|
||||
self.background_tasks = set()
|
||||
self.state_manager = EventRuntimeStateManager.instance()
|
||||
self.replay_buffer = EventReplayBuffer()
|
||||
|
||||
async def do_run(self, context: Context = None):
|
||||
if self.swarm and not self.swarm.initialized:
|
||||
raise AWorldRuntimeException("swarm needs to use `reset` to init first.")
|
||||
if not self.init_messages:
|
||||
raise AWorldRuntimeException("no question event to solve.")
|
||||
|
||||
async with trace.task_span(self.init_messages[0].session_id, self.task):
|
||||
for msg in self.init_messages:
|
||||
await self.event_mng.emit_message(msg)
|
||||
await self._do_run()
|
||||
await self._save_trajectories()
|
||||
return self._response()
|
||||
|
||||
async def pre_run(self):
|
||||
logger.debug(f"[TaskEventRunner] pre_run start {self.task.id}")
|
||||
await super().pre_run()
|
||||
self.event_mng.context = self.context
|
||||
self.context.event_manager = self.event_mng
|
||||
|
||||
if self.swarm and not self.swarm.max_steps:
|
||||
self.swarm.max_steps = self.task.conf.get('max_steps', 10)
|
||||
observation = self.observation
|
||||
if not observation:
|
||||
raise RuntimeError("no observation, check run process")
|
||||
|
||||
self._build_first_message()
|
||||
|
||||
if self.swarm:
|
||||
logger.debug(f"swarm: {self.swarm}")
|
||||
# register agent handler
|
||||
for _, agent in self.swarm.agents.items():
|
||||
if override_in_subclass('async_policy', agent.__class__, Agent):
|
||||
await self.event_mng.register(Constants.AGENT, agent.id(), agent.async_run)
|
||||
else:
|
||||
await self.event_mng.register(Constants.AGENT, agent.id(), agent.run)
|
||||
# register tool handler
|
||||
for key, tool in self.tools.items():
|
||||
if tool.handler:
|
||||
await self.event_mng.register(Constants.TOOL, tool.name(), tool.handler)
|
||||
else:
|
||||
await self.event_mng.register(Constants.TOOL, tool.name(), tool.step)
|
||||
handlers = self.event_mng.event_bus.get_topic_handlers(
|
||||
Constants.TOOL, tool.name())
|
||||
if not handlers:
|
||||
await self.event_mng.register(Constants.TOOL, Constants.TOOL, tool.step)
|
||||
|
||||
self._stopped = asyncio.Event()
|
||||
|
||||
# handler of process in framework
|
||||
handler_list = self.conf.get("handlers")
|
||||
if handler_list:
|
||||
# handler class name
|
||||
for hand in handler_list:
|
||||
self.handlers.append(new_instance(hand, self))
|
||||
else:
|
||||
for handler in HandlerFactory:
|
||||
self.handlers.append(HandlerFactory(handler, runner=self))
|
||||
logger.debug(f"[TaskEventRunner] pre_run finish {self.task.id}")
|
||||
|
||||
def _build_first_message(self):
|
||||
# build the first message
|
||||
if self.agent_oriented:
|
||||
agents = self.swarm.communicate_agent
|
||||
if isinstance(agents, BaseAgent):
|
||||
agents = [agents]
|
||||
|
||||
for agent in agents:
|
||||
self.init_messages.append(AgentMessage(payload=self.observation,
|
||||
sender='runner',
|
||||
receiver=agent.id(),
|
||||
session_id=self.context.session_id,
|
||||
headers={'context': self.context}))
|
||||
else:
|
||||
actions: List[ActionModel] = self.observation.content
|
||||
action_dict = {}
|
||||
for action in actions:
|
||||
if action.tool_name not in action_dict:
|
||||
action_dict[action.tool_name] = []
|
||||
action_dict[action.tool_name].append(action)
|
||||
|
||||
for tool_name, actions in action_dict.items():
|
||||
self.init_messages.append(ToolMessage(payload=actions,
|
||||
sender='runner',
|
||||
receiver=tool_name,
|
||||
session_id=self.context.session_id,
|
||||
headers={'context': self.context}))
|
||||
|
||||
async def _common_process(self, message: Message) -> List[Message]:
|
||||
logger.debug(
|
||||
f"[TaskEventRunner] _common_process start {self.task.id}, message_id = {message.id}")
|
||||
event_bus = self.event_mng.event_bus
|
||||
|
||||
key = message.category
|
||||
transformer = self.event_mng.get_transform_handler(key)
|
||||
if transformer:
|
||||
message = await event_bus.transform(message, handler=transformer)
|
||||
|
||||
results = []
|
||||
handlers = self.event_mng.get_handlers(key)
|
||||
async with trace.message_span(message=message):
|
||||
logger.debug(
|
||||
f"[TaskEventRunner] start_message_node start {self.task.id}, message_id = {message.id}")
|
||||
self.state_manager.start_message_node(message)
|
||||
logger.debug(
|
||||
f"[TaskEventRunner] start_message_node end {self.task.id}, message_id = {message.id}")
|
||||
if handlers:
|
||||
if message.topic:
|
||||
handlers = {message.topic: handlers.get(message.topic, [])}
|
||||
elif message.receiver:
|
||||
handlers = {message.receiver: handlers.get(
|
||||
message.receiver, [])}
|
||||
else:
|
||||
logger.warning(
|
||||
f"{message.id} no receiver and topic, be ignored.")
|
||||
handlers.clear()
|
||||
|
||||
handle_tasks = []
|
||||
for topic, handler_list in handlers.items():
|
||||
if not handler_list:
|
||||
logger.warning(f"{topic} no handler, ignore.")
|
||||
continue
|
||||
|
||||
for handler in handler_list:
|
||||
t = asyncio.create_task(
|
||||
self._handle_task(message, handler))
|
||||
handle_tasks.append(t)
|
||||
logger.debug(
|
||||
f"[TaskEventRunner] _common_process handle_tasks collect finished {self.task.id}, message_id = {message.id}")
|
||||
|
||||
# For _handle_task case, end message node asynchronously
|
||||
async def async_end_message_node():
|
||||
logger.debug(
|
||||
f"[TaskEventRunner] async_end_message_node STARTED {self.task.id}, message_id = {message.id}")
|
||||
try:
|
||||
# Wait for all _handle_task tasks to complete before ending message node
|
||||
if handle_tasks:
|
||||
logger.debug(
|
||||
f"[TaskEventRunner] async_end_message_node {self.task.id} Before gather {len(handle_tasks)} tasks")
|
||||
await asyncio.gather(*handle_tasks)
|
||||
logger.debug(
|
||||
f"[TaskEventRunner] async_end_message_node {self.task.id} After gather tasks completed")
|
||||
logger.debug(
|
||||
f"[TaskEventRunner] _common_process handle_tasks process end_message_node start {self.task.id}, message_id = {message.id}")
|
||||
self.state_manager.end_message_node(message)
|
||||
logger.debug(
|
||||
f"[TaskEventRunner] _common_process handle_tasks process finished {self.task.id}, message_id = {message.id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in async_end_message_node: {e}")
|
||||
raise
|
||||
|
||||
end_node_task = asyncio.create_task(async_end_message_node())
|
||||
self.background_tasks.add(end_node_task)
|
||||
end_node_task.add_done_callback(self.background_tasks.discard)
|
||||
else:
|
||||
# not handler, return raw message
|
||||
results.append(message)
|
||||
|
||||
t = asyncio.create_task(self._raw_task(results))
|
||||
self.background_tasks.add(t)
|
||||
t.add_done_callback(self.background_tasks.discard)
|
||||
# wait until it is complete
|
||||
await t
|
||||
self.state_manager.end_message_node(message)
|
||||
logger.debug(
|
||||
f"[TaskEventRunner] _common_process return results {self.task.id}, message_id = {message.id}, ")
|
||||
return results
|
||||
|
||||
async def _handle_task(self, message: Message, handler: Callable[..., Any]):
|
||||
con = message
|
||||
async with trace.handler_span(message=message, handler=handler):
|
||||
try:
|
||||
logger.debug(
|
||||
f"event_runner _handle_task - self: {self}, swarm: {self.swarm}, event_mng: {self.event_mng}, event_bus: {self.event_mng.event_bus}, message: {message}")
|
||||
logger.info(
|
||||
f"[TaskEventRunner] {self.task.id} _handle_task start, message: {message.id}")
|
||||
if asyncio.iscoroutinefunction(handler):
|
||||
con = await handler(con)
|
||||
else:
|
||||
con = handler(con)
|
||||
|
||||
logger.info(
|
||||
f"[TaskEventRunner] {self.task.id} _handle_task finished message= {message.id}, session_id = {self.task.session_id}")
|
||||
if isinstance(con, Message):
|
||||
# process in framework
|
||||
self.state_manager.save_message_handle_result(name=handler.__name__,
|
||||
message=message,
|
||||
result=con)
|
||||
async for event in self._inner_handler_process(
|
||||
results=[con],
|
||||
handlers=self.handlers
|
||||
):
|
||||
await self.event_mng.emit_message(event)
|
||||
else:
|
||||
self.state_manager.save_message_handle_result(name=handler.__name__,
|
||||
message=message)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"{handler} process fail. {traceback.format_exc()}")
|
||||
error_msg = Message(
|
||||
category=Constants.TASK,
|
||||
payload=TaskItem(msg=str(e), data=message),
|
||||
sender=self.name,
|
||||
session_id=self.context.session_id,
|
||||
topic=TopicType.ERROR,
|
||||
headers={"context": self.context}
|
||||
)
|
||||
self.state_manager.save_message_handle_result(name=handler.__name__,
|
||||
message=message,
|
||||
result=error_msg)
|
||||
await self.event_mng.emit_message(error_msg)
|
||||
|
||||
async def _raw_task(self, messages: List[Message]):
|
||||
# process in framework
|
||||
async for event in self._inner_handler_process(
|
||||
results=messages,
|
||||
handlers=self.handlers
|
||||
):
|
||||
await self.event_mng.emit_message(event)
|
||||
|
||||
async def _inner_handler_process(self, results: List[Message], handlers: List[DefaultHandler]):
|
||||
# can use runtime backend to parallel
|
||||
for handler in handlers:
|
||||
for result in results:
|
||||
async for event in handler.handle(result):
|
||||
yield event
|
||||
|
||||
async def _do_run(self):
|
||||
logger.debug(f"[TaskEventRunner] _do_run start {self.task.id}")
|
||||
|
||||
"""Task execution process in real."""
|
||||
start = time.time()
|
||||
msg = None
|
||||
answer = None
|
||||
message = None
|
||||
try:
|
||||
while True:
|
||||
if self.task.timeout > 0 and time.time() - self.start_time > self.task.timeout:
|
||||
logger.warn(
|
||||
f"[TaskEventRunner] {self.task.id} task timeout after {time.time() - self.start_time} seconds.")
|
||||
self._task_response = TaskResponse(answer='',
|
||||
success=False,
|
||||
context=message.context,
|
||||
id=self.task.id,
|
||||
time_cost=(time.time() - self.start_time),
|
||||
usage=self.context.token_usage,
|
||||
msg='cancellation: task timeout',
|
||||
status='cancelled')
|
||||
await self.stop()
|
||||
if await self.is_stopped():
|
||||
logger.debug(
|
||||
f"[TaskEventRunner] break snap {self.task.id}")
|
||||
await self.event_mng.done()
|
||||
logger.info(
|
||||
f" [TaskEventRunner] stop task {self.task.id}...")
|
||||
if self._task_response is None:
|
||||
# send msg to output
|
||||
self._task_response = TaskResponse(msg=msg,
|
||||
answer=answer,
|
||||
context=message.context,
|
||||
success=True if not msg else False,
|
||||
id=self.task.id,
|
||||
time_cost=(
|
||||
time.time() - start),
|
||||
usage=self.context.token_usage,
|
||||
status='success' if not msg else 'failed')
|
||||
break
|
||||
logger.debug(f"[TaskEventRunner] next snap {self.task.id}")
|
||||
# consume message
|
||||
message: Message = await self.event_mng.consume()
|
||||
logger.debug(
|
||||
f"[TaskEventRunner] next consume finished {self.task.id}, event_bus: {self.event_mng.event_bus},: message = {message}")
|
||||
# use registered handler to process message
|
||||
await self._common_process(message)
|
||||
logger.debug(
|
||||
f"[TaskEventRunner] _common_process finished {self.task.id}")
|
||||
except Exception as e:
|
||||
logger.error(f"consume message fail. {traceback.format_exc()}")
|
||||
error_msg = Message(
|
||||
category=Constants.TASK,
|
||||
payload=TaskItem(msg=str(e), data=message),
|
||||
sender=self.name,
|
||||
session_id=self.context.session_id,
|
||||
topic=TopicType.ERROR,
|
||||
headers={"context": self.context}
|
||||
)
|
||||
self.state_manager.save_message_handle_result(name=TaskEventRunner.__name__,
|
||||
message=message,
|
||||
result=error_msg)
|
||||
await self.event_mng.emit_message(error_msg)
|
||||
finally:
|
||||
logger.debug(
|
||||
f"[TaskEventRunner] _do_run finished await_is_stopped {self.task.id}")
|
||||
if await self.is_stopped():
|
||||
logger.info(
|
||||
f"[TaskEventRunner] _do_run finished is_stopped {self.task.id}")
|
||||
await self.context.update_task_after_run(self._task_response)
|
||||
if not self.task.is_sub_task:
|
||||
logger.info(f"FINISHED|TaskEventRunner|outputs|{self.task.id} {self.task.is_sub_task}")
|
||||
await self.task.outputs.mark_completed()
|
||||
|
||||
if self.swarm and self.swarm.agents:
|
||||
for agent_name, agent in self.swarm.agents.items():
|
||||
try:
|
||||
if hasattr(agent, 'sandbox') and agent.sandbox:
|
||||
await agent.sandbox.cleanup()
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"event_runner Failed to cleanup sandbox for agent {agent_name}: {e}")
|
||||
|
||||
async def stop(self):
|
||||
self._stopped.set()
|
||||
|
||||
async def is_stopped(self):
|
||||
return self._stopped.is_set()
|
||||
|
||||
def response(self):
|
||||
return self._task_response
|
||||
|
||||
def _response(self):
|
||||
if self.context.get_task().conf and self.context.get_task().conf.resp_carry_context == False:
|
||||
self._task_response.context = None
|
||||
return self._task_response
|
||||
|
||||
async def _save_trajectories(self):
|
||||
try:
|
||||
messages = self.event_mng.messages_by_task_id(self.task.id)
|
||||
trajectory = await self.replay_buffer.get_trajectory(messages, self.task.id, self.state_manager)
|
||||
self._task_response.trajectory = trajectory
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get trajectories: {str(e)}.{traceback.format_exc()}")
|
||||
@@ -0,0 +1,6 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
from aworld.runners.handler.base import DefaultHandler
|
||||
from aworld.utils.common import scan_packages
|
||||
|
||||
scan_packages("aworld.runners.handler", [DefaultHandler])
|
||||
@@ -0,0 +1,481 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import abc
|
||||
from typing import AsyncGenerator, Tuple
|
||||
|
||||
from aworld.agents.loop_llm_agent import LoopableAgent
|
||||
from aworld.core.agent.base import is_agent, AgentFactory
|
||||
from aworld.core.agent.swarm import GraphBuildType, AgentGraph
|
||||
from aworld.core.common import ActionModel, Observation, TaskItem
|
||||
from aworld.core.event.base import Message, Constants, TopicType, AgentMessage
|
||||
from aworld.core.exceptions import AWorldRuntimeException
|
||||
from aworld.logs.util import logger
|
||||
from aworld.runners import HandlerFactory
|
||||
from aworld.runners.handler.base import DefaultHandler
|
||||
from aworld.runners.handler.tool import DefaultToolHandler
|
||||
from aworld.runners.state_manager import RunNode, RunNodeStatus, RunNodeBusiType
|
||||
from aworld.runners.utils import endless_detect
|
||||
from aworld.output.base import StepOutput
|
||||
|
||||
|
||||
class AgentHandler(DefaultHandler):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def __init__(self, runner: 'TaskEventRunner'):
|
||||
super().__init__(runner)
|
||||
self.runner = runner
|
||||
self.swarm = runner.swarm
|
||||
self.endless_threshold = runner.endless_threshold
|
||||
self.task_id = runner.task.id
|
||||
|
||||
self.agent_calls = []
|
||||
|
||||
@classmethod
|
||||
def name(cls):
|
||||
return "_agents_handler"
|
||||
|
||||
|
||||
@HandlerFactory.register(name=f'__{Constants.AGENT}__')
|
||||
class DefaultAgentHandler(AgentHandler):
|
||||
def is_valid_message(self, message: Message):
|
||||
if message.category != Constants.AGENT:
|
||||
if self.swarm and message.sender in self.swarm.agents and message.sender in AgentFactory:
|
||||
if self.agent_calls:
|
||||
if self.agent_calls[-1] != message.sender:
|
||||
self.agent_calls.append(message.sender)
|
||||
else:
|
||||
self.agent_calls.append(message.sender)
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _do_handle(self, message: Message) -> AsyncGenerator[Message, None]:
|
||||
if not self.is_valid_message(message):
|
||||
return
|
||||
|
||||
headers = {"context": message.context}
|
||||
session_id = message.session_id
|
||||
data = message.payload
|
||||
if not data:
|
||||
# error message, p2p
|
||||
yield Message(
|
||||
category=Constants.OUTPUT,
|
||||
payload=StepOutput.build_failed_output(name=f"{message.caller or self.name()}",
|
||||
step_num=0,
|
||||
data="no data to process.",
|
||||
task_id=self.task_id),
|
||||
sender=self.name(),
|
||||
session_id=session_id,
|
||||
headers=headers
|
||||
)
|
||||
yield Message(
|
||||
category=Constants.TASK,
|
||||
payload=TaskItem(msg="no data to process.", data=data, stop=True),
|
||||
sender=self.name(),
|
||||
session_id=session_id,
|
||||
topic=TopicType.ERROR,
|
||||
headers=headers
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(data, Tuple) and isinstance(data[0], Observation):
|
||||
data = data[0]
|
||||
message.payload = data
|
||||
# data is Observation
|
||||
if isinstance(data, Observation):
|
||||
if not self.swarm:
|
||||
msg = Message(
|
||||
category=Constants.TASK,
|
||||
payload=data.content,
|
||||
sender=data.observer,
|
||||
session_id=session_id,
|
||||
topic=TopicType.FINISHED,
|
||||
headers=headers
|
||||
)
|
||||
logger.info(f"FINISHED|agent handler send finished message: {msg}")
|
||||
yield msg
|
||||
return
|
||||
|
||||
agent = self.swarm.agents.get(message.receiver)
|
||||
# agent + tool completion protocol.
|
||||
if agent and agent.finished and data.info.get('done'):
|
||||
self.swarm.cur_step += 1
|
||||
|
||||
root_agent = self.swarm.communicate_agent
|
||||
if isinstance(root_agent, list):
|
||||
root_agent = root_agent[0]
|
||||
if agent.id() == root_agent.id():
|
||||
msg = Message(
|
||||
category=Constants.TASK,
|
||||
payload=data.content,
|
||||
sender=agent.id(),
|
||||
session_id=session_id,
|
||||
topic=TopicType.FINISHED,
|
||||
headers=headers
|
||||
)
|
||||
logger.info(f"FINISHED|agent handler send finished message: {msg}")
|
||||
yield msg
|
||||
else:
|
||||
msg = Message(
|
||||
category=Constants.AGENT,
|
||||
payload=Observation(content=data.content),
|
||||
sender=agent.id(),
|
||||
session_id=session_id,
|
||||
receiver=root_agent.id(),
|
||||
headers=message.headers
|
||||
)
|
||||
logger.info(f"agent handler send agent message: {msg}")
|
||||
yield msg
|
||||
else:
|
||||
if data.info.get('done'):
|
||||
agent_name = self.agent_calls[-1]
|
||||
async for event in self._stop_check(ActionModel(agent_name=agent_name, policy_info=data.content),
|
||||
message):
|
||||
yield event
|
||||
elif not message.receiver:
|
||||
agent_name = message.sender
|
||||
async for event in self._stop_check(ActionModel(agent_name=agent_name, policy_info=data.content),
|
||||
message):
|
||||
yield event
|
||||
else:
|
||||
logger.info(f"agent handler send observation message: {message}")
|
||||
yield message
|
||||
return
|
||||
|
||||
# data is List[ActionModel]
|
||||
for action in data:
|
||||
if not isinstance(action, ActionModel):
|
||||
# error message, p2p
|
||||
yield Message(
|
||||
category=Constants.OUTPUT,
|
||||
payload=StepOutput.build_failed_output(name=f"{message.caller or self.name()}",
|
||||
step_num=0,
|
||||
data="action not a ActionModel.",
|
||||
task_id=self.task_id),
|
||||
sender=self.name(),
|
||||
session_id=session_id,
|
||||
headers=headers
|
||||
)
|
||||
msg = Message(
|
||||
category=Constants.TASK,
|
||||
payload=TaskItem(msg="action not a ActionModel.", data=data, stop=True),
|
||||
sender=self.name(),
|
||||
session_id=session_id,
|
||||
topic=TopicType.ERROR,
|
||||
headers=headers
|
||||
)
|
||||
logger.info(f"agent handler send task message: {msg}")
|
||||
yield msg
|
||||
return
|
||||
|
||||
tools = []
|
||||
agents = []
|
||||
for action in data:
|
||||
if is_agent(action):
|
||||
agents.append(action)
|
||||
else:
|
||||
tools.append(action)
|
||||
|
||||
if tools:
|
||||
msg = Message(
|
||||
category=Constants.TOOL,
|
||||
payload=tools,
|
||||
sender=self.name(),
|
||||
session_id=session_id,
|
||||
receiver=DefaultToolHandler.name(),
|
||||
headers=message.headers
|
||||
)
|
||||
logger.info(f"agent handler send tool message: {msg}")
|
||||
yield msg
|
||||
else:
|
||||
yield Message(
|
||||
category=Constants.OUTPUT,
|
||||
payload=StepOutput.build_finished_output(name=f"{message.caller or self.name()}",
|
||||
step_num=0,
|
||||
task_id=self.task_id),
|
||||
sender=self.name(),
|
||||
receiver=agents[0].tool_name,
|
||||
session_id=session_id,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
for agent in agents:
|
||||
async for event in self._agent(agent, message):
|
||||
logger.info(f"agent handler send message: {event}")
|
||||
yield event
|
||||
|
||||
async def _agent(self, action: ActionModel, message: Message):
|
||||
self.agent_calls.append(action.agent_name)
|
||||
agent = self.swarm.agents.get(action.agent_name)
|
||||
# be handoff
|
||||
agent_name = action.tool_name
|
||||
if not agent_name:
|
||||
async for event in self._stop_check(action, message):
|
||||
yield event
|
||||
return
|
||||
|
||||
headers = {"context": message.context}
|
||||
session_id = message.session_id
|
||||
cur_agent = self.swarm.agents.get(agent_name)
|
||||
if not cur_agent or not agent:
|
||||
yield Message(
|
||||
category=Constants.TASK,
|
||||
payload=TaskItem(msg=f"Can not find {agent_name} or {action.agent_name} agent in swarm.",
|
||||
data=action,
|
||||
stop=True),
|
||||
sender=self.name(),
|
||||
session_id=session_id,
|
||||
topic=TopicType.ERROR,
|
||||
headers=headers
|
||||
)
|
||||
return
|
||||
|
||||
cur_agent._finished = False
|
||||
con = action.policy_info
|
||||
if action.params and 'content' in action.params:
|
||||
con = action.params['content']
|
||||
observation = Observation(content=con, observer=agent.id(), from_agent_name=agent.id())
|
||||
|
||||
if agent.handoffs and agent_name not in agent.handoffs:
|
||||
if message.caller:
|
||||
message.receiver = message.caller
|
||||
message.caller = ''
|
||||
yield message
|
||||
else:
|
||||
yield Message(category=Constants.TASK,
|
||||
payload=TaskItem(msg=f"Can not handoffs {agent_name} agent ", data=observation),
|
||||
sender=self.name(),
|
||||
session_id=session_id,
|
||||
topic=TopicType.RERUN,
|
||||
headers=headers)
|
||||
return
|
||||
|
||||
headers = message.headers.copy()
|
||||
# headers.update({"agent_as_tool": True})
|
||||
yield Message(
|
||||
category=Constants.AGENT,
|
||||
payload=observation,
|
||||
caller=message.caller,
|
||||
sender=action.agent_name,
|
||||
session_id=session_id,
|
||||
receiver=action.tool_name,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
async def _stop_check(self, action: ActionModel, message: Message) -> AsyncGenerator[Message, None]:
|
||||
if GraphBuildType.TEAM.value == self.swarm.build_type:
|
||||
async for event in self._team_stop_check(action, message):
|
||||
yield event
|
||||
elif GraphBuildType.HANDOFF.value == self.swarm.build_type:
|
||||
async for event in self._handoff_stop_check(action, message):
|
||||
yield event
|
||||
else:
|
||||
async for event in self._workflow_stop_check(action, message):
|
||||
yield event
|
||||
|
||||
async def _workflow_stop_check(self, action: ActionModel, message: Message) -> AsyncGenerator[Message, None]:
|
||||
# Equivalent to scheduling
|
||||
session_id = message.session_id
|
||||
agent_name = action.agent_name
|
||||
agent = self.swarm.agents.get(agent_name)
|
||||
if not agent:
|
||||
yield Message(
|
||||
category=Constants.TASK,
|
||||
payload=TaskItem(
|
||||
msg=f"Can not find {action.agent_name} agent in ordered_agents: {self.swarm.ordered_agents}.",
|
||||
data=action,
|
||||
stop=True),
|
||||
sender=self.name(),
|
||||
session_id=session_id,
|
||||
topic=TopicType.ERROR,
|
||||
headers=message.headers
|
||||
)
|
||||
return
|
||||
|
||||
receiver = None
|
||||
# loop agent type
|
||||
if isinstance(agent, LoopableAgent):
|
||||
agent.cur_run_times += 1
|
||||
if not agent.finished:
|
||||
receiver = agent.goto
|
||||
|
||||
if receiver:
|
||||
yield Message(
|
||||
category=Constants.AGENT,
|
||||
payload=Observation(content=action.policy_info),
|
||||
sender=agent.id(),
|
||||
session_id=session_id,
|
||||
receiver=receiver,
|
||||
headers=message.headers
|
||||
)
|
||||
else:
|
||||
agent_graph: AgentGraph = self.swarm.agent_graph
|
||||
# next
|
||||
successor = agent_graph.successor.get(agent_name)
|
||||
if not successor:
|
||||
yield Message(
|
||||
category=Constants.TASK,
|
||||
payload=action.policy_info,
|
||||
sender=agent.id(),
|
||||
session_id=session_id,
|
||||
topic=TopicType.FINISHED,
|
||||
headers=message.headers
|
||||
)
|
||||
return
|
||||
|
||||
for k, _ in successor.items():
|
||||
predecessor = agent_graph.predecessor.get(k)
|
||||
if not predecessor:
|
||||
raise AWorldRuntimeException(f"{k} has no predecessor {agent_name}, may changed during iteration.")
|
||||
|
||||
all_input = {}
|
||||
pre_finished = True
|
||||
for pre_k, _ in predecessor.items():
|
||||
if pre_k == agent_name:
|
||||
all_input[agent_name] = action.policy_info
|
||||
continue
|
||||
# check all predecessor agent finished
|
||||
run_node: RunNode = self.runner.state_manager.query_by_task(
|
||||
task_id=message.context.get_task().id,
|
||||
busi_typ=RunNodeBusiType.AGENT,
|
||||
busi_id=pre_k
|
||||
)
|
||||
if run_node:
|
||||
run_node = run_node[0]
|
||||
else:
|
||||
raise AWorldRuntimeException(f"{pre_k} can't find in task: {message.context.get_task().id}.")
|
||||
if run_node.status == RunNodeStatus.RUNNING or run_node.status == RunNodeStatus.INIT:
|
||||
# mean not finished
|
||||
pre_finished = False
|
||||
logger.info(f"{pre_k} not finished, will wait it.")
|
||||
else:
|
||||
logger.info(f"{pre_k} finished, result is: {run_node.results}")
|
||||
payload = run_node.results[-1].result.payload[0]
|
||||
all_input[pre_k] = payload.policy_info
|
||||
|
||||
if pre_finished:
|
||||
yield Message(
|
||||
category=Constants.AGENT,
|
||||
payload=Observation(content=all_input if len(all_input) > 1 else all_input.get(agent_name)),
|
||||
sender=agent.id(),
|
||||
session_id=session_id,
|
||||
receiver=k,
|
||||
headers=message.headers
|
||||
)
|
||||
|
||||
async def _team_stop_check(self, action: ActionModel, message: Message) -> AsyncGenerator[Message, None]:
|
||||
caller = message.caller
|
||||
session_id = message.session_id
|
||||
agent = self.swarm.agents.get(action.agent_name)
|
||||
if ((not caller or caller == self.swarm.communicate_agent.id())
|
||||
and (self.swarm.cur_step >= self.swarm.max_steps or self.swarm.finished or
|
||||
(agent.id() == self.swarm.agent_graph.root_agent.id() and agent.finished))):
|
||||
logger.info(
|
||||
f"FINISHED|_social_stop_check finished|{self.swarm.cur_step}|{self.swarm.max_steps}|{self.swarm.finished}")
|
||||
yield Message(
|
||||
category=Constants.TASK,
|
||||
payload=action.policy_info,
|
||||
sender=agent.id(),
|
||||
session_id=session_id,
|
||||
topic=TopicType.FINISHED,
|
||||
headers={"context": message.context}
|
||||
)
|
||||
agent = self.swarm.agents.get(action.agent_name)
|
||||
caller = self.swarm.agent_graph.root_agent.id() or message.caller
|
||||
if agent.id() != self.swarm.agent_graph.root_agent.id():
|
||||
logger.info(f"_stop_check Team|{agent.id()} --> {caller}")
|
||||
yield Message(
|
||||
category=Constants.AGENT,
|
||||
payload=Observation(content=action.policy_info),
|
||||
sender=agent.id(),
|
||||
session_id=message.session_id,
|
||||
receiver=caller,
|
||||
headers=message.headers
|
||||
)
|
||||
|
||||
async def _handoff_stop_check(self, action: ActionModel, message: Message) -> AsyncGenerator[Message, None]:
|
||||
headers = {"context": message.context}
|
||||
agent = self.swarm.agents.get(action.agent_name)
|
||||
caller = message.caller
|
||||
session_id = message.session_id
|
||||
if endless_detect(self.agent_calls,
|
||||
endless_threshold=self.endless_threshold,
|
||||
root_agent_name=self.swarm.communicate_agent.id()):
|
||||
logger.info(
|
||||
f"FINISHED|_social_stop_check endless_detect|{self.agent_calls}|{self.endless_threshold}|{self.swarm.communicate_agent.id()}")
|
||||
yield Message(
|
||||
category=Constants.TASK,
|
||||
payload=action.policy_info,
|
||||
sender=agent.id(),
|
||||
session_id=session_id,
|
||||
topic=TopicType.FINISHED,
|
||||
headers=headers
|
||||
)
|
||||
return
|
||||
|
||||
if not caller or caller == self.swarm.communicate_agent.id():
|
||||
if self.swarm.cur_step >= self.swarm.max_steps or self.swarm.finished:
|
||||
logger.info(
|
||||
f"FINISHED|_social_stop_check finished|{self.swarm.cur_step}|{self.swarm.max_steps}|{self.swarm.finished}")
|
||||
yield Message(
|
||||
category=Constants.TASK,
|
||||
payload=action.policy_info,
|
||||
sender=agent.id(),
|
||||
session_id=session_id,
|
||||
topic=TopicType.FINISHED,
|
||||
headers=headers
|
||||
)
|
||||
else:
|
||||
self.swarm.cur_step += 1
|
||||
logger.info(f"_social_stop_check execute loop {self.swarm.cur_step}.")
|
||||
yield Message(
|
||||
category=Constants.AGENT,
|
||||
payload=Observation(content=action.policy_info),
|
||||
sender=agent.id(),
|
||||
session_id=session_id,
|
||||
receiver=self.swarm.communicate_agent.id(),
|
||||
headers=message.headers
|
||||
)
|
||||
else:
|
||||
idx = 0
|
||||
for idx, name in enumerate(self.agent_calls[::-1]):
|
||||
if name == agent.id():
|
||||
break
|
||||
idx = len(self.agent_calls) - idx - 1
|
||||
if idx:
|
||||
caller = self.agent_calls[idx - 1]
|
||||
|
||||
yield Message(
|
||||
category=Constants.AGENT,
|
||||
payload=Observation(content=action.policy_info),
|
||||
sender=agent.id(),
|
||||
session_id=session_id,
|
||||
receiver=caller,
|
||||
headers=message.headers
|
||||
)
|
||||
|
||||
def is_group_finish(self, event: Message) -> bool:
|
||||
"""Determine if an event triggers group completion"""
|
||||
if not isinstance(event, Message) or not event.group_id:
|
||||
return False
|
||||
|
||||
agent_id = event.sender
|
||||
if not agent_id:
|
||||
return False
|
||||
|
||||
agent = self.swarm.agents.get(agent_id)
|
||||
if not agent:
|
||||
return False
|
||||
|
||||
return agent._finished and agent.id() == event.headers.get('root_agent_id', '')
|
||||
|
||||
async def post_handle(self, input: Message, output: Message) -> Message:
|
||||
new_context = output.context.deep_copy()
|
||||
new_context._task = output.context.get_task()
|
||||
output.context = new_context
|
||||
if self.is_group_finish(output):
|
||||
from aworld.runners.state_manager import RuntimeStateManager
|
||||
state_mng = RuntimeStateManager.instance()
|
||||
await state_mng.finish_sub_group(output.group_id, output.headers.get('root_message_id'),
|
||||
[output])
|
||||
return None
|
||||
return output
|
||||
@@ -0,0 +1,102 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import abc
|
||||
import time
|
||||
|
||||
from typing import TypeVar, Generic, AsyncGenerator
|
||||
|
||||
from aworld.events.util import send_message
|
||||
|
||||
from aworld.core.common import TaskItem
|
||||
|
||||
from aworld.core.event.base import Message, Constants, TopicType, CancelMessage
|
||||
from aworld.logs.util import logger
|
||||
|
||||
IN = TypeVar('IN')
|
||||
OUT = TypeVar('OUT')
|
||||
|
||||
|
||||
class Handler(Generic[IN, OUT]):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@abc.abstractmethod
|
||||
async def handle(self, data: IN) -> AsyncGenerator[OUT, None]:
|
||||
"""Process the data as the expected result.
|
||||
|
||||
Args:
|
||||
data: Data generated while running the task.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def name(cls):
|
||||
"""Handler name."""
|
||||
return cls.__name__
|
||||
|
||||
|
||||
class DefaultHandler(Handler[Message, AsyncGenerator[Message, None]]):
|
||||
"""Default handler."""
|
||||
|
||||
def __init__(self, runner: 'TaskEventRunner'):
|
||||
self.runner = runner
|
||||
self.hooks = None
|
||||
|
||||
def get_registered_name(self):
|
||||
"""Get the registered name of the handler.
|
||||
|
||||
If the class has a REGISTERED_NAME attribute, return the value of the attribute;
|
||||
otherwise return None.
|
||||
"""
|
||||
return getattr(self.__class__, "REGISTERED_NAME", None)
|
||||
|
||||
def is_valid_message(self, message: Message):
|
||||
"""Validate if the message is valid for this handler.
|
||||
|
||||
If the class has a REGISTERED_NAME attribute, check if the message's category matches the registered name;
|
||||
otherwise return True.
|
||||
"""
|
||||
registered_name = self.get_registered_name()
|
||||
if registered_name is not None:
|
||||
return message.category == registered_name
|
||||
return True
|
||||
|
||||
async def handle(self, message: Message) -> AsyncGenerator[Message, None]:
|
||||
if not self.is_valid_message(message):
|
||||
return
|
||||
timeout = message.context.get_task().timeout
|
||||
time_cost = time.time() - self.runner.start_time
|
||||
if message.topic != TopicType.CANCEL and timeout > 0 and time_cost > timeout:
|
||||
logger.warn(
|
||||
f"[{self.name()}] {message.context.get_task().id} task timeout after {time_cost} seconds.")
|
||||
yield CancelMessage(
|
||||
payload=TaskItem(msg="task timeout.", data=message, stop=True),
|
||||
sender=self.name(),
|
||||
session_id=self.runner.context.session_id,
|
||||
headers={"context": message.context}
|
||||
)
|
||||
return
|
||||
async for event in self._do_handle(message):
|
||||
msg = await self.post_handle(input=message, output=event)
|
||||
if msg:
|
||||
yield msg
|
||||
|
||||
async def _do_handle(self, message: Message) -> AsyncGenerator[Message, None]:
|
||||
yield message
|
||||
|
||||
async def post_handle(self, input:Message, output: Message) -> Message:
|
||||
"""Post handle the message.
|
||||
Args:
|
||||
message: Message generated while running the task.
|
||||
"""
|
||||
return output
|
||||
|
||||
async def run_hooks(self, message: Message, hook_point: str) -> AsyncGenerator[Message, None]:
|
||||
if not self.hooks:
|
||||
return
|
||||
hooks = self.hooks.get(hook_point, [])
|
||||
for hook in hooks:
|
||||
try:
|
||||
msg = await hook.exec(message)
|
||||
if msg:
|
||||
yield msg
|
||||
except:
|
||||
logger.warning(f"{self.name()}|{hook.point()} {hook.name()} execute fail.")
|
||||
@@ -0,0 +1,413 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import abc
|
||||
import copy
|
||||
import json
|
||||
from typing import AsyncGenerator, List, Dict, Any, Tuple
|
||||
|
||||
from aworld.agents.llm_agent import Agent
|
||||
from aworld.core.agent.base import is_agent
|
||||
from aworld.core.common import ActionModel, TaskItem, Observation, ActionResult
|
||||
from aworld.core.context.base import Context
|
||||
from aworld.core.event.base import Message, Constants, TopicType, GroupMessage
|
||||
from aworld.logs.util import logger
|
||||
from aworld.output.base import StepOutput
|
||||
from aworld.runners import HandlerFactory
|
||||
from aworld.runners.handler.base import DefaultHandler
|
||||
from aworld.runners.handler.tool import DefaultToolHandler
|
||||
from aworld.runners.state_manager import RuntimeStateManager, RunNodeStatus
|
||||
from aworld.utils.serialized_util import to_serializable
|
||||
from aworld.utils.run_util import exec_agent
|
||||
|
||||
|
||||
class GroupHandler(DefaultHandler):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def __init__(self, runner: 'TaskEventRunner'):
|
||||
super().__init__(runner)
|
||||
self.runner = runner
|
||||
self.swarm = runner.swarm
|
||||
self.endless_threshold = runner.endless_threshold
|
||||
self.task_id = runner.task.id
|
||||
|
||||
@classmethod
|
||||
def name(cls):
|
||||
return "_group_handler"
|
||||
|
||||
|
||||
@HandlerFactory.register(name=f'__{Constants.GROUP}__')
|
||||
class DefaultGroupHandler(GroupHandler):
|
||||
def is_valid_message(self, message: Message):
|
||||
if message.category != Constants.GROUP:
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _do_handle(self, message: GroupMessage) -> AsyncGenerator[Message, None]:
|
||||
if not self.is_valid_message(message):
|
||||
return
|
||||
|
||||
self.context = message.context
|
||||
group_id = message.group_id
|
||||
headers = {'context': self.context}
|
||||
state_manager = RuntimeStateManager.instance()
|
||||
if message.topic == TopicType.GROUP_ACTIONS:
|
||||
# message.payload is List[ActionModel]
|
||||
node_ids = []
|
||||
action_messages = []
|
||||
agents = []
|
||||
tools = []
|
||||
agent_actions_map = {}
|
||||
for action in message.payload:
|
||||
if not isinstance(action, ActionModel):
|
||||
# error message, p2p
|
||||
async for event in self._send_failed_message(message, message.payload, message):
|
||||
yield event
|
||||
return
|
||||
if is_agent(action):
|
||||
agents.append(action)
|
||||
agent_name = action.tool_name
|
||||
if agent_name not in agent_actions_map:
|
||||
agent_actions_map[agent_name] = []
|
||||
agent_actions_map[agent_name].append(action)
|
||||
else:
|
||||
tools.append(action)
|
||||
|
||||
# Process each agent's actions
|
||||
agent_messages = {}
|
||||
for agent_name, actions in agent_actions_map.items():
|
||||
# Get original agent
|
||||
original_agent = self.swarm.agents.get(agent_name)
|
||||
if not original_agent:
|
||||
error_msg = Message(
|
||||
category=Constants.TASK,
|
||||
payload=TaskItem(msg=f"Can not find {agent_name} agent in swarm.",
|
||||
data=actions,
|
||||
stop=True),
|
||||
sender=self.name(),
|
||||
session_id=message.session_id,
|
||||
topic=TopicType.ERROR,
|
||||
headers={'context': self.context}
|
||||
)
|
||||
yield error_msg
|
||||
return
|
||||
|
||||
# Create agent copies and execute for each action
|
||||
for action in actions:
|
||||
msg = await self._build_agent_message(action, message)
|
||||
if msg.category != Constants.AGENT:
|
||||
yield msg
|
||||
return
|
||||
self._update_headers(msg, message)
|
||||
agent_copy = self.copy_agent(original_agent)
|
||||
con = action.policy_info
|
||||
if action.params and 'content' in action.params:
|
||||
con = action.params['content']
|
||||
|
||||
if agent_name not in agent_messages:
|
||||
agent_messages[agent_name] = []
|
||||
agent_messages[agent_name].append((con, agent_copy, msg))
|
||||
agent_node_ids, agent_tasks = await self._parallel_exec_agents_actions(agent_messages, message)
|
||||
node_ids.extend(agent_node_ids)
|
||||
|
||||
if tools:
|
||||
tool_mapping = {}
|
||||
for action in tools:
|
||||
tool_name = action.tool_name
|
||||
if tool_name not in tool_mapping:
|
||||
tool_mapping[tool_name] = []
|
||||
tool_mapping[tool_name].append(action)
|
||||
for tool_name, actions in tool_mapping.items():
|
||||
msg = await self._build_tool_message(actions, message)
|
||||
self._update_headers(msg, message)
|
||||
action_messages.append(msg)
|
||||
node_ids.append(msg.id)
|
||||
|
||||
# create group
|
||||
group_meta_data = message.headers.copy()
|
||||
group_meta_data["context"] = message.context.deep_copy()
|
||||
group_meta_data["context"].set_task(message.context.get_task())
|
||||
await state_manager.create_group(group_id, message.session_id, node_ids,
|
||||
message.headers.get('parent_group_id'),
|
||||
group_meta_data)
|
||||
for _, acts in agent_messages.items():
|
||||
for act in acts:
|
||||
self.runner.state_manager.start_message_node(act[2])
|
||||
for msg in action_messages:
|
||||
yield msg
|
||||
await self.process_agent_tasks(agent_tasks, message)
|
||||
|
||||
elif message.topic == TopicType.GROUP_RESULTS:
|
||||
# merge group results
|
||||
action_results = []
|
||||
group_results = message.payload
|
||||
group_sender = None
|
||||
group_sender_node_id = None
|
||||
agent_context = self.context.deep_copy()
|
||||
agent_context._task = self.context.get_task()
|
||||
receiver_results = {}
|
||||
|
||||
for node_id, handle_res_list in group_results.items():
|
||||
if not handle_res_list:
|
||||
logger.warn(f"{self.name()} get group result with empty handle_res.")
|
||||
return
|
||||
node = state_manager._find_node(node_id)
|
||||
tool_call_id = node.metadata.get('root_tool_call_id')
|
||||
is_tool = not tool_call_id and not node.metadata.get('root_agent_id')
|
||||
if not group_sender:
|
||||
group_sender = node.metadata.get('group_sender')
|
||||
if not group_sender_node_id:
|
||||
group_sender_node_id = node.metadata.get('group_sender_node_id')
|
||||
node_results = []
|
||||
for handle_res in handle_res_list:
|
||||
res_msg = handle_res.result
|
||||
res_status = handle_res.status
|
||||
if res_status == RunNodeStatus.FAILED or not res_msg:
|
||||
logger.warn(f"{self.name()} get group result with failed handle_res: {handle_res}.")
|
||||
return
|
||||
receiver = res_msg.receiver
|
||||
if not receiver:
|
||||
logger.warn(f"{self.name()} get group result with empty receiver: {res_msg}.")
|
||||
continue
|
||||
|
||||
if receiver != group_sender:
|
||||
if receiver not in receiver_results:
|
||||
receiver_results[receiver] = []
|
||||
receiver_results[receiver].append(res_msg)
|
||||
else:
|
||||
if is_tool and isinstance(res_msg.payload, Observation):
|
||||
action_results.extend(res_msg.payload.action_result)
|
||||
else:
|
||||
node_results.append(res_msg.payload)
|
||||
self._merge_context(agent_context, res_msg.context)
|
||||
|
||||
if node_results and tool_call_id:
|
||||
act_res = ActionResult(
|
||||
content=json.dumps(to_serializable(node_results), ensure_ascii=False),
|
||||
tool_call_id=tool_call_id
|
||||
)
|
||||
action_results.append(act_res)
|
||||
if action_results:
|
||||
group_res_msg = Message(
|
||||
category=Constants.AGENT,
|
||||
payload=Observation(content="", action_result=action_results),
|
||||
caller=message.caller,
|
||||
sender=self.name(),
|
||||
session_id=message.session_id,
|
||||
receiver=group_sender,
|
||||
headers={'context': agent_context}
|
||||
)
|
||||
receiver_results[group_sender] = [group_res_msg]
|
||||
|
||||
for receiver, res_msgs in receiver_results.items():
|
||||
result_message = self._merge_result_messages(res_msgs, message, group_sender_node_id)
|
||||
group_headers = {}
|
||||
group_sender_node = state_manager._find_node(group_sender_node_id)
|
||||
if group_sender_node:
|
||||
group_headers.update(group_sender_node.metadata.copy())
|
||||
group_headers['level'] = headers.get('level', 0) + 1
|
||||
group_headers['context'] = result_message.context or self.context
|
||||
result_message.headers = group_headers
|
||||
yield result_message
|
||||
|
||||
def copy_agent(self, agent: Agent):
|
||||
"""Create a copy of the agent
|
||||
|
||||
Args:
|
||||
agent: Original agent object
|
||||
|
||||
Returns:
|
||||
Deep copy of the agent
|
||||
"""
|
||||
return agent
|
||||
|
||||
async def _parallel_exec_agents_actions(self, agent_messages: Dict[str, List[Tuple[str, Agent, Message]]],
|
||||
message: Message):
|
||||
"""Execute multiple agent actions in parallel
|
||||
|
||||
Args:
|
||||
agent_messages: Messages for agent actions
|
||||
"""
|
||||
tasks = {}
|
||||
messages_ids = []
|
||||
for agent_name, acts in agent_messages.items():
|
||||
for act in acts:
|
||||
agent_message = act[2]
|
||||
messages_ids.append(agent_message.id)
|
||||
tasks[agent_message.id] = exec_agent(act[0], act[1], self.context, sub_task=True, outputs=self.context.outputs)
|
||||
|
||||
return messages_ids, tasks
|
||||
|
||||
async def process_agent_tasks(self, agent_tasks, input_message):
|
||||
"""Process agent async tasks
|
||||
|
||||
Args:
|
||||
agent_tasks: Agent async tasks
|
||||
"""
|
||||
root_agent_set = set()
|
||||
for node_id, task in agent_tasks.items():
|
||||
res = await task
|
||||
logger.info(f"{node_id} finished task: {res}")
|
||||
state_manager = self.runner.state_manager
|
||||
node = state_manager._find_node(node_id)
|
||||
if not node:
|
||||
logger.warn(f"{self.name()} get group result with empty node.")
|
||||
return
|
||||
root_agent_id = node.metadata.get('root_agent_id')
|
||||
root_agent_set.add(root_agent_id)
|
||||
self.context.merge_sub_context(res.context)
|
||||
msg = Message(
|
||||
category=Constants.AGENT,
|
||||
payload=[ActionModel(policy_info=res.answer, agent_name=root_agent_id)],
|
||||
sender=root_agent_id,
|
||||
session_id=node.session_id,
|
||||
headers={'context': self.context,
|
||||
'root_agent_id': root_agent_id,
|
||||
'root_tool_call_id': node.metadata.get('root_tool_call_id')}
|
||||
)
|
||||
finish_group_messages = []
|
||||
async for event in self.runner._inner_handler_process(
|
||||
results=[msg],
|
||||
handlers=self.runner.handlers
|
||||
):
|
||||
# Only AGENT and TASK messages
|
||||
if isinstance(event, Message) and (
|
||||
event.category == Constants.AGENT or event.category == Constants.TASK):
|
||||
finish_group_messages.append(event)
|
||||
print(f"======== event context: {event.context},.context.task: {event.context.get_task()}")
|
||||
await state_manager.finish_sub_group(node.metadata.get('group_id'), node_id, finish_group_messages)
|
||||
for agent_id in root_agent_set:
|
||||
agent = self.swarm.agents.get(agent_id)
|
||||
if agent:
|
||||
agent._finished = True
|
||||
|
||||
def _merge_result_messages(self, res_msgs: List[Message], input_message: Message, group_sender_node_id: str):
|
||||
"""Merge multiple result messages
|
||||
|
||||
Args:
|
||||
res_msgs: Result messages
|
||||
"""
|
||||
if len(res_msgs) == 1:
|
||||
return res_msgs[0]
|
||||
input_list = []
|
||||
new_context = input_message.context.deep_copy()
|
||||
new_context._task = self.context.get_task()
|
||||
for message in res_msgs:
|
||||
map = {}
|
||||
map[message.sender] = message.payload
|
||||
input_list.append(map)
|
||||
new_context.merge_context(message.context)
|
||||
return Message(
|
||||
category=Constants.AGENT,
|
||||
payload=Observation(content=input_list),
|
||||
sender=self.name(),
|
||||
receiver=res_msgs[0].receiver,
|
||||
session_id=res_msgs[0].session_id,
|
||||
headers={
|
||||
'context': new_context
|
||||
}
|
||||
)
|
||||
|
||||
async def _build_agent_message(self, action: ActionModel, message: Message) -> Message:
|
||||
session_id = message.session_id
|
||||
headers = {
|
||||
"context": message.context,
|
||||
"root_tool_call_id": action.tool_call_id
|
||||
}
|
||||
from_agent = self.swarm.agents.get(action.agent_name)
|
||||
tool_name = action.tool_name
|
||||
|
||||
if not tool_name:
|
||||
logger.warn(f"{self.name()} get agent action with empty tool_name.")
|
||||
return Message(
|
||||
category=Constants.TASK,
|
||||
payload=TaskItem(msg=f"Empty tool_name in group_action: {action}.",
|
||||
data=action,
|
||||
stop=True),
|
||||
sender=self.name(),
|
||||
session_id=session_id,
|
||||
topic=TopicType.ERROR,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
cur_agent = self.swarm.agents.get(tool_name)
|
||||
if not cur_agent:
|
||||
return Message(
|
||||
category=Constants.TASK,
|
||||
payload=TaskItem(msg=f"Can not find {tool_name} agent in swarm.",
|
||||
data=action,
|
||||
stop=True),
|
||||
sender=self.name(),
|
||||
session_id=session_id,
|
||||
topic=TopicType.ERROR,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
cur_agent._finished = False
|
||||
con = action.policy_info
|
||||
if action.params and 'content' in action.params:
|
||||
con = action.params['content']
|
||||
observation = Observation(content=con, observer=from_agent.id(), from_agent_name=from_agent.id())
|
||||
|
||||
return Message(
|
||||
category=Constants.AGENT,
|
||||
payload=observation,
|
||||
caller=message.caller,
|
||||
sender=action.agent_name,
|
||||
session_id=session_id,
|
||||
receiver=cur_agent.id(),
|
||||
headers=headers
|
||||
)
|
||||
|
||||
async def _build_tool_message(self, actions: List[ActionModel], message: Message):
|
||||
session_id = message.session_id
|
||||
headers = {"context": message.context.deep_copy()}
|
||||
return Message(
|
||||
category=Constants.TOOL,
|
||||
payload=actions,
|
||||
sender=self.name(),
|
||||
session_id=session_id,
|
||||
receiver=DefaultToolHandler.name(),
|
||||
headers=headers
|
||||
)
|
||||
|
||||
async def _send_failed_message(self, message, data, result_msg):
|
||||
yield Message(
|
||||
category=Constants.OUTPUT,
|
||||
payload=StepOutput.build_failed_output(name=f"{message.caller or self.name()}",
|
||||
step_num=0,
|
||||
data=result_msg,
|
||||
task_id=self.task_id),
|
||||
sender=self.name(),
|
||||
session_id=self.context.session_id,
|
||||
headers=message.headers
|
||||
)
|
||||
yield Message(
|
||||
category=Constants.TASK,
|
||||
payload=TaskItem(msg=result_msg, data=data, stop=True),
|
||||
sender=self.name(),
|
||||
session_id=self.context.session_id,
|
||||
topic=TopicType.ERROR,
|
||||
headers=message.headers
|
||||
)
|
||||
|
||||
def _update_headers(self, message: Message, parent_message: Message):
|
||||
headers = message.headers.copy()
|
||||
context = message.context.deep_copy()
|
||||
context.set_task(self.context.get_task())
|
||||
headers['context'] = context
|
||||
headers['group_id'] = parent_message.group_id
|
||||
headers['root_message_id'] = message.id
|
||||
headers['root_agent_id'] = message.receiver if message.category == Constants.AGENT else ''
|
||||
headers['level'] = 0
|
||||
headers['group_sender'] = parent_message.sender
|
||||
headers['group_sender_node_id'] = parent_message.id
|
||||
headers['parent_group_id'] = parent_message.headers.get('parent_group_id')
|
||||
message.headers = headers
|
||||
|
||||
def _merge_context(self, context: Context, new_context: Context):
|
||||
if not new_context:
|
||||
return
|
||||
if not context:
|
||||
context = new_context
|
||||
return
|
||||
context.merge_context(new_context)
|
||||
@@ -0,0 +1,95 @@
|
||||
# aworld/runners/handler/output.py
|
||||
import json
|
||||
from typing import AsyncGenerator
|
||||
from aworld.core.task import TaskResponse
|
||||
from aworld.models.model_response import ModelResponse
|
||||
from aworld.runners import HandlerFactory
|
||||
from aworld.runners.handler.base import DefaultHandler
|
||||
from aworld.output.base import StepOutput, MessageOutput, Output
|
||||
from aworld.core.common import TaskItem
|
||||
from aworld.core.event.base import Message, Constants, TopicType
|
||||
from aworld.logs.util import logger
|
||||
from aworld.runners.hook.hook_factory import HookFactory
|
||||
from aworld.runners.hook.hooks import HookPoint
|
||||
|
||||
|
||||
@HandlerFactory.register(name=f'__{Constants.OUTPUT}__')
|
||||
class DefaultOutputHandler(DefaultHandler):
|
||||
def __init__(self, runner):
|
||||
super().__init__(runner)
|
||||
self.runner = runner
|
||||
self.hooks = {}
|
||||
if runner.task.hooks:
|
||||
for k, vals in runner.task.hooks.items():
|
||||
self.hooks[k] = []
|
||||
for v in vals:
|
||||
cls = HookFactory.get_class(v)
|
||||
if cls:
|
||||
self.hooks[k].append(cls)
|
||||
|
||||
def is_valid_message(self, message: Message):
|
||||
if message.category != Constants.OUTPUT:
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _do_handle(self, message):
|
||||
if not self.is_valid_message(message):
|
||||
return
|
||||
# 1. get outputs
|
||||
outputs = self.runner.task.outputs
|
||||
if not outputs:
|
||||
yield Message(
|
||||
category=Constants.TASK,
|
||||
payload=TaskItem(msg="Cannot get outputs.",
|
||||
data=message, stop=True),
|
||||
sender=self.name(),
|
||||
session_id=self.runner.context.session_id,
|
||||
topic=TopicType.ERROR,
|
||||
headers={"context": message.context}
|
||||
)
|
||||
return
|
||||
|
||||
# 2. Call OUTPUT_PROCESS hooks to process data in the message
|
||||
async for event in self.run_hooks(message, HookPoint.OUTPUT_PROCESS):
|
||||
# If hook returns a processed message, use the processed message
|
||||
if event and isinstance(event, Message) and event.payload:
|
||||
message.payload = event.payload
|
||||
|
||||
# 3. build Output
|
||||
payload = message.payload
|
||||
mark_complete = False
|
||||
output = None
|
||||
try:
|
||||
if isinstance(payload, Output):
|
||||
output = payload
|
||||
output.task_id = self.runner.task.id
|
||||
elif isinstance(payload, TaskResponse):
|
||||
logger.info(
|
||||
f"FINISHED|output get task_response with usage: {json.dumps(payload.usage)}")
|
||||
if message.topic == TopicType.FINISHED or message.topic == TopicType.ERROR:
|
||||
mark_complete = True
|
||||
elif isinstance(payload, ModelResponse) or isinstance(payload, AsyncGenerator):
|
||||
output = MessageOutput(source=payload, task_id=self.runner.task.id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to parse output: {e}")
|
||||
yield Message(
|
||||
category=Constants.TASK,
|
||||
payload=TaskItem(msg="Failed to parse output.",
|
||||
data=payload, stop=True),
|
||||
sender=self.name(),
|
||||
session_id=self.runner.context.session_id,
|
||||
topic=TopicType.ERROR,
|
||||
headers={"context": message.context}
|
||||
)
|
||||
finally:
|
||||
if output:
|
||||
if not output.metadata:
|
||||
output.metadata = {}
|
||||
output.metadata['sender'] = message.sender
|
||||
output.metadata['receiver'] = message.receiver
|
||||
await outputs.add_output(output)
|
||||
if mark_complete:
|
||||
logger.info(f"FINISHED|output mark_completed|{self.runner.task.id}")
|
||||
await outputs.mark_completed()
|
||||
|
||||
return
|
||||
@@ -0,0 +1,138 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import abc
|
||||
import time
|
||||
|
||||
from typing import AsyncGenerator, TYPE_CHECKING
|
||||
|
||||
from aworld.core.common import TaskItem
|
||||
from aworld.core.tool.base import Tool, AsyncTool
|
||||
|
||||
from aworld.core.event.base import Message, Constants, TopicType
|
||||
from aworld.core.task import TaskResponse
|
||||
from aworld.logs.util import logger
|
||||
from aworld.output import Output
|
||||
from aworld.runners import HandlerFactory
|
||||
from aworld.runners.handler.base import DefaultHandler
|
||||
from aworld.runners.hook.hook_factory import HookFactory
|
||||
from aworld.runners.hook.hooks import HookPoint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aworld.runners.event_runner import TaskEventRunner
|
||||
|
||||
|
||||
class TaskHandler(DefaultHandler):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def __init__(self, runner: 'TaskEventRunner'):
|
||||
super().__init__(runner)
|
||||
self.runner = runner
|
||||
self.retry_count = runner.task.max_retry_count
|
||||
self.hooks = {}
|
||||
if runner.task.hooks:
|
||||
for k, vals in runner.task.hooks.items():
|
||||
self.hooks[k] = []
|
||||
for v in vals:
|
||||
cls = HookFactory.get_class(v)
|
||||
if cls:
|
||||
self.hooks[k].append(cls)
|
||||
|
||||
@classmethod
|
||||
def name(cls):
|
||||
return "_task_handler"
|
||||
|
||||
|
||||
@HandlerFactory.register(name=f'__{Constants.TASK}__')
|
||||
class DefaultTaskHandler(TaskHandler):
|
||||
def is_valid_message(self, message: Message):
|
||||
if message.category != Constants.TASK:
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _do_handle(self, message: Message) -> AsyncGenerator[Message, None]:
|
||||
if not self.is_valid_message(message):
|
||||
return
|
||||
|
||||
logger.debug(f"task handler receive message: {message}")
|
||||
|
||||
headers = {"context": message.context}
|
||||
topic = message.topic
|
||||
task_item: TaskItem = message.payload
|
||||
if topic == TopicType.SUBSCRIBE_TOOL:
|
||||
new_tools = message.payload.data
|
||||
for name, tool in new_tools.items():
|
||||
if isinstance(tool, Tool) or isinstance(tool, AsyncTool):
|
||||
await self.runner.event_mng.register(Constants.TOOL, name, tool.step)
|
||||
logger.info(f"dynamic register {name} tool.")
|
||||
else:
|
||||
logger.warning(f"Unknown tool instance: {tool}")
|
||||
return
|
||||
elif topic == TopicType.SUBSCRIBE_AGENT:
|
||||
return
|
||||
elif topic == TopicType.ERROR:
|
||||
async for event in self.run_hooks(message, HookPoint.ERROR):
|
||||
yield event
|
||||
|
||||
logger.warning(f"task {self.runner.task.id} stop, cause: {task_item.msg}")
|
||||
self.runner._task_response = TaskResponse(msg=task_item.msg,
|
||||
answer='',
|
||||
context=message.context,
|
||||
success=False,
|
||||
id=self.runner.task.id,
|
||||
time_cost=(time.time() - self.runner.start_time),
|
||||
usage=self.runner.context.token_usage)
|
||||
if not self.runner.task.is_sub_task:
|
||||
logger.info(f"FINISHED|DefaultTaskHandler|outputs|{self.runner.task.id} {self.runner.task.is_sub_task}")
|
||||
await self.runner.task.outputs.mark_completed()
|
||||
await self.runner.stop()
|
||||
elif topic == TopicType.FINISHED:
|
||||
async for event in self.run_hooks(message, HookPoint.FINISHED):
|
||||
yield event
|
||||
|
||||
self.runner._task_response = TaskResponse(answer=message.payload,
|
||||
success=True,
|
||||
context=message.context,
|
||||
id=self.runner.task.id,
|
||||
time_cost=(time.time() - self.runner.start_time),
|
||||
usage=self.runner.context.token_usage)
|
||||
|
||||
logger.info(f"FINISHED|task|{self.runner.task.id} finished. {self.runner.task.is_sub_task}")
|
||||
if not self.runner.task.is_sub_task:
|
||||
logger.info(f"FINISHED|DefaultTaskHandler|outputs|{self.runner.task.id} {self.runner.task.is_sub_task}")
|
||||
await self.runner.task.outputs.mark_completed()
|
||||
await self.runner.stop()
|
||||
elif topic == TopicType.START:
|
||||
async for event in self.run_hooks(message, HookPoint.START):
|
||||
yield event
|
||||
|
||||
logger.info(f"task start event: {message}, will send init message.")
|
||||
if message.payload:
|
||||
yield message
|
||||
else:
|
||||
yield self.runner.init_message
|
||||
elif topic == TopicType.OUTPUT:
|
||||
yield message
|
||||
elif topic == TopicType.HUMAN_CONFIRM:
|
||||
logger.warn("=============== Get human confirm, pause execution ===============")
|
||||
if self.runner.task.outputs and message.payload:
|
||||
await self.runner.task.outputs.add_output(Output(data=message.payload))
|
||||
self.runner._task_response = TaskResponse(answer=message.payload,
|
||||
success=True,
|
||||
context=message.context,
|
||||
id=self.runner.task.id,
|
||||
time_cost=(time.time() - self.runner.start_time),
|
||||
usage=self.runner.context.token_usage)
|
||||
await self.runner.stop()
|
||||
elif topic == TopicType.CANCEL:
|
||||
# Avoid waiting to receive events and send a mock event for quick cancel
|
||||
yield Message(session_id=self.runner.context.session_id, sender=self.name(), category='mock', headers={"context": message.context})
|
||||
# mark task response as cancelled
|
||||
self.runner._task_response = TaskResponse(answer='',
|
||||
success=False,
|
||||
context=message.context,
|
||||
id=self.runner.task.id,
|
||||
time_cost=(time.time() - self.runner.start_time),
|
||||
usage=self.runner.context.token_usage,
|
||||
msg=f'cancellation message received: {task_item.msg}',
|
||||
status='cancelled')
|
||||
await self.runner.stop()
|
||||
@@ -0,0 +1,123 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import abc
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from aworld.config import ConfigDict
|
||||
from aworld.core.agent.base import is_agent
|
||||
from aworld.core.common import ActionModel, TaskItem
|
||||
from aworld.core.event.base import Message, Constants, TopicType
|
||||
from aworld.core.tool.base import AsyncTool, Tool, ToolFactory
|
||||
from aworld.logs.util import logger
|
||||
from aworld.runners import HandlerFactory
|
||||
from aworld.runners.handler.base import DefaultHandler
|
||||
|
||||
|
||||
class ToolHandler(DefaultHandler):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def __init__(self, runner: 'TaskEventRunner'):
|
||||
super().__init__(runner)
|
||||
self.tools = runner.tools
|
||||
self.tools_conf = runner.tools_conf
|
||||
|
||||
@classmethod
|
||||
def name(cls):
|
||||
return "_tool_handler"
|
||||
|
||||
|
||||
@HandlerFactory.register(name=f'__{Constants.TOOL}__')
|
||||
class DefaultToolHandler(ToolHandler):
|
||||
def is_valid_message(self, message: Message):
|
||||
if message.category != Constants.TOOL:
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _do_handle(self, message: Message) -> AsyncGenerator[Message, None]:
|
||||
if not self.is_valid_message(message):
|
||||
return
|
||||
|
||||
headers = {"context": message.context}
|
||||
# data is List[ActionModel]
|
||||
data = message.payload
|
||||
if not data:
|
||||
# error message, p2p
|
||||
yield Message(
|
||||
category=Constants.TASK,
|
||||
payload=TaskItem(msg="no data to process.", data=data, stop=True),
|
||||
sender='agent_handler',
|
||||
session_id=message.session_id,
|
||||
topic=TopicType.ERROR,
|
||||
headers=headers
|
||||
)
|
||||
return
|
||||
|
||||
for action in data:
|
||||
if not isinstance(action, ActionModel):
|
||||
# error message, p2p
|
||||
yield Message(
|
||||
category=Constants.TASK,
|
||||
payload=TaskItem(msg="action not a ActionModel.", data=data, stop=True),
|
||||
sender=self.name(),
|
||||
session_id=message.session_id,
|
||||
topic=TopicType.ERROR,
|
||||
headers=headers
|
||||
)
|
||||
return
|
||||
|
||||
new_tools = dict()
|
||||
tool_mapping = dict()
|
||||
# Directly use or use tools after creation.
|
||||
for act in data:
|
||||
if is_agent(act):
|
||||
logger.warning(f"somethings wrong, {act} is an agent.")
|
||||
continue
|
||||
|
||||
if not self.tools or (self.tools and act.tool_name not in self.tools):
|
||||
# dynamic only use default config in module.
|
||||
conf = self.tools_conf.get(act.tool_name)
|
||||
if isinstance(conf, dict):
|
||||
conf = ConfigDict(conf)
|
||||
tool = ToolFactory(act.tool_name, conf=conf, asyn=conf.use_async if conf else False)
|
||||
tool.event_driven = True
|
||||
if isinstance(tool, Tool):
|
||||
tool.reset()
|
||||
elif isinstance(tool, AsyncTool):
|
||||
await tool.reset()
|
||||
tool_mapping[act.tool_name] = []
|
||||
self.tools[act.tool_name] = tool
|
||||
new_tools[act.tool_name] = tool
|
||||
if act.tool_name not in tool_mapping:
|
||||
tool_mapping[act.tool_name] = []
|
||||
tool_mapping[act.tool_name].append(act)
|
||||
|
||||
if new_tools:
|
||||
yield Message(
|
||||
category=Constants.TASK,
|
||||
payload=TaskItem(data=new_tools),
|
||||
sender=self.name(),
|
||||
session_id=message.session_id,
|
||||
topic=TopicType.SUBSCRIBE_TOOL,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
for tool_name, actions in tool_mapping.items():
|
||||
if not (isinstance(self.tools[tool_name], Tool) or isinstance(self.tools[tool_name], AsyncTool)):
|
||||
logger.warning(f"Unsupported tool type: {self.tools[tool_name]}")
|
||||
continue
|
||||
|
||||
# send to the tool
|
||||
yield Message(
|
||||
category=Constants.TOOL,
|
||||
payload=actions,
|
||||
sender=actions[0].agent_name if actions else '',
|
||||
session_id=message.session_id,
|
||||
receiver=tool_name,
|
||||
headers=message.headers
|
||||
)
|
||||
|
||||
async def post_handle(self, input:Message, output: Message) -> Message:
|
||||
new_context = output.context.deep_copy()
|
||||
new_context._task = output.context.get_task()
|
||||
output.context = new_context
|
||||
return output
|
||||
@@ -0,0 +1,2 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
@@ -0,0 +1,37 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import abc
|
||||
|
||||
from aworld.core.context.base import Context
|
||||
from aworld.core.event.base import Message
|
||||
from aworld.runners.hook.hook_factory import HookFactory
|
||||
from aworld.runners.hook.hooks import PostLLMCallHook, PreLLMCallHook
|
||||
from aworld.utils.common import convert_to_snake
|
||||
|
||||
|
||||
@HookFactory.register(name="PreLLMCallContextProcessHook",
|
||||
desc="PreLLMCallContextProcessHook")
|
||||
class PreLLMCallContextProcessHook(PreLLMCallHook):
|
||||
"""Process in the hook point of the pre_llm_call."""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def name(self):
|
||||
return convert_to_snake("PreLLMCallContextProcessHook")
|
||||
|
||||
async def exec(self, message: Message, context: Context = None) -> Message:
|
||||
# and do something
|
||||
pass
|
||||
|
||||
@HookFactory.register(name="PostLLMCallContextProcessHook",
|
||||
desc="PostLLMCallContextProcessHook")
|
||||
class PostLLMCallContextProcessHook(PostLLMCallHook):
|
||||
"""Process in the hook point of the post_llm_call."""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def name(self):
|
||||
return convert_to_snake("PostLLMCallContextProcessHook")
|
||||
|
||||
async def exec(self, message: Message, context: Context = None) -> Message:
|
||||
# get context
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import sys
|
||||
from typing import Dict, List
|
||||
|
||||
from aworld.core.factory import Factory
|
||||
from aworld.logs.util import logger
|
||||
from aworld.runners.hook.hooks import Hook, StartHook, HookPoint
|
||||
|
||||
|
||||
class HookManager(Factory):
|
||||
def __init__(self, type_name: str = None):
|
||||
super(HookManager, self).__init__(type_name)
|
||||
|
||||
def __call__(self, name: str, **kwargs):
|
||||
if name is None:
|
||||
raise ValueError("hook name is None")
|
||||
|
||||
try:
|
||||
if name in self._cls:
|
||||
act = self._cls[name](**kwargs)
|
||||
else:
|
||||
raise RuntimeError("The hook was not registered.\nPlease confirm the package has been imported.")
|
||||
except Exception:
|
||||
err = sys.exc_info()
|
||||
logger.warning(f"Failed to create hook with name {name}:\n{err[1]}")
|
||||
act = None
|
||||
return act
|
||||
|
||||
def hooks(self, name: str = None) -> Dict[str, List[Hook]]:
|
||||
vals = list(filter(lambda s: not s.startswith('__'), dir(HookPoint)))
|
||||
results = {val.lower(): [] for val in vals}
|
||||
|
||||
for k, v in self._cls.items():
|
||||
hook = v()
|
||||
if name and hook.point() != name:
|
||||
continue
|
||||
|
||||
results.get(hook.point(), []).append(hook)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
HookFactory = HookManager("hook_type")
|
||||
@@ -0,0 +1,85 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import abc
|
||||
|
||||
from aworld.core.context.base import Context
|
||||
from aworld.core.event.base import Message
|
||||
from aworld.models.model_response import ModelResponse
|
||||
|
||||
|
||||
class HookPoint:
|
||||
START = "start"
|
||||
FINISHED = "finished"
|
||||
ERROR = "error"
|
||||
PRE_LLM_CALL = "pre_llm_call"
|
||||
POST_LLM_CALL = "post_llm_call"
|
||||
OUTPUT_PROCESS = "output_process"
|
||||
|
||||
class Hook:
|
||||
"""Runner hook."""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@abc.abstractmethod
|
||||
def point(self):
|
||||
"""Hook point."""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def exec(self, message: Message, context: Context = None) -> Message:
|
||||
"""Execute hook function."""
|
||||
|
||||
|
||||
class StartHook(Hook):
|
||||
"""Process in the hook point of the start."""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def point(self):
|
||||
return HookPoint.START
|
||||
|
||||
|
||||
class FinishedHook(Hook):
|
||||
"""Process in the hook point of the finished."""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def point(self):
|
||||
return HookPoint.FINISHED
|
||||
|
||||
|
||||
class ErrorHook(Hook):
|
||||
"""Process in the hook point of the error."""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def point(self):
|
||||
return HookPoint.ERROR
|
||||
|
||||
class PreLLMCallHook(Hook):
|
||||
"""Process in the hook point of the pre_llm_call."""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def point(self):
|
||||
return HookPoint.PRE_LLM_CALL
|
||||
|
||||
class PostLLMCallHook(Hook):
|
||||
"""Process in the hook point of the post_llm_call."""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def point(self):
|
||||
return HookPoint.POST_LLM_CALL
|
||||
|
||||
class OutputProcessHook(Hook):
|
||||
"""Output process hook for processing output data for display."""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def point(self):
|
||||
return HookPoint.OUTPUT_PROCESS
|
||||
|
||||
def process_output_content(self, content: str) -> str:
|
||||
"""process output content
|
||||
|
||||
Args:
|
||||
content: original content
|
||||
|
||||
Returns:
|
||||
processed content
|
||||
"""
|
||||
return content
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import re
|
||||
import copy
|
||||
from typing import Dict, Any, AsyncGenerator
|
||||
|
||||
from aworld.core.context.base import Context
|
||||
from aworld.core.event.base import Message
|
||||
from aworld.logs.util import logger
|
||||
from aworld.models.model_response import ModelResponse
|
||||
from aworld.output.base import Output, MessageOutput
|
||||
from aworld.runners.hook.hook_factory import HookFactory
|
||||
from aworld.runners.hook.hooks import OutputProcessHook
|
||||
from aworld.utils.common import convert_to_snake
|
||||
|
||||
|
||||
@HookFactory.register(name="ModelResponseProcessHook",
|
||||
desc="Process ModelResponse type messages before sending to frontend display")
|
||||
class ModelResponseProcessHook(OutputProcessHook):
|
||||
"""Process ModelResponse type messages before sending to frontend display"""
|
||||
|
||||
def name(self):
|
||||
return convert_to_snake("ModelResponseProcessHook")
|
||||
|
||||
async def exec(self, message: Message, context: Context = None) -> Message:
|
||||
"""Process ModelResponse type messages
|
||||
|
||||
Args:
|
||||
message: Message object
|
||||
context: Context object
|
||||
|
||||
Returns:
|
||||
Processed message object
|
||||
"""
|
||||
# Get payload
|
||||
if not message or not message.payload:
|
||||
return message
|
||||
|
||||
payload = message.payload
|
||||
|
||||
# Process different types of payload
|
||||
if isinstance(payload, ModelResponse):
|
||||
# Directly process ModelResponse type
|
||||
processed_payload = self.process_model_response(payload)
|
||||
message.payload = processed_payload
|
||||
|
||||
# Record processing results
|
||||
self._log_processing_result(payload, processed_payload, context)
|
||||
|
||||
elif isinstance(payload, MessageOutput) and hasattr(payload, 'source'):
|
||||
# Process ModelResponse in MessageOutput
|
||||
source = payload.source
|
||||
if isinstance(source, ModelResponse):
|
||||
processed_source = self.process_model_response(source)
|
||||
payload.source = processed_source
|
||||
|
||||
# Record processing results
|
||||
self._log_processing_result(source, processed_source, context)
|
||||
return message
|
||||
|
||||
def process_model_response(self, model_response: ModelResponse) -> ModelResponse:
|
||||
"""Process ModelResponse
|
||||
|
||||
Args:
|
||||
model_response: ModelResponse object
|
||||
|
||||
Returns:
|
||||
Processed ModelResponse object
|
||||
"""
|
||||
if not model_response:
|
||||
return model_response
|
||||
|
||||
# Create a new ModelResponse object to avoid modifying the original
|
||||
processed_response = copy.deepcopy(model_response)
|
||||
content = self.process_output_content(processed_response.content)
|
||||
processed_response.content = content
|
||||
return processed_response
|
||||
|
||||
def _log_processing_result(self, original: ModelResponse, processed: ModelResponse, context: Context = None):
|
||||
"""Record processing results
|
||||
|
||||
Args:
|
||||
original: Original ModelResponse
|
||||
processed: Processed ModelResponse
|
||||
context: Context object
|
||||
"""
|
||||
# Record content length before and after processing for analysis
|
||||
original_length = len(original.content) if original and original.content else 0
|
||||
processed_length = len(processed.content) if processed and processed.content else 0
|
||||
|
||||
# Save processing results to context for later retrieval
|
||||
if context:
|
||||
if not hasattr(context, 'hook_results'):
|
||||
context.hook_results = {}
|
||||
if not hasattr(context.hook_results, 'output_process'):
|
||||
context.hook_results.output_process = {}
|
||||
|
||||
# Save processing results
|
||||
context.hook_results.output_process = {
|
||||
'hook_name': self.name(),
|
||||
'original_length': original_length,
|
||||
'processed_length': processed_length,
|
||||
'removed_content': original_length - processed_length,
|
||||
'processed_at': context.get_current_timestamp() if hasattr(context, 'get_current_timestamp') else None,
|
||||
'processing_details': {
|
||||
'removed_html_tags': True,
|
||||
'removed_think_tags': True
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"ModelResponse processing result: Original length {original_length}, Processed length {processed_length}, Removed content {original_length - processed_length}")
|
||||
@@ -0,0 +1,41 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
|
||||
HOOK_TEMPLATE = """
|
||||
import traceback
|
||||
|
||||
from aworld.core.context.base import Context
|
||||
|
||||
from aworld.core.event.base import Message, Constants, TopicType
|
||||
from aworld.runners.hook.hooks import *
|
||||
from aworld.runners.hook.hook_factory import HookFactory
|
||||
from aworld.logs.util import logger
|
||||
|
||||
from aworld.utils.common import convert_to_snake
|
||||
|
||||
|
||||
@HookFactory.register(name="{name}",
|
||||
desc="{desc}")
|
||||
class {name}({point}Hook):
|
||||
def name(self):
|
||||
return convert_to_snake("{name}")
|
||||
|
||||
async def exec(self, message: Message) -> Message:
|
||||
{func_import}import {func}
|
||||
try:
|
||||
res = {func}(message)
|
||||
if not res:
|
||||
raise ValueError(f"{func} no result return.")
|
||||
return Message(payload=res,
|
||||
session_id=message.context.session_id,
|
||||
sender="{name}",
|
||||
category=Constants.TASK,
|
||||
topic="{topic}")
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Message(payload=str(e),
|
||||
session_id=message.context.session_id,
|
||||
sender="{name}",
|
||||
category=Constants.TASK,
|
||||
topic=TopicType.ERROR)
|
||||
"""
|
||||
@@ -0,0 +1,55 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
import os
|
||||
from typing import Callable, Any
|
||||
|
||||
from aworld.runners.hook.template import HOOK_TEMPLATE
|
||||
from aworld.utils.common import snake_to_camel
|
||||
|
||||
|
||||
def hook(hook_point: str, name: str = None):
|
||||
"""Hook decorator.
|
||||
|
||||
NOTE: Hooks can be annotated, but they need to comply with the protocol agreement.
|
||||
The input parameter of the hook function is `Message` type, and the @hook needs to specify `hook_point`.
|
||||
|
||||
Examples:
|
||||
>>> @hook(hook_point=HookPoint.ERROR)
|
||||
>>> def error_process(message: Message) -> Message | None:
|
||||
>>> print("process error")
|
||||
The function `error_process` will be executed when an error message appears in the task,
|
||||
you can choose return nothing or return a message.
|
||||
|
||||
Args:
|
||||
hook_point: Hook point that wants to process the message.
|
||||
name: Hook name.
|
||||
"""
|
||||
|
||||
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
# converts python function into a hoop with associated hoop point
|
||||
func_import = func.__module__
|
||||
if func_import == '__main__':
|
||||
path = inspect.getsourcefile(func)
|
||||
package = path.replace(os.getcwd(), '').replace('.py', '')
|
||||
if package[0] == '/':
|
||||
package = package[1:]
|
||||
func_import = f"from {package} "
|
||||
else:
|
||||
func_import = f"from {func_import} "
|
||||
|
||||
real_name = name if name else func.__name__
|
||||
con = HOOK_TEMPLATE.format(func_import=func_import,
|
||||
func=func.__name__,
|
||||
point=snake_to_camel(hook_point),
|
||||
name=real_name,
|
||||
topic=hook_point,
|
||||
desc='')
|
||||
with open(f"{real_name}.py", 'w+') as write:
|
||||
write.writelines(con)
|
||||
importlib.import_module(real_name)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
@@ -0,0 +1,824 @@
|
||||
import abc
|
||||
import time
|
||||
import asyncio
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from aworld.core.event.base import Message
|
||||
from enum import Enum
|
||||
from abc import ABC, abstractmethod, ABCMeta
|
||||
from aworld.core.agent.base import is_agent_by_name
|
||||
from aworld.core.tool.tool_desc import is_tool_by_name
|
||||
from aworld.core.singleton import InheritanceSingleton, SingletonMeta
|
||||
from aworld.core.event.base import Constants
|
||||
from aworld.logs.util import logger
|
||||
from aworld.events.util import send_message
|
||||
|
||||
|
||||
class RunNodeBusiType(Enum):
|
||||
AGENT = 'AGENT'
|
||||
TOOL = 'TOOL'
|
||||
TASK = 'TASK'
|
||||
TOOL_CALLBACK = 'TOOL_CALLBACK'
|
||||
HUMAN = 'HUMAN'
|
||||
|
||||
@staticmethod
|
||||
def from_message_category(category: str) -> 'RunNodeBusiType':
|
||||
if category == Constants.AGENT:
|
||||
return RunNodeBusiType.AGENT
|
||||
if category == Constants.TOOL:
|
||||
return RunNodeBusiType.TOOL
|
||||
if category == Constants.TASK:
|
||||
return RunNodeBusiType.TASK
|
||||
if category == Constants.TOOL_CALLBACK:
|
||||
return RunNodeBusiType.TOOL_CALLBACK
|
||||
if category == Constants.HUMAN:
|
||||
return RunNodeBusiType.HUMAN
|
||||
return None
|
||||
|
||||
|
||||
class RunNodeStatus(Enum):
|
||||
INIT = 'INIT'
|
||||
RUNNING = 'RUNNING'
|
||||
BREAKED = 'BREAKED'
|
||||
SUCCESS = 'SUCCESS'
|
||||
FAILED = 'FAILED'
|
||||
TIMEOUT = 'TIMEOUT'
|
||||
|
||||
|
||||
class HandleResult(BaseModel):
|
||||
name: str = None
|
||||
status: RunNodeStatus = None
|
||||
result_msg: Optional[str] = None
|
||||
result: Optional[Message] = None
|
||||
|
||||
|
||||
class RunNode(BaseModel):
|
||||
# {busi_id}_{busi_type}
|
||||
node_id: Optional[str] = None
|
||||
task_id: Optional[str] = None
|
||||
busi_type: str = None
|
||||
busi_id: Optional[str] = None
|
||||
session_id: Optional[str] = None
|
||||
msg_id: Optional[str] = None # input message id
|
||||
# busi_id of node that send the input message
|
||||
msg_from: Optional[str] = None
|
||||
parent_node_id: Optional[str] = None
|
||||
status: RunNodeStatus = None
|
||||
result_msg: Optional[str] = None
|
||||
results: Optional[List[HandleResult]] = None
|
||||
create_time: Optional[float] = None
|
||||
execute_time: Optional[float] = None
|
||||
end_time: Optional[float] = None
|
||||
group_id: Optional[str] = None
|
||||
# sub_group_root_id required when group_id is not None
|
||||
sub_group_root_id: Optional[str] = None
|
||||
# metadata is used to store the context of the sub task when group_id is not None
|
||||
metadata: Optional[dict] = None
|
||||
|
||||
def has_finished(self):
|
||||
return self.status in [RunNodeStatus.SUCCESS, RunNodeStatus.FAILED, RunNodeStatus.TIMEOUT]
|
||||
|
||||
|
||||
class SubGroup(BaseModel):
|
||||
'''
|
||||
SubGroup represents an execution chain pointing to the root node
|
||||
'''
|
||||
root_node_id: Optional[str] = None
|
||||
session_id: Optional[str] = None
|
||||
group_id: Optional[str] = None
|
||||
create_time: Optional[float] = None
|
||||
execute_time: Optional[float] = None
|
||||
end_time: Optional[float] = None
|
||||
status: RunNodeStatus = None
|
||||
result_msg: Optional[str] = None
|
||||
results: Optional[List[HandleResult]] = None
|
||||
metadata: Optional[dict] = None
|
||||
|
||||
def has_finished(self):
|
||||
return self.status in [RunNodeStatus.SUCCESS, RunNodeStatus.FAILED, RunNodeStatus.TIMEOUT]
|
||||
|
||||
|
||||
class NodeGroup(BaseModel):
|
||||
'''
|
||||
Node group, used to manage sub group
|
||||
'''
|
||||
group_id: str = None
|
||||
session_id: str = None
|
||||
# subtask root node id list
|
||||
root_node_ids: List[str] = None
|
||||
finished: Optional[bool] = False
|
||||
finish_notified: Optional[bool] = False
|
||||
create_time: Optional[float] = None
|
||||
execute_time: Optional[float] = None
|
||||
end_time: Optional[float] = None
|
||||
status: RunNodeStatus = None
|
||||
# failed subtask root node id list
|
||||
failed_root_node_ids: Optional[List[str]] = None
|
||||
parent_group_id: Optional[str] = None
|
||||
metadata: Optional[dict] = None
|
||||
|
||||
def has_finished(self):
|
||||
return self.status in [RunNodeStatus.SUCCESS, RunNodeStatus.FAILED, RunNodeStatus.TIMEOUT]
|
||||
|
||||
|
||||
class NodeGroupDetail(NodeGroup):
|
||||
sub_groups: Optional[List[SubGroup]] = None
|
||||
|
||||
|
||||
class StateStorage:
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def get(self, node_id: str) -> RunNode:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def insert(self, node: RunNode):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update(self, node: RunNode):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def query(self, session_id: str) -> List[RunNode]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def query_by_task_id(self, task_id: str) -> List[RunNode]:
|
||||
pass
|
||||
|
||||
|
||||
class NodeGroupStorage:
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def get(self, group_id: str) -> NodeGroup:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def insert(self, node_group: NodeGroup):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update(self, node_group: NodeGroup):
|
||||
pass
|
||||
|
||||
|
||||
class SubGroupStorage:
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def get(self, node_id: str) -> SubGroup:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def insert(self, sub_group: SubGroup):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update(self, sub_group: SubGroup):
|
||||
pass
|
||||
|
||||
|
||||
class StateStorageMeta(SingletonMeta, ABCMeta):
|
||||
pass
|
||||
|
||||
|
||||
class InMemoryStateStorage(StateStorage, InheritanceSingleton, metaclass=StateStorageMeta):
|
||||
'''
|
||||
In memory state storage
|
||||
'''
|
||||
|
||||
def __init__(self, max_session=1000):
|
||||
self._max_session = max_session
|
||||
self._nodes = {} # {node_id: RunNode}
|
||||
self._ordered_session_ids = []
|
||||
self._session_nodes = {} # {session_id: [RunNode, RunNode]}
|
||||
|
||||
def get(self, node_id: str) -> RunNode:
|
||||
return self._nodes.get(node_id)
|
||||
|
||||
def insert(self, node: RunNode):
|
||||
if node.session_id not in self._ordered_session_ids:
|
||||
self._ordered_session_ids.append(node.session_id)
|
||||
self._session_nodes.update({node.session_id: []})
|
||||
if node.node_id not in self._nodes:
|
||||
self._nodes.update({node.node_id: node})
|
||||
self._session_nodes[node.session_id].append(node)
|
||||
|
||||
if len(self._ordered_session_ids) > self._max_session:
|
||||
oldest_session_id = self._ordered_session_ids.pop(0)
|
||||
session_nodes = self._session_nodes.pop(oldest_session_id)
|
||||
for node in session_nodes:
|
||||
self._nodes.pop(node.node_id)
|
||||
# logger.info(f"storage nodes: {self._nodes}")
|
||||
|
||||
def update(self, node: RunNode):
|
||||
self._nodes[node.node_id] = node
|
||||
|
||||
def query(self, session_id: str, msg_id: str = None) -> List[RunNode]:
|
||||
session_nodes = self._session_nodes.get(session_id, [])
|
||||
if msg_id:
|
||||
return [node for node in session_nodes if node.msg_id == msg_id]
|
||||
return session_nodes
|
||||
|
||||
def query_by_task_id(self, task_id: str) -> List[RunNode]:
|
||||
return [node for node in self._nodes.values() if node.task_id == task_id]
|
||||
|
||||
|
||||
class InMemoryNodeGroupStorage(NodeGroupStorage, InheritanceSingleton, metaclass=StateStorageMeta):
|
||||
'''
|
||||
In memory node group storage
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
self.node_groups = {}
|
||||
|
||||
def get(self, group_id: str) -> NodeGroup:
|
||||
return self.node_groups.get(group_id)
|
||||
|
||||
def insert(self, node_group: NodeGroup):
|
||||
self.node_groups[node_group.group_id] = node_group
|
||||
|
||||
def update(self, node_group: NodeGroup):
|
||||
self.node_groups[node_group.group_id] = node_group
|
||||
|
||||
|
||||
class InMemorySubGroupStorage(SubGroupStorage, InheritanceSingleton, metaclass=StateStorageMeta):
|
||||
'''
|
||||
In memory sub task storage
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
self.sub_groups = {}
|
||||
|
||||
def get(self, node_id: str) -> SubGroup:
|
||||
return self.sub_groups.get(node_id)
|
||||
|
||||
def insert(self, sub_group: SubGroup):
|
||||
self.sub_groups[sub_group.root_node_id] = sub_group
|
||||
|
||||
def update(self, sub_group: SubGroup):
|
||||
self.sub_groups[sub_group.root_node_id] = sub_group
|
||||
|
||||
|
||||
class RuntimeStateManager(InheritanceSingleton):
|
||||
'''
|
||||
Runtime state manager
|
||||
'''
|
||||
|
||||
def __init__(self,
|
||||
storage: StateStorage = InMemoryStateStorage.instance()):
|
||||
self.storage = storage
|
||||
self._node_group_manager = None
|
||||
|
||||
@property
|
||||
def node_group_manager(self):
|
||||
if not self._node_group_manager:
|
||||
self._node_group_manager = NodeGroupManager(node_state_manager=self)
|
||||
return self._node_group_manager
|
||||
|
||||
def create_node(self,
|
||||
busi_type: RunNodeBusiType,
|
||||
busi_id: str,
|
||||
session_id: str,
|
||||
node_id: str = None,
|
||||
task_id: str = None,
|
||||
parent_node_id: str = None,
|
||||
msg_id: str = None,
|
||||
msg_from: str = None,
|
||||
group_id: str = None,
|
||||
sub_group_root_id: str = None,
|
||||
metadata: Optional[dict] = None) -> RunNode:
|
||||
'''
|
||||
create node and insert to storage
|
||||
'''
|
||||
node_id = node_id or msg_id
|
||||
node = self._find_node(node_id)
|
||||
if node:
|
||||
# raise Exception(f"node already exist, node_id: {node_id}")
|
||||
return
|
||||
if parent_node_id:
|
||||
parent_node = self._find_node(parent_node_id)
|
||||
if not parent_node:
|
||||
logger.warning(
|
||||
f"parent node not exist, parent_node_id: {parent_node_id}")
|
||||
node = RunNode(node_id=node_id,
|
||||
busi_type=busi_type.name,
|
||||
busi_id=busi_id,
|
||||
session_id=session_id,
|
||||
task_id=task_id,
|
||||
msg_id=msg_id,
|
||||
msg_from=msg_from,
|
||||
parent_node_id=parent_node_id,
|
||||
status=RunNodeStatus.INIT,
|
||||
create_time=time.time(),
|
||||
group_id=group_id,
|
||||
sub_group_root_id=sub_group_root_id,
|
||||
metadata=metadata)
|
||||
self.storage.insert(node)
|
||||
# create sub group if node is the root node of sub group
|
||||
if group_id and sub_group_root_id and node_id == sub_group_root_id:
|
||||
self.node_group_manager.create_sub_group(group_id, session_id, sub_group_root_id, metadata)
|
||||
return node
|
||||
|
||||
def run_node(self, node_id: str):
|
||||
'''
|
||||
set node status to RUNNING and update to storage
|
||||
'''
|
||||
logger.debug(f"====== set node {node_id} running =======")
|
||||
node = self._node_exist(node_id)
|
||||
node.status = RunNodeStatus.RUNNING
|
||||
node.execute_time = time.time()
|
||||
self.storage.update(node)
|
||||
# update sub group status if node is the root node
|
||||
if node.group_id and node.sub_group_root_id and node.node_id == node.sub_group_root_id:
|
||||
self.node_group_manager.run_sub_group(node_id)
|
||||
|
||||
def save_result(self,
|
||||
node_id: str,
|
||||
result: HandleResult):
|
||||
'''
|
||||
save node execute result and update to storage
|
||||
'''
|
||||
node = self._node_exist(node_id)
|
||||
if not node.results:
|
||||
node.results = []
|
||||
node.results.append(result)
|
||||
self.storage.update(node)
|
||||
|
||||
def break_node(self, node_id):
|
||||
'''
|
||||
set node status to BREAKED and update to storage
|
||||
'''
|
||||
node = self._node_exist(node_id)
|
||||
node.status = RunNodeStatus.BREAKED
|
||||
self.storage.update(node)
|
||||
|
||||
def run_succeed(self,
|
||||
node_id,
|
||||
result_msg=None,
|
||||
results: List[HandleResult] = None):
|
||||
'''
|
||||
set node status to SUCCESS and update to storage
|
||||
'''
|
||||
node = self._node_exist(node_id)
|
||||
node.status = RunNodeStatus.SUCCESS
|
||||
node.result_msg = result_msg
|
||||
node.end_time = time.time()
|
||||
if results:
|
||||
if not node.results:
|
||||
node.results = []
|
||||
node.results.extend(results)
|
||||
logger.debug(f"====== run_succeed set node {node_id} succeed: {node} =======")
|
||||
|
||||
self.storage.update(node)
|
||||
|
||||
def run_failed(self,
|
||||
node_id,
|
||||
result_msg=None,
|
||||
results: List[HandleResult] = None):
|
||||
'''
|
||||
set node status to FAILED and update to storage
|
||||
'''
|
||||
node = self._node_exist(node_id)
|
||||
node.status = RunNodeStatus.FAILED
|
||||
node.result_msg = result_msg
|
||||
node.end_time = time.time()
|
||||
if results:
|
||||
if not node.results:
|
||||
node.results = []
|
||||
node.results.extend(results)
|
||||
self.storage.update(node)
|
||||
|
||||
def run_timeout(self,
|
||||
node_id,
|
||||
result_msg=None):
|
||||
'''
|
||||
set node status to TIMEOUT and update to storage
|
||||
'''
|
||||
node = self._node_exist(node_id)
|
||||
node.status = RunNodeStatus.TIMEOUT
|
||||
node.result_msg = result_msg
|
||||
self.storage.update(node)
|
||||
|
||||
def finish_sub_task(self, node_id: str):
|
||||
'''
|
||||
finish sub task with node_id as the root node
|
||||
'''
|
||||
node = self._node_exist(node_id)
|
||||
node.sub_task_finished = True
|
||||
self.storage.update(node)
|
||||
|
||||
def get_node(self, node_id: str) -> RunNode:
|
||||
'''
|
||||
get node from storage
|
||||
'''
|
||||
return self._find_node(node_id)
|
||||
|
||||
def get_nodes(self, session_id: str) -> List[RunNode]:
|
||||
'''
|
||||
get nodes from storage
|
||||
'''
|
||||
return self.storage.query(session_id)
|
||||
|
||||
def _node_exist(self, node_id: str):
|
||||
node = self._find_node(node_id)
|
||||
if not node:
|
||||
raise Exception(f"node not found, node_id: {node_id}")
|
||||
return node
|
||||
|
||||
def _find_node(self, node_id: str):
|
||||
return self.storage.get(node_id)
|
||||
|
||||
def _judge_msg_from_busi_type(self, msg_from: str) -> RunNodeBusiType:
|
||||
'''
|
||||
judge msg_from busi_type
|
||||
'''
|
||||
if is_agent_by_name(msg_from):
|
||||
return RunNodeBusiType.AGENT
|
||||
if is_tool_by_name(msg_from):
|
||||
return RunNodeBusiType.TOOL
|
||||
return RunNodeBusiType.TASK
|
||||
|
||||
async def wait_for_node_completion(self, node_id: str, timeout: float = 600.0, interval: float = 1.0) -> RunNode:
|
||||
'''Poll for node status until completion or timeout.
|
||||
|
||||
Args:
|
||||
node_id: Node ID
|
||||
timeout: Timeout threshold in seconds
|
||||
interval: Polling interval in seconds
|
||||
|
||||
Returns:
|
||||
RunNode: Node object
|
||||
|
||||
Raises:
|
||||
Exception: If node does not exist
|
||||
TimeoutError: If waiting times out
|
||||
'''
|
||||
start_time = time.time()
|
||||
log_start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
logger.info(f"wait for node completion: {node_id}, start_time:{log_start_time}")
|
||||
|
||||
while True:
|
||||
node = self._find_node(node_id)
|
||||
if not node:
|
||||
raise Exception(f"Node not found, node_id: {node_id}")
|
||||
|
||||
# Check if node has completed
|
||||
if node.status in [RunNodeStatus.SUCCESS, RunNodeStatus.FAILED, RunNodeStatus.BREAKED,
|
||||
RunNodeStatus.TIMEOUT]:
|
||||
return node
|
||||
|
||||
# Check if timed out
|
||||
if time.time() - start_time > timeout:
|
||||
self.run_timeout(node_id, result_msg=f"Waiting for node completion timed out after {timeout} seconds")
|
||||
node = self._find_node(node_id)
|
||||
return node
|
||||
|
||||
# Wait for the specified interval before polling again
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
async def create_group(self, group_id: str,
|
||||
session_id: str,
|
||||
root_node_ids: List[str] = None,
|
||||
parent_group_id: Optional[str] = None,
|
||||
metadata: Optional[dict] = None) -> NodeGroup:
|
||||
'''
|
||||
create node group
|
||||
'''
|
||||
return await self.node_group_manager.create_group(group_id, session_id, root_node_ids, parent_group_id, metadata)
|
||||
|
||||
async def finish_sub_group(self,
|
||||
group_id: str,
|
||||
root_node_id: str,
|
||||
results: List[Message] = None,
|
||||
result_msg: str = None):
|
||||
'''
|
||||
finish sub group
|
||||
'''
|
||||
handle_results = []
|
||||
for msg in results:
|
||||
handle_result = HandleResult(
|
||||
status=RunNodeStatus.FAILED if msg.is_error() else RunNodeStatus.SUCCESS,
|
||||
result=msg,
|
||||
name=msg.sender
|
||||
)
|
||||
handle_results.append(handle_result)
|
||||
await self.node_group_manager.finish_sub_group(group_id, root_node_id, handle_results, result_msg)
|
||||
|
||||
def get_group(self, group_id: str) -> NodeGroup:
|
||||
'''
|
||||
get group basic info
|
||||
'''
|
||||
return self.node_group_manager.get_group(group_id)
|
||||
|
||||
def query_group_detail(self, group_id: str) -> NodeGroupDetail:
|
||||
'''
|
||||
query group detail info with all sub group info
|
||||
'''
|
||||
return self.node_group_manager.query_group_detail(group_id)
|
||||
|
||||
def query_by_task(self, task_id: str, busi_typ: RunNodeBusiType = None, busi_id: str = None) -> List[RunNode]:
|
||||
all_task_nodes = self.storage.query_by_task_id(task_id)
|
||||
if (not busi_typ and busi_id) or (busi_typ and not busi_id):
|
||||
raise Exception("busi_typ and busi_id must be both None or not None")
|
||||
if busi_typ and busi_id:
|
||||
result_nodes = [node for node in all_task_nodes if node.busi_type == busi_typ.name and node.busi_id == busi_id]
|
||||
else:
|
||||
result_nodes = all_task_nodes
|
||||
result_nodes.sort(key=lambda x: x.create_time if x.create_time else 0, reverse=True)
|
||||
return result_nodes
|
||||
|
||||
|
||||
class NodeGroupManager(InheritanceSingleton):
|
||||
'''
|
||||
Node group manager, used to manage node group
|
||||
'''
|
||||
|
||||
def __init__(self,
|
||||
sub_group_storage: SubGroupStorage = InMemorySubGroupStorage.instance(),
|
||||
node_group_storage: NodeGroupStorage = InMemoryNodeGroupStorage.instance(),
|
||||
node_state_manager: RuntimeStateManager = None):
|
||||
self.sub_group_storage = sub_group_storage
|
||||
self.node_group_storage = node_group_storage
|
||||
self.node_state_manager = node_state_manager
|
||||
|
||||
async def create_group(self, group_id: str,
|
||||
session_id: str,
|
||||
root_node_ids: List[str] = None,
|
||||
parent_group_id: Optional[str] = None,
|
||||
metadata: Optional[dict] = None) -> NodeGroup:
|
||||
'''
|
||||
create node group
|
||||
'''
|
||||
group = self._find_group(group_id)
|
||||
if group:
|
||||
raise Exception(f"group already exist, group_id: {group_id}")
|
||||
node_group = NodeGroup(
|
||||
session_id=session_id,
|
||||
group_id=group_id,
|
||||
root_node_ids=root_node_ids,
|
||||
parent_group_id=parent_group_id,
|
||||
metadata=metadata,
|
||||
create_time=time.time(),
|
||||
update_time=time.time(),
|
||||
status=RunNodeStatus.INIT,
|
||||
)
|
||||
self.node_group_storage.insert(node_group)
|
||||
await self._check_subgroup_status(group_id, root_node_ids)
|
||||
|
||||
def create_sub_group(self,
|
||||
group_id: str,
|
||||
session_id: str,
|
||||
root_node_id: str,
|
||||
metadata: Optional[dict] = None) -> SubGroup:
|
||||
'''
|
||||
create sub group
|
||||
'''
|
||||
subgroup = self._find_subgroup(root_node_id)
|
||||
if subgroup:
|
||||
raise Exception(f"subgroup already exist, group_id: {group_id}, root_node_id: {root_node_id}")
|
||||
run_node = self.node_state_manager.get_node(root_node_id)
|
||||
if not run_node:
|
||||
raise Exception(f"run node not found, root_node_id: {root_node_id}")
|
||||
|
||||
sub_group = SubGroup(
|
||||
session_id=session_id,
|
||||
group_id=group_id,
|
||||
root_node_id=root_node_id,
|
||||
metadata=metadata,
|
||||
create_time=time.time(),
|
||||
update_time=time.time(),
|
||||
status=RunNodeStatus.INIT,
|
||||
)
|
||||
self.sub_group_storage.insert(sub_group)
|
||||
return sub_group
|
||||
|
||||
def run_sub_group(self,
|
||||
root_node_id: str):
|
||||
'''
|
||||
run sub group
|
||||
'''
|
||||
subgroup = self._subgroup_exist(root_node_id)
|
||||
if not subgroup:
|
||||
raise Exception(f"subgroup not found, root_node_id: {root_node_id}")
|
||||
|
||||
subgroup.execute_time = time.time()
|
||||
subgroup.status = RunNodeStatus.RUNNING
|
||||
self.sub_group_storage.update(subgroup)
|
||||
self.run_group(subgroup.group_id)
|
||||
|
||||
def run_group(self, group_id):
|
||||
group = self.node_group_storage.get(group_id)
|
||||
if group.status == RunNodeStatus.INIT:
|
||||
group.status = RunNodeStatus.RUNNING
|
||||
group.execute_time = time.time()
|
||||
self.node_group_storage.update(group)
|
||||
|
||||
async def finish_sub_group(self,
|
||||
group_id: str,
|
||||
root_node_id: str,
|
||||
results: List[HandleResult] = None,
|
||||
result_msg: str = None):
|
||||
'''
|
||||
finish sub task with node_id as the root node
|
||||
'''
|
||||
subgroup = self.sub_group_storage.get(root_node_id)
|
||||
if not subgroup:
|
||||
raise Exception(f"subgroup not found, group_id: {group_id}, root_node_id: {root_node_id}")
|
||||
if subgroup.group_id != group_id:
|
||||
raise Exception(f"subgroup group_id not match, group_id: {group_id}, root_node_id: {root_node_id}")
|
||||
|
||||
group = self._group_exist(group_id)
|
||||
subgroup.end_time = time.time()
|
||||
subgroup.results = results
|
||||
subgroup.result_msg = result_msg
|
||||
subgroup.status = RunNodeStatus.SUCCESS
|
||||
for result in results:
|
||||
if result.status == RunNodeStatus.FAILED:
|
||||
subgroup.status = RunNodeStatus.FAILED
|
||||
self.sub_group_storage.update(subgroup)
|
||||
# check all subgroup status and update group status
|
||||
await self._check_subgroup_status(group_id, group.root_node_ids)
|
||||
|
||||
async def _check_subgroup_status(self, group_id, root_node_ids: List[str]):
|
||||
'''
|
||||
check subgroups status and update group status, if group finished, send group finish message
|
||||
'''
|
||||
all_subgroups_finished = True
|
||||
failed_subgroups = []
|
||||
for root_node_id in root_node_ids:
|
||||
subgroup = self.sub_group_storage.get(root_node_id)
|
||||
if not subgroup or not subgroup.has_finished():
|
||||
all_subgroups_finished = False
|
||||
break
|
||||
if subgroup.status == RunNodeStatus.FAILED or subgroup.status == RunNodeStatus.TIMEOUT:
|
||||
failed_subgroups.append(subgroup)
|
||||
|
||||
if all_subgroups_finished:
|
||||
group = self._group_exist(group_id)
|
||||
if failed_subgroups:
|
||||
group.status = RunNodeStatus.FAILED
|
||||
group.failed_root_node_ids = [subgroup.root_node_id for subgroup in failed_subgroups]
|
||||
else:
|
||||
group.status = RunNodeStatus.SUCCESS
|
||||
group.end_time = time.time()
|
||||
self.node_group_storage.update(group)
|
||||
await self._send_group_finish_message(group_id)
|
||||
|
||||
async def _send_group_finish_message(self, group_id: str):
|
||||
'''
|
||||
Currently, for simple implementation, concurrency control needs to be considered in a distributed environment
|
||||
'''
|
||||
group = self._group_exist(group_id)
|
||||
if group.finish_notified:
|
||||
logger.warning(f"group finish message already sent, group_id: {group_id}")
|
||||
return
|
||||
group_results = {}
|
||||
metadata = group.metadata
|
||||
for root_node_id in group.root_node_ids:
|
||||
subgroup = self.sub_group_storage.get(root_node_id)
|
||||
group_results[root_node_id] = subgroup.results
|
||||
if not metadata:
|
||||
metadata = subgroup.metadata
|
||||
|
||||
metadata = metadata or {}
|
||||
if group.parent_group_id:
|
||||
metadata.update({
|
||||
"parent_group_id": group.parent_group_id
|
||||
})
|
||||
message = Message(
|
||||
category="group",
|
||||
payload=group_results,
|
||||
sender="node_group_manager",
|
||||
session_id=group.session_id,
|
||||
topic="__group_results",
|
||||
headers=metadata
|
||||
)
|
||||
await send_message(message)
|
||||
group.finish_notified = True
|
||||
self.node_group_storage.update(group)
|
||||
|
||||
def get_group(self, group_id: str) -> NodeGroup:
|
||||
'''
|
||||
get group basic info
|
||||
'''
|
||||
return self._find_group(group_id)
|
||||
|
||||
def query_group_detail(self, group_id: str) -> NodeGroupDetail:
|
||||
'''
|
||||
query group detail info with all sub group info
|
||||
'''
|
||||
group = self._find_group(group_id)
|
||||
if not group:
|
||||
return None
|
||||
sub_groups = []
|
||||
for root_node_id in group.root_node_ids:
|
||||
subgroup = self._find_subgroup(root_node_id)
|
||||
if subgroup:
|
||||
sub_groups.append(subgroup)
|
||||
return NodeGroupDetail(
|
||||
group_id=group.group_id,
|
||||
root_node_ids=group.root_node_ids,
|
||||
parent_group_id=group.parent_group_id,
|
||||
metadata=group.metadata,
|
||||
create_time=group.create_time,
|
||||
execute_time=group.execute_time,
|
||||
end_time=group.end_time,
|
||||
status=group.status,
|
||||
failed_root_node_ids=group.failed_root_node_ids,
|
||||
sub_groups=sub_groups
|
||||
)
|
||||
|
||||
def _find_subgroup(self, root_node_id: str) -> SubGroup:
|
||||
return self.sub_group_storage.get(root_node_id)
|
||||
|
||||
def _subgroup_exist(self, root_node_id: str) -> SubGroup:
|
||||
subgroup = self._find_subgroup(root_node_id)
|
||||
if not subgroup:
|
||||
raise Exception(f"subgroup not found, root_node_id: {root_node_id}")
|
||||
return subgroup
|
||||
|
||||
def _find_group(self, group_id: str) -> NodeGroup:
|
||||
return self.node_group_storage.get(group_id)
|
||||
|
||||
def _group_exist(self, group_id: str) -> NodeGroup:
|
||||
group = self._find_group(group_id)
|
||||
if not group:
|
||||
raise Exception(f"group not found, group_id: {group_id}")
|
||||
return group
|
||||
|
||||
|
||||
class EventRuntimeStateManager(RuntimeStateManager):
|
||||
|
||||
def __init__(self, storage: StateStorage = InMemoryStateStorage.instance()):
|
||||
super().__init__(storage)
|
||||
|
||||
def start_message_node(self, message: Message):
|
||||
'''
|
||||
create and start node while message handle started.
|
||||
'''
|
||||
metadata = message.headers
|
||||
run_node_busi_type = RunNodeBusiType.from_message_category(
|
||||
message.category)
|
||||
logger.debug(
|
||||
f"start message node: {message.receiver}, busi_type={run_node_busi_type}, node_id={message.id}")
|
||||
if run_node_busi_type:
|
||||
self.create_node(
|
||||
node_id=message.id,
|
||||
busi_type=run_node_busi_type,
|
||||
busi_id=message.receiver or "",
|
||||
session_id=message.session_id,
|
||||
task_id=message.task_id,
|
||||
msg_id=message.id,
|
||||
msg_from=message.sender,
|
||||
group_id=metadata.get("group_id") if metadata else None,
|
||||
sub_group_root_id=metadata.get("root_message_id") if metadata else None,
|
||||
metadata=metadata)
|
||||
self.run_node(message.id)
|
||||
|
||||
def save_message_handle_result(self, name: str, message: Message, result: Message = None):
|
||||
'''
|
||||
save message handle result
|
||||
'''
|
||||
run_node_busi_type = RunNodeBusiType.from_message_category(
|
||||
message.category)
|
||||
if run_node_busi_type:
|
||||
if result and result.is_error():
|
||||
handle_result = HandleResult(
|
||||
name=name,
|
||||
status=RunNodeStatus.FAILED,
|
||||
result=result)
|
||||
else:
|
||||
handle_result = HandleResult(
|
||||
name=name,
|
||||
status=self.get_node(message.id).status if self.get_node(message.id) else RunNodeStatus.FAILED,
|
||||
result=result)
|
||||
self.save_result(node_id=message.id, result=handle_result)
|
||||
|
||||
def end_message_node(self, message: Message):
|
||||
'''
|
||||
end node while message handle finished.
|
||||
'''
|
||||
run_node_busi_type = RunNodeBusiType.from_message_category(
|
||||
message.category)
|
||||
if run_node_busi_type:
|
||||
node = self._node_exist(node_id=message.id)
|
||||
status = RunNodeStatus.SUCCESS
|
||||
if node.results:
|
||||
for result in node.results:
|
||||
if result.status == RunNodeStatus.FAILED:
|
||||
status = RunNodeStatus.FAILED
|
||||
break
|
||||
if status == RunNodeStatus.FAILED:
|
||||
self.run_failed(node_id=message.id)
|
||||
else:
|
||||
self.run_succeed(node_id=message.id)
|
||||
|
||||
def get_message_node_status(self, message: Message) -> RunNodeStatus:
|
||||
node = self.get_node(node_id=message.id)
|
||||
if not node:
|
||||
return RunNodeStatus.INIT
|
||||
return node.status
|
||||
@@ -0,0 +1,160 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import abc
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from typing import Callable, Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
import aworld.tools
|
||||
from aworld.config import ConfigDict
|
||||
from aworld.config.conf import ToolConfig
|
||||
from aworld.core.agent.swarm import Swarm
|
||||
from aworld.core.common import Observation
|
||||
from aworld.core.context.base import Context
|
||||
from aworld.core.context.session import Session
|
||||
from aworld.core.tool.base import Tool, AsyncTool
|
||||
from aworld.core.task import Task, TaskResponse, Runner
|
||||
from aworld.logs.util import logger
|
||||
from aworld import trace, cleanup
|
||||
from aworld.utils.common import load_module_by_path
|
||||
|
||||
|
||||
class TaskRunner(Runner):
|
||||
"""Task based runner api class."""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def __init__(self,
|
||||
task: Task,
|
||||
*,
|
||||
agent_oriented: bool = True,
|
||||
daemon_target: Callable[..., Any] = None):
|
||||
"""Task runner initialize.
|
||||
|
||||
Args:
|
||||
task: Task entity to be executed.
|
||||
agent_oriented: Is it an agent oriented task, default is True.
|
||||
"""
|
||||
if task.tools is None:
|
||||
task.tools = []
|
||||
if task.tool_names is None:
|
||||
task.tool_names = []
|
||||
|
||||
if agent_oriented:
|
||||
if not task.agent and not task.swarm:
|
||||
raise ValueError("agent and swarm all is None.")
|
||||
if task.agent and task.swarm:
|
||||
logger.warning("agent and swarm all is not None.")
|
||||
raise ValueError("agent and swarm choose one only.")
|
||||
if task.agent:
|
||||
# uniform agent
|
||||
task.swarm = Swarm(task.agent)
|
||||
|
||||
if task.conf is None:
|
||||
task.conf = dict()
|
||||
if isinstance(task.conf, BaseModel):
|
||||
task.conf = task.conf.model_dump()
|
||||
task.conf = ConfigDict(task.conf)
|
||||
check_input = task.conf.get("check_input", False)
|
||||
if check_input and not task.input:
|
||||
raise ValueError("task no input")
|
||||
|
||||
if not task.is_sub_task:
|
||||
self.context = task.context if task.context else Context()
|
||||
self.context.set_task(task)
|
||||
self.task = task
|
||||
self.agent_oriented = agent_oriented
|
||||
self.daemon_target = daemon_target
|
||||
self._use_demon = False if not task.conf else task.conf.get(
|
||||
'use_demon', False)
|
||||
self._exception = None
|
||||
self.start_time = time.time()
|
||||
self.step_agent_counter = {}
|
||||
|
||||
async def pre_run(self):
|
||||
task = self.task
|
||||
# copy context from parent_task(if exists)
|
||||
if task.is_sub_task:
|
||||
task.context = await task.context.build_sub_context(
|
||||
task.input, task.id,
|
||||
agents=task.swarm.agents if task.swarm and task.swarm.agents else None
|
||||
)
|
||||
self.context = task.context
|
||||
self.context.set_task(task)
|
||||
self.swarm = task.swarm
|
||||
self.input = task.input
|
||||
self.outputs = task.outputs
|
||||
self.name = task.name
|
||||
self.conf = task.conf if task.conf else ConfigDict()
|
||||
self.tools = {
|
||||
tool.name(): tool for tool in task.tools} if task.tools else {}
|
||||
task.tool_names.extend(self.tools.keys())
|
||||
# lazy load
|
||||
self.tool_names = task.tool_names
|
||||
self.tools_conf = task.tools_conf
|
||||
if self.tools_conf is None:
|
||||
self.tools_conf = {}
|
||||
# mcp performs special process, use async only in the runn
|
||||
self.tools_conf['mcp'] = ToolConfig(use_async=True, name='mcp')
|
||||
self.endless_threshold = task.endless_threshold
|
||||
|
||||
# build context
|
||||
if task.session_id:
|
||||
session = Session(session_id=task.session_id)
|
||||
else:
|
||||
session = Session(session_id=uuid.uuid4().hex)
|
||||
trace_id = uuid.uuid1().hex if trace.get_current_span(
|
||||
) is None else trace.get_current_span().get_trace_id()
|
||||
self.context.task_id = self.task.id
|
||||
self.context.trace_id = trace_id
|
||||
self.context.session = session
|
||||
self.context.swarm = self.swarm
|
||||
|
||||
# init tool state by reset(), and ignore them observation
|
||||
observation = None
|
||||
if self.tools:
|
||||
for _, tool in self.tools.items():
|
||||
# use the observation and info of the last one
|
||||
if isinstance(tool, Tool):
|
||||
tool.context = self.context
|
||||
observation, info = tool.reset()
|
||||
elif isinstance(tool, AsyncTool):
|
||||
observation, info = await tool.reset()
|
||||
else:
|
||||
logger.warning(f"Unsupported tool type: {tool}, will ignored.")
|
||||
|
||||
if observation:
|
||||
if not observation.content:
|
||||
observation.content = self.input
|
||||
else:
|
||||
observation = Observation(content=self.input)
|
||||
|
||||
self.observation = observation
|
||||
if self.swarm:
|
||||
self.swarm.event_driven = task.event_driven
|
||||
self.swarm.reset(observation.content,
|
||||
context=self.context, tools=self.tool_names)
|
||||
|
||||
self._load_tool_module()
|
||||
logger.info(f'{"sub task: " if self.task.is_sub_task else "main task: "}{self.task.id} started...')
|
||||
|
||||
def _load_tool_module(self):
|
||||
# used to distributed running local tools
|
||||
try:
|
||||
value = os.environ.get(aworld.tools.LOCAL_TOOLS_ENV_VAR, '')
|
||||
if value:
|
||||
for val in value.split(";"):
|
||||
load_module_by_path(os.path.basename(val).replace("_action", ""),
|
||||
val.replace("_action.py", ".py"))
|
||||
load_module_by_path(os.path.basename(val), val)
|
||||
except:
|
||||
logger.warning(f"{os.environ.get(aworld.tools.LOCAL_TOOLS_ENV_VAR, '')} tools load fail, can't use them!!")
|
||||
|
||||
async def post_run(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def do_run(self, context: Context = None) -> TaskResponse:
|
||||
"""Task do run."""
|
||||
@@ -0,0 +1,124 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
from typing import List, Dict
|
||||
|
||||
from aworld.config import RunConfig, EngineName, ConfigDict
|
||||
from aworld.core.agent.swarm import GraphBuildType
|
||||
|
||||
from aworld.core.task import Task, TaskResponse, Runner
|
||||
from aworld.logs.util import logger
|
||||
from aworld.runners.task_runner import TaskRunner
|
||||
from aworld.utils.common import new_instance, snake_to_camel
|
||||
|
||||
|
||||
async def choose_runners(tasks: List[Task], agent_oriented: bool = True) -> List[Runner]:
|
||||
"""Choose the correct runner to run the task.
|
||||
|
||||
Args:
|
||||
task: A task that contains agents, tools and datas.
|
||||
|
||||
Returns:
|
||||
Runner instance or exception.
|
||||
"""
|
||||
runners = []
|
||||
for task in tasks:
|
||||
# user custom runner class
|
||||
runner_cls = task.runner_cls
|
||||
if runner_cls:
|
||||
return new_instance(runner_cls, task)
|
||||
else:
|
||||
# user runner class in the framework
|
||||
if task.swarm:
|
||||
task.swarm.event_driven = task.event_driven
|
||||
execute_type = task.swarm.build_type
|
||||
else:
|
||||
execute_type = GraphBuildType.WORKFLOW.value
|
||||
|
||||
if task.event_driven:
|
||||
runner = new_instance("aworld.runners.event_runner.TaskEventRunner",
|
||||
task,
|
||||
agent_oriented=agent_oriented)
|
||||
else:
|
||||
runner = new_instance(
|
||||
f"aworld.runners.call_driven_runner.{snake_to_camel(execute_type)}Runner",
|
||||
task
|
||||
)
|
||||
runners.append(runner)
|
||||
return runners
|
||||
|
||||
|
||||
async def execute_runner(runners: List[Runner], run_conf: RunConfig) -> Dict[str, TaskResponse]:
|
||||
"""Execute runner in the runtime engine.
|
||||
|
||||
Args:
|
||||
runners: The task processing flow.
|
||||
run_conf: Runtime config, can choose the special computing engine to execute the runner.
|
||||
"""
|
||||
if not run_conf:
|
||||
run_conf = RunConfig()
|
||||
|
||||
name = run_conf.engine_name
|
||||
if run_conf.cls:
|
||||
runtime_backend = new_instance(run_conf.cls, run_conf)
|
||||
else:
|
||||
runtime_backend = new_instance(
|
||||
f"aworld.core.runtime_engine.{snake_to_camel(name)}Runtime", run_conf)
|
||||
runtime_engine = runtime_backend.build_engine()
|
||||
|
||||
if run_conf.engine_name != EngineName.LOCAL or run_conf.reuse_process == False:
|
||||
# distributed in AWorld, the `context` can't carry by response
|
||||
for runner in runners:
|
||||
if not isinstance(runner, TaskRunner):
|
||||
logger.info("not task runner in AWorld, skip...")
|
||||
continue
|
||||
if runner.task.conf:
|
||||
runner.task.conf.resp_carry_context = False
|
||||
else:
|
||||
runner.task.conf = ConfigDict(resp_carry_context=False)
|
||||
return await runtime_engine.execute([runner.run for runner in runners])
|
||||
|
||||
|
||||
def endless_detect(records: List[str], endless_threshold: int, root_agent_name: str):
|
||||
"""A very simple implementation of endless loop detection.
|
||||
|
||||
Args:
|
||||
records: Call sequence of agent.
|
||||
endless_threshold: Threshold for the number of repetitions.
|
||||
root_agent_name: Name of the entrance agent.
|
||||
"""
|
||||
if not records:
|
||||
return False
|
||||
|
||||
threshold = endless_threshold
|
||||
last_agent_name = root_agent_name
|
||||
count = 1
|
||||
for i in range(len(records) - 2, -1, -1):
|
||||
if last_agent_name == records[i]:
|
||||
count += 1
|
||||
else:
|
||||
last_agent_name = records[i]
|
||||
count = 1
|
||||
|
||||
if count >= threshold:
|
||||
logger.warning("detect loop, will exit the loop.")
|
||||
return True
|
||||
|
||||
if len(records) > 6:
|
||||
last_agent_name = None
|
||||
# latest
|
||||
for j in range(1, 3):
|
||||
for i in range(len(records) - j, 0, -2):
|
||||
if last_agent_name and last_agent_name == (records[i], records[i - 1]):
|
||||
count += 1
|
||||
elif last_agent_name is None:
|
||||
last_agent_name = (records[i], records[i - 1])
|
||||
count = 1
|
||||
else:
|
||||
last_agent_name = None
|
||||
break
|
||||
|
||||
if count >= threshold:
|
||||
logger.warning(f"detect loop: {last_agent_name}, will exit the loop.")
|
||||
return True
|
||||
|
||||
return False
|
||||
Reference in New Issue
Block a user