optional deps for most

This commit is contained in:
Lorenze Jay
2025-01-10 13:51:39 -08:00
parent 06f99fc6cd
commit 40dcf63a70
14 changed files with 374 additions and 175 deletions

View File

@@ -11,12 +11,10 @@ class BrowserbaseLoadToolSchema(BaseModel):
class BrowserbaseLoadTool(BaseTool): class BrowserbaseLoadTool(BaseTool):
name: str = "Browserbase web load tool" name: str = "Browserbase web load tool"
description: str = ( description: str = "Load webpages url in a headless browser using Browserbase and return the contents"
"Load webpages url in a headless browser using Browserbase and return the contents"
)
args_schema: Type[BaseModel] = BrowserbaseLoadToolSchema args_schema: Type[BaseModel] = BrowserbaseLoadToolSchema
api_key: Optional[str] = os.getenv('BROWSERBASE_API_KEY') api_key: Optional[str] = os.getenv("BROWSERBASE_API_KEY")
project_id: Optional[str] = os.getenv('BROWSERBASE_PROJECT_ID') project_id: Optional[str] = os.getenv("BROWSERBASE_PROJECT_ID")
text_content: Optional[bool] = False text_content: Optional[bool] = False
session_id: Optional[str] = None session_id: Optional[str] = None
proxy: Optional[bool] = None proxy: Optional[bool] = None
@@ -33,13 +31,24 @@ class BrowserbaseLoadTool(BaseTool):
): ):
super().__init__(**kwargs) super().__init__(**kwargs)
if not self.api_key: if not self.api_key:
raise EnvironmentError("BROWSERBASE_API_KEY environment variable is required for initialization") raise EnvironmentError(
"BROWSERBASE_API_KEY environment variable is required for initialization"
)
try: try:
from browserbase import Browserbase # type: ignore from browserbase import Browserbase # type: ignore
except ImportError: except ImportError:
raise ImportError( import click
"`browserbase` package not found, please run `pip install browserbase`"
) if click.confirm(
"`browserbase` package not found, would you like to install it?"
):
import subprocess
subprocess.run(["uv", "add", "browserbase"], check=True)
else:
raise ImportError(
"`browserbase` package not found, please run `uv add browserbase`"
)
self.browserbase = Browserbase(api_key=self.api_key) self.browserbase = Browserbase(api_key=self.api_key)
self.text_content = text_content self.text_content = text_content

View File

@@ -35,9 +35,21 @@ class FirecrawlCrawlWebsiteTool(BaseTool):
try: try:
from firecrawl import FirecrawlApp # type: ignore from firecrawl import FirecrawlApp # type: ignore
except ImportError: except ImportError:
raise ImportError( import click
"`firecrawl` package not found, please run `pip install firecrawl-py`"
) if click.confirm(
"You are missing the 'firecrawl-py' package. Would you like to install it? (y/N)"
):
import subprocess
subprocess.run(["uv", "add", "firecrawl-py"], check=True)
from firecrawl import (
FirecrawlApp,
)
else:
raise ImportError(
"`firecrawl-py` package not found, please run `uv add firecrawl-py`"
)
if not self.firecrawl: if not self.firecrawl:
client_api_key = api_key or os.getenv("FIRECRAWL_API_KEY") client_api_key = api_key or os.getenv("FIRECRAWL_API_KEY")

View File

@@ -31,9 +31,21 @@ class FirecrawlScrapeWebsiteTool(BaseTool):
try: try:
from firecrawl import FirecrawlApp # type: ignore from firecrawl import FirecrawlApp # type: ignore
except ImportError: except ImportError:
raise ImportError( import click
"`firecrawl` package not found, please run `pip install firecrawl-py`"
) if click.confirm(
"You are missing the 'firecrawl-py' package. Would you like to install it? (y/N)"
):
import subprocess
subprocess.run(["uv", "add", "firecrawl-py"], check=True)
from firecrawl import (
FirecrawlApp,
)
else:
raise ImportError(
"`firecrawl-py` package not found, please run `uv add firecrawl-py`"
)
self.firecrawl = FirecrawlApp(api_key=api_key) self.firecrawl = FirecrawlApp(api_key=api_key)

View File

@@ -41,9 +41,21 @@ class FirecrawlSearchTool(BaseTool):
try: try:
from firecrawl import FirecrawlApp # type: ignore from firecrawl import FirecrawlApp # type: ignore
except ImportError: except ImportError:
raise ImportError( import click
"`firecrawl` package not found, please run `pip install firecrawl-py`"
) if click.confirm(
"You are missing the 'firecrawl-py' package. Would you like to install it? (y/N)"
):
import subprocess
subprocess.run(["uv", "add", "firecrawl-py"], check=True)
from firecrawl import (
FirecrawlApp,
)
else:
raise ImportError(
"`firecrawl-py` package not found, please run `uv add firecrawl-py`"
)
self.firecrawl = FirecrawlApp(api_key=api_key) self.firecrawl = FirecrawlApp(api_key=api_key)

View File

