DeepImagix commited on
Commit
b8f2b49
Β·
verified Β·
1 Parent(s): 545a4cf

Update agent/tools/web_tools.py

Browse files
Files changed (1) hide show
  1. agent/tools/web_tools.py +17 -29
agent/tools/web_tools.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- NeuraPrompt Agent β€” Web Tools v8.0 (Multi-Engine Fallback, No API Keys)
3
  """
4
 
5
  import requests
@@ -12,16 +12,16 @@ from requests.adapters import HTTPAdapter
12
  from urllib3.util.retry import Retry
13
  import logging
14
 
15
- log = logging.getLogger("agent.tools.web.v8.0")
16
 
17
  # ─────────────────────────────────────────────────────────────
18
- # CONFIG
19
  # ─────────────────────────────────────────────────────────────
20
 
21
- TIMEOUT_SEARCH = 12
22
- TIMEOUT_FETCH = 20
23
- MAX_RETRIES = 3
24
- BACKOFF_FACTOR = 1.5
25
  MAX_RESULTS = 6
26
 
27
  USER_AGENTS = [
@@ -32,10 +32,13 @@ USER_AGENTS = [
32
  ]
33
 
34
  SEARXNG_INSTANCES = [
35
- "https://search.sapti.me",
36
  "https://searx.be",
 
37
  "https://searx.tiekoetter.com",
38
  "https://searx.prvcy.eu",
 
 
 
39
  ]
40
 
41
 
@@ -140,7 +143,6 @@ def _search_ddg_lite(session: requests.Session, query: str) -> list[dict] | None
140
  soup = BeautifulSoup(response.text, "lxml")
141
  results = []
142
 
143
- # DDG Lite uses table.result rows
144
  for tr in soup.select("table.result")[:MAX_RESULTS]:
145
  link = tr.select_one("a.result-link")
146
  if not link:
@@ -148,7 +150,6 @@ def _search_ddg_lite(session: requests.Session, query: str) -> list[dict] | None
148
 
149
  title = link.get_text(strip=True)
150
  href = link.get("href", "")
151
- # DDG Lite sometimes uses relative or redirect URLs
152
  if href.startswith("/"):
153
  href = f"https://lite.duckduckgo.com{href}"
154
  elif href.startswith("//"):
@@ -187,7 +188,6 @@ def _search_ddg_api(session: requests.Session, query: str) -> list[dict] | None:
187
 
188
  results = []
189
 
190
- # Main abstract answer
191
  abstract = (data.get("AbstractText") or data.get("Answer") or "").strip()
192
  abstract_url = data.get("AbstractURL", "")
193
  if abstract and abstract_url:
@@ -198,7 +198,6 @@ def _search_ddg_api(session: requests.Session, query: str) -> list[dict] | None:
198
  "engine": "ddg-api"
199
  })
200
 
201
- # Related topics
202
  for topic in data.get("RelatedTopics", [])[:MAX_RESULTS - 1]:
203
  text = topic.get("Text", "").strip()
204
  first_url = topic.get("FirstURL", "")
@@ -214,7 +213,7 @@ def _search_ddg_api(session: requests.Session, query: str) -> list[dict] | None:
214
 
215
 
216
  def _search_bing(session: requests.Session, query: str) -> list[dict] | None:
217
- """Engine 3: Bing HTML (moderate blocking, rich results)."""
218
  url = f"https://www.bing.com/search?q={quote_plus(query)}"
219
 
220
  response = _safe_request(
@@ -229,7 +228,6 @@ def _search_bing(session: requests.Session, query: str) -> list[dict] | None:
229
  soup = BeautifulSoup(response.text, "lxml")
230
  results = []
231
 
232
- # Bing uses li.b_algo for organic results
233
  for li in soup.select("li.b_algo")[:MAX_RESULTS]:
234
  a = li.select_one("a")
235
  if not a:
@@ -238,7 +236,6 @@ def _search_bing(session: requests.Session, query: str) -> list[dict] | None:
238
  title = a.get_text(strip=True)
239
  href = a.get("href", "")
240
 
241
- # Extract snippet from various Bing structures
242
  snippet = ""
243
  for sel in ["p", ".b_caption p", "div.b_attribution+div", ".b_snippet"]:
244
  el = li.select_one(sel)
@@ -258,10 +255,10 @@ def _search_bing(session: requests.Session, query: str) -> list[dict] | None:
258
 
259
 
260
  def _search_searxng(session: requests.Session, query: str) -> list[dict] | None:
261
- """Engine 4: SearXNG public instances (meta-search, JSON output)."""
262
  random.shuffle(SEARXNG_INSTANCES)
263
 
264
- for base in SEARXNG_INSTANCES[:3]: # Try 3 random instances
265
  url = f"{base}/search?q={quote_plus(query)}&format=json&language=en"
266
 
267
  response = _safe_request(
@@ -304,7 +301,6 @@ def _search_searxng(session: requests.Session, query: str) -> list[dict] | None:
304
  def web_search(query: str) -> str:
305
  """
