"""Tool system for the AI agent.""" from abc import ABC, abstractmethod from typing import Any, Optional from enum import Enum import os class ToolType(str, Enum): BROWSE = "browse" TERMINAL = "terminal" FILE = "file" SEARCH = "search" API = "api" CUSTOM = "custom" class BaseTool(ABC): """Base class for all tools.""" name: str description: str tool_type: ToolType @abstractmethod async def execute(self, **kwargs) -> dict[str, Any]: """Execute the tool with given parameters.""" pass def validate_params(self, **kwargs) -> bool: """Validate tool parameters before execution.""" return True class BrowserTool(BaseTool): """Browser automation tool using Playwright.""" name = "browser" description = "Navigate web pages, interact with elements, take screenshots" tool_type = ToolType.BROWSE def __init__(self, sandbox): self.sandbox = sandbox async def execute(self, action: str, url: Optional[str] = None, selector: Optional[str] = None, value: Optional[str] = None, **kwargs) -> dict[str, Any]: """Execute browser action.""" try: if action == "navigate": result = await self.sandbox.browser.navigate(url) return {"success": True, "url": url} elif action == "click": result = await self.sandbox.browser.click(selector) return {"success": True, "selector": selector} elif action == "type": result = await self.sandbox.browser.type(selector, value) return {"success": True, "selector": selector, "value": value} elif action == "screenshot": screenshot = await self.sandbox.screenshot.get() return {"success": True, "screenshot": "captured"} else: return {"success": False, "error": f"Unknown action: {action}"} except Exception as e: return {"success": False, "error": str(e)} class PlaywrightBrowserTool(BaseTool): """ Standalone Playwright browser tool for Manus. Can be used outside of E2B sandbox for local browsing. """ name = "playwright_browser" description = "Full browser automation using Playwright. Navigate, click, type, get content, screenshot" tool_type = ToolType.BROWSE def __init__(self): self.playwright = None self.browser = None self.context = None self.page = None self._initialized = False async def initialize(self): """Initialize Playwright browser.""" if self._initialized: return try: from playwright.async_api import async_playwright self.playwright = await async_playwright().start() self.browser = await self.playwright.chromium.launch(headless=True) self.context = await self.browser.new_context(viewport={"width": 1920, "height": 1080}) self.page = await self.context.new_page() self._initialized = True except ImportError: raise Exception("Playwright not installed. Run: pip install playwright") except Exception as e: raise Exception(f"Browser init failed: {e}") async def execute(self, action: str, url: Optional[str] = None, selector: Optional[str] = None, value: Optional[str] = None, **kwargs) -> dict[str, Any]: """Execute browser action.""" try: if not self._initialized: await self.initialize() if action == "navigate" or action == "goto": if not url: return {"success": False, "error": "URL required"} response = await self.page.goto(url, wait_until="domcontentloaded", timeout=30000) title = await self.page.title() return { "success": True, "url": str(self.page.url), "title": title, "status_code": response.status if response else None } elif action == "click": if not selector: return {"success": False, "error": "Selector required"} await self.page.click(selector, timeout=5000) return {"success": True, "selector": selector} elif action == "type" or action == "fill": if not selector or value is None: return {"success": False, "error": "Selector and value required"} await self.page.fill(selector, value) return {"success": True, "selector": selector, "value": value} elif action == "get_content" or action == "get_text": if selector: content = await self.page.text_content(selector) else: content = await self.page.content() return {"success": True, "content": content} elif action == "screenshot": encoded = await self.page.screenshot() return {"success": True, "screenshot": "captured", "size": len(encoded)} elif action == "evaluate" or action == "script": script = kwargs.get("script", "") if not script: return {"success": False, "error": "Script required"} result = await self.page.evaluate(script) return {"success": True, "result": result} elif action == "wait": selector = kwargs.get("selector") timeout = kwargs.get("timeout", 5000) if selector: await self.page.wait_for_selector(selector, timeout=timeout) return {"success": True} elif action == "close": if self.browser: await self.browser.close() if self.playwright: await self.playwright.stop() self._initialized = False return {"success": True} else: return {"success": False, "error": f"Unknown action: {action}"} except Exception as e: return {"success": False, "error": str(e)} class TerminalTool(BaseTool): """Terminal execution tool.""" name = "terminal" description = "Execute shell commands in the sandbox environment" tool_type = ToolType.TERMINAL def __init__(self, sandbox): self.sandbox = sandbox async def execute(self, command: str, timeout: int = 60, **kwargs) -> dict[str, Any]: """Execute a terminal command.""" try: result = await self.sandbox.commands.run(command, timeout=timeout) return { "success": True, "stdout": result.stdout, "stderr": result.stderr, "exit_code": result.exit_code } except Exception as e: return {"success": False, "error": str(e)} class FileTool(BaseTool): """File operations tool.""" name = "file" description = "Read, write, and manipulate files in the sandbox" tool_type = ToolType.FILE def __init__(self, sandbox): self.sandbox = sandbox async def execute(self, action: str, path: str, content: Optional[str] = None, **kwargs) -> dict[str, Any]: """Execute file operation.""" try: if action == "read": content = await self.sandbox.files.read(path) return {"success": True, "content": content} elif action == "write": await self.sandbox.files.write(path, content) return {"success": True, "path": path} elif action == "list": files = await self.sandbox.files.list(path) return {"success": True, "files": [{"name": f.name} for f in files]} elif action == "delete": await self.sandbox.files.delete(path) return {"success": True, "path": path} else: return {"success": False, "error": f"Unknown action: {action}"} except Exception as e: return {"success": False, "error": str(e)} class SearchTool(BaseTool): """Web search tool.""" name = "search" description = "Search the web for information" tool_type = ToolType.SEARCH def __init__(self, api_key: Optional[str] = None): self.api_key = api_key async def execute(self, query: str, limit: int = 10, **kwargs) -> dict[str, Any]: """Execute a web search.""" try: import httpx # Use DuckDuckGo for free search url = f"https://api.duckduckgo.com/" params = { "q": query, "format": "json", "no_redirect": 1 } async with httpx.AsyncClient() as client: response = await client.get(url, params=params) response.raise_for_status() data = response.json() results = [] for topic in data.get("RelatedTopics", [])[:limit]: if "Text" in topic: results.append({ "title": topic.get("Text", "").split(" - ")[0] if " - " in topic.get("Text", "") else "", "url": topic.get("FirstURL", ""), "snippet": topic.get("Text", "") }) return { "success": True, "query": query, "results": results } except Exception as e: return {"success": False, "error": str(e)} class TavilySearchTool(BaseTool): """Tavily AI web search tool - for comprehensive, accurate search results.""" name = "tavily_search" description = "Search the web for current information using Tavily AI. Returns relevant results with sources." tool_type = ToolType.SEARCH def __init__(self, api_key: Optional[str] = None): self.api_key = api_key or os.environ.get("TAVILY_API_KEY") self._client = None def _get_client(self): if not self._client and self.api_key: from tavily import TavilyClient self._client = TavilyClient(api_key=self.api_key) return self._client async def execute(self, query: str, max_results: int = 10, search_depth: str = "basic", include_answer: bool = True, **kwargs) -> dict[str, Any]: """Execute a web search using Tavily AI.""" try: client = self._get_client() if not client: return {"success": False, "error": "Tavily API key not configured"} results = client.search( query=query, max_results=max_results, search_depth=search_depth, include_answer=include_answer ) return { "success": True, "query": query, "answer": results.get("answer"), "results": [ { "title": r.get("title", ""), "url": r.get("url", ""), "content": r.get("content", "")[:500] } for r in results.get("results", [])[:max_results] ] } except Exception as e: return {"success": False, "error": str(e)} class TavilyExtractTool(BaseTool): """Extract content from URLs using Tavily.""" name = "tavily_extract" description = "Extract and crawl content from specific URLs" tool_type = ToolType.SEARCH def __init__(self, api_key: Optional[str] = None): self.api_key = api_key or os.environ.get("TAVILY_API_KEY") self._client = None def _get_client(self): if not self._client and self.api_key: from tavily import TavilyClient self._client = TavilyClient(api_key=self.api_key) return self._client async def execute(self, urls: list, **kwargs) -> dict[str, Any]: """Extract content from URLs.""" try: client = self._get_client() if not client: return {"success": False, "error": "Tavily API key not configured"} results = client.extract(urls=urls) return { "success": True, "results": [ { "url": r.get("url", ""), "raw_content": r.get("raw_content", "")[:2000] } for r in results.get("results", []) ] } except Exception as e: return {"success": False, "error": str(e)} class TavilyCrawlTool(BaseTool): """Crawl a website and extract content using Tavily.""" name = "tavily_crawl" description = "Crawl a website starting from a URL with configurable depth and breadth" tool_type = ToolType.SEARCH def __init__(self, api_key: Optional[str] = None): self.api_key = api_key or os.environ.get("TAVILY_API_KEY") self._client = None def _get_client(self): if not self._client and self.api_key: from tavily import TavilyClient self._client = TavilyClient(api_key=self.api_key) return self._client async def execute(self, url: str, max_depth: int = 2, max_breadth: int = 10, include_images: bool = False, **kwargs) -> dict[str, Any]: """Crawl a website and extract content.""" try: client = self._get_client() if not client: return {"success": False, "error": "Tavily API key not configured"} results = client.crawl( url=url, max_depth=max_depth, max_breadth=max_breadth, include_images=include_images ) return { "success": True, "url": url, "results": [ { "url": r.get("url", ""), "content": r.get("content", "")[:1000] } for r in results.get("results", [])[:20] ] } except Exception as e: return {"success": False, "error": str(e)} class GoogleWorkspaceTool(BaseTool): """Google Workspace integration tool (Drive, Sheets, Docs).""" name = "google_workspace" description = "Access and manipulate Google Drive, Sheets, and Docs" tool_type = ToolType.API def __init__(self, credentials_path: str): self.credentials_path = credentials_path self._service = None async def execute(self, service: str, action: str, **kwargs) -> dict[str, Any]: """Execute Google Workspace action.""" try: if not self._service: from google.oauth2 import service_account self._service = service_account.Credentials.from_service_account_file( self.credentials_path, scopes=[ "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/documents" ] ) if service == "sheets": return await self._sheets_action(action, **kwargs) elif service == "drive": return await self._drive_action(action, **kwargs) elif service == "docs": return await self._docs_action(action, **kwargs) else: return {"success": False, "error": f"Unknown service: {service}"} except Exception as e: return {"success": False, "error": str(e)} async def _sheets_action(self, action: str, spreadsheet_id: str, range: str = "A1:Z100", values: Optional[list] = None, **kwargs) -> dict[str, Any]: """Execute Google Sheets action.""" from googleapiclient.discovery import build service = build("sheets", "v4", credentials=self._service) if action == "read": result = service.spreadsheets().values().get( spreadsheetId=spreadsheet_id, range=range ).execute() return {"success": True, "values": result.get("values", [])} elif action == "write": body = {"values": values} result = service.spreadsheets().values().update( spreadsheetId=spreadsheet_id, range=range, valueInputOption="USER_ENTERED", body=body ).execute() return {"success": True, "updated_cells": result.get("updatedCells")} return {"success": False, "error": f"Unknown action: {action}"} async def _drive_action(self, action: str, name: Optional[str] = None, file_id: Optional[str] = None, **kwargs) -> dict[str, Any]: """Execute Google Drive action.""" from googleapiclient.discovery import build service = build("drive", "v3", credentials=self._service) if action == "list": results = service.files().list( pageSize=10, fields="files(id, name, mimeType)" ).execute() return {"success": True, "files": results.get("files", [])} elif action == "create": file_metadata = {"name": name, "mimeType": "application/json"} result = service.files().create(body=file_metadata, fields="id").execute() return {"success": True, "file_id": result.get("id")} return {"success": False, "error": f"Unknown action: {action}"} async def _docs_action(self, action: str, document_id: str, text: Optional[str] = None, **kwargs) -> dict[str, Any]: """Execute Google Docs action.""" from googleapiclient.discovery import build service = build("docs", "v1", credentials=self._service) if action == "read": doc = service.documents().get(documentId=document_id).execute() return {"success": True, "content": doc.get("body").get("content")} elif action == "insert": # Insert text at the end doc = service.documents().get(documentId=document_id).execute() end_index = doc.get("body").get("content")[-1].get("endIndex") requests = [{ "insertText": { "location": {"index": end_index - 1}, "text": text } }] service.documents().batchUpdate( documentId=document_id, body={"requests": requests} ).execute() return {"success": True} return {"success": False, "error": f"Unknown action: {action}"} class ToolRegistry: """Registry for all available tools.""" def __init__(self): self._tools: dict[str, BaseTool] = {} def register(self, tool: BaseTool): """Register a tool.""" self._tools[tool.name] = tool def get(self, name: str) -> Optional[BaseTool]: """Get a tool by name.""" return self._tools.get(name) def list_tools(self) -> list[dict]: """List all available tools.""" return [ {"name": tool.name, "description": tool.description, "type": tool.tool_type.value} for tool in self._tools.values() ] async def execute(self, tool_name: str, **kwargs) -> dict[str, Any]: """Execute a tool by name.""" tool = self.get(tool_name) if not tool: return {"success": False, "error": f"Tool not found: {tool_name}"} return await tool.execute(**kwargs) # Default tool registry factory def create_tool_registry(sandbox=None, google_creds: Optional[str] = None, tavily_api_key: Optional[str] = None, enable_playwright: bool = False) -> ToolRegistry: """Create a default tool registry with all built-in tools.""" registry = ToolRegistry() if sandbox: # E2B sandbox-based tools registry.register(BrowserTool(sandbox)) registry.register(TerminalTool(sandbox)) registry.register(FileTool(sandbox)) # Always register search tools registry.register(SearchTool()) # DuckDuckGo fallback # Register Tavily tools if API key provided tavily_key = tavily_api_key or os.environ.get("TAVILY_API_KEY") if tavily_key: registry.register(TavilySearchTool(tavily_key)) registry.register(TavilyExtractTool(tavily_key)) registry.register(TavilyCrawlTool(tavily_key)) # Register standalone Playwright browser tool if enable_playwright: registry.register(PlaywrightBrowserTool()) if google_creds: registry.register(GoogleWorkspaceTool(google_creds)) return registry