@@ -1,7 +1,10 @@
from typing import Any from typing import Any
from crewai.tools import BaseTool
try: try:
from linkup import LinkupClient from linkup import LinkupClient
LINKUP_AVAILABLE = True LINKUP_AVAILABLE = True
except ImportError: except ImportError:
LINKUP_AVAILABLE = False LINKUP_AVAILABLE = False
@@ -9,23 +12,42 @@ except ImportError:
from pydantic import PrivateAttr from pydantic import PrivateAttr
class LinkupSearchTool:
class LinkupSearchTool(BaseTool):
name: str = "Linkup Search Tool" name: str = "Linkup Search Tool"
description: str = "Performs an API call to Linkup to retrieve contextual information." description: str = (
_client: LinkupClient = PrivateAttr() # type: ignore "Performs an API call to Linkup to retrieve contextual information."
)
_client: LinkupClient = PrivateAttr() # type: ignore
def __init__(self, api_key: str): def __init__(self, api_key: str):
""" """
Initialize the tool with an API key. Initialize the tool with an API key.
""" """
if not LINKUP_AVAILABLE: super().__init__()
raise ImportError( try:
"The 'linkup' package is required to use the LinkupSearchTool. " from linkup import LinkupClient
"Please install it with: uv add linkup" except ImportError:
) import click
if click.confirm(
"You are missing the 'linkup-sdk' package. Would you like to install it? (y/N)"
):
import subprocess
subprocess.run(["uv", "add", "linkup-sdk"], check=True)
from linkup import LinkupClient
else:
raise ImportError(
"The 'linkup-sdk' package is required to use the LinkupSearchTool. "
"Please install it with: uv add linkup-sdk"
)
self._client = LinkupClient(api_key=api_key) self._client = LinkupClient(api_key=api_key)
def _run(self, query: str, depth: str = "standard", output_type: str = "searchResults") -> dict: def _run(
self, query: str, depth: str = "standard", output_type: str = "searchResults"
) -> dict:
""" """
Executes a search using the Linkup API. Executes a search using the Linkup API.
@@ -36,9 +58,7 @@ class LinkupSearchTool:
""" """
try: try:
response = self._client.search( response = self._client.search(
query=query, query=query, depth=depth, output_type=output_type
depth=depth,
output_type=output_type
) )
results = [ results = [
{"name": result.name, "url": result.url, "content": result.content} {"name": result.name, "url": result.url, "content": result.content}

View File

@@ -28,9 +28,18 @@ class MultiOnTool(BaseTool):
try: try:
from multion.client import MultiOn # type: ignore from multion.client import MultiOn # type: ignore
except ImportError: except ImportError:
raise ImportError( import click
"`multion` package not found, please run `pip install multion`"
) if click.confirm(
"You are missing the 'multion' package. Would you like to install it? (y/N)"
):
import subprocess
subprocess.run(["uv", "add", "multion"], check=True)
else:
raise ImportError(
"`multion` package not found, please run `uv add multion`"
)
self.session_id = None self.session_id = None
self.local = local self.local = local
self.multion = MultiOn(api_key=api_key) self.multion = MultiOn(api_key=api_key)

View File

@@ -1,7 +1,14 @@
from typing import Any, Type from typing import Any, Type
from crewai.tools import BaseTool from crewai.tools import BaseTool
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from patronus import Client
try:
from patronus import Client
PYPATRONUS_AVAILABLE = True
except ImportError:
PYPATRONUS_AVAILABLE = False
Client = Any
class FixedLocalEvaluatorToolSchema(BaseModel): class FixedLocalEvaluatorToolSchema(BaseModel):
@@ -24,26 +31,44 @@ class PatronusLocalEvaluatorTool(BaseTool):
name: str = "Patronus Local Evaluator Tool" name: str = "Patronus Local Evaluator Tool"
evaluator: str = "The registered local evaluator" evaluator: str = "The registered local evaluator"
evaluated_model_gold_answer: str = "The agent's gold answer" evaluated_model_gold_answer: str = "The agent's gold answer"
description: str = ( description: str = "This tool is used to evaluate the model input and output using custom function evaluators."
"This tool is used to evaluate the model input and output using custom function evaluators."
)
client: Any = None client: Any = None
args_schema: Type[BaseModel] = FixedLocalEvaluatorToolSchema args_schema: Type[BaseModel] = FixedLocalEvaluatorToolSchema
class Config: class Config:
arbitrary_types_allowed = True arbitrary_types_allowed = True
def __init__(self, patronus_client: Client, evaluator: str, evaluated_model_gold_answer: str, **kwargs: Any): def __init__(
self,
patronus_client: Client,
evaluator: str,
evaluated_model_gold_answer: str,
**kwargs: Any,
):
super().__init__(**kwargs) super().__init__(**kwargs)
self.client = patronus_client if PYPATRONUS_AVAILABLE:
if evaluator: self.client = patronus_client
self.evaluator = evaluator if evaluator:
self.evaluated_model_gold_answer = evaluated_model_gold_answer self.evaluator = evaluator
self.evaluated_model_gold_answer = evaluated_model_gold_answer
self.description = f"This tool calls the Patronus Evaluation API that takes an additional argument in addition to the following new argument:\n evaluators={evaluator}, evaluated_model_gold_answer={evaluated_model_gold_answer}" self.description = f"This tool calls the Patronus Evaluation API that takes an additional argument in addition to the following new argument:\n evaluators={evaluator}, evaluated_model_gold_answer={evaluated_model_gold_answer}"
self._generate_description() self._generate_description()
print( print(
f"Updating judge evaluator, gold_answer to: {self.evaluator}, {self.evaluated_model_gold_answer}" f"Updating judge evaluator, gold_answer to: {self.evaluator}, {self.evaluated_model_gold_answer}"
) )
else:
import click
if click.confirm(
"You are missing the 'patronus' package. Would you like to install it? (y/N)"
):
import subprocess
subprocess.run(["uv", "add", "patronus"], check=True)
else:
raise ImportError(
"You are missing the patronus package. Would you like to install it?"
)
def _run( def _run(
self, self,
@@ -79,7 +104,7 @@ class PatronusLocalEvaluatorTool(BaseTool):
if isinstance(evaluated_model_gold_answer, str) if isinstance(evaluated_model_gold_answer, str)
else evaluated_model_gold_answer.get("description") else evaluated_model_gold_answer.get("description")
), ),
tags={}, # Optional metadata, supports arbitrary kv pairs tags={}, # Optional metadata, supports arbitrary kv pairs
) )
output = f"Evaluation result: {result.pass_}, Explanation: {result.explanation}" output = f"Evaluation result: {result.pass_}, Explanation: {result.explanation}"
return output return output

