Files
crewAI/crewai_tools/tools/parallel_tools/parallel_search_tool.py
Greyson Lalonde e16606672a Squashed 'packages/tools/' content from commit 78317b9c
git-subtree-dir: packages/tools
git-subtree-split: 78317b9c127f18bd040c1d77e3c0840cdc9a5b38
2025-09-12 21:58:02 -04:00

120 lines
4.1 KiB
Python

import os
from typing import Any, Dict, List, Optional, Type, Annotated
import requests
from crewai.tools import BaseTool, EnvVar
from pydantic import BaseModel, Field
class ParallelSearchInput(BaseModel):
"""Input schema for ParallelSearchTool using the Search API (v1beta).
At least one of objective or search_queries is required.
"""
objective: Optional[str] = Field(
None,
description="Natural-language goal for the web research (<=5000 chars)",
max_length=5000,
)
search_queries: Optional[List[Annotated[str, Field(max_length=200)]]] = Field(
default=None,
description="Optional list of keyword queries (<=5 items, each <=200 chars)",
min_length=1,
max_length=5,
)
processor: str = Field(
default="base",
description="Search processor: 'base' (fast/low cost) or 'pro' (higher quality/freshness)",
pattern=r"^(base|pro)$",
)
max_results: int = Field(
default=10,
ge=1,
le=40,
description="Maximum number of search results to return (processor limits apply)",
)
max_chars_per_result: int = Field(
default=6000,
ge=100,
description="Maximum characters per result excerpt (values >30000 not guaranteed)",
)
source_policy: Optional[Dict[str, Any]] = Field(
default=None, description="Optional source policy configuration"
)
class ParallelSearchTool(BaseTool):
name: str = "Parallel Web Search Tool"
description: str = (
"Search the web using Parallel's Search API (v1beta). Returns ranked results with "
"compressed excerpts optimized for LLMs."
)
args_schema: Type[BaseModel] = ParallelSearchInput
env_vars: List[EnvVar] = [
EnvVar(
name="PARALLEL_API_KEY",
description="API key for Parallel",
required=True,
),
]
package_dependencies: List[str] = ["requests"]
search_url: str = "https://api.parallel.ai/v1beta/search"
def _run(
self,
objective: Optional[str] = None,
search_queries: Optional[List[str]] = None,
processor: str = "base",
max_results: int = 10,
max_chars_per_result: int = 6000,
source_policy: Optional[Dict[str, Any]] = None,
**_: Any,
) -> str:
api_key = os.environ.get("PARALLEL_API_KEY")
if not api_key:
return "Error: PARALLEL_API_KEY environment variable is required"
if not objective and not search_queries:
return "Error: Provide at least one of 'objective' or 'search_queries'"
headers = {
"x-api-key": api_key,
"Content-Type": "application/json",
}
try:
payload: Dict[str, Any] = {
"processor": processor,
"max_results": max_results,
"max_chars_per_result": max_chars_per_result,
}
if objective is not None:
payload["objective"] = objective
if search_queries is not None:
payload["search_queries"] = search_queries
if source_policy is not None:
payload["source_policy"] = source_policy
request_timeout = 90 if processor == "pro" else 30
resp = requests.post(self.search_url, json=payload, headers=headers, timeout=request_timeout)
if resp.status_code >= 300:
return f"Parallel Search API error: {resp.status_code} {resp.text[:200]}"
data = resp.json()
return self._format_output(data)
except requests.Timeout:
return "Parallel Search API timeout. Please try again later."
except Exception as exc: # noqa: BLE001
return f"Unexpected error calling Parallel Search API: {exc}"
def _format_output(self, result: Dict[str, Any]) -> str:
# Return the full JSON payload (search_id + results) as a compact JSON string
try:
import json
return json.dumps(result or {}, ensure_ascii=False)
except Exception:
return str(result or {})