Spaces:
Running
Running
Update agent/tools/web_tools.py
Browse files- agent/tools/web_tools.py +17 -29
agent/tools/web_tools.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
NeuraPrompt Agent β Web Tools v8.
|
| 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.
|
| 16 |
|
| 17 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 18 |
-
# CONFIG
|
| 19 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 20 |
|
| 21 |
-
TIMEOUT_SEARCH =
|
| 22 |
-
TIMEOUT_FETCH =
|
| 23 |
-
MAX_RETRIES =
|
| 24 |
-
BACKOFF_FACTOR =
|
| 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
|
| 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
|
| 262 |
random.shuffle(SEARXNG_INSTANCES)
|
| 263 |
|
| 264 |
-
for base in SEARXNG_INSTANCES[:3]:
|
| 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 |
-
|
|
|
|
| 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])
|
|
|