View File

@@ -1,25 +1,30 @@
import os import os
from typing import Any, Optional, Type from typing import Any, Optional, Type, TYPE_CHECKING
from urllib.parse import urlparse from urllib.parse import urlparse
from crewai.tools import BaseTool from crewai.tools import BaseTool
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator, ConfigDict
from scrapegraph_py import Client
from scrapegraph_py.logger import sgai_logger # Type checking import
if TYPE_CHECKING:
from scrapegraph_py import Client
class ScrapegraphError(Exception): class ScrapegraphError(Exception):
"""Base exception for Scrapegraph-related errors""" """Base exception for Scrapegraph-related errors"""
pass pass
class RateLimitError(ScrapegraphError): class RateLimitError(ScrapegraphError):
"""Raised when API rate limits are exceeded""" """Raised when API rate limits are exceeded"""
pass pass
class FixedScrapegraphScrapeToolSchema(BaseModel): class FixedScrapegraphScrapeToolSchema(BaseModel):
"""Input for ScrapegraphScrapeTool when website_url is fixed.""" """Input for ScrapegraphScrapeTool when website_url is fixed."""
pass pass
@@ -32,7 +37,7 @@ class ScrapegraphScrapeToolSchema(FixedScrapegraphScrapeToolSchema):
description="Prompt to guide the extraction of content", description="Prompt to guide the extraction of content",
) )
@validator('website_url') @validator("website_url")
def validate_url(cls, v): def validate_url(cls, v):
"""Validate URL format""" """Validate URL format"""
try: try:
@@ -41,25 +46,32 @@ class ScrapegraphScrapeToolSchema(FixedScrapegraphScrapeToolSchema):
raise ValueError raise ValueError
return v return v
except Exception: except Exception:
raise ValueError("Invalid URL format. URL must include scheme (http/https) and domain") raise ValueError(
"Invalid URL format. URL must include scheme (http/https) and domain"
)
class ScrapegraphScrapeTool(BaseTool): class ScrapegraphScrapeTool(BaseTool):
""" """
A tool that uses Scrapegraph AI to intelligently scrape website content. A tool that uses Scrapegraph AI to intelligently scrape website content.
Raises: Raises:
ValueError: If API key is missing or URL format is invalid ValueError: If API key is missing or URL format is invalid
RateLimitError: If API rate limits are exceeded RateLimitError: If API rate limits are exceeded
RuntimeError: If scraping operation fails RuntimeError: If scraping operation fails
""" """
model_config = ConfigDict(arbitrary_types_allowed=True)
name: str = "Scrapegraph website scraper" name: str = "Scrapegraph website scraper"
description: str = "A tool that uses Scrapegraph AI to intelligently scrape website content." description: str = (
"A tool that uses Scrapegraph AI to intelligently scrape website content."
)
args_schema: Type[BaseModel] = ScrapegraphScrapeToolSchema args_schema: Type[BaseModel] = ScrapegraphScrapeToolSchema
website_url: Optional[str] = None website_url: Optional[str] = None
user_prompt: Optional[str] = None user_prompt: Optional[str] = None
api_key: Optional[str] = None api_key: Optional[str] = None
_client: Optional["Client"] = None
def __init__( def __init__(
self, self,
@@ -69,8 +81,31 @@ class ScrapegraphScrapeTool(BaseTool):
**kwargs, **kwargs,
): ):
super().__init__(**kwargs) super().__init__(**kwargs)
try:
from scrapegraph_py import Client
from scrapegraph_py.logger import sgai_logger
except ImportError:
import click
if click.confirm(
"You are missing the 'scrapegraph-py' package. Would you like to install it? (y/N)"
):
import subprocess
subprocess.run(["uv", "add", "scrapegraph-py"], check=True)
from scrapegraph_py import Client
from scrapegraph_py.logger import sgai_logger
else:
raise ImportError(
"`scrapegraph-py` package not found, please run `uv add scrapegraph-py`"
)
self._client = Client(api_key=api_key)
self.api_key = api_key or os.getenv("SCRAPEGRAPH_API_KEY") self.api_key = api_key or os.getenv("SCRAPEGRAPH_API_KEY")
if not self.api_key: if not self.api_key:
raise ValueError("Scrapegraph API key is required") raise ValueError("Scrapegraph API key is required")
@@ -79,7 +114,7 @@ class ScrapegraphScrapeTool(BaseTool):
self.website_url = website_url self.website_url = website_url
self.description = f"A tool that uses Scrapegraph AI to intelligently scrape {website_url}'s content." self.description = f"A tool that uses Scrapegraph AI to intelligently scrape {website_url}'s content."
self.args_schema = FixedScrapegraphScrapeToolSchema self.args_schema = FixedScrapegraphScrapeToolSchema
if user_prompt is not None: if user_prompt is not None:
self.user_prompt = user_prompt self.user_prompt = user_prompt
@@ -94,22 +129,24 @@ class ScrapegraphScrapeTool(BaseTool):
if not all([result.scheme, result.netloc]): if not all([result.scheme, result.netloc]):
raise ValueError raise ValueError
except Exception: except Exception:
raise ValueError("Invalid URL format. URL must include scheme (http/https) and domain") raise ValueError(
"Invalid URL format. URL must include scheme (http/https) and domain"
)
def _handle_api_response(self, response: dict) -> str: def _handle_api_response(self, response: dict) -> str:
"""Handle and validate API response""" """Handle and validate API response"""
if not response: if not response:
raise RuntimeError("Empty response from Scrapegraph API") raise RuntimeError("Empty response from Scrapegraph API")
if "error" in response: if "error" in response:
error_msg = response.get("error", {}).get("message", "Unknown error") error_msg = response.get("error", {}).get("message", "Unknown error")
if "rate limit" in error_msg.lower(): if "rate limit" in error_msg.lower():
raise RateLimitError(f"Rate limit exceeded: {error_msg}") raise RateLimitError(f"Rate limit exceeded: {error_msg}")
raise RuntimeError(f"API error: {error_msg}") raise RuntimeError(f"API error: {error_msg}")
if "result" not in response: if "result" not in response:
raise RuntimeError("Invalid response format from Scrapegraph API") raise RuntimeError("Invalid response format from Scrapegraph API")
return response["result"] return response["result"]
def _run( def _run(
@@ -117,7 +154,10 @@ class ScrapegraphScrapeTool(BaseTool):
**kwargs: Any, **kwargs: Any,
) -> Any: ) -> Any:
website_url = kwargs.get("website_url", self.website_url) website_url = kwargs.get("website_url", self.website_url)
user_prompt = kwargs.get("user_prompt", self.user_prompt) or "Extract the main content of the webpage" user_prompt = (
kwargs.get("user_prompt", self.user_prompt)
or "Extract the main content of the webpage"
)
if not website_url: if not website_url:
raise ValueError("website_url is required") raise ValueError("website_url is required")
@@ -125,12 +165,9 @@ class ScrapegraphScrapeTool(BaseTool):
# Validate URL format # Validate URL format
self._validate_url(website_url) self._validate_url(website_url)
# Initialize the client
sgai_client = Client(api_key=self.api_key)
try: try:
# Make the SmartScraper request # Make the SmartScraper request
response = sgai_client.smartscraper( response = self.client.smartscraper(
website_url=website_url, website_url=website_url,
user_prompt=user_prompt, user_prompt=user_prompt,
) )
@@ -144,4 +181,4 @@ class ScrapegraphScrapeTool(BaseTool):
raise RuntimeError(f"Scraping failed: {str(e)}") raise RuntimeError(f"Scraping failed: {str(e)}")
finally: finally:
# Always close the client # Always close the client
sgai_client.close() self.client.close()

