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

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
@@ -0,0 +1,79 @@
# Environments
Virtual environments for execution of various tools.
Running on the local, we assume that the virtual environment completes startup when the python application starts.
![Environment Architecture](../../readme_assets/framework_environment.png)
# Be tool
You also can convert locally defined functions into tools for use in a task.
NOTE: The function must have a return value, preferably a string, as observed content.
```python
from pydantic import Field
from aworld.core.tool.func_to_tool import be_tool
@be_tool(tool_name='example', tool_desc="example description")
def example_1() -> str:
return "example_1"
@be_tool(tool_name='example')
def example_2(param: str) -> str:
return f"example_2{param}"
@be_tool(tool_name='example', name="example_3_alias_name", desc="example_3 description")
def example_3(param_1: str = "param",
param_2: str = Field(default="", description="param2 description")) -> str:
return f"example_3{param_1}{param_2}"
```
The name of the tool is `example`, now, you can use these functions as tools in the framework.
# Write tool
Detailed steps for building a tool:
1. Register action of your tool to action factory, and inherit `ExecutableAction`
2. Optional implement the `act` or `async_act` method
3. Register your tool to tool factory, and inherit `Tool` or `AsyncTool`
4. Write the `step` method to execute the abilities in the tool and generate observation, update finished Status.
```python
from typing import List, Tuple, Dict, Any
from aworld.core.common import ActionModel, Observation
from aworld.core.tool.action import ExecutableAction
from aworld.core.tool.base import ActionFactory, ToolFactory, AgentInput
from aworld.tools.template_tool import TemplateTool
from examples.common.tools.tool_action import GymAction
@ToolFactory.register(name="openai_gym", desc="gym classic control game", supported_action=GymAction)
class OpenAIGym(TemplateTool):
def step(self, action: List[ActionModel], **kwargs) -> Tuple[AgentInput, float, bool, bool, Dict[str, Any]]:
...
state, reward, terminal, truncate, info = self.env.step(action)
...
return (Observation(content=state),
reward,
terminal,
truncate,
info)
@ActionFactory.register(name=GymAction.PLAY.value.name,
desc=GymAction.PLAY.value.desc,
tool_name="openai_gym")
class Play(ExecutableAction):
"""There is only one Action, it can be implemented in the tool, registration is required here."""
```
You can view the example [code](gym_tool/openai_gym.py) to learn more.
@@ -0,0 +1,14 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
from aworld.core.tool.base import Tool, AsyncTool
from aworld.core.tool.action import ExecutableAction
from aworld.utils.common import scan_packages
scan_packages("aworld.tools", [Tool, AsyncTool, ExecutableAction])
from aworld.tools.function_tools import FunctionTools, get_function_tools, list_function_tools
from aworld.tools.function_tools_adapter import FunctionToolsMCPAdapter, get_function_tools_mcp_adapter
from aworld.tools.function_tools_executor import FunctionToolsExecutor
LOCAL_TOOLS_ENV_VAR = "LOCAL_TOOLS_ENV_VAR"
@@ -0,0 +1,75 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import json
from typing import List, Tuple, Dict, Any
from aworld.core.tool.base import AsyncTool
from aworld.core.common import Observation, ActionModel, Config
from aworld.logs.util import logger
from aworld.tools.utils import build_observation
class TemplateTool(AsyncTool):
def __init__(self, conf: Config, **kwargs) -> None:
super(TemplateTool, self).__init__(conf, **kwargs)
async def reset(self, *, seed: int | None = None, options: Dict[str, str] | None = None) -> Tuple[
Observation, dict[str, Any]]:
# from options obtain user query
return build_observation(observer=self.name(),
ability='',
content=options.get("query", None) if options else None), {}
async def do_step(self,
action: List[ActionModel],
**kwargs) -> Tuple[Observation, float, bool, bool, Dict[str, Any]]:
reward = 0
fail_error = ""
action_result = None
invalid_acts: List[int] = []
for i, act in enumerate(action):
if act.tool_name != self.name():
logger.warning(f"tool {act.tool_name} is not a {self.name()} tool!")
invalid_acts.append(i)
if invalid_acts:
for i in invalid_acts:
action[i] = None
resp = ""
try:
action_result, resp = await self.action_executor.async_execute_action(action, **kwargs)
reward = 1
except Exception as e:
fail_error = str(e)
terminated = kwargs.get("terminated", False)
for res in action_result:
if res.is_done:
terminated = res.is_done
self._finished = True
info = {"exception": fail_error}
info.update(kwargs)
if resp:
resp = json.dumps(resp)
else:
resp = action_result[0].content
observation = build_observation(observer=self.name(),
action_result=action_result,
ability=action[-1].action_name,
content=resp)
return (observation,
reward,
terminated,
kwargs.get("truncated", False),
info)
async def close(self) -> None:
pass
async def finished(self) -> bool:
# one time
return True
@@ -0,0 +1,497 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import inspect
import json
import logging
import traceback
from typing import Any, Dict, List, Optional, Union, get_type_hints
from mcp.types import TextContent, ImageContent, CallToolResult
from mcp import Tool as MCPTool
from pydantic import Field, create_model
from pydantic.fields import FieldInfo # Import FieldInfo type
from aworld.core.common import ActionResult
from aworld.logs.util import logger
# Global function tools server registry
_FUNCTION_TOOLS_REGISTRY = {}
def _register_function_tools(function_tools):
"""Register function tools server to global registry"""
_FUNCTION_TOOLS_REGISTRY[function_tools.name] = function_tools
logger.info(f"Registered FunctionTools server: {function_tools.name}")
def get_function_tools(name):
"""Get specified function tools server"""
return _FUNCTION_TOOLS_REGISTRY.get(name)
def list_function_tools():
"""List all registered function tools servers"""
return list(_FUNCTION_TOOLS_REGISTRY.keys())
class FunctionTools:
"""Function tools server, providing tool registration and calling mechanism similar to MCP
Example:
```python
# Create function tools server
function = FunctionTools("my-server", description="My function tools server")
# Define tool function
@function.tool(description="Example search function")
def search(query: str, limit: int = 10) -> str:
# Actual search logic
results = [f"Result {i} for {query}" for i in range(limit)]
return json.dumps(results)
# Using Field decorator
@function.tool(description="Example search function")
def search(
query: str = Field(description="Search query"),
limit: int = Field(10, description="Max results")
) -> str:
# Actual search logic
results = [f"Result {i} for {query}" for i in range(limit)]
return json.dumps(results)
```
"""
def __new__(cls, name: str, description: Optional[str] = None, version: str = "1.0"):
"""Implement singleton pattern, return existing instance if one with same name exists
Args:
name: Server name
description: Server description
version: Server version
"""
# Check if instance with same name already exists
if name in _FUNCTION_TOOLS_REGISTRY:
logger.info(f"Returning existing FunctionTools instance: {name}")
return _FUNCTION_TOOLS_REGISTRY[name]
# Create new instance
instance = super().__new__(cls)
return instance
def __init__(self, name: str, description: Optional[str] = None, version: str = "1.0"):
"""Initialize function tools server
Args:
name: Server name
description: Server description
version: Server version
"""
# Skip if already initialized
if hasattr(self, 'name') and self.name == name:
return
self.name = name
self.description = description or f"Function tools server: {name}"
self.version = version
self.tools = {}
# Register server to global registry
_register_function_tools(self)
def tool(self, description: Optional[str] = None, parameters: Optional[Dict[str, Any]] = None):
"""Tool function decorator
Args:
description: Tool description
parameters: Additional parameter information to supplement auto-generated parameter schema
Returns:
Decorator function
"""
def decorator(func):
# Get function metadata
tool_name = func.__name__
tool_desc = description or f"Tool function: {tool_name}"
# Auto-generate parameter schema from function signature
param_schema = self._generate_param_schema(func, parameters)
# Register tool
self._register_tool(tool_name, func, tool_desc, param_schema)
# Return original function, maintaining its callable nature
return func
return decorator
def _register_tool(self, name: str, func, description: str, param_schema: Dict[str, Any]):
"""Register tool to server"""
self.tools[name] = {
"function": func,
"description": description,
"parameters": param_schema,
"is_async": inspect.iscoroutinefunction(func)
}
logger.info(f"Registered tool '{name}' to server '{self.name}'")
def _generate_param_schema(self, func, additional_params: Optional[Dict[str, Any]] = None):
"""Generate parameter schema from function signature, maintaining MCP sample format"""
# Get function signature and type annotations
sig = inspect.signature(func)
type_hints = get_type_hints(func)
properties = {}
required = []
# Process each parameter
for name, param in sig.parameters.items():
# Skip self parameter
if name == 'self':
continue
param_type = type_hints.get(name, inspect.Parameter.empty)
has_default = param.default != inspect.Parameter.empty
# Build parameter properties
param_info = self._type_to_schema(param_type)
# Add title field - space-separated capitalized words
param_info["title"] = " ".join(word.capitalize() for word in name.split("_"))
# Handle Field decorator
if has_default and isinstance(param.default, FieldInfo):
field_info = param.default
# Add description
if field_info.description:
param_info["description"] = field_info.description
# Only add default field when Field has actual default value
if field_info.default is not None and field_info.default is not ...:
# Simple check to ensure it's not PydanticUndefined
if not str(field_info.default).endswith("PydanticUndefined"):
param_info["default"] = field_info.default
else:
# No actual default value, add to required
required.append(name)
else:
# No default value, add to required
required.append(name)
# Handle regular default values
elif has_default and param.default is not None:
param_info["default"] = param.default
else:
# Parameters without default values are required
required.append(name)
# Add description (if provided in additional_params)
if additional_params and name in additional_params:
param_info.update(additional_params[name])
properties[name] = param_info
# Special handling: ensure query_list is in required list
if "query_list" in properties and "query_list" not in required:
required.append("query_list")
# Create schema consistent with MCP sample
schema = {
"properties": properties,
"type": "object",
"required": required,
"title": func.__name__ + "Arguments"
}
return schema
def _type_to_schema(self, type_hint):
"""Convert Python type to JSON Schema type"""
import typing
# Basic type mapping
if type_hint == str:
return {"type": "string"}
elif type_hint == int:
return {"type": "integer"}
elif type_hint == float:
return {"type": "number"}
elif type_hint == bool:
return {"type": "boolean"}
elif type_hint == list or getattr(type_hint, "__origin__", None) == list:
item_type = getattr(type_hint, "__args__", [None])[0]
return {
"type": "array",
"items": self._type_to_schema(item_type)
}
elif type_hint == dict or getattr(type_hint, "__origin__", None) == dict:
return {"type": "object"}
else:
# Default to string type
return {"type": "string"}
def list_tools(self) -> List[MCPTool]:
"""List all tools and their descriptions
Returns:
List of MCPTool objects
"""
mcp_tools = []
for name, info in self.tools.items():
# Create MCPTool object, consistent with MCP sample format
mcp_tool = MCPTool(
name=name,
description=info["description"],
inputSchema=info["parameters"]
# Don't set annotations field
)
mcp_tools.append(mcp_tool)
return mcp_tools
async def call_tool_async(self, tool_name: str, arguments: Optional[Dict[str, Any]] = None):
"""Asynchronously call the specified tool function
Args:
tool_name: Tool name
arguments: Tool arguments
Returns:
Tool call result
Raises:
ValueError: When tool doesn't exist
Exception: Exceptions during tool execution
"""
if tool_name not in self.tools:
raise ValueError(f"Tool '{tool_name}' not found in server '{self.name}'")
tool_info = self.tools[tool_name]
func = tool_info["function"]
is_async = tool_info["is_async"]
arguments = arguments or {}
# Filter parameters, only keep parameters defined in the function
filtered_args = self._filter_arguments(func, arguments)
try:
# Call based on function type
if is_async:
# Async call
result = await func(**filtered_args)
else:
# Sync call
import asyncio
# Use run_in_executor to run sync function, avoid blocking
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, lambda: func(**filtered_args))
return self._format_result(result)
except Exception as e:
logger.error(f"Error calling tool '{tool_name}': {str(e)}")
logger.debug(traceback.format_exc())
# Return error message
return CallToolResult(
content=[TextContent(type="text", text=f"Error: {str(e)}")]
)
def call_tool(self, tool_name: str, arguments: Optional[Dict[str, Any]] = None):
"""Synchronously call the specified tool function
For async tools, it will run in the event loop.
Args:
tool_name: Tool name
arguments: Tool arguments
Returns:
Tool call result
Raises:
ValueError: When tool doesn't exist
Exception: Exceptions during tool execution
"""
if tool_name not in self.tools:
raise ValueError(f"Tool '{tool_name}' not found in server '{self.name}'")
tool_info = self.tools[tool_name]
func = tool_info["function"]
is_async = tool_info["is_async"]
arguments = arguments or {}
# Filter parameters, only keep parameters defined in the function
filtered_args = self._filter_arguments(func, arguments)
try:
# Call based on function type
if is_async:
import asyncio
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop and loop.is_running():
# If in a running event loop, schedule the coroutine
future = asyncio.run_coroutine_threadsafe(func(**filtered_args), loop)
result = future.result(timeout=60)
else:
# If not in a running event loop, run it directly
result = asyncio.run(func(**filtered_args))
else:
# Sync call
result = func(**filtered_args)
return self._format_result(result)
except Exception as e:
logger.error(f"Error calling tool '{tool_name}': {str(e)}")
logger.debug(traceback.format_exc())
# Return error message
return CallToolResult(
content=[TextContent(type="text", text=f"Error: {str(e)}")]
)
def _filter_arguments(self, func, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Filter arguments, only keep parameters defined in the function
Args:
func: Function to call
arguments: Input argument dictionary
Returns:
Filtered argument dictionary
"""
# Get function signature
sig = inspect.signature(func)
param_names = set(sig.parameters.keys())
# Filter arguments
filtered_args = {}
for name, value in arguments.items():
if name in param_names:
filtered_args[name] = value
else:
# Log filtered arguments
logger.debug(f"Filtered out argument '{name}' not defined in function {func.__name__}")
return filtered_args
def _format_result(self, result):
"""Format function return value to MCP compatible format"""
# If result is already MCP type, return directly
if isinstance(result, CallToolResult):
return result
# Create content list
content = []
# Handle different result types
if isinstance(result, str):
# String result
content.append(TextContent(type="text", text=result))
elif isinstance(result, bytes):
# Image data
import base64
image_base64 = base64.b64encode(result).decode('utf-8')
content.append(ImageContent(type="image", data=image_base64))
elif isinstance(result, TextContent):
# If already TextContent, use directly
content.append(result)
elif isinstance(result, dict):
if result.get("type") in ["text", "image"]:
# Dictionary already in content format
if result["type"] == "text":
# Ensure text field is plain text, without type= format issues
text_content = result.get("text", "")
# If text field looks like serialized content, try to extract actual text
if isinstance(text_content, str) and text_content.startswith("type="):
# Try to extract actual text content
import re
match = re.search(r"text=['\"](.+?)['\"]", text_content)
if match:
text_content = match.group(1)
content.append(TextContent(type="text", text=text_content))
elif result["type"] == "image":
content.append(ImageContent(type="image", data=result.get("data", "")))
elif "metadata" in result and "text" in result:
# Special handling for results with metadata
content.append(TextContent(
type="text",
text=result["text"],
metadata=result["metadata"]
))
else:
# Other dictionary types, convert to JSON
try:
content.append(TextContent(type="text", text=json.dumps(result, ensure_ascii=False)))
except:
content.append(TextContent(type="text", text=str(result)))
else:
# Other types try JSON serialization
try:
content.append(TextContent(type="text", text=json.dumps(result, ensure_ascii=False)))
except:
content.append(TextContent(type="text", text=str(result)))
return CallToolResult(content=content)
class FunctionToolsAdapter:
"""Adapter base class for adapting FunctionTools to MCPServer interface
This class provides basic adaptation functionality, but needs to be inherited and extended in specific implementations.
"""
def __init__(self, name: str):
"""Initialize adapter
Args:
name: Function tools server name
"""
self._function_tools = get_function_tools(name)
if not self._function_tools:
raise ValueError(f"FunctionTools '{name}' not found")
self._name = name
@property
def name(self) -> str:
"""Server name"""
return self._name
async def list_tools(self) -> List[MCPTool]:
"""List all tools and their descriptions"""
return self._function_tools.list_tools()
async def call_tool(self, tool_name: str, arguments: Optional[Dict[str, Any]] = None):
"""Asynchronously call the specified tool function"""
return await self._function_tools.call_tool_async(tool_name, arguments)
def to_action_result(self, result) -> ActionResult:
"""Convert call result to ActionResult
This method is used to convert MCP call results to AWorld framework's ActionResult objects.
Args:
result: MCP call result
Returns:
ActionResult object
"""
action_result = ActionResult(
content="",
keep=True
)
if result and result.content:
if len(result.content) > 0:
if isinstance(result.content[0], TextContent):
action_result = ActionResult(
content=result.content[0].text,
keep=True,
metadata=getattr(result.content[0], "metadata", {})
)
elif isinstance(result.content[0], ImageContent):
action_result = ActionResult(
content=f"data:image/jpeg;base64,{result.content[0].data}",
keep=True,
metadata=getattr(result.content[0], "metadata", {})
)
return action_result
@@ -0,0 +1,96 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import asyncio
from typing import Any, Dict, List, Optional
from mcp.types import CallToolResult, Tool as MCPTool
from aworld.core.common import ActionResult
from aworld.logs.util import logger
from aworld.mcp_client.server import MCPServer
from aworld.tools.function_tools import get_function_tools, FunctionToolsAdapter as BaseAdapter
class FunctionToolsMCPAdapter(MCPServer):
"""Adapter for FunctionTools to MCPServer interface
This adapter allows FunctionTools to be used like a standard MCPServer,
supporting list_tools and call_tool methods.
"""
def __init__(self, name: str):
"""Initialize the adapter
Args:
name: Function tool server name
"""
self._adapter = BaseAdapter(name)
self._name = self._adapter.name
self._connected = False
@property
def name(self) -> str:
"""Server name"""
return self._name
async def connect(self):
"""Connect to the server
For FunctionTools, this is a no-op since no actual connection is needed.
"""
self._connected = True
async def cleanup(self):
"""Clean up server resources
For FunctionTools, this is a no-op since there are no resources to clean up.
"""
self._connected = False
async def list_tools(self) -> List[MCPTool]:
"""List all tools and their descriptions
Returns:
List of tools
"""
if not self._connected:
await self.connect()
# Directly return the tool list from FunctionTools, which now returns MCPTool objects
return await self._adapter.list_tools()
async def call_tool(self, tool_name: str, arguments: Optional[Dict[str, Any]] = None) -> CallToolResult:
"""Call the specified tool function
Args:
tool_name: Tool name
arguments: Tool parameters
Returns:
Tool call result
"""
if not self._connected:
await self.connect()
# Use async method to call the tool
return await self._adapter.call_tool(tool_name, arguments)
def get_function_tools_mcp_adapter(name: str) -> FunctionToolsMCPAdapter:
"""Get MCP adapter for FunctionTools
Args:
name: Function tool server name
Returns:
MCPServer adapter
Raises:
ValueError: When the function tool server with the specified name does not exist
"""
function_tools = get_function_tools(name)
if not function_tools:
raise ValueError(f"FunctionTools '{name}' not found")
return FunctionToolsMCPAdapter(name)
@@ -0,0 +1,145 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import asyncio
import inspect
from typing import Any, Dict, List, Tuple, Union
from aworld.core.common import ActionModel, ActionResult
from aworld.core.tool.base import ToolActionExecutor, Tool, AsyncTool
from aworld.logs.util import logger
from aworld.tools.function_tools import get_function_tools
class FunctionToolsExecutor(ToolActionExecutor):
"""Function Tools Executor
This executor is used to execute tools defined by FunctionTools in the AWorld framework.
"""
def __init__(self, tool: Union[Tool, AsyncTool] = None):
"""Initialize the executor
Args:
tool: Tool instance
"""
super().__init__(tool)
self.function_tools_cache = {}
def execute_action(self, actions: List[ActionModel], **kwargs) -> Tuple[List[ActionResult], Any]:
"""Synchronously execute tool actions
Args:
actions: List of actions
**kwargs: Additional parameters
Returns:
List of execution results and additional information
"""
# For synchronous execution, we use asyncio to run the async method
loop = asyncio.get_event_loop()
return loop.run_until_complete(self.async_execute_action(actions, **kwargs))
async def async_execute_action(self, actions: List[ActionModel], **kwargs) -> Tuple[List[ActionResult], Any]:
"""Asynchronously execute tool actions
Args:
actions: List of actions
**kwargs: Additional parameters
Returns:
List of execution results and additional information
"""
results = []
for action in actions:
# Parse action name, format: server_name.tool_name
if "." not in action.name:
logger.warning(f"Invalid action name format: {action.name}, expected: server_name.tool_name")
results.append(ActionResult(
content=f"Error: Invalid action name format: {action.name}",
keep=False
))
continue
server_name, tool_name = action.name.split(".", 1)
# Get function tools server
function_tools = self.function_tools_cache.get(server_name)
if not function_tools:
function_tools = get_function_tools(server_name)
if not function_tools:
logger.warning(f"FunctionTools server not found: {server_name}")
results.append(ActionResult(
content=f"Error: FunctionTools server not found: {server_name}",
keep=False
))
continue
self.function_tools_cache[server_name] = function_tools
# Check if the tool exists
if tool_name not in function_tools.tools:
logger.warning(f"Tool not found: {tool_name} in server {server_name}")
results.append(ActionResult(
content=f"Error: Tool not found: {tool_name}",
keep=False
))
continue
# Get tool function
tool_info = function_tools.tools[tool_name]
func = tool_info["function"]
try:
# Parse arguments
arguments = action.arguments or {}
# Check if the function is asynchronous
is_async = inspect.iscoroutinefunction(func)
# Call the function
if is_async:
# Asynchronous call
result = await func(**arguments)
else:
# Synchronous call
result = func(**arguments)
# Process the result
mcp_result = function_tools._format_result(result)
action_result = ActionResult(
content="",
keep=True
)
# Extract content from MCP result
if mcp_result and mcp_result.content:
if len(mcp_result.content) > 0:
from mcp.types import TextContent, ImageContent
if isinstance(mcp_result.content[0], TextContent):
action_result = ActionResult(
content=mcp_result.content[0].text,
keep=True,
metadata=getattr(mcp_result.content[0], "metadata", {})
)
elif isinstance(mcp_result.content[0], ImageContent):
action_result = ActionResult(
content=f"data:image/jpeg;base64,{mcp_result.content[0].data}",
keep=True,
metadata=getattr(mcp_result.content[0], "metadata", {})
)
results.append(action_result)
except Exception as e:
logger.error(f"Error executing tool {tool_name}: {str(e)}")
import traceback
logger.debug(traceback.format_exc())
results.append(ActionResult(
content=f"Error: {str(e)}",
keep=False
))
return results, None
@@ -0,0 +1,14 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
from aworld.tools.human.human import HumanTool
from aworld.tools.human.human_handler import DefaultHumanHandler
from aworld.tools.human.actions import ExecuteAction
from aworld.tools.tool_action import HumanExecuteAction
__all__ = [
"HumanTool",
"DefaultHumanHandler",
"ExecuteAction",
"HumanExecuteAction"
]
@@ -0,0 +1,13 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
from aworld.core.tool.action_factory import ActionFactory
from aworld.core.tool.action import ExecutableAction
from aworld.tools.tool_action import HumanExecuteAction
@ActionFactory.register(name=HumanExecuteAction.HUMAN_CONFIRM.value.name,
desc=HumanExecuteAction.HUMAN_CONFIRM.value.desc,
tool_name="human_confirm")
class ExecuteAction(ExecutableAction):
"""Only one action, define it, implemented can be omitted. Act in tool."""
@@ -0,0 +1,136 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import traceback
from typing import Any, Dict, Tuple
from aworld.config import ToolConfig
from aworld.core.common import Observation, ActionModel, ActionResult
from aworld.core.event.base import Constants, TopicType, HumanMessage, Message
from aworld.core.tool.base import ToolFactory, AsyncTool
from aworld.events.util import send_message
from aworld.logs.util import logger
from aworld.tools.human.actions import HumanExecuteAction
from aworld.tools.utils import build_observation
HUMAN = "human"
@ToolFactory.register(name=HUMAN,
desc=HUMAN,
supported_action=HumanExecuteAction)
class HumanTool(AsyncTool):
def __init__(self, conf: ToolConfig, **kwargs) -> None:
"""Init document tool."""
super(HumanTool, self).__init__(conf, **kwargs)
self.cur_observation = None
self.content = None
self.keyframes = []
self.init()
self.step_finished = True
async def reset(self, *, seed: int | None = None, options: Dict[str, str] | None = None) -> Tuple[
Observation, dict[str, Any]]:
await super().reset(seed=seed, options=options)
await self.close()
self.step_finished = True
return build_observation(observer=self.name(),
ability=HumanExecuteAction.HUMAN_CONFIRM.value.name), {}
def init(self) -> None:
self.initialized = True
async def close(self) -> None:
pass
async def finished(self) -> bool:
return self.step_finished
async def do_step(self, actions: list[ActionModel], **kwargs) -> Tuple[
Observation, float, bool, bool, Dict[str, Any]]:
self.step_finished = False
reward = 0.
fail_error = ""
observation = build_observation(observer=self.name(),
ability=HumanExecuteAction.HUMAN_CONFIRM.value.name)
info = {}
try:
if not actions:
raise ValueError("actions is empty")
action = actions[0]
confirm_content = action.params.get("confirm_content", "")
if not confirm_content:
raise ValueError("content invalid")
# send human message to read human input
message, error = await self.send_human_message(confirm_content=confirm_content)
if error:
raise ValueError(f"HumanTool|send human message failed: {error}")
# hanging on human message
logger.info(f"HumanTool|waiting for human input")
result = await self.long_wait_message_state(message=message)
logger.info(f"HumanTool|human input succeed: {message.payload}")
observation.content = result
observation.action_result.append(
ActionResult(is_done=True,
success=False if error else True,
content=f"{result}",
error=f"{error}",
keep=False))
reward = 1.
except Exception as e:
fail_error = str(e)
logger.warn(f"HumanTool|failed do_step: {traceback.format_exc()}")
finally:
self.step_finished = True
info["exception"] = fail_error
info.update(kwargs)
return (observation, reward, kwargs.get("terminated", False),
kwargs.get("truncated", False), info)
async def long_wait_message_state(self, message: Message):
from aworld.runners.state_manager import HandleResult, RunNodeBusiType
from aworld.runners.state_manager import RuntimeStateManager, RunNodeStatus
state_mng = RuntimeStateManager.instance()
msg_id = message.id
# init node
state_mng.create_node(
node_id=msg_id,
busi_type=RunNodeBusiType.from_message_category(Constants.HUMAN),
busi_id=message.receiver or "",
session_id=message.session_id,
task_id=message.task_id,
msg_id=msg_id,
msg_from=message.sender)
# wait for message node completion
res_node = await state_mng.wait_for_node_completion(node_id=msg_id)
if res_node.status == RunNodeStatus.SUCCESS or res_node.results:
# get result and status from node
handle_result: HandleResult = res_node.results[0]
logger.info(f"HumanTool|human input origin result: {res_node.results}")
return handle_result.result.payload
else:
logger.debug(f"HumanTool|tool {self.name()} callback failed with node: {res_node}.")
raise ValueError(f"HumanTool|send human message failed: {res_node}")
async def send_human_message(self, confirm_content):
error = None
try:
message = HumanMessage(
category=Constants.HUMAN,
payload=confirm_content,
sender=self.name(),
session_id=self.context.session_id,
topic=TopicType.HUMAN_CONFIRM,
headers={"context": self.context}
)
await send_message(message)
return message, error
except Exception as e:
error = str(e)
logger.warning(f"HumanTool|human_confirm error: {str(e)} {traceback.format_exc()}")
return None, error
finally:
pass
@@ -0,0 +1,76 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import abc
from typing import AsyncGenerator
from aworld.core.agent.base import AgentFactory
from aworld.core.event.base import Message, Constants
from aworld.runners import HandlerFactory
from aworld.runners.handler import DefaultHandler
from aworld.runners.state_manager import RuntimeStateManager, HandleResult, RunNodeStatus
@HandlerFactory.register(name=f'__{Constants.HUMAN}__')
class DefaultHumanHandler(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 = []
def is_valid_message(self, message: Message):
if message.category != Constants.HUMAN:
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 handle_user_input(self, data):
# rewrite this method to handle user input
return input(f"Human Confirm Info: {data}\nPlease Input:")
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
human_input = await self.handle_user_input(message)
yield Message(
category=Constants.HUMAN_RESPONSE,
sender=self.name(),
receiver=message.sender,
session_id=session_id,
payload=human_input,
headers=headers,
)
return
async def post_handle(self, input:Message, output: Message) -> Message:
if not self.is_valid_message(input):
return output
if output.category is not Constants.HUMAN_RESPONSE:
return output
# update handle_result to state manager
results = [HandleResult(
name = output.category,
status = RunNodeStatus.SUCCESS,
result = output
)]
state_mng = RuntimeStateManager.instance()
state_mng.run_succeed(node_id=input.id,
result_msg="run DefaultHumanHandler succeed",
results=results)
return output
@@ -0,0 +1,146 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
from typing import Any, Dict, Tuple, Union
from aworld.core.context.base import Context
from aworld.config.conf import ToolConfig, ConfigDict
from aworld.core.common import ActionModel, Observation, ActionResult
from aworld.core.tool.base import ToolFactory, AsyncTool
from aworld.logs.util import logger
from aworld.tools.mcp_tool.executor import MCPToolExecutor
from aworld.tools.utils import build_observation
@ToolFactory.register(name="mcp",
desc="mcp execute tool",
asyn=True)
class McpTool(AsyncTool):
def __init__(self, conf: Union[Dict[str, Any], ConfigDict, ToolConfig], **kwargs) -> None:
"""Initialize the McpTool.
Args:
conf: tool config
"""
super(McpTool, self).__init__(conf, **kwargs)
self.action_executor = MCPToolExecutor(self)
async def reset(self, *, seed: int | None = None, options: Dict[str, str] | None = None) -> Tuple[
Observation, dict[str, Any]]:
self._finished = False
return build_observation(observer=self.name(), ability=""), {}
async def close(self) -> None:
self._finished = True
# default only close playwright
await self.action_executor.close(self.conf.get('close_servers', ['ms-playwright']))
async def do_step(self,
actions: list[ActionModel],
**kwargs) -> Tuple[Observation, float, bool, bool, dict[str, Any]]:
"""Step of tool.
Args:
actions: actions
**kwargs: -
Returns:
Observation, float, bool, bool, dict[str, Any]: -
"""
from aworld.core.agent.base import AgentFactory
self._finished = False
reward = 0
fail_error = ""
terminated = kwargs.get("terminated", False)
agent = AgentFactory.agent_instance(actions[0].agent_name)
if not agent:
logger.warning(
f"async_mcp_tool can not get agent,agent_name:{actions[0].agent_name}")
task_id = self.context.task_id
session_id = self.context.session_id
if not actions:
self._finished = True
observation = build_observation(observer=self.name(),
content="raw actions is empty",
ability="")
return (observation,
reward,
terminated,
kwargs.get("truncated", False),
{"exception": "actions is empty"})
mcp_actions = []
for action in actions:
tool_name = action.tool_name
if 'mcp' != tool_name:
logger.warning(f"Unsupported tool: {tool_name}. {actions}")
continue
full_tool_name = action.action_name
names = full_tool_name.split("__")
if len(names) < 2:
logger.warning(f"{full_tool_name} illegal format")
continue
action.action_name = names[1]
action.tool_name = names[0]
mcp_actions.append(action)
if not mcp_actions:
self._finished = True
action_results = [ActionResult(success=False,
content="something wrong, no mcp tool find",
error="something wrong, no mcp tool find")
for _ in actions]
observation = build_observation(observer=self.name(),
content="no valid mcp actions",
ability=actions[-1].action_name,
action_result=action_results)
return (observation, reward,
terminated,
kwargs.get("truncated", False),
{"exception": "no valid mcp actions"})
action_results = None
try:
if agent and agent.sandbox:
sand_box = agent.sandbox
action_results = await sand_box.mcpservers.call_tool(action_list=mcp_actions, task_id=task_id, session_id=session_id,context=self.context)
else:
action_results, ignore = await self.action_executor.async_execute_action(mcp_actions)
reward = 1
except Exception as e:
fail_error = str(e)
finally:
self._finished = True
observation = build_observation(observer=self.name(),
ability=actions[-1].action_name)
if action_results:
for res in action_results:
if res.is_done:
terminated = res.is_done
if res.error:
fail_error += res.error
observation.action_result = action_results
observation.content = action_results[-1].content
else:
if self.conf.get('exit_on_failure'):
raise Exception(fail_error)
else:
logger.warning(
f"{actions} no action results, fail info: {fail_error}, will use fail action results")
# every action need has the result
action_results = [ActionResult(
success=False, content=fail_error, error=fail_error) for _ in actions]
observation.action_result = action_results
observation.content = fail_error
info = {"exception": fail_error, **kwargs}
return (observation,
reward,
terminated,
kwargs.get("truncated", False),
info)
@@ -0,0 +1,280 @@
import json
import os
import traceback
import asyncio
from typing import Any, Dict, List, Tuple, Union
from mcp.types import TextContent, ImageContent
from aworld.core.common import ActionModel, ActionResult, Observation
from aworld.core.tool.base import ToolActionExecutor, Tool, AsyncTool
from aworld.logs.util import logger
from aworld.mcp_client.server import MCPServer, MCPServerSse
import aworld.mcp_client.utils as mcp_utils
from aworld.utils.common import sync_exec, find_file
class MCPToolExecutor(ToolActionExecutor):
"""A tool executor that uses MCP server to execute actions."""
def __init__(self, tool: Union[Tool, AsyncTool] = None):
"""Initialize the MCP tool executor."""
super().__init__(tool)
self.initialized = False
self.mcp_servers: Dict[str, MCPServer] = {}
self._load_mcp_config()
def _replace_env_variables(self, config):
if isinstance(config, dict):
for key, value in config.items():
if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
env_var_name = value[2:-1]
config[key] = os.getenv(env_var_name, value)
logger.info(f"Replaced {value} with {config[key]}")
elif isinstance(value, dict) or isinstance(value, list):
self._replace_env_variables(value)
elif isinstance(config, list):
for index, item in enumerate(config):
if isinstance(item, str) and item.startswith("${") and item.endswith("}"):
env_var_name = item[2:-1]
config[index] = os.getenv(env_var_name, item)
logger.info(f"Replaced {item} with {config[index]}")
elif isinstance(item, dict) or isinstance(item, list):
self._replace_env_variables(item)
def _load_mcp_config(self) -> None:
"""Load MCP server configurations from config file."""
try:
config_data = {}
if mcp_utils.MCP_SERVERS_CONFIG:
config_data=mcp_utils.MCP_SERVERS_CONFIG
else:
# Priority given to the running path.
config_path = find_file(filename='mcp.json')
if not os.path.exists(config_path):
# Use relative path for config file
current_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.normpath(os.path.join(current_dir, "../../config/mcp.json"))
logger.info(f"mcp conf path: {config_path}")
with open(config_path, "r") as f:
config_data = json.load(f)
# Replace environment variables in the configuration
self._replace_env_variables(config_data)
# Load all server configurations
for server_name, server_config in config_data.get("mcpServers", {}).items():
# Skip disabled servers
if server_config.get("disabled", False):
continue
# Handle SSE server
if "url" in server_config:
self.mcp_servers[server_name] = {
"type": "sse",
"url": server_config["url"],
"instance": None,
"timeout": server_config.get('timeout', 5.),
"sse_read_timeout": server_config.get('sse_read_timeout', 300.0),
"headers": server_config.get('headers')
}
# Handle stdio server
elif "command" in server_config:
self.mcp_servers[server_name] = {
"type": "stdio",
"command": server_config["command"],
"args": server_config.get("args", []),
"env": server_config.get("env", {}),
"cwd": server_config.get("cwd"),
"encoding": server_config.get("encoding", "utf-8"),
"encoding_error_handler": server_config.get("encoding_error_handler", "strict"),
"instance": None
}
self.initialized = True
except Exception as e:
logger.error(f"Failed to load MCP config: {traceback.format_exc()}")
async def _get_or_create_server(self, server_name: str) -> MCPServer:
"""Get an existing MCP server instance or create a new one."""
if server_name not in self.mcp_servers:
raise ValueError(f"MCP server '{server_name}' not found in configuration")
server_info = self.mcp_servers[server_name]
# If an instance already exists, check if it's available and reuse it
if server_info.get("instance"):
return server_info["instance"]
server_type = server_info.get("type", "sse")
try:
if server_type == "sse":
# Create new SSE server instance
server_params = {
"url": server_info["url"],
"timeout": server_info['timeout'],
"sse_read_timeout": server_info['sse_read_timeout'],
"headers": server_info['headers']
}
server = MCPServerSse(server_params, cache_tools_list=True, name=server_name)
elif server_type == "stdio":
# Create new stdio server instance
server_params = {
"command": server_info["command"],
"args": server_info["args"],
"env": server_info["env"],
"cwd": server_info.get("cwd"),
"encoding": server_info["encoding"],
"encoding_error_handler": server_info["encoding_error_handler"]
}
from aworld.mcp_client.server import MCPServerStdio
server = MCPServerStdio(server_params, cache_tools_list=True, name=server_name)
else:
raise ValueError(f"Unsupported MCP server type: {server_type}")
# Try to connect, with special handling for cancellation exceptions
try:
await server.connect()
except asyncio.CancelledError:
# When the task is cancelled, ensure resources are cleaned up
logger.warning(f"Connection to server '{server_name}' was cancelled")
await server.cleanup()
raise
server_info["instance"] = server
return server
except asyncio.CancelledError:
# Pass cancellation exceptions up to be handled by the caller
raise
except Exception as e:
logger.error(f"Failed to connect to MCP server '{server_name}': {e}")
raise
async def async_execute_action(self, actions: List[ActionModel], **kwargs) -> Tuple[
List[ActionResult], Any]:
"""Execute actions using the MCP server.
Args:
actions: A list of action models to execute
**kwargs: Additional arguments
Returns:
A list of action results
"""
if not self.initialized:
raise RuntimeError("MCP Tool Executor not initialized")
if not actions:
return [], None
results = []
for action in actions:
# Get server and operation information
server_name = action.tool_name
if not server_name:
raise ValueError("Missing tool_name in action model")
action_name = action.action_name
if not action_name:
raise ValueError("Missing action_name in action model")
params = action.params or {}
try:
server = self.mcp_servers.get(server_name, {}).get('instance', None)
if not server:
# Get or create MCP server
server = await self._get_or_create_server(server_name)
# Call the tool and process results
try:
result = await server.call_tool(action_name, params)
if result and result.content:
if isinstance(result.content[0], TextContent):
action_result = ActionResult(
content=result.content[0].text,
keep=True
)
elif isinstance(result.content[0], ImageContent):
action_result = ActionResult(
content=f"data:image/jpeg;base64,{result.content[0].data}",
keep=True
)
else:
action_result = ActionResult(
content="",
keep=True
)
logger.warning("Unsupported content type is error:")
else:
action_result = ActionResult(
content="",
keep=True
)
logger.warning("mcp result is null")
results.append(action_result)
except asyncio.CancelledError:
# Log cancellation exception, reset server connection to avoid async context confusion
logger.warning(f"Tool call to {action_name} on {server_name} was cancelled")
if server_name in self.mcp_servers and self.mcp_servers[server_name].get("instance"):
try:
await self.mcp_servers[server_name]["instance"].cleanup()
self.mcp_servers[server_name]["instance"] = None
except Exception as cleanup_error:
logger.error(f"Error cleaning up server after cancellation: {cleanup_error}")
# Re-raise exception to notify upper level caller
raise
except asyncio.CancelledError:
# Pass cancellation exception
logger.warning("Async execution was cancelled")
raise
except Exception as e:
# Handle general errors
error_msg = str(e)
logger.error(f"Error executing MCP action: {error_msg}")
action_result = ActionResult(
content=f"Error executing tool: {error_msg}",
keep=True
)
results.append(action_result)
return results, None
async def cleanup(self) -> None:
"""Clean up all MCP server connections."""
for server_name, server_info in self.mcp_servers.items():
if server_info.get("instance"):
try:
await server_info["instance"].cleanup()
except Exception as e:
logger.error(f"Error cleaning up MCP server {server_name}: {e}")
async def close(self, keys: List[str] = []) -> None:
if keys:
for key in keys:
if key in self.mcp_servers:
server_info = self.mcp_servers[key]
# Fixme: Have resources leak, MCP server may clean fail.
if server_info.get("type") == "stdio":
server_info["instance"] = None
continue
try:
await server_info["instance"].cleanup()
except Exception as e:
logger.error(f"Error cleaning up MCP server {key}: {e}")
def execute_action(self, actions: List[ActionModel], **kwargs) -> Tuple[
List[ActionResult], Any]:
return sync_exec(self.async_execute_action, actions, **kwargs)
@@ -0,0 +1,75 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import json
from typing import List, Tuple, Dict, Any
from aworld.core.tool.base import Tool
from aworld.core.common import Observation, ActionModel, Config
from aworld.logs.util import logger
from aworld.tools.utils import build_observation
class TemplateTool(Tool):
def __init__(self, conf: Config, **kwargs) -> None:
super(TemplateTool, self).__init__(conf, **kwargs)
def reset(self, *, seed: int | None = None, options: Dict[str, str] | None = None) -> Tuple[
Observation, dict[str, Any]]:
# from options obtain user query
return build_observation(observer=self.name(),
ability='',
content=options.get("query", None) if options else None), {}
def do_step(self,
action: List[ActionModel],
**kwargs) -> Tuple[Observation, float, bool, bool, Dict[str, Any]]:
reward = 0
fail_error = ""
action_result = None
invalid_acts: List[int] = []
for i, act in enumerate(action):
if act.tool_name != self.name():
logger.warning(f"tool {act.tool_name} is not a {self.name()} tool!")
invalid_acts.append(i)
if invalid_acts:
for i in invalid_acts:
action[i] = None
resp = ""
try:
action_result, resp = self.action_executor.execute_action(action, **kwargs)
reward = 1
except Exception as e:
fail_error = str(e)
terminated = kwargs.get("terminated", False)
for res in action_result:
if res.is_done:
terminated = res.is_done
self._finished = True
info = {"exception": fail_error}
info.update(kwargs)
if resp:
resp = json.dumps(resp)
else:
resp = action_result[0].content
observation = build_observation(observer=self.name(),
action_result=action_result,
ability=action[-1].action_name,
content=resp)
return (observation,
reward,
terminated,
kwargs.get("truncated", False),
info)
def close(self) -> None:
pass
def finished(self) -> bool:
# one time
return True
@@ -0,0 +1,16 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
from aworld.core.common import ToolActionInfo, ParamInfo
from aworld.core.tool.action import ToolAction
class HumanExecuteAction(ToolAction):
"""Definition of Human execute supported action."""
HUMAN_CONFIRM = ToolActionInfo(
name="human_confirm",
input_params={"confirm_content": ParamInfo(name="confirm_content",
type="str",
required=True,
desc="Content for user confirmation")},
desc="The main purpose of this tool is to pass given content to the user for confirmation.")
@@ -0,0 +1,27 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
from typing import Any, List
from aworld.core.common import ActionResult, Observation
DEFAULT_VIRTUAL_ENV_ID = "env_0"
def build_observation(observer: str,
ability: str,
container_id: str = None,
content: Any = None,
dom_tree: Any = None,
action_result: List[ActionResult] = [],
image: str = '',
images: List[str] = [],
**kwargs):
return Observation(container_id=container_id if container_id else DEFAULT_VIRTUAL_ENV_ID,
observer=observer,
ability=ability,
content=content,
action_result=action_result,
dom_tree=dom_tree,
image=image,
images=images,
info=kwargs)