mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-12 09:38:31 +00:00
git-subtree-dir: packages/tools git-subtree-split: 78317b9c127f18bd040c1d77e3c0840cdc9a5b38
120 lines
4.1 KiB
Python
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 {})
|