from contextlib import asynccontextmanager import json from typing import Optional from duckduckgo_search import DDGS from duckduckgo_search.exceptions import RatelimitException import expiringdict from fastapi import FastAPI from pydantic import BaseModel, Field from playwright.async_api import async_playwright, Browser, BrowserContext, Page from urllib.parse import quote_plus import logging import re import uvicorn from backends import APISearchResults, APIPatentResults, query_bing_search, query_brave_search, query_ddg_search, query_google_patents logging.basicConfig( level=logging.INFO, format='[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d]: %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) # playwright global context playwright = None pw_browser: Optional[Browser] = None @asynccontextmanager async def api_lifespan(app: FastAPI): global playwright, pw_browser playwright = await async_playwright().start() pw_browser = await playwright.chromium.launch(headless=True) yield await pw_browser.close() await playwright.stop() app = FastAPI(lifespan=api_lifespan) backend_status = expiringdict.ExpiringDict(max_len=5, max_age_seconds=15*60) class APISearchParams(BaseModel): queries: list[str] = Field(..., description="The list of queries to search for") n_results: int = Field( 10, description="Number of results to return for each query. Valid values are 10, 25, 50 and 100") @app.post("/search_scholar") async def query_google_scholar(params: APISearchParams): """Queries google scholar for the specified query""" return {"error": "Unimplemented"} @app.get('/') async def status(): backend_keys = [k[0] for k in backend_status.items()] backend_status_dict = {} for k in backend_keys: backend_status_dict[k] = backend_status.get(k) return {"status": "running", "backend_status": backend_status_dict} @app.post("/search_patents") async def search_patents(params: APISearchParams) -> APIPatentResults: """Searches google patents for the specified queries and returns the found documents.""" results = [] for q in params.queries: logging.info(f"Searching Google Patents with query `{q}`") try: res = await query_google_patents(pw_browser, q, params.n_results) results.extend(res) except Exception as e: backend_status["gpatents"] = "rate-limited" logging.error( f"Failed to query Google Patents with query `{q}`: {e}") return APIPatentResults(results=results, error=None) @app.post("/search_brave") async def search_brave(params: APISearchParams) -> APISearchResults: """Searches brave search for the specified queries and returns the found documents.""" results = [] last_exception: Optional[Exception] = None for q in params.queries: logging.info(f"Searching Brave search with query `{q}`") try: res = await query_brave_search(pw_browser, q, params.n_results) results.extend(res) except Exception as e: last_exception = e backend_status["brave"] = "rate-limited" logging.error( f"Failed to query Brave search with query `{q}`: {e}") return APISearchResults(results=results, error=str(last_exception) if len(results) == 0 and last_exception else None) @app.post("/search_bing") async def search_bing(params: APISearchParams) -> APISearchResults: """Searches Bing search for the specified queries and returns the found documents.""" results = [] last_exception: Optional[Exception] = None for q in params.queries: logging.info(f"Searching Bing search with query `{q}`") try: res = await query_brave_search(pw_browser, q, params.n_results) results.extend(res) except Exception as e: last_exception = e backend_status["bing"] = "rate-limited" logging.error( f"Failed to query Bing search with query `{q}`: {e}") return APISearchResults(results=results, error=str(last_exception) if len(results) == 0 and last_exception else None) @app.post("/search_duck") async def search_duck(params: APISearchParams) -> APISearchResults: """Searches duckduckgo for the specified queries and returns the found documents""" results = [] last_exception: Optional[Exception] = None for q in params.queries: logging.info(f"Querying DDG with query: `{q}`") try: res = await query_ddg_search(q, params.n_results) results.extend(res) except Exception as e: last_exception = e backend_status["duckduckgo"] = "rate-limited" logging.error(f"Failed to query DDG with query `{q}`: {e}") return APISearchResults(results=results, error=str(last_exception) if len(results) == 0 and last_exception else None) @app.post("/search") async def search(params: APISearchParams): """Attempts to search the specified queries using ALL backends""" results = [] for q in params.queries: try: logging.info(f"Querying DDG with query: `{q}`") res = await query_ddg_search(q, params.n_results) results.extend(res) continue except Exception as e: logging.error(f"Failed to query DDG with query `{q}`: {e}") logging.info("Trying with next browser backend.") try: logging.info(f"Querying Brave Search with query: `{q}`") res = await query_brave_search(pw_browser, q, params.n_results) results.extend(res) continue except Exception as e: logging.error( f"Failed to query Brave Search with query `{q}`: {e}") logging.info("Trying with next browser backend.") try: logging.info(f"Querying Bing with query: `{q}`") res = await query_bing_search(pw_browser, q, params.n_results) results.extend(res) continue except Exception as e: logging.error(f"Failed to query Bing search with query `{q}`: {e}") logging.info("Trying with next browser backend.") if len(results) == 0: return APISearchResults(results=[], error="All backends are rate-limited.") return APISearchResults(results=results, error=None) uvicorn.run(app, host="0.0.0.0", port=7860)