aip9105 commited on
Commit
1f01c99
ยท
verified ยท
1 Parent(s): 5e07f1b

Create tavily_search.py

Browse files
Files changed (1) hide show
  1. tavily_search.py +159 -0
tavily_search.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import json
4
+ import os
5
+ import pathlib
6
+ import re
7
+ import sys
8
+ import urllib.request
9
+
10
+ TAVILY_URL = "https://api.tavily.com/search"
11
+
12
+
13
+ def load_key():
14
+ key = os.environ.get("TAVILY_API_KEY")
15
+ if key:
16
+ return key.strip()
17
+
18
+ env_path = pathlib.Path.home() / ".openclaw" / ".env"
19
+ if env_path.exists():
20
+ try:
21
+ txt = env_path.read_text(encoding="utf-8", errors="ignore")
22
+ m = re.search(r"^\s*TAVILY_API_KEY\s*=\s*(.+?)\s*$", txt, re.M)
23
+ if m:
24
+ v = m.group(1).strip().strip('"').strip("'")
25
+ if v:
26
+ return v
27
+ except Exception:
28
+ pass
29
+
30
+ return None
31
+
32
+
33
+ def tavily_search(query: str, max_results: int, include_answer: bool, search_depth: str):
34
+ key = load_key()
35
+ if not key:
36
+ raise SystemExit(
37
+ "Missing TAVILY_API_KEY. Set env var TAVILY_API_KEY or add it to ~/.openclaw/.env"
38
+ )
39
+
40
+ payload = {
41
+ "api_key": key,
42
+ "query": query,
43
+ "max_results": max_results,
44
+ "search_depth": search_depth,
45
+ "include_answer": bool(include_answer),
46
+ "include_images": False,
47
+ "include_raw_content": False,
48
+ }
49
+
50
+ data = json.dumps(payload).encode("utf-8")
51
+ req = urllib.request.Request(
52
+ TAVILY_URL,
53
+ data=data,
54
+ headers={"Content-Type": "application/json", "Accept": "application/json"},
55
+ method="POST",
56
+ )
57
+
58
+ with urllib.request.urlopen(req, timeout=30) as resp:
59
+ body = resp.read().decode("utf-8", errors="replace")
60
+
61
+ try:
62
+ obj = json.loads(body)
63
+ except json.JSONDecodeError:
64
+ raise SystemExit(f"Tavily returned non-JSON: {body[:300]}")
65
+
66
+ out = {
67
+ "query": query,
68
+ "answer": obj.get("answer"),
69
+ "results": [],
70
+ }
71
+
72
+ for r in (obj.get("results") or [])[:max_results]:
73
+ out["results"].append(
74
+ {
75
+ "title": r.get("title"),
76
+ "url": r.get("url"),
77
+ "content": r.get("content"),
78
+ }
79
+ )
80
+
81
+ if not include_answer:
82
+ out.pop("answer", None)
83
+
84
+ return out
85
+
86
+
87
+ def to_brave_like(obj: dict) -> dict:
88
+ # A lightweight, stable shape similar to web_search: results with title/url/snippet.
89
+ results = []
90
+ for r in obj.get("results", []) or []:
91
+ results.append(
92
+ {
93
+ "title": r.get("title"),
94
+ "url": r.get("url"),
95
+ "snippet": r.get("content"),
96
+ }
97
+ )
98
+ out = {"query": obj.get("query"), "results": results}
99
+ if "answer" in obj:
100
+ out["answer"] = obj.get("answer")
101
+ return out
102
+
103
+
104
+ def to_markdown(obj: dict) -> str:
105
+ lines = []
106
+ if obj.get("answer"):
107
+ lines.append(obj["answer"].strip())
108
+ lines.append("")
109
+ for i, r in enumerate(obj.get("results", []) or [], 1):
110
+ title = (r.get("title") or "").strip() or r.get("url") or "(no title)"
111
+ url = r.get("url") or ""
112
+ snippet = (r.get("content") or "").strip()
113
+ lines.append(f"{i}. {title}")
114
+ if url:
115
+ lines.append(f" {url}")
116
+ if snippet:
117
+ lines.append(f" - {snippet}")
118
+ return "\n".join(lines).strip() + "\n"
119
+
120
+
121
+ def main():
122
+ ap = argparse.ArgumentParser()
123
+ ap.add_argument("--query", required=True)
124
+ ap.add_argument("--max-results", type=int, default=5)
125
+ ap.add_argument("--include-answer", action="store_true")
126
+ ap.add_argument(
127
+ "--search-depth",
128
+ default="basic",
129
+ choices=["basic", "advanced"],
130
+ help="Tavily search depth",
131
+ )
132
+ ap.add_argument(
133
+ "--format",
134
+ default="raw",
135
+ choices=["raw", "brave", "md"],
136
+ help="Output format: raw (default) | brave (title/url/snippet) | md (human-readable)",
137
+ )
138
+ args = ap.parse_args()
139
+
140
+ res = tavily_search(
141
+ query=args.query,
142
+ max_results=max(1, min(args.max_results, 10)),
143
+ include_answer=args.include_answer,
144
+ search_depth=args.search_depth,
145
+ )
146
+
147
+ if args.format == "md":
148
+ sys.stdout.write(to_markdown(res))
149
+ return
150
+
151
+ if args.format == "brave":
152
+ res = to_brave_like(res)
153
+
154
+ json.dump(res, sys.stdout, ensure_ascii=False)
155
+ sys.stdout.write("\n")
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()