Spaces:
No application file
No application file
| """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 | |
| 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 |