from fastapi import FastAPI, HTTPException,Request from pydantic import BaseModel import urllib.request import urllib.parse from bs4 import BeautifulSoup import time import json import uvicorn import functools app = FastAPI() import aiohttp import os API_KEY = os.getenv("OPENROUTER_API_KEY") OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" MODEL_NAME = "google/gemma-3-12b-it" async def prompt(q,p): search_results = search_duckduckgo(q) headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"} payload = {"model": MODEL_NAME, "messages": [{"role": "system", "content": f"You will do with the data what the prompt expects from you. You respond in valid JSON format like {{\"response\":,}}.\n\n# DATA\n{search_results}"},{"role": "user", "content": p}]} async with aiohttp.ClientSession() as session: try: async with session.post(OPENROUTER_URL, headers=headers, json=payload) as response: response.raise_for_status() result = await response.json() raw_content = result["choices"][0]["message"]["content"] json_start = raw_content.find('{') json_end = raw_content.rfind('}') json_content = raw_content[json_start:json_end+1] return json.loads(json_content) except Exception as e: return {"error": str(e)} CACHE = {} def manual_cache(expire: int): """ A custom decorator to cache the result of a FastAPI route function based on path and query parameters. """ def decorator(func): @functools.wraps(func) async def wrapper(request: Request, *args, **kwargs): sorted_params = sorted(request.query_params.items()) key = f"{request.url.path}?{sorted_params}" current_time = time.time() if key in CACHE: result, timestamp = CACHE[key] if current_time - timestamp < expire: print(f"✅ CACHE HIT for key: {key}") return result else: print(f"⌛️ CACHE EXPIRED for key: {key}") print(f"❌ CACHE MISS for key: {key}") new_result = await func(request=request, *args, **kwargs) CACHE[key] = (new_result, current_time) return new_result return wrapper return decorator # class SearchQuery(BaseModel): query: str class FetchQuery(BaseModel): url: str def web_request(url: str) -> str: """Fetch and return content from a URL.""" try: request = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (compatible; AI Assistant)'}) with urllib.request.urlopen(request) as response: return response.read().decode('utf-8') except Exception as error: return f"Error: {str(error)}" def search_duckduckgo(query: str) -> str: """Search DuckDuckGo and extract snippets.""" query_encoded = urllib.parse.quote(query) url = f"https://duckduckgo.com/html/?q={query_encoded}" content = web_request(url) if "Error" in content: return content try: soup = BeautifulSoup(content, 'html.parser') results = [] for result in soup.find_all('div', class_='result')[:5]: # Top 5 results title_tag = result.find('h2', class_='result__title') title = title_tag.get_text() if title_tag else "No title" snippet_tag = result.find('a', class_='result__snippet') snippet = snippet_tag.get_text() if snippet_tag else "No snippet" link_tag = result.find('a', class_='result__url') link = link_tag.get_text() if link_tag else "No link" results.append({"Title": title, "Snippet": snippet, "Link": link}) return json.dumps(results, indent=2, default=str) except ImportError: # If bs4 not available, fallback to grep-like extraction lines = content.split('\n') snippets = [line for line in lines if 'pony' in line.lower()][:10] return "\n".join(snippets) or "No relevant snippets found." @app.get("/search") @manual_cache(expire=60*60) async def search_endpoint(query: str,p:str=None,request:Request=None): if not query: raise HTTPException(status_code=400, detail="Query parameter is required") if p: return await prompt(query,p) return search_duckduckgo(query) @app.get("/fetch") def fetch_endpoint(url: str): if not url: raise HTTPException(status_code=400, detail="URL parameter is required") return web_request(url) if __name__ == "__main__": uvicorn.run(app, host="localhost", port=8777)