cazyundee commited on
Commit
b047b4e
·
verified ·
1 Parent(s): 3c1c18c

Fix regex quoting for DuckDuckGo search

Browse files
Files changed (1) hide show
  1. app.py +31 -53
app.py CHANGED
@@ -148,15 +148,14 @@ API_RESOURCES = {
148
  },
149
  "web_search": {
150
  "name": "Web search",
151
- "description": "Search the web using a self-hosted SearXNG metasearch engine.",
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, engine, score per result",
160
  }
161
  }
162
 
@@ -206,67 +205,46 @@ def _get_runtime_specs():
206
 
207
 
208
  # ---------------------------------------------------------------------------
209
- # SearXNG search (uses public instances with JSON API)
210
  # ---------------------------------------------------------------------------
211
 
212
- SEARXNG_INSTANCES = [
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 _probe_searxng_instances():
222
- """Find a working SearXNG instance."""
223
- global _searxng_active_instance
224
- for instance in SEARXNG_INSTANCES:
225
- try:
226
- url = f"{instance}/search?q=test&format=json"
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
- data = _json.loads(resp.read())
 
256
  results = []
257
- for r in data.get("results", [])[:max_results]:
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  results.append({
259
- "title": r.get("title", ""),
260
- "url": r.get("url", ""),
261
- "content": r.get("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(data.get("results", [])),
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