View File

@@ -34,9 +34,18 @@ class ScrapflyScrapeWebsiteTool(BaseTool):
try: try:
from scrapfly import ScrapflyClient from scrapfly import ScrapflyClient
except ImportError: except ImportError:
raise ImportError( import click
"`scrapfly` package not found, please run `pip install scrapfly-sdk`"
) if click.confirm(
"You are missing the 'scrapfly-sdk' package. Would you like to install it? (y/N)"
):
import subprocess
subprocess.run(["uv", "add", "scrapfly-sdk"], check=True)
else:
raise ImportError(
"`scrapfly-sdk` package not found, please run `uv add scrapfly-sdk`"
)
self.scrapfly = ScrapflyClient(key=api_key) self.scrapfly = ScrapflyClient(key=api_key)
def _run( def _run(

View File

@@ -5,9 +5,6 @@ from urllib.parse import urlparse
from crewai.tools import BaseTool from crewai.tools import BaseTool
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
class FixedSeleniumScrapingToolSchema(BaseModel): class FixedSeleniumScrapingToolSchema(BaseModel):
@@ -17,33 +14,36 @@ class FixedSeleniumScrapingToolSchema(BaseModel):
class SeleniumScrapingToolSchema(FixedSeleniumScrapingToolSchema): class SeleniumScrapingToolSchema(FixedSeleniumScrapingToolSchema):
"""Input for SeleniumScrapingTool.""" """Input for SeleniumScrapingTool."""
website_url: str = Field(..., description="Mandatory website url to read the file. Must start with http:// or https://") website_url: str = Field(
...,
description="Mandatory website url to read the file. Must start with http:// or https://",
)
css_element: str = Field( css_element: str = Field(
..., ...,
description="Mandatory css reference for element to scrape from the website", description="Mandatory css reference for element to scrape from the website",
) )
@validator('website_url') @validator("website_url")
def validate_website_url(cls, v): def validate_website_url(cls, v):
if not v: if not v:
raise ValueError("Website URL cannot be empty") raise ValueError("Website URL cannot be empty")
if len(v) > 2048: # Common maximum URL length if len(v) > 2048: # Common maximum URL length
raise ValueError("URL is too long (max 2048 characters)") raise ValueError("URL is too long (max 2048 characters)")
if not re.match(r'^https?://', v): if not re.match(r"^https?://", v):
raise ValueError("URL must start with http:// or https://") raise ValueError("URL must start with http:// or https://")
try: try:
result = urlparse(v) result = urlparse(v)
if not all([result.scheme, result.netloc]): if not all([result.scheme, result.netloc]):
raise ValueError("Invalid URL format") raise ValueError("Invalid URL format")
except Exception as e: except Exception as e:
raise ValueError(f"Invalid URL: {str(e)}") raise ValueError(f"Invalid URL: {str(e)}")
if re.search(r'\s', v): if re.search(r"\s", v):
raise ValueError("URL cannot contain whitespace") raise ValueError("URL cannot contain whitespace")
return v return v
@@ -52,7 +52,7 @@ class SeleniumScrapingTool(BaseTool):
description: str = "A tool that can be used to read a website content." description: str = "A tool that can be used to read a website content."
args_schema: Type[BaseModel] = SeleniumScrapingToolSchema args_schema: Type[BaseModel] = SeleniumScrapingToolSchema
website_url: Optional[str] = None website_url: Optional[str] = None
driver: Optional[Any] = webdriver.Chrome driver: Optional[Any] = None
cookie: Optional[dict] = None cookie: Optional[dict] = None
wait_time: Optional[int] = 3 wait_time: Optional[int] = 3
css_element: Optional[str] = None css_element: Optional[str] = None
@@ -66,6 +66,30 @@ class SeleniumScrapingTool(BaseTool):
**kwargs, **kwargs,
): ):
super().__init__(**kwargs) super().__init__(**kwargs)
try:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
except ImportError:
import click
if click.confirm(
"You are missing the 'selenium' and 'webdriver-manager' packages. Would you like to install it? (y/N)"
):
import subprocess
subprocess.run(
["uv", "pip", "install", "selenium", "webdriver-manager"],
check=True,
)
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
else:
raise ImportError(
"`selenium` and `webdriver-manager` package not found, please run `uv add selenium webdriver-manager`"
)
self.driver = webdriver.Chrome()
if cookie is not None: if cookie is not None:
self.cookie = cookie self.cookie = cookie
@@ -130,11 +154,11 @@ class SeleniumScrapingTool(BaseTool):
def _create_driver(self, url, cookie, wait_time): def _create_driver(self, url, cookie, wait_time):
if not url: if not url:
raise ValueError("URL cannot be empty") raise ValueError("URL cannot be empty")
# Validate URL format # Validate URL format
if not re.match(r'^https?://', url): if not re.match(r"^https?://", url):
raise ValueError("URL must start with http:// or https://") raise ValueError("URL must start with http:// or https://")
options = Options() options = Options()
options.add_argument("--headless") options.add_argument("--headless")
driver = self.driver(options=options) driver = self.driver(options=options)

