feat: 日常增量 - 小红书配图/舆情记录/日报/草稿归档
This commit is contained in:
241
skills/tavily-web-search-for-openclaw/scripts/tavily_search.py
Normal file
241
skills/tavily-web-search-for-openclaw/scripts/tavily_search.py
Normal file
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
API_URL = "https://api.tavily.com/search"
|
||||
|
||||
|
||||
def load_api_key():
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
key_path = os.path.normpath(
|
||||
os.path.join(base_dir, "..", ".secrets", "tavily.key")
|
||||
)
|
||||
|
||||
try:
|
||||
with open(key_path, "r", encoding="utf-8") as f:
|
||||
raw = f.read().strip()
|
||||
|
||||
if "=" in raw:
|
||||
left, right = raw.split("=", 1)
|
||||
if left.strip() == "TAVILY_API_KEY":
|
||||
return right.strip()
|
||||
|
||||
return raw or None
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def clamp_max_results(value: int) -> int:
|
||||
if value < 1:
|
||||
return 1
|
||||
if value > 10:
|
||||
return 10
|
||||
return value
|
||||
|
||||
|
||||
def build_payload(args: argparse.Namespace, api_key: str) -> dict:
|
||||
payload = {
|
||||
"api_key": api_key,
|
||||
"query": args.query,
|
||||
"search_depth": args.depth,
|
||||
"topic": args.topic,
|
||||
"max_results": clamp_max_results(args.max_results),
|
||||
"include_answer": not args.no_answer,
|
||||
"include_raw_content": args.raw_content,
|
||||
"include_images": args.images,
|
||||
}
|
||||
|
||||
if args.include_domains:
|
||||
payload["include_domains"] = args.include_domains
|
||||
|
||||
if args.exclude_domains:
|
||||
payload["exclude_domains"] = args.exclude_domains
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def tavily_search(payload: dict, timeout: int = 30) -> dict:
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
API_URL,
|
||||
data=data,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
body = resp.read().decode("utf-8", errors="replace")
|
||||
return json.loads(body)
|
||||
except urllib.error.HTTPError as exc:
|
||||
details = ""
|
||||
try:
|
||||
details = exc.read().decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
details = ""
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"HTTP {exc.code}",
|
||||
"details": details,
|
||||
}
|
||||
except urllib.error.URLError as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Network error",
|
||||
"details": str(exc.reason),
|
||||
}
|
||||
except Exception as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Unexpected error",
|
||||
"details": str(exc),
|
||||
}
|
||||
|
||||
|
||||
def print_human(result: dict) -> int:
|
||||
if not isinstance(result, dict):
|
||||
print("Error: invalid response format", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if "error" in result and not result.get("results"):
|
||||
print(f"Error: {result.get('error')}", file=sys.stderr)
|
||||
if result.get("details"):
|
||||
print(result["details"], file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"Query: {result.get('query', 'N/A')}")
|
||||
print(f"Response time: {result.get('response_time', 'N/A')}")
|
||||
usage = result.get("usage", {})
|
||||
if isinstance(usage, dict):
|
||||
print(f"Credits used: {usage.get('credits', 'N/A')}")
|
||||
print()
|
||||
|
||||
answer = result.get("answer")
|
||||
if answer:
|
||||
print("=== ANSWER ===")
|
||||
print(answer)
|
||||
print()
|
||||
|
||||
results = result.get("results", [])
|
||||
if results:
|
||||
print("=== RESULTS ===")
|
||||
for index, item in enumerate(results, start=1):
|
||||
title = item.get("title") or "No title"
|
||||
url = item.get("url") or "N/A"
|
||||
score = item.get("score")
|
||||
content = item.get("content") or ""
|
||||
|
||||
print(f"\n{index}. {title}")
|
||||
print(f" URL: {url}")
|
||||
|
||||
if isinstance(score, (int, float)):
|
||||
print(f" Score: {score:.3f}")
|
||||
|
||||
if content:
|
||||
snippet = content[:280].replace("\n", " ").strip()
|
||||
if len(content) > 280:
|
||||
snippet += "..."
|
||||
print(f" {snippet}")
|
||||
|
||||
images = result.get("images", [])
|
||||
if images:
|
||||
print(f"\n=== IMAGES ({len(images)}) ===")
|
||||
for image in images[:5]:
|
||||
if isinstance(image, dict):
|
||||
print(f" {image.get('url', 'N/A')}")
|
||||
else:
|
||||
print(f" {image}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Tavily Search via direct REST API call",
|
||||
)
|
||||
|
||||
parser.add_argument("--query", required=True, help="Search query")
|
||||
parser.add_argument(
|
||||
"--api-key",
|
||||
help="Tavily API key. If omitted, file or TAVILY_API_KEY is used.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--depth",
|
||||
choices=["basic", "advanced"],
|
||||
default="basic",
|
||||
help="Search depth",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--topic",
|
||||
choices=["general", "news"],
|
||||
default="general",
|
||||
help="Search topic",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-results",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Number of results to return (1-10)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-answer",
|
||||
action="store_true",
|
||||
help="Do not request Tavily answer summary",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--raw-content",
|
||||
action="store_true",
|
||||
help="Include parsed raw content",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--images",
|
||||
action="store_true",
|
||||
help="Include image results",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-domains",
|
||||
nargs="+",
|
||||
help="Only include these domains",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--exclude-domains",
|
||||
nargs="+",
|
||||
help="Exclude these domains",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Print raw JSON response",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
api_key = load_api_key()
|
||||
if not api_key:
|
||||
print(
|
||||
"Error: Tavily API key not found in ../.secrets/tavily.key",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
payload = build_payload(args, api_key)
|
||||
result = tavily_search(payload)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
return 0 if "error" not in result else 1
|
||||
|
||||
return print_human(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user