306
  Search the web using multiple no-key engines with automatic fallback.
307
-
308
  Priority: DDG Lite β†’ DDG API β†’ Bing β†’ SearXNG
309
  """
310
  if not query or not query.strip():
@@ -381,22 +377,18 @@ def fetch_url(url: str) -> str:
381
  "Tip: Try a different URL or check if the site requires JavaScript."
382
  )
383
 
384
- # Handle non-HTML content
385
  content_type = response.headers.get("content-type", "").lower()
386
  if "text/html" not in content_type:
387
  preview = response.text[:8000]
388
  return f"[Non-HTML content: {content_type}]\n\n{preview}"
389
 
390
- # Parse and clean HTML
391
  soup = BeautifulSoup(response.text, "lxml")
392
 
393
- # Remove noise elements
394
  for tag in soup(["script", "style", "nav", "header", "footer", "aside",
395
  "form", "iframe", "noscript", "svg", "canvas",
396
  "advertisement", ".ad", ".ads", ".cookie-banner"]):
397
  tag.decompose()
398
 
399
- # Strategy 1: Look for semantic content containers
400
  content_blocks = []
401
  for selector in ["article", "main", "[role='main']", ".content", ".post", ".entry"]:
402
  for el in soup.select(selector):
@@ -404,18 +396,15 @@ def fetch_url(url: str) -> str:
404
  if len(text) > 300:
405
  content_blocks.append(text)
406
 
407
- # Strategy 2: Fallback to headings + paragraphs
408
  if not content_blocks:
409
  for tag in soup.find_all(["h1", "h2", "h3", "h4", "p", "li", "td"]):
410
  text = tag.get_text(" ", strip=True)
411
  if len(text) > 30:
412
  content_blocks.append(text)
413
 
414
- # Deduplicate and join
415
  seen = set()
416
  final_blocks = []
417
  for block in content_blocks:
418
- # Simple dedup: skip if very similar to already-seen
419
  sig = block[:100].lower()
420
  if sig not in seen:
421
  seen.add(sig)
@@ -426,7 +415,6 @@ def fetch_url(url: str) -> str:
426
  if not text:
427
  return "No readable content found. The page may be JavaScript-rendered or heavily obfuscated."
428
 
429
- # Add metadata header
430
  title = ""
431
  title_tag = soup.find("title")
432
  if title_tag:
@@ -442,7 +430,8 @@ def fetch_url(url: str) -> str:
442
  # ─────────────────────────────────────────────────────────────
443
 
444
  if __name__ == "__main__":
445
- # Quick self-test
 
446
  print("=" * 60)
447
  print("TEST: web_search('Python programming language')")
448
  print("=" * 60)
@@ -450,5 +439,4 @@ if __name__ == "__main__":
450
  print("\n" + "=" * 60)
451
  print("TEST: fetch_url('https://en.wikipedia.org/wiki/Python_(programming_language)')")
452
  print("=" * 60)
453
- print(fetch_url("https://en.wikipedia.org/wiki/Python_(programming_language)")[:1500])
454
-
 
1
  """