View File

@@ -4,6 +4,7 @@ from typing import Optional, Any, Union
from crewai.tools import BaseTool from crewai.tools import BaseTool
class SerpApiBaseTool(BaseTool): class SerpApiBaseTool(BaseTool):
"""Base class for SerpApi functionality with shared capabilities.""" """Base class for SerpApi functionality with shared capabilities."""
@@ -15,9 +16,18 @@ class SerpApiBaseTool(BaseTool):
try: try:
from serpapi import Client from serpapi import Client
except ImportError: except ImportError:
raise ImportError( import click
"`serpapi` package not found, please install with `pip install serpapi`"
) if click.confirm(
"You are missing the 'serpapi' package. Would you like to install it? (y/N)"
):
import subprocess
subprocess.run(["uv", "add", "serpapi"], check=True)
else:
raise ImportError(
"`serpapi` package not found, please install with `uv add serpapi`"
)
api_key = os.getenv("SERPAPI_API_KEY") api_key = os.getenv("SERPAPI_API_KEY")
if not api_key: if not api_key:
raise ValueError( raise ValueError(

View File

@@ -87,13 +87,21 @@ class SpiderTool(BaseTool):
try: try:
from spider import Spider # type: ignore from spider import Spider # type: ignore
self.spider = Spider(api_key=api_key)
except ImportError: except ImportError:
raise ImportError( import click
"`spider-client` package not found, please run `uv add spider-client`"
) if click.confirm(
except Exception as e: "You are missing the 'spider-client' package. Would you like to install it? (y/N)"
raise RuntimeError(f"Failed to initialize Spider client: {str(e)}") ):
import subprocess
subprocess.run(["uv", "pip", "install", "spider-client"], check=True)
from spider import Spider
else:
raise ImportError(
"`spider-client` package not found, please run `uv add spider-client`"
)
self.spider = Spider(api_key=api_key)
def _validate_url(self, url: str) -> bool: def _validate_url(self, url: str) -> bool:
"""Validate URL format and security constraints. """Validate URL format and security constraints.

View File

@@ -25,6 +25,7 @@ logger = logging.getLogger(__name__)
STAGEHAND_AVAILABLE = False STAGEHAND_AVAILABLE = False
try: try:
import stagehand import stagehand
STAGEHAND_AVAILABLE = True STAGEHAND_AVAILABLE = True
except ImportError: except ImportError:
pass # Keep STAGEHAND_AVAILABLE as False pass # Keep STAGEHAND_AVAILABLE as False
@@ -32,33 +33,45 @@ except ImportError:
class StagehandResult(BaseModel): class StagehandResult(BaseModel):
"""Result from a Stagehand operation. """Result from a Stagehand operation.
Attributes: Attributes:
success: Whether the operation completed successfully success: Whether the operation completed successfully
data: The result data from the operation data: The result data from the operation
error: Optional error message if the operation failed error: Optional error message if the operation failed
""" """
success: bool = Field(..., description="Whether the operation completed successfully")
data: Union[str, Dict, List] = Field(..., description="The result data from the operation") success: bool = Field(
error: Optional[str] = Field(None, description="Optional error message if the operation failed") ..., description="Whether the operation completed successfully"
)
data: Union[str, Dict, List] = Field(
..., description="The result data from the operation"
)
error: Optional[str] = Field(
None, description="Optional error message if the operation failed"
)
class StagehandToolConfig(BaseModel): class StagehandToolConfig(BaseModel):
"""Configuration for the StagehandTool. """Configuration for the StagehandTool.
Attributes: Attributes:
api_key: OpenAI API key for Stagehand authentication api_key: OpenAI API key for Stagehand authentication
timeout: Maximum time in seconds to wait for operations (default: 30) timeout: Maximum time in seconds to wait for operations (default: 30)
retry_attempts: Number of times to retry failed operations (default: 3) retry_attempts: Number of times to retry failed operations (default: 3)
""" """
api_key: str = Field(..., description="OpenAI API key for Stagehand authentication") api_key: str = Field(..., description="OpenAI API key for Stagehand authentication")
timeout: int = Field(30, description="Maximum time in seconds to wait for operations") timeout: int = Field(
retry_attempts: int = Field(3, description="Number of times to retry failed operations") 30, description="Maximum time in seconds to wait for operations"
)
retry_attempts: int = Field(
3, description="Number of times to retry failed operations"
)
class StagehandToolSchema(BaseModel): class StagehandToolSchema(BaseModel):
"""Schema for the StagehandTool input parameters. """Schema for the StagehandTool input parameters.
Examples: Examples:
```python ```python
# Using the 'act' API to click a button # Using the 'act' API to click a button
@@ -66,13 +79,13 @@ class StagehandToolSchema(BaseModel):
api_method="act", api_method="act",
instruction="Click the 'Sign In' button" instruction="Click the 'Sign In' button"
) )
# Using the 'extract' API to get text # Using the 'extract' API to get text
tool.run( tool.run(
api_method="extract", api_method="extract",
instruction="Get the text content of the main article" instruction="Get the text content of the main article"
) )
# Using the 'observe' API to monitor changes # Using the 'observe' API to monitor changes
tool.run( tool.run(
api_method="observe", api_method="observe",
@@ -80,48 +93,49 @@ class StagehandToolSchema(BaseModel):
) )
``` ```
""" """
api_method: str = Field( api_method: str = Field(
..., ...,
description="The Stagehand API to use: 'act' for interactions, 'extract' for getting content, or 'observe' for monitoring changes", description="The Stagehand API to use: 'act' for interactions, 'extract' for getting content, or 'observe' for monitoring changes",
pattern="^(act|extract|observe)$" pattern="^(act|extract|observe)$",
) )
instruction: str = Field( instruction: str = Field(
..., ...,
description="An atomic instruction for Stagehand to execute. Instructions should be simple and specific to increase reliability.", description="An atomic instruction for Stagehand to execute. Instructions should be simple and specific to increase reliability.",
min_length=1, min_length=1,
max_length=500 max_length=500,
) )
class StagehandTool(BaseTool): class StagehandTool(BaseTool):
"""A tool for using Stagehand's AI-powered web automation capabilities. """A tool for using Stagehand's AI-powered web automation capabilities.
This tool provides access to Stagehand's three core APIs: This tool provides access to Stagehand's three core APIs:
- act: Perform web interactions (e.g., clicking buttons, filling forms) - act: Perform web interactions (e.g., clicking buttons, filling forms)
- extract: Extract information from web pages (e.g., getting text content) - extract: Extract information from web pages (e.g., getting text content)
- observe: Monitor web page changes (e.g., watching for updates) - observe: Monitor web page changes (e.g., watching for updates)
Each function takes atomic instructions to increase reliability. Each function takes atomic instructions to increase reliability.
Required Environment Variables: Required Environment Variables:
OPENAI_API_KEY: API key for OpenAI (required by Stagehand) OPENAI_API_KEY: API key for OpenAI (required by Stagehand)
Examples: Examples:
```python ```python
tool = StagehandTool() tool = StagehandTool()
# Perform a web interaction # Perform a web interaction
result = tool.run( result = tool.run(
api_method="act", api_method="act",
instruction="Click the 'Sign In' button" instruction="Click the 'Sign In' button"
) )
# Extract content from a page # Extract content from a page
content = tool.run( content = tool.run(
api_method="extract", api_method="extract",
instruction="Get the text content of the main article" instruction="Get the text content of the main article"
) )
# Monitor for changes # Monitor for changes
changes = tool.run( changes = tool.run(
api_method="observe", api_method="observe",
@@ -129,7 +143,7 @@ class StagehandTool(BaseTool):
) )
``` ```
""" """
name: str = "StagehandTool" name: str = "StagehandTool"
description: str = ( description: str = (
"A tool that uses Stagehand's AI-powered web automation to interact with websites. " "A tool that uses Stagehand's AI-powered web automation to interact with websites. "
@@ -137,27 +151,33 @@ class StagehandTool(BaseTool):
"Each instruction should be atomic (simple and specific) to increase reliability." "Each instruction should be atomic (simple and specific) to increase reliability."
) )
args_schema: Type[BaseModel] = StagehandToolSchema args_schema: Type[BaseModel] = StagehandToolSchema
def __init__(self, config: StagehandToolConfig | None = None, **kwargs: Any) -> None: def __init__(
self, config: StagehandToolConfig | None = None, **kwargs: Any
) -> None:
"""Initialize the StagehandTool. """Initialize the StagehandTool.
Args: Args:
config: Optional configuration for the tool. If not provided, config: Optional configuration for the tool. If not provided,
will attempt to use OPENAI_API_KEY from environment. will attempt to use OPENAI_API_KEY from environment.
**kwargs: Additional keyword arguments passed to the base class. **kwargs: Additional keyword arguments passed to the base class.
Raises: Raises:
ImportError: If the stagehand package is not installed ImportError: If the stagehand package is not installed
ValueError: If no API key is provided via config or environment ValueError: If no API key is provided via config or environment
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
if not STAGEHAND_AVAILABLE: if not STAGEHAND_AVAILABLE:
raise ImportError( import click
"The 'stagehand' package is required to use this tool. "
"Please install it with: pip install stagehand" if click.confirm(
) "You are missing the 'stagehand-sdk' package. Would you like to install it? (y/N)"
):
import subprocess
subprocess.run(["uv", "add", "stagehand-sdk"], check=True)
# Use config if provided, otherwise try environment variable # Use config if provided, otherwise try environment variable
if config is not None: if config is not None:
self.config = config self.config = config
@@ -168,24 +188,22 @@ class StagehandTool(BaseTool):
"Either provide config with api_key or set OPENAI_API_KEY environment variable" "Either provide config with api_key or set OPENAI_API_KEY environment variable"
) )
self.config = StagehandToolConfig( self.config = StagehandToolConfig(
api_key=api_key, api_key=api_key, timeout=30, retry_attempts=3
timeout=30,
retry_attempts=3
) )
@lru_cache(maxsize=100) @lru_cache(maxsize=100)
def _cached_run(self, api_method: str, instruction: str) -> Any: def _cached_run(self, api_method: str, instruction: str) -> Any:
"""Execute a cached Stagehand command. """Execute a cached Stagehand command.
This method is cached to improve performance for repeated operations. This method is cached to improve performance for repeated operations.
Args: Args:
api_method: The Stagehand API to use ('act', 'extract', or 'observe') api_method: The Stagehand API to use ('act', 'extract', or 'observe')
instruction: An atomic instruction for Stagehand to execute instruction: An atomic instruction for Stagehand to execute
Returns: Returns:
The raw result from the Stagehand API call The raw result from the Stagehand API call
Raises: Raises:
ValueError: If an invalid api_method is provided ValueError: If an invalid api_method is provided
Exception: If the Stagehand API call fails Exception: If the Stagehand API call fails
@@ -193,23 +211,25 @@ class StagehandTool(BaseTool):
logger.debug( logger.debug(
"Cache operation - Method: %s, Instruction length: %d", "Cache operation - Method: %s, Instruction length: %d",
api_method, api_method,
len(instruction) len(instruction),
) )
# Initialize Stagehand with configuration # Initialize Stagehand with configuration
logger.info( logger.info(
"Initializing Stagehand (timeout=%ds, retries=%d)", "Initializing Stagehand (timeout=%ds, retries=%d)",
self.config.timeout, self.config.timeout,
self.config.retry_attempts self.config.retry_attempts,
) )
st = stagehand.Stagehand( st = stagehand.Stagehand(
api_key=self.config.api_key, api_key=self.config.api_key,
timeout=self.config.timeout, timeout=self.config.timeout,
retry_attempts=self.config.retry_attempts retry_attempts=self.config.retry_attempts,
) )
# Call the appropriate Stagehand API based on the method # Call the appropriate Stagehand API based on the method
logger.info("Executing %s operation with instruction: %s", api_method, instruction[:100]) logger.info(
"Executing %s operation with instruction: %s", api_method, instruction[:100]
)
try: try:
if api_method == "act": if api_method == "act":
result = st.act(instruction) result = st.act(instruction)
@@ -219,28 +239,27 @@ class StagehandTool(BaseTool):
result = st.observe(instruction) result = st.observe(instruction)
else: else:
raise ValueError(f"Unknown api_method: {api_method}") raise ValueError(f"Unknown api_method: {api_method}")
logger.info("Successfully executed %s operation", api_method) logger.info("Successfully executed %s operation", api_method)
return result return result
except Exception as e: except Exception as e:
logger.warning( logger.warning(
"Operation failed (method=%s, error=%s), will be retried on next attempt", "Operation failed (method=%s, error=%s), will be retried on next attempt",
api_method, api_method,
str(e) str(e),
) )
raise raise
def _run(self, api_method: str, instruction: str, **kwargs: Any) -> StagehandResult: def _run(self, api_method: str, instruction: str, **kwargs: Any) -> StagehandResult:
"""Execute a Stagehand command using the specified API method. """Execute a Stagehand command using the specified API method.
Args: Args:
api_method: The Stagehand API to use ('act', 'extract', or 'observe') api_method: The Stagehand API to use ('act', 'extract', or 'observe')
instruction: An atomic instruction for Stagehand to execute instruction: An atomic instruction for Stagehand to execute
**kwargs: Additional keyword arguments passed to the Stagehand API **kwargs: Additional keyword arguments passed to the Stagehand API
Returns: Returns:
StagehandResult containing the operation result and status StagehandResult containing the operation result and status
""" """
try: try:
@@ -249,56 +268,36 @@ class StagehandTool(BaseTool):
"Starting operation - Method: %s, Instruction length: %d, Args: %s", "Starting operation - Method: %s, Instruction length: %d, Args: %s",
api_method, api_method,
len(instruction), len(instruction),
kwargs kwargs,
) )
# Use cached execution # Use cached execution
result = self._cached_run(api_method, instruction) result = self._cached_run(api_method, instruction)
logger.info("Operation completed successfully") logger.info("Operation completed successfully")
return StagehandResult(success=True, data=result) return StagehandResult(success=True, data=result)
except stagehand.AuthenticationError as e: except stagehand.AuthenticationError as e:
logger.error( logger.error(
"Authentication failed - Method: %s, Error: %s", "Authentication failed - Method: %s, Error: %s", api_method, str(e)
api_method,
str(e)
) )
return StagehandResult( return StagehandResult(
success=False, success=False, data={}, error=f"Authentication failed: {str(e)}"
data={},
error=f"Authentication failed: {str(e)}"
) )
except stagehand.APIError as e: except stagehand.APIError as e:
logger.error( logger.error("API error - Method: %s, Error: %s", api_method, str(e))
"API error - Method: %s, Error: %s", return StagehandResult(success=False, data={}, error=f"API error: {str(e)}")
api_method,
str(e)
)
return StagehandResult(
success=False,
data={},
error=f"API error: {str(e)}"
)
except stagehand.BrowserError as e: except stagehand.BrowserError as e:
logger.error( logger.error("Browser error - Method: %s, Error: %s", api_method, str(e))
"Browser error - Method: %s, Error: %s",
api_method,
str(e)
)
return StagehandResult( return StagehandResult(
success=False, success=False, data={}, error=f"Browser error: {str(e)}"
data={},
error=f"Browser error: {str(e)}"
) )
except Exception as e: except Exception as e:
logger.error( logger.error(
"Unexpected error - Method: %s, Error type: %s, Message: %s", "Unexpected error - Method: %s, Error type: %s, Message: %s",
api_method, api_method,
type(e).__name__, type(e).__name__,
str(e) str(e),
) )
return StagehandResult( return StagehandResult(
success=False, success=False, data={}, error=f"Unexpected error: {str(e)}"
data={},
error=f"Unexpected error: {str(e)}"
) )

View File

@@ -68,12 +68,25 @@ class WeaviateVectorSearchTool(BaseTool):
model="gpt-4o", model="gpt-4o",
) )
) )
else:
import click
if click.confirm(
"You are missing the 'weaviate-client' package. Would you like to install it? (y/N)"
):
import subprocess
subprocess.run(["uv", "pip", "install", "weaviate-client"], check=True)
else:
raise ImportError(
"You are missing the 'weaviate-client' package. Would you like to install it? (y/N)"
)
def _run(self, query: str) -> str: def _run(self, query: str) -> str:
if not WEAVIATE_AVAILABLE: if not WEAVIATE_AVAILABLE:
raise ImportError( raise ImportError(
"The 'weaviate-client' package is required to use the WeaviateVectorSearchTool. " "You are missing the 'weaviate-client' package. Would you like to install it? (y/N)"
"Please install it with: uv add weaviate-client"
) )
if not self.weaviate_cluster_url or not self.weaviate_api_key: if not self.weaviate_cluster_url or not self.weaviate_api_key: