Duibonduil commited on
Commit
7d18ad1
·
verified ·
1 Parent(s): 19bf17b

Upload 9 files

Browse files
aworld/tools/README.md ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environments
2
+
3
+ Virtual environments for execution of various tools.
4
+
5
+ Running on the local, we assume that the virtual environment completes startup when the python application starts.
6
+
7
+ ![Environment Architecture](../../readme_assets/framework_environment.png)
8
+
9
+ # Be tool
10
+
11
+ You also can convert locally defined functions into tools for use in a task.
12
+
13
+ NOTE: The function must have a return value, preferably a string, as observed content.
14
+
15
+ ```python
16
+ from pydantic import Field
17
+
18
+ from aworld.core.tool.func_to_tool import be_tool
19
+
20
+
21
+ @be_tool(tool_name='example', tool_desc="example description")
22
+ def example_1() -> str:
23
+ return "example_1"
24
+
25
+
26
+ @be_tool(tool_name='example')
27
+ def example_2(param: str) -> str:
28
+ return f"example_2{param}"
29
+
30
+
31
+ @be_tool(tool_name='example', name="example_3_alias_name", desc="example_3 description")
32
+ def example_3(param_1: str = "param",
33
+ param_2: str = Field(default="", description="param2 description")) -> str:
34
+ return f"example_3{param_1}{param_2}"
35
+ ```
36
+
37
+ The name of the tool is `example`, now, you can use these functions as tools in the framework.
38
+
39
+ # Write tool
40
+
41
+ Detailed steps for building a tool:
42
+
43
+ 1. Register action of your tool to action factory, and inherit `ExecutableAction`
44
+ 2. Optional implement the `act` or `async_act` method
45
+ 3. Register your tool to tool factory, and inherit `Tool` or `AsyncTool`
46
+ 4. Write the `step` method to execute the abilities in the tool and generate observation, update finished Status.
47
+
48
+ ```python
49
+ from typing import List, Tuple, Dict, Any
50
+
51
+ from aworld.core.common import ActionModel, Observation
52
+ from aworld.core.tool.action import ExecutableAction
53
+ from aworld.core.tool.base import ActionFactory, ToolFactory, AgentInput
54
+ from aworld.tools.template_tool import TemplateTool
55
+
56
+ from examples.tools.tool_action import GymAction
57
+
58
+
59
+ @ToolFactory.register(name="openai_gym", desc="gym classic control game", supported_action=GymAction)
60
+ class OpenAIGym(TemplateTool):
61
+ def step(self, action: List[ActionModel], **kwargs) -> Tuple[AgentInput, float, bool, bool, Dict[str, Any]]:
62
+ ...
63
+ state, reward, terminal, truncate, info = self.env.step(action)
64
+ ...
65
+ return (Observation(content=state),
66
+ reward,
67
+ terminal,
68
+ truncate,
69
+ info)
70
+
71
+
72
+ @ActionFactory.register(name=GymAction.PLAY.value.name,
73
+ desc=GymAction.PLAY.value.desc,
74
+ tool_name="openai_gym")
75
+ class Play(ExecutableAction):
76
+ """There is only one Action, it can be implemented in the tool, registration is required here."""
77
+ ```
78
+
79
+ You can view the example [code](gym_tool/openai_gym.py) to learn more.
aworld/tools/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Copyright (c) 2025 inclusionAI.
3
+
4
+ from aworld.core.tool.base import Tool, AsyncTool
5
+ from aworld.core.tool.action import ExecutableAction
6
+ from aworld.utils.common import scan_packages
7
+
8
+ scan_packages("aworld.tools", [Tool, AsyncTool, ExecutableAction])
9
+
10
+ from aworld.tools.function_tools import FunctionTools, get_function_tools, list_function_tools
11
+ from aworld.tools.function_tools_adapter import FunctionToolsMCPAdapter, get_function_tools_mcp_adapter
12
+ from aworld.tools.function_tools_executor import FunctionToolsExecutor
aworld/tools/async_template_tool.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Copyright (c) 2025 inclusionAI.
3
+ import json
4
+ from typing import List, Tuple, Dict, Any
5
+
6
+ from aworld.core.tool.base import AsyncTool
7
+ from aworld.core.common import Observation, ActionModel, Config
8
+ from aworld.logs.util import logger
9
+ from aworld.tools.utils import build_observation
10
+
11
+
12
+ class TemplateTool(AsyncTool):
13
+ def __init__(self, conf: Config, **kwargs) -> None:
14
+ super(TemplateTool, self).__init__(conf, **kwargs)
15
+
16
+ async def reset(self, *, seed: int | None = None, options: Dict[str, str] | None = None) -> Tuple[
17
+ Observation, dict[str, Any]]:
18
+ # from options obtain user query
19
+ return build_observation(observer=self.name(),
20
+ ability='',
21
+ content=options.get("query", None) if options else None), {}
22
+
23
+ async def do_step(self,
24
+ action: List[ActionModel],
25
+ **kwargs) -> Tuple[Observation, float, bool, bool, Dict[str, Any]]:
26
+ reward = 0
27
+ fail_error = ""
28
+ action_result = None
29
+
30
+ invalid_acts: List[int] = []
31
+ for i, act in enumerate(action):
32
+ if act.tool_name != self.name():
33
+ logger.warning(f"tool {act.tool_name} is not a {self.name()} tool!")
34
+ invalid_acts.append(i)
35
+
36
+ if invalid_acts:
37
+ for i in invalid_acts:
38
+ action[i] = None
39
+
40
+ resp = ""
41
+ try:
42
+ action_result, resp = await self.action_executor.async_execute_action(action, **kwargs)
43
+ reward = 1
44
+ except Exception as e:
45
+ fail_error = str(e)
46
+
47
+ terminated = kwargs.get("terminated", False)
48
+ for res in action_result:
49
+ if res.is_done:
50
+ terminated = res.is_done
51
+ self._finished = True
52
+
53
+ info = {"exception": fail_error}
54
+ info.update(kwargs)
55
+ if resp:
56
+ resp = json.dumps(resp)
57
+ else:
58
+ resp = action_result[0].content
59
+
60
+ observation = build_observation(observer=self.name(),
61
+ action_result=action_result,
62
+ ability=action[-1].action_name,
63
+ content=resp)
64
+ return (observation,
65
+ reward,
66
+ terminated,
67
+ kwargs.get("truncated", False),
68
+ info)
69
+
70
+ async def close(self) -> None:
71
+ pass
72
+
73
+ async def finished(self) -> bool:
74
+ # one time
75
+ return True
aworld/tools/function_tools.py ADDED
@@ -0,0 +1,586 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Copyright (c) 2025 inclusionAI.
3
+
4
+ import inspect
5
+ import json
6
+ import logging
7
+ import traceback
8
+ from typing import Any, Dict, List, Optional, Union, get_type_hints
9
+
10
+ from mcp.types import TextContent, ImageContent, CallToolResult
11
+ from mcp import Tool as MCPTool
12
+ from pydantic import Field, create_model
13
+ from pydantic.fields import FieldInfo # Import FieldInfo type
14
+
15
+ from aworld.core.common import ActionResult
16
+ from aworld.logs.util import logger
17
+
18
+ # Global function tools server registry
19
+ _FUNCTION_TOOLS_REGISTRY = {}
20
+
21
+ def _register_function_tools(function_tools):
22
+ """Register function tools server to global registry"""
23
+ _FUNCTION_TOOLS_REGISTRY[function_tools.name] = function_tools
24
+ logger.info(f"Registered FunctionTools server: {function_tools.name}")
25
+
26
+ def get_function_tools(name):
27
+ """Get specified function tools server"""
28
+ return _FUNCTION_TOOLS_REGISTRY.get(name)
29
+
30
+ def list_function_tools():
31
+ """List all registered function tools servers"""
32
+ return list(_FUNCTION_TOOLS_REGISTRY.keys())
33
+
34
+
35
+ class FunctionTools:
36
+ """Function tools server, providing tool registration and calling mechanism similar to MCP
37
+
38
+ Example:
39
+ ```python
40
+ # Create function tools server
41
+ function = FunctionTools("my-server", description="My function tools server")
42
+
43
+ # Define tool function
44
+ @function.tool(description="Example search function")
45
+ def search(query: str, limit: int = 10) -> str:
46
+ # Actual search logic
47
+ results = [f"Result {i} for {query}" for i in range(limit)]
48
+ return json.dumps(results)
49
+
50
+ # Using Field decorator
51
+ @function.tool(description="Example search function")
52
+ def search(
53
+ query: str = Field(description="Search query"),
54
+ limit: int = Field(10, description="Max results")
55
+ ) -> str:
56
+ # Actual search logic
57
+ results = [f"Result {i} for {query}" for i in range(limit)]
58
+ return json.dumps(results)
59
+ ```
60
+ """
61
+
62
+ def __new__(cls, name: str, description: Optional[str] = None, version: str = "1.0"):
63
+ """Implement singleton pattern, return existing instance if one with same name exists
64
+
65
+ Args:
66
+ name: Server name
67
+ description: Server description
68
+ version: Server version
69
+ """
70
+ # Check if instance with same name already exists
71
+ if name in _FUNCTION_TOOLS_REGISTRY:
72
+ logger.info(f"Returning existing FunctionTools instance: {name}")
73
+ return _FUNCTION_TOOLS_REGISTRY[name]
74
+
75
+ # Create new instance
76
+ instance = super().__new__(cls)
77
+ return instance
78
+
79
+ def __init__(self, name: str, description: Optional[str] = None, version: str = "1.0"):
80
+ """Initialize function tools server
81
+
82
+ Args:
83
+ name: Server name
84
+ description: Server description
85
+ version: Server version
86
+ """
87
+ # Skip if already initialized
88
+ if hasattr(self, 'name') and self.name == name:
89
+ return
90
+
91
+ self.name = name
92
+ self.description = description or f"Function tools server: {name}"
93
+ self.version = version
94
+ self.tools = {}
95
+
96
+ # Register server to global registry
97
+ _register_function_tools(self)
98
+
99
+ def tool(self, description: Optional[str] = None, parameters: Optional[Dict[str, Any]] = None):
100
+ """Tool function decorator
101
+
102
+ Args:
103
+ description: Tool description
104
+ parameters: Additional parameter information to supplement auto-generated parameter schema
105
+
106
+ Returns:
107
+ Decorator function
108
+ """
109
+ def decorator(func):
110
+ # Get function metadata
111
+ tool_name = func.__name__
112
+ tool_desc = description or f"Tool function: {tool_name}"
113
+
114
+ # Auto-generate parameter schema from function signature
115
+ param_schema = self._generate_param_schema(func, parameters)
116
+
117
+ # Register tool
118
+ self._register_tool(tool_name, func, tool_desc, param_schema)
119
+
120
+ # Return original function, maintaining its callable nature
121
+ return func
122
+ return decorator
123
+
124
+ def _register_tool(self, name: str, func, description: str, param_schema: Dict[str, Any]):
125
+ """Register tool to server"""
126
+ self.tools[name] = {
127
+ "function": func,
128
+ "description": description,
129
+ "parameters": param_schema,
130
+ "is_async": inspect.iscoroutinefunction(func)
131
+ }
132
+ logger.info(f"Registered tool '{name}' to server '{self.name}'")
133
+
134
+ def _generate_param_schema(self, func, additional_params: Optional[Dict[str, Any]] = None):
135
+ """Generate parameter schema from function signature, maintaining MCP sample format"""
136
+ # Get function signature and type annotations
137
+ sig = inspect.signature(func)
138
+ type_hints = get_type_hints(func)
139
+
140
+ properties = {}
141
+ required = []
142
+
143
+ # Process each parameter
144
+ for name, param in sig.parameters.items():
145
+ # Skip self parameter
146
+ if name == 'self':
147
+ continue
148
+
149
+ param_type = type_hints.get(name, inspect.Parameter.empty)
150
+ has_default = param.default != inspect.Parameter.empty
151
+
152
+ # Build parameter properties
153
+ param_info = self._type_to_schema(param_type)
154
+
155
+ # Add title field - space-separated capitalized words
156
+ param_info["title"] = " ".join(word.capitalize() for word in name.split("_"))
157
+
158
+ # Handle Field decorator
159
+ if has_default and isinstance(param.default, FieldInfo):
160
+ field_info = param.default
161
+
162
+ # Add description
163
+ if field_info.description:
164
+ param_info["description"] = field_info.description
165
+
166
+ # Only add default field when Field has actual default value
167
+ if field_info.default is not None and field_info.default is not ...:
168
+ # Simple check to ensure it's not PydanticUndefined
169
+ if not str(field_info.default).endswith("PydanticUndefined"):
170
+ param_info["default"] = field_info.default
171
+ else:
172
+ # No actual default value, add to required
173
+ required.append(name)
174
+ else:
175
+ # No default value, add to required
176
+ required.append(name)
177
+ # Handle regular default values
178
+ elif has_default and param.default is not None:
179
+ param_info["default"] = param.default
180
+ else:
181
+ # Parameters without default values are required
182
+ required.append(name)
183
+
184
+ # Add description (if provided in additional_params)
185
+ if additional_params and name in additional_params:
186
+ param_info.update(additional_params[name])
187
+
188
+ properties[name] = param_info
189
+
190
+ # Special handling: ensure query_list is in required list
191
+ if "query_list" in properties and "query_list" not in required:
192
+ required.append("query_list")
193
+
194
+ # Create schema consistent with MCP sample
195
+ schema = {
196
+ "properties": properties,
197
+ "type": "object",
198
+ "required": required,
199
+ "title": func.__name__ + "Arguments"
200
+ }
201
+
202
+ return schema
203
+
204
+ def _type_to_schema(self, type_hint):
205
+ """Convert Python type to JSON Schema type"""
206
+ import typing
207
+
208
+ # Basic type mapping
209
+ if type_hint == str:
210
+ return {"type": "string"}
211
+ elif type_hint == int:
212
+ return {"type": "integer"}
213
+ elif type_hint == float:
214
+ return {"type": "number"}
215
+ elif type_hint == bool:
216
+ return {"type": "boolean"}
217
+ elif type_hint == list or getattr(type_hint, "__origin__", None) == list:
218
+ item_type = getattr(type_hint, "__args__", [None])[0]
219
+ return {
220
+ "type": "array",
221
+ "items": self._type_to_schema(item_type)
222
+ }
223
+ elif type_hint == dict or getattr(type_hint, "__origin__", None) == dict:
224
+ return {"type": "object"}
225
+ else:
226
+ # Default to string type
227
+ return {"type": "string"}
228
+
229
+ def list_tools(self) -> List[MCPTool]:
230
+ """List all tools and their descriptions
231
+
232
+ Returns:
233
+ List of MCPTool objects
234
+ """
235
+ mcp_tools = []
236
+ for name, info in self.tools.items():
237
+ # Create MCPTool object, consistent with MCP sample format
238
+ mcp_tool = MCPTool(
239
+ name=name,
240
+ description=info["description"],
241
+ inputSchema=info["parameters"]
242
+ # Don't set annotations field
243
+ )
244
+ mcp_tools.append(mcp_tool)
245
+
246
+ return mcp_tools
247
+
248
+ async def call_tool_async(self, tool_name: str, arguments: Optional[Dict[str, Any]] = None):
249
+ """Asynchronously call the specified tool function
250
+
251
+ Args:
252
+ tool_name: Tool name
253
+ arguments: Tool arguments
254
+
255
+ Returns:
256
+ Tool call result
257
+
258
+ Raises:
259
+ ValueError: When tool doesn't exist
260
+ Exception: Exceptions during tool execution
261
+ """
262
+ if tool_name not in self.tools:
263
+ raise ValueError(f"Tool '{tool_name}' not found in server '{self.name}'")
264
+
265
+ tool_info = self.tools[tool_name]
266
+ func = tool_info["function"]
267
+ is_async = tool_info["is_async"]
268
+ arguments = arguments or {}
269
+
270
+ # Filter parameters, only keep parameters defined in the function
271
+ filtered_args = self._filter_arguments(func, arguments)
272
+
273
+ try:
274
+ # Call based on function type
275
+ if is_async:
276
+ # Async call
277
+ result = await func(**filtered_args)
278
+ else:
279
+ # Sync call
280
+ import asyncio
281
+ # Use run_in_executor to run sync function, avoid blocking
282
+ loop = asyncio.get_event_loop()
283
+ result = await loop.run_in_executor(None, lambda: func(**filtered_args))
284
+
285
+ return self._format_result(result)
286
+ except Exception as e:
287
+ logger.error(f"Error calling tool '{tool_name}': {str(e)}")
288
+ logger.debug(traceback.format_exc())
289
+ # Return error message
290
+ return CallToolResult(
291
+ content=[TextContent(type="text", text=f"Error: {str(e)}")]
292
+ )
293
+
294
+ def call_tool(self, tool_name: str, arguments: Optional[Dict[str, Any]] = None):
295
+ """Synchronously call the specified tool function
296
+
297
+ For async tools, it will run in the event loop.
298
+
299
+ Args:
300
+ tool_name: Tool name
301
+ arguments: Tool arguments
302
+
303
+ Returns:
304
+ Tool call result
305
+
306
+ Raises:
307
+ ValueError: When tool doesn't exist
308
+ Exception: Exceptions during tool execution
309
+ """
310
+ if tool_name not in self.tools:
311
+ raise ValueError(f"Tool '{tool_name}' not found in server '{self.name}'")
312
+
313
+ tool_info = self.tools[tool_name]
314
+ func = tool_info["function"]
315
+ is_async = tool_info["is_async"]
316
+ arguments = arguments or {}
317
+
318
+ # Filter parameters, only keep parameters defined in the function
319
+ filtered_args = self._filter_arguments(func, arguments)
320
+
321
+ try:
322
+ # Call based on function type
323
+ if is_async:
324
+ # Async functions need to run in event loop
325
+ import asyncio
326
+
327
+ # Safer way to handle async calls
328
+ try:
329
+ # Check if already in event loop
330
+ running_loop = asyncio._get_running_loop()
331
+ if running_loop is not None:
332
+ # Already in event loop, use nest_asyncio to solve nesting issues
333
+ try:
334
+ import nest_asyncio
335
+ nest_asyncio.apply()
336
+ logger.debug(f"Applied nest_asyncio for {tool_name}")
337
+ except ImportError:
338
+ logger.warning("nest_asyncio not available, using alternative approach")
339
+ # If nest_asyncio not available, use alternative method
340
+ # Create new thread to run async function
341
+ import threading
342
+ import queue
343
+
344
+ result_queue = queue.Queue()
345
+
346
+ def run_async_in_thread():
347
+ try:
348
+ # Create new event loop
349
+ new_loop = asyncio.new_event_loop()
350
+ asyncio.set_event_loop(new_loop)
351
+ # Run async function
352
+ result = new_loop.run_until_complete(func(**filtered_args))
353
+ # Put in queue
354
+ result_queue.put(("result", result))
355
+ except Exception as e:
356
+ # Put in queue
357
+ result_queue.put(("error", e))
358
+ finally:
359
+ new_loop.close()
360
+
361
+ # Start thread
362
+ thread = threading.Thread(target=run_async_in_thread)
363
+ thread.start()
364
+ thread.join(timeout=60) # Wait up to 60 seconds
365
+
366
+ if thread.is_alive():
367
+ raise TimeoutError(f"Timeout waiting for {tool_name} to complete")
368
+
369
+ # Get result
370
+ result_type, result_value = result_queue.get()
371
+ if result_type == "error":
372
+ raise result_value
373
+ result = result_value
374
+ return self._format_result(result)
375
+
376
+ # Get or create event loop
377
+ try:
378
+ loop = asyncio.get_event_loop()
379
+ except RuntimeError:
380
+ loop = asyncio.new_event_loop()
381
+ asyncio.set_event_loop(loop)
382
+
383
+ # Run async function
384
+ result = loop.run_until_complete(func(**filtered_args))
385
+
386
+ except RuntimeError as e:
387
+ if "This event loop is already running" in str(e):
388
+ # If event loop already running, use thread method
389
+ logger.warning(f"Event loop already running, using thread approach for {tool_name}")
390
+ import threading
391
+ import queue
392
+
393
+ result_queue = queue.Queue()
394
+
395
+ def run_async_in_thread():
396
+ try:
397
+ # Create new event loop
398
+ new_loop = asyncio.new_event_loop()
399
+ asyncio.set_event_loop(new_loop)
400
+ # Run async function
401
+ result = new_loop.run_until_complete(func(**filtered_args))
402
+ # Put in queue
403
+ result_queue.put(("result", result))
404
+ except Exception as e:
405
+ # Put in queue
406
+ result_queue.put(("error", e))
407
+ finally:
408
+ new_loop.close()
409
+
410
+ # Start thread
411
+ thread = threading.Thread(target=run_async_in_thread)
412
+ thread.start()
413
+ thread.join(timeout=60) # Wait up to 60 seconds
414
+
415
+ if thread.is_alive():
416
+ raise TimeoutError(f"Timeout waiting for {tool_name} to complete")
417
+
418
+ # Get result
419
+ result_type, result_value = result_queue.get()
420
+ if result_type == "error":
421
+ raise result_value
422
+ result = result_value
423
+ else:
424
+ # Other RuntimeError
425
+ raise
426
+ else:
427
+ # Sync call
428
+ result = func(**filtered_args)
429
+
430
+ return self._format_result(result)
431
+ except Exception as e:
432
+ logger.error(f"Error calling tool '{tool_name}': {str(e)}")
433
+ logger.debug(traceback.format_exc())
434
+ # Return error message
435
+ return CallToolResult(
436
+ content=[TextContent(type="text", text=f"Error: {str(e)}")]
437
+ )
438
+
439
+ def _filter_arguments(self, func, arguments: Dict[str, Any]) -> Dict[str, Any]:
440
+ """Filter arguments, only keep parameters defined in the function
441
+
442
+ Args:
443
+ func: Function to call
444
+ arguments: Input argument dictionary
445
+
446
+ Returns:
447
+ Filtered argument dictionary
448
+ """
449
+ # Get function signature
450
+ sig = inspect.signature(func)
451
+ param_names = set(sig.parameters.keys())
452
+
453
+ # Filter arguments
454
+ filtered_args = {}
455
+ for name, value in arguments.items():
456
+ if name in param_names:
457
+ filtered_args[name] = value
458
+ else:
459
+ # Log filtered arguments
460
+ logger.debug(f"Filtered out argument '{name}' not defined in function {func.__name__}")
461
+
462
+ return filtered_args
463
+
464
+ def _format_result(self, result):
465
+ """Format function return value to MCP compatible format"""
466
+ # If result is already MCP type, return directly
467
+ if isinstance(result, CallToolResult):
468
+ return result
469
+
470
+ # Create content list
471
+ content = []
472
+
473
+ # Handle different result types
474
+ if isinstance(result, str):
475
+ # String result
476
+ content.append(TextContent(type="text", text=result))
477
+ elif isinstance(result, bytes):
478
+ # Image data
479
+ import base64
480
+ image_base64 = base64.b64encode(result).decode('utf-8')
481
+ content.append(ImageContent(type="image", data=image_base64))
482
+ elif isinstance(result, TextContent):
483
+ # If already TextContent, use directly
484
+ content.append(result)
485
+ elif isinstance(result, dict):
486
+ if result.get("type") in ["text", "image"]:
487
+ # Dictionary already in content format
488
+ if result["type"] == "text":
489
+ # Ensure text field is plain text, without type= format issues
490
+ text_content = result.get("text", "")
491
+ # If text field looks like serialized content, try to extract actual text
492
+ if isinstance(text_content, str) and text_content.startswith("type="):
493
+ # Try to extract actual text content
494
+ import re
495
+ match = re.search(r"text=['\"](.+?)['\"]", text_content)
496
+ if match:
497
+ text_content = match.group(1)
498
+
499
+ content.append(TextContent(type="text", text=text_content))
500
+ elif result["type"] == "image":
501
+ content.append(ImageContent(type="image", data=result.get("data", "")))
502
+ elif "metadata" in result and "text" in result:
503
+ # Special handling for results with metadata
504
+ content.append(TextContent(
505
+ type="text",
506
+ text=result["text"],
507
+ metadata=result["metadata"]
508
+ ))
509
+ else:
510
+ # Other dictionary types, convert to JSON
511
+ try:
512
+ content.append(TextContent(type="text", text=json.dumps(result, ensure_ascii=False)))
513
+ except:
514
+ content.append(TextContent(type="text", text=str(result)))
515
+ else:
516
+ # Other types try JSON serialization
517
+ try:
518
+ content.append(TextContent(type="text", text=json.dumps(result, ensure_ascii=False)))
519
+ except:
520
+ content.append(TextContent(type="text", text=str(result)))
521
+
522
+ return CallToolResult(content=content)
523
+
524
+
525
+ class FunctionToolsAdapter:
526
+ """Adapter base class for adapting FunctionTools to MCPServer interface
527
+
528
+ This class provides basic adaptation functionality, but needs to be inherited and extended in specific implementations.
529
+ """
530
+
531
+ def __init__(self, name: str):
532
+ """Initialize adapter
533
+
534
+ Args:
535
+ name: Function tools server name
536
+ """
537
+ self._function_tools = get_function_tools(name)
538
+ if not self._function_tools:
539
+ raise ValueError(f"FunctionTools '{name}' not found")
540
+ self._name = name
541
+
542
+ @property
543
+ def name(self) -> str:
544
+ """Server name"""
545
+ return self._name
546
+
547
+ async def list_tools(self) -> List[MCPTool]:
548
+ """List all tools and their descriptions"""
549
+ return self._function_tools.list_tools()
550
+
551
+ async def call_tool(self, tool_name: str, arguments: Optional[Dict[str, Any]] = None):
552
+ """Asynchronously call the specified tool function"""
553
+ return await self._function_tools.call_tool_async(tool_name, arguments)
554
+
555
+ def to_action_result(self, result) -> ActionResult:
556
+ """Convert call result to ActionResult
557
+
558
+ This method is used to convert MCP call results to AWorld framework's ActionResult objects.
559
+
560
+ Args:
561
+ result: MCP call result
562
+
563
+ Returns:
564
+ ActionResult object
565
+ """
566
+ action_result = ActionResult(
567
+ content="",
568
+ keep=True
569
+ )
570
+
571
+ if result and result.content:
572
+ if len(result.content) > 0:
573
+ if isinstance(result.content[0], TextContent):
574
+ action_result = ActionResult(
575
+ content=result.content[0].text,
576
+ keep=True,
577
+ metadata=getattr(result.content[0], "metadata", {})
578
+ )
579
+ elif isinstance(result.content[0], ImageContent):
580
+ action_result = ActionResult(
581
+ content=f"data:image/jpeg;base64,{result.content[0].data}",
582
+ keep=True,
583
+ metadata=getattr(result.content[0], "metadata", {})
584
+ )
585
+
586
+ return action_result
aworld/tools/function_tools_adapter.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Copyright (c) 2025 inclusionAI.
3
+
4
+ import asyncio
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from mcp.types import CallToolResult, Tool as MCPTool
8
+
9
+ from aworld.core.common import ActionResult
10
+ from aworld.logs.util import logger
11
+ from aworld.mcp_client.server import MCPServer
12
+ from aworld.tools.function_tools import get_function_tools, FunctionToolsAdapter as BaseAdapter
13
+
14
+
15
+ class FunctionToolsMCPAdapter(MCPServer):
16
+ """Adapter for FunctionTools to MCPServer interface
17
+
18
+ This adapter allows FunctionTools to be used like a standard MCPServer,
19
+ supporting list_tools and call_tool methods.
20
+ """
21
+
22
+ def __init__(self, name: str):
23
+ """Initialize the adapter
24
+
25
+ Args:
26
+ name: Function tool server name
27
+ """
28
+ self._adapter = BaseAdapter(name)
29
+ self._name = self._adapter.name
30
+ self._connected = False
31
+
32
+ @property
33
+ def name(self) -> str:
34
+ """Server name"""
35
+ return self._name
36
+
37
+ async def connect(self):
38
+ """Connect to the server
39
+
40
+ For FunctionTools, this is a no-op since no actual connection is needed.
41
+ """
42
+ self._connected = True
43
+
44
+ async def cleanup(self):
45
+ """Clean up server resources
46
+
47
+ For FunctionTools, this is a no-op since there are no resources to clean up.
48
+ """
49
+ self._connected = False
50
+
51
+ async def list_tools(self) -> List[MCPTool]:
52
+ """List all tools and their descriptions
53
+
54
+ Returns:
55
+ List of tools
56
+ """
57
+ if not self._connected:
58
+ await self.connect()
59
+
60
+ # Directly return the tool list from FunctionTools, which now returns MCPTool objects
61
+ return await self._adapter.list_tools()
62
+
63
+ async def call_tool(self, tool_name: str, arguments: Optional[Dict[str, Any]] = None) -> CallToolResult:
64
+ """Call the specified tool function
65
+
66
+ Args:
67
+ tool_name: Tool name
68
+ arguments: Tool parameters
69
+
70
+ Returns:
71
+ Tool call result
72
+ """
73
+ if not self._connected:
74
+ await self.connect()
75
+
76
+ # Use async method to call the tool
77
+ return await self._adapter.call_tool(tool_name, arguments)
78
+
79
+
80
+ def get_function_tools_mcp_adapter(name: str) -> FunctionToolsMCPAdapter:
81
+ """Get MCP adapter for FunctionTools
82
+
83
+ Args:
84
+ name: Function tool server name
85
+
86
+ Returns:
87
+ MCPServer adapter
88
+
89
+ Raises:
90
+ ValueError: When the function tool server with the specified name does not exist
91
+ """
92
+ function_tools = get_function_tools(name)
93
+ if not function_tools:
94
+ raise ValueError(f"FunctionTools '{name}' not found")
95
+
96
+ return FunctionToolsMCPAdapter(name)
aworld/tools/function_tools_executor.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Copyright (c) 2025 inclusionAI.
3
+
4
+ import asyncio
5
+ import inspect
6
+ from typing import Any, Dict, List, Tuple, Union
7
+
8
+ from aworld.core.common import ActionModel, ActionResult
9
+ from aworld.core.tool.base import ToolActionExecutor, Tool, AsyncTool
10
+ from aworld.logs.util import logger
11
+ from aworld.tools.function_tools import get_function_tools
12
+
13
+
14
+ class FunctionToolsExecutor(ToolActionExecutor):
15
+ """Function Tools Executor
16
+
17
+ This executor is used to execute tools defined by FunctionTools in the AWorld framework.
18
+ """
19
+
20
+ def __init__(self, tool: Union[Tool, AsyncTool] = None):
21
+ """Initialize the executor
22
+
23
+ Args:
24
+ tool: Tool instance
25
+ """
26
+ super().__init__(tool)
27
+ self.function_tools_cache = {}
28
+
29
+ def execute_action(self, actions: List[ActionModel], **kwargs) -> Tuple[List[ActionResult], Any]:
30
+ """Synchronously execute tool actions
31
+
32
+ Args:
33
+ actions: List of actions
34
+ **kwargs: Additional parameters
35
+
36
+ Returns:
37
+ List of execution results and additional information
38
+ """
39
+ # For synchronous execution, we use asyncio to run the async method
40
+ loop = asyncio.get_event_loop()
41
+ return loop.run_until_complete(self.async_execute_action(actions, **kwargs))
42
+
43
+ async def async_execute_action(self, actions: List[ActionModel], **kwargs) -> Tuple[List[ActionResult], Any]:
44
+ """Asynchronously execute tool actions
45
+
46
+ Args:
47
+ actions: List of actions
48
+ **kwargs: Additional parameters
49
+
50
+ Returns:
51
+ List of execution results and additional information
52
+ """
53
+ results = []
54
+
55
+ for action in actions:
56
+ # Parse action name, format: server_name.tool_name
57
+ if "." not in action.name:
58
+ logger.warning(f"Invalid action name format: {action.name}, expected: server_name.tool_name")
59
+ results.append(ActionResult(
60
+ content=f"Error: Invalid action name format: {action.name}",
61
+ keep=False
62
+ ))
63
+ continue
64
+
65
+ server_name, tool_name = action.name.split(".", 1)
66
+
67
+ # Get function tools server
68
+ function_tools = self.function_tools_cache.get(server_name)
69
+ if not function_tools:
70
+ function_tools = get_function_tools(server_name)
71
+ if not function_tools:
72
+ logger.warning(f"FunctionTools server not found: {server_name}")
73
+ results.append(ActionResult(
74
+ content=f"Error: FunctionTools server not found: {server_name}",
75
+ keep=False
76
+ ))
77
+ continue
78
+ self.function_tools_cache[server_name] = function_tools
79
+
80
+ # Check if the tool exists
81
+ if tool_name not in function_tools.tools:
82
+ logger.warning(f"Tool not found: {tool_name} in server {server_name}")
83
+ results.append(ActionResult(
84
+ content=f"Error: Tool not found: {tool_name}",
85
+ keep=False
86
+ ))
87
+ continue
88
+
89
+ # Get tool function
90
+ tool_info = function_tools.tools[tool_name]
91
+ func = tool_info["function"]
92
+
93
+ try:
94
+ # Parse arguments
95
+ arguments = action.arguments or {}
96
+
97
+ # Check if the function is asynchronous
98
+ is_async = inspect.iscoroutinefunction(func)
99
+
100
+ # Call the function
101
+ if is_async:
102
+ # Asynchronous call
103
+ result = await func(**arguments)
104
+ else:
105
+ # Synchronous call
106
+ result = func(**arguments)
107
+
108
+ # Process the result
109
+ mcp_result = function_tools._format_result(result)
110
+ action_result = ActionResult(
111
+ content="",
112
+ keep=True
113
+ )
114
+
115
+ # Extract content from MCP result
116
+ if mcp_result and mcp_result.content:
117
+ if len(mcp_result.content) > 0:
118
+ from mcp.types import TextContent, ImageContent
119
+
120
+ if isinstance(mcp_result.content[0], TextContent):
121
+ action_result = ActionResult(
122
+ content=mcp_result.content[0].text,
123
+ keep=True,
124
+ metadata=getattr(mcp_result.content[0], "metadata", {})
125
+ )
126
+ elif isinstance(mcp_result.content[0], ImageContent):
127
+ action_result = ActionResult(
128
+ content=f"data:image/jpeg;base64,{mcp_result.content[0].data}",
129
+ keep=True,
130
+ metadata=getattr(mcp_result.content[0], "metadata", {})
131
+ )
132
+
133
+ results.append(action_result)
134
+
135
+ except Exception as e:
136
+ logger.error(f"Error executing tool {tool_name}: {str(e)}")
137
+ import traceback
138
+ logger.debug(traceback.format_exc())
139
+
140
+ results.append(ActionResult(
141
+ content=f"Error: {str(e)}",
142
+ keep=False
143
+ ))
144
+
145
+ return results, None
aworld/tools/template_tool.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Copyright (c) 2025 inclusionAI.
3
+ import json
4
+ from typing import List, Tuple, Dict, Any
5
+
6
+ from aworld.core.tool.base import Tool
7
+ from aworld.core.common import Observation, ActionModel, Config
8
+ from aworld.logs.util import logger
9
+ from aworld.tools.utils import build_observation
10
+
11
+
12
+ class TemplateTool(Tool):
13
+ def __init__(self, conf: Config, **kwargs) -> None:
14
+ super(TemplateTool, self).__init__(conf, **kwargs)
15
+
16
+ def reset(self, *, seed: int | None = None, options: Dict[str, str] | None = None) -> Tuple[
17
+ Observation, dict[str, Any]]:
18
+ # from options obtain user query
19
+ return build_observation(observer=self.name(),
20
+ ability='',
21
+ content=options.get("query", None) if options else None), {}
22
+
23
+ def do_step(self,
24
+ action: List[ActionModel],
25
+ **kwargs) -> Tuple[Observation, float, bool, bool, Dict[str, Any]]:
26
+ reward = 0
27
+ fail_error = ""
28
+ action_result = None
29
+
30
+ invalid_acts: List[int] = []
31
+ for i, act in enumerate(action):
32
+ if act.tool_name != self.name():
33
+ logger.warning(f"tool {act.tool_name} is not a {self.name()} tool!")
34
+ invalid_acts.append(i)
35
+
36
+ if invalid_acts:
37
+ for i in invalid_acts:
38
+ action[i] = None
39
+
40
+ resp = ""
41
+ try:
42
+ action_result, resp = self.action_executor.execute_action(action, **kwargs)
43
+ reward = 1
44
+ except Exception as e:
45
+ fail_error = str(e)
46
+
47
+ terminated = kwargs.get("terminated", False)
48
+ for res in action_result:
49
+ if res.is_done:
50
+ terminated = res.is_done
51
+ self._finished = True
52
+
53
+ info = {"exception": fail_error}
54
+ info.update(kwargs)
55
+ if resp:
56
+ resp = json.dumps(resp)
57
+ else:
58
+ resp = action_result[0].content
59
+
60
+ observation = build_observation(observer=self.name(),
61
+ action_result=action_result,
62
+ ability=action[-1].action_name,
63
+ content=resp)
64
+ return (observation,
65
+ reward,
66
+ terminated,
67
+ kwargs.get("truncated", False),
68
+ info)
69
+
70
+ def close(self) -> None:
71
+ pass
72
+
73
+ def finished(self) -> bool:
74
+ # one time
75
+ return True
aworld/tools/tool_action.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Copyright (c) 2025 inclusionAI.
3
+
4
+ from aworld.core.common import ToolActionInfo, ParamInfo
5
+ from aworld.core.tool.action import ToolAction
6
+
7
+
8
+ class HumanExecuteAction(ToolAction):
9
+ """Definition of Human execute supported action."""
10
+ HUMAN_CONFIRM = ToolActionInfo(
11
+ name="human_confirm",
12
+ input_params={"confirm_content": ParamInfo(name="confirm_content",
13
+ type="str",
14
+ required=True,
15
+ desc="Content for user confirmation")},
16
+ desc="The main purpose of this tool is to pass given content to the user for confirmation.")
aworld/tools/utils.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Copyright (c) 2025 inclusionAI.
3
+ from typing import Any, List
4
+
5
+ from aworld.core.common import ActionResult, Observation
6
+
7
+ DEFAULT_VIRTUAL_ENV_ID = "env_0"
8
+
9
+
10
+ def build_observation(observer: str,
11
+ ability: str,
12
+ container_id: str = None,
13
+ content: Any = None,
14
+ dom_tree: Any = None,
15
+ action_result: List[ActionResult] = [],
16
+ image: str = '',
17
+ images: List[str] = [],
18
+ **kwargs):
19
+ return Observation(container_id=container_id if container_id else DEFAULT_VIRTUAL_ENV_ID,
20
+ observer=observer,
21
+ ability=ability,
22
+ content=content,
23
+ action_result=action_result,
24
+ dom_tree=dom_tree,
25
+ image=image,
26
+ images=images,
27
+ info=kwargs)