#!/usr/bin/env python3
|
"""
|
Tavily AI Search API Client
|
Usage: python tavily_search.py "your search query" [--max-results 5] [--depth basic|advanced]
|
"""
|
|
import os
|
import sys
|
import json
|
import argparse
|
from pathlib import Path
|
from typing import List, Dict, Any, Optional
|
|
|
def get_tavily_key_from_env_file() -> Optional[str]:
|
"""Read Tavily API key from ~/.openclaw/.env file.
|
|
Expected format: {"env": {"TAVILY_API_KEY": "your_key"}}
|
"""
|
env_file = Path.home() / ".openclaw" / ".env"
|
if not env_file.exists():
|
return None
|
|
try:
|
with open(env_file, 'r', encoding='utf-8') as f:
|
env_data = json.load(f)
|
# Support nested structure: {"env": {"TAVILY_API_KEY": "..."}}
|
if "env" in env_data:
|
return env_data["env"].get("TAVILY_API_KEY")
|
# Fallback to flat structure: {"TAVILY_API_KEY": "..."}
|
return env_data.get("TAVILY_API_KEY")
|
except (json.JSONDecodeError, IOError):
|
return None
|
|
|
def get_api_key(api_key: Optional[str] = None) -> str:
|
"""Get Tavily API key from various sources."""
|
# Priority: parameter > env var > .env file
|
if api_key:
|
return api_key
|
|
env_key = os.environ.get("TAVILY_API_KEY")
|
if env_key:
|
return env_key
|
|
env_file_key = get_tavily_key_from_env_file()
|
if env_file_key:
|
return env_file_key
|
|
raise ValueError(
|
"Tavily API key required. Set via one of:\n"
|
" 1. TAVILY_API_KEY environment variable\n"
|
" 2. ~/.openclaw/.env file (JSON format: {\"env\": {\"TAVILY_API_KEY\": \"...\"}})\n"
|
" 3. Pass as api_key parameter\n"
|
"\nGet your API key at: https://tavily.com"
|
)
|
|
|
def tavily_search(
|
query: str,
|
max_results: int = 5,
|
search_depth: str = "basic",
|
include_answer: bool = False,
|
include_images: bool = False,
|
api_key: Optional[str] = None
|
) -> Dict[str, Any]:
|
"""
|
Search using Tavily AI Search API.
|
|
Args:
|
query: Search query string
|
max_results: Number of results to return (1-20)
|
search_depth: "basic" or "advanced"
|
include_answer: Include AI-generated answer
|
include_images: Include image URLs
|
api_key: Tavily API key (optional, auto-detected from env/config)
|
|
Returns:
|
Dictionary containing search results
|
"""
|
api_key = get_api_key(api_key)
|
|
try:
|
import requests
|
except ImportError:
|
raise ImportError("requests package required. Install with: pip install requests")
|
|
url = "https://api.tavily.com/search"
|
|
payload = {
|
"api_key": api_key,
|
"query": query,
|
"max_results": min(max(max_results, 1), 20),
|
"search_depth": search_depth,
|
"include_answer": include_answer,
|
"include_images": include_images,
|
}
|
|
response = requests.post(url, json=payload, timeout=30)
|
response.raise_for_status()
|
|
return response.json()
|
|
|
def format_results(results: Dict[str, Any]) -> str:
|
"""Format search results for display."""
|
output = []
|
|
if "answer" in results and results["answer"]:
|
output.append("=" * 60)
|
output.append("AI ANSWER")
|
output.append("=" * 60)
|
output.append(results["answer"])
|
output.append("")
|
|
output.append("=" * 60)
|
output.append("SEARCH RESULTS")
|
output.append("=" * 60)
|
output.append(f"Query: {results.get('query', 'N/A')}")
|
output.append("")
|
|
for i, result in enumerate(results.get("results", []), 1):
|
output.append(f"{i}. {result.get('title', 'No title')}")
|
output.append(f" URL: {result.get('url', 'N/A')}")
|
|
if result.get('published_date'):
|
output.append(f" Published: {result['published_date']}")
|
|
if result.get('score'):
|
output.append(f" Relevance: {result['score']:.2f}")
|
|
content = result.get('content', '')
|
if content:
|
# Truncate long content
|
if len(content) > 300:
|
content = content[:297] + "..."
|
output.append(f" {content}")
|
|
output.append("")
|
|
return "\n".join(output)
|
|
|
def main():
|
parser = argparse.ArgumentParser(description="Tavily AI Search")
|
parser.add_argument("query", help="Search query")
|
parser.add_argument("--max-results", type=int, default=5, help="Number of results (1-20)")
|
parser.add_argument("--depth", choices=["basic", "advanced"], default="basic", help="Search depth")
|
parser.add_argument("--answer", action="store_true", help="Include AI-generated answer")
|
parser.add_argument("--images", action="store_true", help="Include images")
|
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
args = parser.parse_args()
|
|
try:
|
results = tavily_search(
|
query=args.query,
|
max_results=args.max_results,
|
search_depth=args.depth,
|
include_answer=args.answer,
|
include_images=args.images
|
)
|
|
if args.json:
|
print(json.dumps(results, indent=2, ensure_ascii=False))
|
else:
|
print(format_results(results))
|
|
except ValueError as e:
|
print(f"Error: {e}", file=sys.stderr)
|
sys.exit(1)
|
except Exception as e:
|
print(f"Search failed: {e}", file=sys.stderr)
|
sys.exit(1)
|
|
|
if __name__ == "__main__":
|
main()
|