Spaces:
Running on Zero
Running on Zero
Fix regex quoting for DuckDuckGo search
Browse files
app.py
CHANGED
|
@@ -148,15 +148,14 @@ API_RESOURCES = {
|
|
| 148 |
},
|
| 149 |
"web_search": {
|
| 150 |
"name": "Web search",
|
| 151 |
-
"description": "Search the web
|
| 152 |
"endpoint": "/respite/search",
|
| 153 |
"method": "GET",
|
| 154 |
"input": {
|
| 155 |
"q": "string (search query)",
|
| 156 |
-
"categories": "string (general, images, news, etc.)",
|
| 157 |
"max_results": "integer (1-50, default 10)",
|
| 158 |
},
|
| 159 |
-
"output": "JSON with title, url, content
|
| 160 |
}
|
| 161 |
}
|
| 162 |
|
|
@@ -206,67 +205,46 @@ def _get_runtime_specs():
|
|
| 206 |
|
| 207 |
|
| 208 |
# ---------------------------------------------------------------------------
|
| 209 |
-
#
|
| 210 |
# ---------------------------------------------------------------------------
|
| 211 |
|
| 212 |
-
|
| 213 |
-
"https://search.ononoki.org",
|
| 214 |
-
"https://searx.be",
|
| 215 |
-
"https://search.sapti.me",
|
| 216 |
-
"https://searxng.site",
|
| 217 |
-
]
|
| 218 |
-
_searxng_active_instance = None
|
| 219 |
|
| 220 |
|
| 221 |
-
def
|
| 222 |
-
"""
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
req = urllib.request.Request(url, headers={"User-Agent": "RespiteAPI/1.0"})
|
| 228 |
-
resp = urllib.request.urlopen(req, timeout=5)
|
| 229 |
-
_json.loads(resp.read())
|
| 230 |
-
_searxng_active_instance = instance
|
| 231 |
-
_log(f"SearXNG instance ready: {instance}")
|
| 232 |
-
return
|
| 233 |
-
except Exception:
|
| 234 |
-
continue
|
| 235 |
-
_log("Warning: No working SearXNG instance found")
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
# Probe instances in background so it doesn't block startup
|
| 239 |
-
threading.Thread(target=_probe_searxng_instances, daemon=True).start()
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
def _search_web(query, categories="general", max_results=10):
|
| 243 |
-
"""Query a public SearXNG instance."""
|
| 244 |
-
if not _searxng_active_instance:
|
| 245 |
-
raise RuntimeError("No SearXNG instance available")
|
| 246 |
-
params = {
|
| 247 |
-
"q": query,
|
| 248 |
-
"format": "json",
|
| 249 |
-
"categories": categories,
|
| 250 |
-
"language": "en",
|
| 251 |
-
}
|
| 252 |
-
url = f"{_searxng_active_instance}/search?{urllib.parse.urlencode(params)}"
|
| 253 |
-
req = urllib.request.Request(url, headers={"User-Agent": "RespiteAPI/1.0"})
|
| 254 |
resp = urllib.request.urlopen(req, timeout=10)
|
| 255 |
-
|
|
|
|
| 256 |
results = []
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
results.append({
|
| 259 |
-
"title":
|
| 260 |
-
"url":
|
| 261 |
-
"content":
|
| 262 |
-
"engine": r.get("engine", ""),
|
| 263 |
-
"score": r.get("score", 0),
|
| 264 |
})
|
|
|
|
| 265 |
return {
|
| 266 |
"query": query,
|
| 267 |
-
"instance": _searxng_active_instance,
|
| 268 |
"results": results,
|
| 269 |
-
"total_results": len(
|
| 270 |
}
|
| 271 |
|
| 272 |
|
|
|
|
| 148 |
},
|
| 149 |
"web_search": {
|
| 150 |
"name": "Web search",
|
| 151 |
+
"description": "Search the web via DuckDuckGo. No API key required.",
|
| 152 |
"endpoint": "/respite/search",
|
| 153 |
"method": "GET",
|
| 154 |
"input": {
|
| 155 |
"q": "string (search query)",
|
|
|
|
| 156 |
"max_results": "integer (1-50, default 10)",
|
| 157 |
},
|
| 158 |
+
"output": "JSON with title, url, content per result",
|
| 159 |
}
|
| 160 |
}
|
| 161 |
|
|
|
|
| 205 |
|
| 206 |
|
| 207 |
# ---------------------------------------------------------------------------
|
| 208 |
+
# Web search (DuckDuckGo HTML scraping, no API key required)
|
| 209 |
# ---------------------------------------------------------------------------
|
| 210 |
|
| 211 |
+
import re as _re
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
|
| 213 |
|
| 214 |
+
def _search_web(query, max_results=10):
|
| 215 |
+
"""Search the web via DuckDuckGo HTML endpoint."""
|
| 216 |
+
url = f"https://html.duckduckgo.com/html/?{urllib.parse.urlencode({'q': query})}"
|
| 217 |
+
req = urllib.request.Request(url, headers={
|
| 218 |
+
"User-Agent": "Mozilla/5.0 (compatible; RespiteAPI/1.0)",
|
| 219 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
resp = urllib.request.urlopen(req, timeout=10)
|
| 221 |
+
html = resp.read().decode("utf-8", errors="replace")
|
| 222 |
+
|
| 223 |
results = []
|
| 224 |
+
# Extract search results from DuckDuckGo HTML
|
| 225 |
+
for match in _re.finditer(
|
| 226 |
+
r'<a[^>]+class="result__a"[^>]*href="([^"]+)"[^>]*>(.+?)</a>.*?'
|
| 227 |
+
r'<a[^>]+class="result__snippet"[^>]*>(.*?)</a>',
|
| 228 |
+
html, _re.DOTALL,
|
| 229 |
+
):
|
| 230 |
+
if len(results) >= max_results:
|
| 231 |
+
break
|
| 232 |
+
raw_url = match.group(1)
|
| 233 |
+
title = _re.sub(r'<[^>]+>', '', match.group(2)).strip()
|
| 234 |
+
snippet = _re.sub(r'<[^>]+>', '', match.group(3)).strip()
|
| 235 |
+
# DuckDuckGo wraps URLs in a redirect; extract the real URL
|
| 236 |
+
ud_match = _re.search(r'uddg=([^"&]+)', raw_url)
|
| 237 |
+
real_url = urllib.parse.unquote(ud_match.group(1)) if ud_match else raw_url
|
| 238 |
results.append({
|
| 239 |
+
"title": title,
|
| 240 |
+
"url": real_url,
|
| 241 |
+
"content": snippet,
|
|
|
|
|
|
|
| 242 |
})
|
| 243 |
+
|
| 244 |
return {
|
| 245 |
"query": query,
|
|
|
|
| 246 |
"results": results,
|
| 247 |
+
"total_results": len(results),
|
| 248 |
}
|
| 249 |
|
| 250 |
|