2
+ NeuraPrompt Agent β€” Web Tools v8.1 (Faster Failover + Resilience)
3
  """
4
 
5
  import requests
 
12
  from urllib3.util.retry import Retry
13
  import logging
14
 
15
+ log = logging.getLogger("agent.tools.web.v8.1")
16
 
17
  # ─────────────────────────────────────────────────────────────
18
+ # CONFIG - TUNED FOR SPEED & RELIABILITY
19
  # ─────────────────────────────────────────────────────────────
20
 
21
+ TIMEOUT_SEARCH = 8
22
+ TIMEOUT_FETCH = 15
23
+ MAX_RETRIES = 1 # Reduced β†’ fail fast
24
+ BACKOFF_FACTOR = 0.3 # Minimal backoff
25
  MAX_RESULTS = 6
26
 
27
  USER_AGENTS = [
 
32
  ]
33
 
34
  SEARXNG_INSTANCES = [
 
35
  "https://searx.be",
36
+ "https://search.ononoki.org",
37
  "https://searx.tiekoetter.com",
38
  "https://searx.prvcy.eu",
39
+ "https://search.sapti.me",
40
+ "https://darmarit.org/searx",
41
+ "https://searxng.site",
42
  ]
43
 
44
 
 
143
  soup = BeautifulSoup(response.text, "lxml")
144
  results = []
145
 
 
146
  for tr in soup.select("table.result")[:MAX_RESULTS]:
147
  link = tr.select_one("a.result-link")
148
  if not link:
 
150
 
151
  title = link.get_text(strip=True)
152
  href = link.get("href", "")
 
153
  if href.startswith("/"):
154
  href = f"https://lite.duckduckgo.com{href}"
155
  elif href.startswith("//"):
 
188
 
189
  results = []
190
 
 
191
  abstract = (data.get("AbstractText") or data.get("Answer") or "").strip()
192
  abstract_url = data.get("AbstractURL", "")
193
  if abstract and abstract_url:
 
198
  "engine": "ddg-api"
199
  })
200
 
 
201
  for topic in data.get("RelatedTopics", [])[:MAX_RESULTS - 1]:
202
  text = topic.get("Text", "").strip()
203
  first_url = topic.get("FirstURL", "")
 
213
 
214
 
215
  def _search_bing(session: requests.Session, query: str) -> list[dict] | None:
216
+ """Engine 3: Bing HTML."""
217
  url = f"https://www.bing.com/search?q={quote_plus(query)}"
218
 
219
  response = _safe_request(
 
228
  soup = BeautifulSoup(response.text, "lxml")
229
  results = []
230
 
 
231
  for li in soup.select("li.b_algo")[:MAX_RESULTS]:
232
  a = li.select_one("a")
233
  if not a:
 
236
  title = a.get_text(strip=True)
237
  href = a.get("href", "")
238
 
 
239
  snippet = ""
240
  for sel in ["p", ".b_caption p", "div.b_attribution+div", ".b_snippet"]:
241
  el = li.select_one(sel)
 
255
 
256
 
257
  def _search_searxng(session: requests.Session, query: str) -> list[dict] | None:
258
+ """Engine 4: SearXNG public instances."""
259
  random.shuffle(SEARXNG_INSTANCES)
260
 
261
+ for base in SEARXNG_INSTANCES[:3]:
262
  url = f"{base}/search?q={quote_plus(query)}&format=json&language=en"
263
 
264
  response = _safe_request(
 
301
  def web_search(query: str) -> str:
302
  """
303
  Search the web using multiple no-key engines with automatic fallback.
 
304
  Priority: DDG Lite β†’ DDG API β†’ Bing β†’ SearXNG
305
  """
306
  if not query or not query.strip():
 
377
  "Tip: Try a different URL or check if the site requires JavaScript."
378
  )
379
 
 
380
  content_type = response.headers.get("content-type", "").lower()
381
  if "text/html" not in content_type:
382
  preview = response.text[:8000]
383
  return f"[Non-HTML content: {content_type}]\n\n{preview}"
384
 
 
385
  soup = BeautifulSoup(response.text, "lxml")
386
 
 
387
  for tag in soup(["script", "style", "nav", "header", "footer", "aside",
388
  "form", "iframe", "noscript", "svg", "canvas",
389
  "advertisement", ".ad", ".ads", ".cookie-banner"]):
390
  tag.decompose()
391
 
 
392
  content_blocks = []
393
  for selector in ["article", "main", "[role='main']", ".content", ".post", ".entry"]:
394
  for el in soup.select(selector):
 
396
  if len(text) > 300:
397
  content_blocks.append(text)
398
 
 
399
  if not content_blocks:
400
  for tag in soup.find_all(["h1", "h2", "h3", "h4", "p", "li", "td"]):
401
  text = tag.get_text(" ", strip=True)
402
  if len(text) > 30:
403
  content_blocks.append(text)
404
 
 
405
  seen = set()
406
  final_blocks = []
407
  for block in content_blocks:
 
408
  sig = block[:100].lower()
409
  if sig not in seen:
410
  seen.add(sig)
 
415
  if not text:
416
  return "No readable content found. The page may be JavaScript-rendered or heavily obfuscated."
417
 
 
418
  title = ""
419
  title_tag = soup.find("title")
420
  if title_tag:
 
430
  # ─────────────────────────────────────────────────────────────
431
 
432
  if __name__ == "__main__":
433
+ import logging
434
+ logging.basicConfig(level=logging.INFO)
435
  print("=" * 60)
436
  print("TEST: web_search('Python programming language')")
437
  print("=" * 60)
 
439
  print("\n" + "=" * 60)
440
  print("TEST: fetch_url('https://en.wikipedia.org/wiki/Python_(programming_language)')")
441
  print("=" * 60)
442
+ print(fetch_url("https://en.wikipedia.org/wiki/Python_(programming_language)")[:1500])