Coverage for agentos/tools/fusion.py: 32%
157 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 19:15 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 19:15 +0800
1"""
2Fusion Toolkit for NexusAgent.
4Multi-tool coordination system. Allows agents to use
5multiple tools in sequence or parallel, with automatic
6result fusion and conflict resolution.
7"""
9from __future__ import annotations
11import asyncio
12import time
13import uuid
14from collections.abc import Callable
15from dataclasses import dataclass, field
16from enum import StrEnum
17from typing import Any
20class FusionMode(StrEnum):
21 """Tool fusion modes."""
23 SEQUENTIAL = "sequential" # Run tools one by one
24 PARALLEL = "parallel" # Run tools in parallel
25 CHAIN = "chain" # Output of one feeds into next
28@dataclass
29class ToolSpec:
30 """
31 Tool specification.
33 Attributes:
34 name: Tool name
35 description: Tool description
36 func: Tool function
37 parameters: Parameter schema
38 timeout: Execution timeout
39 retry_count: Number of retries
40 """
42 name: str
43 description: str = ""
44 func: Callable[..., Any] = None
45 parameters: dict[str, Any] = field(default_factory=dict)
46 timeout: float = 30.0
47 retry_count: int = 0
49 def to_dict(self) -> dict[str, Any]:
50 """Convert to dict."""
51 return {
52 "name": self.name,
53 "description": self.description,
54 "parameters": self.parameters,
55 "timeout": self.timeout,
56 "retry_count": self.retry_count,
57 }
60@dataclass
61class ToolResult:
62 """
63 Result of a single tool execution.
65 Attributes:
66 tool_name: Name of the tool
67 success: Whether execution succeeded
68 output: Tool output
69 error: Error message (if failed)
70 duration: Execution duration
71 """
73 tool_name: str
74 success: bool
75 output: Any = None
76 error: str | None = None
77 duration: float = 0.0
80@dataclass
81class FusionResult:
82 """
83 Result of tool fusion.
85 Attributes:
86 id: Unique identifier
87 mode: Fusion mode used
88 results: List of individual tool results
89 fused_output: Fused final output
90 total_duration: Total execution duration
91 success: Whether fusion succeeded
92 """
94 id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
95 mode: FusionMode = FusionMode.SEQUENTIAL
96 results: list[ToolResult] = field(default_factory=list)
97 fused_output: Any = None
98 total_duration: float = 0.0
99 success: bool = True
101 def to_dict(self) -> dict[str, Any]:
102 """Convert to dict."""
103 return {
104 "id": self.id,
105 "mode": self.mode.value,
106 "results": [
107 {
108 "tool_name": r.tool_name,
109 "success": r.success,
110 "output": r.output,
111 "error": r.error,
112 "duration": r.duration,
113 }
114 for r in self.results
115 ],
116 "fused_output": self.fused_output,
117 "total_duration": self.total_duration,
118 "success": self.success,
119 }
122class FusionToolkit:
123 """
124 Multi-tool coordination system.
126 Allows agents to use multiple tools in different modes:
127 - Sequential: Run tools one by one
128 - Parallel: Run tools in parallel
129 - Chain: Output of one feeds into next
131 Usage:
132 toolkit = FusionToolkit()
133 toolkit.register(ToolSpec(name="search", func=search_func))
134 toolkit.register(ToolSpec(name="summarize", func=summarize_func))
136 # Sequential execution
137 result = await toolkit.execute(["search", "summarize"], {"query": "AI"})
139 # Parallel execution
140 result = await toolkit.execute_parallel(["search", "summarize"], {"query": "AI"})
141 """
143 def __init__(self, default_timeout: float = 30.0):
144 """
145 Initialize fusion toolkit.
147 Args:
148 default_timeout: Default tool timeout
149 """
150 self._tools: dict[str, ToolSpec] = {}
151 self._default_timeout = default_timeout
153 def register(self, tool: ToolSpec) -> None:
154 """
155 Register a tool.
157 Args:
158 tool: Tool specification
159 """
160 self._tools[tool.name] = tool
162 def unregister(self, tool_name: str) -> bool:
163 """
164 Unregister a tool.
166 Args:
167 tool_name: Tool name
169 Returns:
170 True if unregistered, False if not found
171 """
172 if tool_name in self._tools:
173 del self._tools[tool_name]
174 return True
175 return False
177 def get_tool(self, tool_name: str) -> ToolSpec | None:
178 """
179 Get a tool by name.
181 Args:
182 tool_name: Tool name
184 Returns:
185 ToolSpec if found, None otherwise
186 """
187 return self._tools.get(tool_name)
189 def list_tools(self) -> list[ToolSpec]:
190 """
191 List all registered tools.
193 Returns:
194 List of ToolSpec
195 """
196 return list(self._tools.values())
198 async def execute(
199 self,
200 tool_names: list[str],
201 inputs: dict[str, Any],
202 mode: FusionMode = FusionMode.SEQUENTIAL,
203 ) -> FusionResult:
204 """
205 Execute multiple tools.
207 Args:
208 tool_names: List of tool names
209 inputs: Input parameters
210 mode: Fusion mode
212 Returns:
213 FusionResult
214 """
215 start_time = time.time()
217 if mode == FusionMode.SEQUENTIAL:
218 result = await self._execute_sequential(tool_names, inputs)
219 elif mode == FusionMode.PARALLEL:
220 result = await self._execute_parallel(tool_names, inputs)
221 elif mode == FusionMode.CHAIN:
222 result = await self._execute_chain(tool_names, inputs)
223 else:
224 raise ValueError(f"Unknown fusion mode: {mode}")
226 result.total_duration = time.time() - start_time
228 return result
230 async def _execute_sequential(
231 self,
232 tool_names: list[str],
233 inputs: dict[str, Any],
234 ) -> FusionResult:
235 """Execute tools sequentially."""
236 result = FusionResult(mode=FusionMode.SEQUENTIAL)
238 for tool_name in tool_names:
239 tool = self._tools.get(tool_name)
240 if not tool:
241 result.results.append(
242 ToolResult(
243 tool_name=tool_name,
244 success=False,
245 error=f"Tool not found: {tool_name}",
246 )
247 )
248 result.success = False
249 continue
251 try:
252 tool_start = time.time()
253 output = await self._execute_tool(tool, inputs)
254 duration = time.time() - tool_start
256 result.results.append(
257 ToolResult(
258 tool_name=tool_name,
259 success=True,
260 output=output,
261 duration=duration,
262 )
263 )
264 except Exception as e:
265 result.results.append(
266 ToolResult(
267 tool_name=tool_name,
268 success=False,
269 error=str(e),
270 )
271 )
272 result.success = False
274 # Fuse outputs
275 result.fused_output = self._fuse_outputs(result.results)
277 return result
279 async def _execute_parallel(
280 self,
281 tool_names: list[str],
282 inputs: dict[str, Any],
283 ) -> FusionResult:
284 """Execute tools in parallel."""
285 result = FusionResult(mode=FusionMode.PARALLEL)
287 tasks = []
288 for tool_name in tool_names:
289 tool = self._tools.get(tool_name)
290 if tool:
291 tasks.append(self._execute_tool_with_result(tool, inputs))
292 else:
293 result.results.append(
294 ToolResult(
295 tool_name=tool_name,
296 success=False,
297 error=f"Tool not found: {tool_name}",
298 )
299 )
301 # Execute in parallel
302 if tasks:
303 tool_results = await asyncio.gather(*tasks, return_exceptions=True)
304 for tr in tool_results:
305 if isinstance(tr, Exception):
306 result.results.append(
307 ToolResult(
308 tool_name="unknown",
309 success=False,
310 error=str(tr),
311 )
312 )
313 result.success = False
314 else:
315 result.results.append(tr)
316 if not tr.success:
317 result.success = False
319 # Fuse outputs
320 result.fused_output = self._fuse_outputs(result.results)
322 return result
324 async def _execute_chain(
325 self,
326 tool_names: list[str],
327 inputs: dict[str, Any],
328 ) -> FusionResult:
329 """Execute tools in chain (output feeds into next)."""
330 result = FusionResult(mode=FusionMode.CHAIN)
331 current_input = inputs.copy()
333 for tool_name in tool_names:
334 tool = self._tools.get(tool_name)
335 if not tool:
336 result.results.append(
337 ToolResult(
338 tool_name=tool_name,
339 success=False,
340 error=f"Tool not found: {tool_name}",
341 )
342 )
343 result.success = False
344 break
346 try:
347 tool_start = time.time()
348 output = await self._execute_tool(tool, current_input)
349 duration = time.time() - tool_start
351 result.results.append(
352 ToolResult(
353 tool_name=tool_name,
354 success=True,
355 output=output,
356 duration=duration,
357 )
358 )
360 # Feed output into next tool
361 current_input = {"input": output, **inputs}
362 except Exception as e:
363 result.results.append(
364 ToolResult(
365 tool_name=tool_name,
366 success=False,
367 error=str(e),
368 )
369 )
370 result.success = False
371 break
373 # Final output is last tool's output
374 if result.results:
375 last_result = result.results[-1]
376 if last_result.success:
377 result.fused_output = last_result.output
379 return result
381 async def _execute_tool(
382 self,
383 tool: ToolSpec,
384 inputs: dict[str, Any],
385 ) -> Any:
386 """Execute a single tool."""
387 if not tool.func:
388 raise ValueError(f"Tool {tool.name} has no function")
390 # Apply timeout
391 try:
392 if asyncio.iscoroutinefunction(tool.func):
393 return await asyncio.wait_for(
394 tool.func(**inputs),
395 timeout=tool.timeout or self._default_timeout,
396 )
397 else:
398 return await asyncio.wait_for(
399 asyncio.get_event_loop().run_in_executor(None, lambda: tool.func(**inputs)),
400 timeout=tool.timeout or self._default_timeout,
401 )
402 except TimeoutError:
403 raise TimeoutError(f"Tool {tool.name} timed out")
405 async def _execute_tool_with_result(
406 self,
407 tool: ToolSpec,
408 inputs: dict[str, Any],
409 ) -> ToolResult:
410 """Execute tool and return ToolResult."""
411 try:
412 tool_start = time.time()
413 output = await self._execute_tool(tool, inputs)
414 duration = time.time() - tool_start
416 return ToolResult(
417 tool_name=tool.name,
418 success=True,
419 output=output,
420 duration=duration,
421 )
422 except Exception as e:
423 return ToolResult(
424 tool_name=tool.name,
425 success=False,
426 error=str(e),
427 )
429 def _fuse_outputs(self, results: list[ToolResult]) -> Any:
430 """Fuse multiple tool outputs."""
431 outputs = [r.output for r in results if r.success and r.output is not None]
433 if not outputs:
434 return None
436 if len(outputs) == 1:
437 return outputs[0]
439 # Default fusion: merge dicts, concatenate lists
440 if all(isinstance(o, dict) for o in outputs):
441 fused = {}
442 for o in outputs:
443 fused.update(o)
444 return fused
446 if all(isinstance(o, list) for o in outputs):
447 return [item for o in outputs for item in o]
449 # Default: return list of outputs
450 return outputs