Update.
This commit is contained in:
parent
5dd6e8ebfd
commit
4971d36204
@ -1,10 +1,12 @@
|
||||
from typing import Dict, List, Any
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException, Query
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
|
||||
from ..core import Cache, ExaClient, OpenRouterClient, MarkdownRenderer, settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def _format_results_as_markdown(query: str, results: List[Dict]) -> str:
|
||||
if not results:
|
||||
@ -43,60 +45,159 @@ async def home(request: Request):
|
||||
|
||||
|
||||
@router.get("/ai", response_class=HTMLResponse)
|
||||
|
||||
|
||||
async def search_ai(request: Request, q: str = Query(""), num: int = 10):
|
||||
|
||||
|
||||
query = q.strip() if q else ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if not query:
|
||||
|
||||
|
||||
return request.state.templates.TemplateResponse("home.html", {"request": request})
|
||||
|
||||
cached = cache.get(query, "ai", num)
|
||||
|
||||
if cached:
|
||||
markdown_answer = cached.get("markdown_answer", "")
|
||||
html_answer = renderer.render(markdown_answer)
|
||||
search_results = cached.get("search_results", [])
|
||||
search_type = cached.get("search_type", "web")
|
||||
else:
|
||||
try:
|
||||
search_data = exa.answer_ai(query, num)
|
||||
search_results = search_data.get("results", [])
|
||||
search_type = search_data.get("search_type", "web")
|
||||
|
||||
markdown_answer = openrouter.format_results_as_markdown(query, search_results)
|
||||
|
||||
|
||||
logger.info(f"AI search request for: '{query}' (num={num})")
|
||||
|
||||
|
||||
try:
|
||||
|
||||
|
||||
cached = cache.get(query, "ai", num)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if cached:
|
||||
|
||||
|
||||
logger.info(f"Cache hit for AI search: '{query}'")
|
||||
|
||||
|
||||
markdown_answer = cached.get("markdown_answer", "")
|
||||
|
||||
|
||||
html_answer = renderer.render(markdown_answer)
|
||||
|
||||
cache.set(query, "ai", num, {
|
||||
"html_answer": html_answer,
|
||||
|
||||
search_results = cached.get("search_results", [])
|
||||
|
||||
|
||||
search_type = cached.get("search_type", "web")
|
||||
|
||||
|
||||
else:
|
||||
|
||||
|
||||
logger.info(f"Cache miss for AI search: '{query}'. Fetching from Exa.")
|
||||
|
||||
|
||||
try:
|
||||
|
||||
|
||||
search_data = exa.answer_ai(query, num)
|
||||
|
||||
|
||||
search_results = search_data.get("results", [])
|
||||
|
||||
|
||||
search_type = search_data.get("search_type", "web")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if not search_results:
|
||||
|
||||
|
||||
logger.warning(f"No results found from Exa for: '{query}'")
|
||||
|
||||
|
||||
raise Exception("No results found from Exa")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
logger.info(f"Generating AI answer with OpenRouter for: '{query}'")
|
||||
|
||||
|
||||
markdown_answer = await openrouter.format_results_as_markdown(query, search_results)
|
||||
|
||||
|
||||
html_answer = renderer.render(markdown_answer)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
cache.set(query, "ai", num, {
|
||||
|
||||
|
||||
"html_answer": html_answer,
|
||||
|
||||
|
||||
"search_results": search_results,
|
||||
|
||||
|
||||
"search_type": search_type,
|
||||
|
||||
|
||||
"markdown_answer": markdown_answer
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
except Exception as e:
|
||||
|
||||
|
||||
logger.error(f"AI search failed, falling back to web search for: '{query}'. Error: {e}")
|
||||
|
||||
|
||||
|
||||
# Fallback to standard web search
|
||||
fallback_results = exa.search_web(query, num)
|
||||
markdown_answer = _format_results_as_markdown(query, fallback_results)
|
||||
html_answer = renderer.render(markdown_answer)
|
||||
|
||||
result_data = {
|
||||
"html_answer": html_answer,
|
||||
"search_results": fallback_results,
|
||||
"search_type": "web",
|
||||
"markdown_answer": markdown_answer,
|
||||
"fallback": True
|
||||
}
|
||||
cache.set(query, "ai", num, result_data)
|
||||
|
||||
search_results = fallback_results
|
||||
search_type = "web"
|
||||
|
||||
return request.state.templates.TemplateResponse(
|
||||
"results_ai.html",
|
||||
{
|
||||
"request": request,
|
||||
"query": query,
|
||||
"answer": html_answer,
|
||||
"search_results": search_results,
|
||||
"search_type": search_type,
|
||||
"markdown_answer": markdown_answer
|
||||
})
|
||||
except Exception as e:
|
||||
fallback_results = exa.search_web(query, num)
|
||||
markdown_answer = _format_results_as_markdown(query, fallback_results)
|
||||
html_answer = renderer.render(markdown_answer)
|
||||
|
||||
cache.set(query, "ai", num, {
|
||||
"html_answer": html_answer,
|
||||
"search_results": fallback_results,
|
||||
"search_type": "web",
|
||||
"markdown_answer": markdown_answer,
|
||||
"fallback": True
|
||||
})
|
||||
|
||||
search_results = fallback_results
|
||||
search_type = "web"
|
||||
|
||||
return request.state.templates.TemplateResponse(
|
||||
"results_ai.html",
|
||||
{
|
||||
"request": request,
|
||||
"query": query,
|
||||
"answer": html_answer,
|
||||
"search_results": search_results,
|
||||
"search_type": search_type
|
||||
}
|
||||
)
|
||||
"search_type": search_type
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Critical error in /ai endpoint: {e}")
|
||||
return request.state.templates.TemplateResponse(
|
||||
"home.html",
|
||||
{"request": request, "error": "An internal error occurred. Please try again."}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/ai")
|
||||
@ -109,56 +210,67 @@ async def api_ai(q: str = Query(""), num: int = Query(10)):
|
||||
"query": ""
|
||||
}
|
||||
|
||||
cached = cache.get(query, "ai", num)
|
||||
|
||||
if cached:
|
||||
markdown_answer = cached.get("markdown_answer", "")
|
||||
html_answer = renderer.render(markdown_answer)
|
||||
search_results = cached.get("search_results", [])
|
||||
search_type = cached.get("search_type", "web")
|
||||
return {
|
||||
"html_answer": html_answer,
|
||||
"markdown_answer": markdown_answer,
|
||||
"search_results": search_results,
|
||||
"search_type": search_type,
|
||||
"cached": True
|
||||
}
|
||||
|
||||
try:
|
||||
search_data = exa.answer_ai(query, num)
|
||||
search_results = search_data.get("results", [])
|
||||
search_type = search_data.get("search_type", "web")
|
||||
cached = cache.get(query, "ai", num)
|
||||
|
||||
markdown_answer = openrouter.format_results_as_markdown(query, search_results)
|
||||
html_answer = renderer.render(markdown_answer)
|
||||
if cached:
|
||||
markdown_answer = cached.get("markdown_answer", "")
|
||||
html_answer = renderer.render(markdown_answer)
|
||||
search_results = cached.get("search_results", [])
|
||||
search_type = cached.get("search_type", "web")
|
||||
return {
|
||||
"html_answer": html_answer,
|
||||
"markdown_answer": markdown_answer,
|
||||
"search_results": search_results,
|
||||
"search_type": search_type,
|
||||
"cached": True
|
||||
}
|
||||
|
||||
result = {
|
||||
"html_answer": html_answer,
|
||||
"markdown_answer": markdown_answer,
|
||||
"search_results": search_results,
|
||||
"search_type": search_type,
|
||||
"cached": False
|
||||
}
|
||||
try:
|
||||
search_data = exa.answer_ai(query, num)
|
||||
search_results = search_data.get("results", [])
|
||||
search_type = search_data.get("search_type", "web")
|
||||
|
||||
cache.set(query, "ai", num, result)
|
||||
return result
|
||||
if not search_results:
|
||||
raise Exception("No results found from Exa")
|
||||
|
||||
markdown_answer = await openrouter.format_results_as_markdown(query, search_results)
|
||||
html_answer = renderer.render(markdown_answer)
|
||||
|
||||
result = {
|
||||
"html_answer": html_answer,
|
||||
"markdown_answer": markdown_answer,
|
||||
"search_results": search_results,
|
||||
"search_type": search_type,
|
||||
"cached": False
|
||||
}
|
||||
|
||||
cache.set(query, "ai", num, result)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"API AI search failed, falling back: {e}")
|
||||
fallback_results = exa.search_web(query, num)
|
||||
markdown_answer = _format_results_as_markdown(query, fallback_results)
|
||||
html_answer = renderer.render(markdown_answer)
|
||||
|
||||
result = {
|
||||
"html_answer": html_answer,
|
||||
"markdown_answer": markdown_answer,
|
||||
"search_results": fallback_results,
|
||||
"search_type": "web",
|
||||
"cached": False,
|
||||
"error": str(e),
|
||||
"fallback": True
|
||||
}
|
||||
|
||||
cache.set(query, "ai", num, result)
|
||||
return result
|
||||
except Exception as e:
|
||||
fallback_results = exa.search_web(query, num)
|
||||
markdown_answer = _format_results_as_markdown(query, fallback_results)
|
||||
html_answer = renderer.render(markdown_answer)
|
||||
|
||||
result = {
|
||||
"html_answer": html_answer,
|
||||
"markdown_answer": markdown_answer,
|
||||
"search_results": fallback_results,
|
||||
"search_type": "web",
|
||||
"cached": False,
|
||||
"error": str(e),
|
||||
"fallback": True
|
||||
}
|
||||
|
||||
cache.set(query, "ai", num, result)
|
||||
return result
|
||||
logger.error(f"Critical error in /api/ai endpoint: {e}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": "Internal server error", "detail": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/search", response_class=HTMLResponse)
|
||||
@ -168,17 +280,27 @@ async def search_web(request: Request, q: str = Query(""), num: int = 10):
|
||||
if not query:
|
||||
return request.state.templates.TemplateResponse("home.html", {"request": request})
|
||||
|
||||
cached_results = cache.get(query, "web", num)
|
||||
if cached_results:
|
||||
results = cached_results.get("results", [])
|
||||
else:
|
||||
results = exa.search_web(query, num)
|
||||
cache.set(query, "web", num, {"results": results})
|
||||
logger.info(f"Web search request for: '{query}' (num={num})")
|
||||
try:
|
||||
cached_results = cache.get(query, "web", num)
|
||||
if cached_results:
|
||||
logger.info(f"Cache hit for web search: '{query}'")
|
||||
results = cached_results.get("results", [])
|
||||
else:
|
||||
logger.info(f"Cache miss for web search: '{query}'. Fetching from Exa.")
|
||||
results = exa.search_web(query, num)
|
||||
cache.set(query, "web", num, {"results": results})
|
||||
|
||||
return request.state.templates.TemplateResponse(
|
||||
"results_web.html",
|
||||
{"request": request, "query": query, "results": results},
|
||||
)
|
||||
return request.state.templates.TemplateResponse(
|
||||
"results_web.html",
|
||||
{"request": request, "query": query, "results": results},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in /search endpoint: {e}")
|
||||
return request.state.templates.TemplateResponse(
|
||||
"home.html",
|
||||
{"request": request, "error": "Search failed. Please try again."}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/images", response_class=HTMLResponse)
|
||||
@ -188,17 +310,24 @@ async def search_images(request: Request, q: str = Query(""), num: int = 20):
|
||||
if not query:
|
||||
return request.state.templates.TemplateResponse("home.html", {"request": request})
|
||||
|
||||
cached_results = cache.get(query, "images", num)
|
||||
if cached_results:
|
||||
results = cached_results.get("results", [])
|
||||
else:
|
||||
results = exa.search_images(query, num)
|
||||
cache.set(query, "images", num, {"results": results})
|
||||
try:
|
||||
cached_results = cache.get(query, "images", num)
|
||||
if cached_results:
|
||||
results = cached_results.get("results", [])
|
||||
else:
|
||||
results = exa.search_images(query, num)
|
||||
cache.set(query, "images", num, {"results": results})
|
||||
|
||||
return request.state.templates.TemplateResponse(
|
||||
"results_images.html",
|
||||
{"request": request, "query": query, "results": results},
|
||||
)
|
||||
return request.state.templates.TemplateResponse(
|
||||
"results_images.html",
|
||||
{"request": request, "query": query, "results": results},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in /images endpoint: {e}")
|
||||
return request.state.templates.TemplateResponse(
|
||||
"home.html",
|
||||
{"request": request, "error": "Image search failed. Please try again."}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/search")
|
||||
@ -211,13 +340,20 @@ async def api_search(q: str = Query(""), num: int = Query(10)):
|
||||
"results": []
|
||||
}
|
||||
|
||||
cached_results = cache.get(query, "web", num)
|
||||
if cached_results:
|
||||
return {"results": cached_results.get("results", []), "cached": True}
|
||||
try:
|
||||
cached_results = cache.get(query, "web", num)
|
||||
if cached_results:
|
||||
return {"results": cached_results.get("results", []), "cached": True}
|
||||
|
||||
results = exa.search_web(query, num)
|
||||
cache.set(query, "web", num, {"results": results})
|
||||
return {"results": results, "cached": False}
|
||||
results = exa.search_web(query, num)
|
||||
cache.set(query, "web", num, {"results": results})
|
||||
return {"results": results, "cached": False}
|
||||
except Exception as e:
|
||||
logger.error(f"Error in /api/search endpoint: {e}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": "Internal server error", "detail": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/images")
|
||||
@ -230,13 +366,20 @@ async def api_images(q: str = Query(""), num: int = Query(20)):
|
||||
"results": []
|
||||
}
|
||||
|
||||
cached_results = cache.get(query, "images", num)
|
||||
if cached_results:
|
||||
return {"results": cached_results.get("results", []), "cached": True}
|
||||
try:
|
||||
cached_results = cache.get(query, "images", num)
|
||||
if cached_results:
|
||||
return {"results": cached_results.get("results", []), "cached": True}
|
||||
|
||||
results = exa.search_images(query, num)
|
||||
cache.set(query, "images", num, {"results": results})
|
||||
return {"results": results, "cached": False}
|
||||
results = exa.search_images(query, num)
|
||||
cache.set(query, "images", num, {"results": results})
|
||||
return {"results": results, "cached": False}
|
||||
except Exception as e:
|
||||
logger.error(f"Error in /api/images endpoint: {e}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": "Internal server error", "detail": str(e)}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/mixed")
|
||||
@ -244,10 +387,17 @@ async def api_mixed(q: str = Query(...), num: int = Query(10)):
|
||||
if not q.strip():
|
||||
raise HTTPException(status_code=400, detail="Query parameter 'q' is required")
|
||||
|
||||
cached_results = cache.get(q, "mixed", num)
|
||||
if cached_results:
|
||||
return cached_results
|
||||
try:
|
||||
cached_results = cache.get(q, "mixed", num)
|
||||
if cached_results:
|
||||
return cached_results
|
||||
|
||||
results = exa.search_mixed(q, num)
|
||||
cache.set(q, "mixed", num, {"results": results})
|
||||
return results
|
||||
results = exa.search_mixed(q, num)
|
||||
cache.set(q, "mixed", num, {"results": results})
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.error(f"Error in /api/mixed endpoint: {e}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": "Internal server error", "detail": str(e)}
|
||||
)
|
||||
|
||||
@ -1,45 +1,75 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
import dataset
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Cache:
|
||||
def __init__(self, db_path: str):
|
||||
self.db = dataset.connect(f"sqlite:///{db_path}")
|
||||
self.table = self.db["cache"]
|
||||
try:
|
||||
self.db = dataset.connect(f"sqlite:///{db_path}")
|
||||
self.table = self.db["cache"]
|
||||
# Create index if table exists
|
||||
if self.table is not None:
|
||||
self.table.create_index(["cache_key"])
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to cache database: {e}")
|
||||
self.db = None
|
||||
self.table = None
|
||||
|
||||
def _generate_key(self, query: str, search_type: str, num_results: int) -> str:
|
||||
key_string = f"{query}:{search_type}:{num_results}"
|
||||
return hashlib.md5(key_string.encode()).hexdigest()
|
||||
|
||||
def get(self, query: str, search_type: str, num_results: int) -> Optional[dict]:
|
||||
cache_key = self._generate_key(query, search_type, num_results)
|
||||
cached = self.table.find_one(cache_key=cache_key)
|
||||
if not self.table:
|
||||
return None
|
||||
|
||||
if cached:
|
||||
cached_at = datetime.fromisoformat(cached["cached_at"])
|
||||
if datetime.now() - cached_at < timedelta(hours=24):
|
||||
return json.loads(cached["results_json"])
|
||||
else:
|
||||
self.table.delete(cache_key=cache_key)
|
||||
try:
|
||||
cache_key = self._generate_key(query, search_type, num_results)
|
||||
cached = self.table.find_one(cache_key=cache_key)
|
||||
|
||||
if cached:
|
||||
cached_at = datetime.fromisoformat(cached["cached_at"])
|
||||
if datetime.now() - cached_at < timedelta(hours=24):
|
||||
return json.loads(cached["results_json"])
|
||||
else:
|
||||
self.table.delete(cache_key=cache_key)
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving from cache: {e}")
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
def set(self, query: str, search_type: str, num_results: int, results: dict) -> None:
|
||||
cache_key = self._generate_key(query, search_type, num_results)
|
||||
self.table.upsert(
|
||||
{
|
||||
"cache_key": cache_key,
|
||||
"search_type": search_type,
|
||||
"query_text": query,
|
||||
"results_json": json.dumps(results),
|
||||
"cached_at": datetime.now().isoformat(),
|
||||
},
|
||||
["cache_key"],
|
||||
)
|
||||
if not self.table:
|
||||
return
|
||||
|
||||
try:
|
||||
cache_key = self._generate_key(query, search_type, num_results)
|
||||
self.table.upsert(
|
||||
{
|
||||
"cache_key": cache_key,
|
||||
"search_type": search_type,
|
||||
"query_text": query,
|
||||
"results_json": json.dumps(results),
|
||||
"cached_at": datetime.now().isoformat(),
|
||||
},
|
||||
["cache_key"],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error writing to cache: {e}")
|
||||
|
||||
def clear(self) -> None:
|
||||
self.table.delete()
|
||||
if not self.table:
|
||||
return
|
||||
|
||||
try:
|
||||
self.table.delete()
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing cache: {e}")
|
||||
|
||||
@ -1,61 +1,83 @@
|
||||
from typing import List, Dict
|
||||
import logging
|
||||
|
||||
from exa_py import Exa
|
||||
|
||||
from .config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExaClient:
|
||||
def __init__(self):
|
||||
self.client = Exa(settings.EXA_API_KEY)
|
||||
|
||||
def search_web(self, query: str, num_results: int = 10) -> List[Dict]:
|
||||
response = self.client.search_and_contents(
|
||||
query,
|
||||
num_results=num_results,
|
||||
text=True,
|
||||
highlights=True,
|
||||
)
|
||||
return self._format_results(response.results)
|
||||
try:
|
||||
response = self.client.search_and_contents(
|
||||
query,
|
||||
num_results=num_results,
|
||||
text=True,
|
||||
highlights=True,
|
||||
)
|
||||
return self._format_results(response.results)
|
||||
except Exception as e:
|
||||
logger.error(f"Exa web search failed: {e}")
|
||||
return []
|
||||
|
||||
def search_images(self, query: str, num_results: int = 20) -> List[Dict]:
|
||||
response = self.client.search_and_contents(
|
||||
query,
|
||||
num_results=num_results,
|
||||
text=True,
|
||||
)
|
||||
return [r for r in self._format_results(response.results) if r.get("image")]
|
||||
try:
|
||||
response = self.client.search_and_contents(
|
||||
query,
|
||||
num_results=num_results,
|
||||
text=True,
|
||||
)
|
||||
return [r for r in self._format_results(response.results) if r.get("image")]
|
||||
except Exception as e:
|
||||
logger.error(f"Exa image search failed: {e}")
|
||||
return []
|
||||
|
||||
def search_mixed(self, query: str, num_results: int = 10) -> Dict[str, List[Dict]]:
|
||||
response = self.client.search_and_contents(
|
||||
query,
|
||||
num_results=num_results * 2,
|
||||
text=True,
|
||||
)
|
||||
results = self._format_results(response.results)
|
||||
|
||||
web_results = results[:num_results]
|
||||
image_results = [r for r in results if r.get("image")][:num_results]
|
||||
|
||||
return {"web": web_results, "images": image_results}
|
||||
try:
|
||||
response = self.client.search_and_contents(
|
||||
query,
|
||||
num_results=num_results * 2,
|
||||
text=True,
|
||||
)
|
||||
results = self._format_results(response.results)
|
||||
|
||||
web_results = results[:num_results]
|
||||
image_results = [r for r in results if r.get("image")][:num_results]
|
||||
|
||||
return {"web": web_results, "images": image_results}
|
||||
except Exception as e:
|
||||
logger.error(f"Exa mixed search failed: {e}")
|
||||
return {"web": [], "images": []}
|
||||
|
||||
def answer_ai(self, query: str, num_results: int = 10) -> Dict:
|
||||
image_keywords = ["image", "photo", "picture", "pic", "img", "screenshot", "drawing", "art", "graphic"]
|
||||
query_lower = query.lower()
|
||||
|
||||
for keyword in image_keywords:
|
||||
if keyword in query_lower:
|
||||
results = self.search_images(query, num_results)
|
||||
return {
|
||||
"results": results,
|
||||
"search_type": "images"
|
||||
}
|
||||
|
||||
results = self.search_web(query, num_results)
|
||||
return {
|
||||
"results": results,
|
||||
"search_type": "web"
|
||||
}
|
||||
try:
|
||||
for keyword in image_keywords:
|
||||
if keyword in query_lower:
|
||||
results = self.search_images(query, num_results)
|
||||
return {
|
||||
"results": results,
|
||||
"search_type": "images"
|
||||
}
|
||||
|
||||
results = self.search_web(query, num_results)
|
||||
return {
|
||||
"results": results,
|
||||
"search_type": "web"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Exa answer AI failed: {e}")
|
||||
return {
|
||||
"results": [],
|
||||
"search_type": "web"
|
||||
}
|
||||
|
||||
def _format_results(self, results: List) -> List[Dict]:
|
||||
formatted = []
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
from typing import List, Dict, Any
|
||||
import json
|
||||
import logging
|
||||
|
||||
from openai import OpenAI
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
from .config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenRouterClient:
|
||||
def __init__(self):
|
||||
self.client = OpenAI(
|
||||
self.client = AsyncOpenAI(
|
||||
base_url="https://openrouter.ai/api/v1",
|
||||
api_key=settings.OPENROUTER_API_KEY
|
||||
)
|
||||
@ -22,7 +25,7 @@ class OpenRouterClient:
|
||||
return "images"
|
||||
return "web"
|
||||
|
||||
def format_results_as_markdown(self, query: str, results: List[Dict]) -> str:
|
||||
async def format_results_as_markdown(self, query: str, results: List[Dict]) -> str:
|
||||
search_type = self._determine_search_type(query)
|
||||
|
||||
system_prompt = """You are a helpful AI assistant. Your task is to synthesize and summarize information from search results.
|
||||
@ -57,7 +60,7 @@ Format your response as clean, professional markdown."""
|
||||
]
|
||||
|
||||
try:
|
||||
response = self.client.chat.completions.create(
|
||||
response = await self.client.chat.completions.create(
|
||||
model=settings.OPENROUTER_MODEL,
|
||||
messages=messages,
|
||||
temperature=0.1,
|
||||
@ -66,4 +69,5 @@ Format your response as clean, professional markdown."""
|
||||
content = response.choices[0].message.content
|
||||
return content if content else ""
|
||||
except Exception as e:
|
||||
raise Exception(f"OpenRouter API error: {e}")
|
||||
logger.error(f"OpenRouter API error: {e}")
|
||||
return ""
|
||||
|
||||
33
rexa/main.py
33
rexa/main.py
@ -1,11 +1,20 @@
|
||||
import logging
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse, HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from .api.router import router
|
||||
from .core import settings
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Application:
|
||||
def __init__(self):
|
||||
@ -14,6 +23,7 @@ class Application:
|
||||
self._setup_static_files()
|
||||
self._setup_templates()
|
||||
self._setup_routes()
|
||||
self._setup_exception_handlers()
|
||||
|
||||
def _setup_middleware(self):
|
||||
@self.app.middleware("http")
|
||||
@ -33,7 +43,6 @@ class Application:
|
||||
|
||||
@self.app.get("/manifest.json")
|
||||
async def manifest():
|
||||
from fastapi.responses import JSONResponse
|
||||
return JSONResponse(
|
||||
{
|
||||
"name": "Rexa Search",
|
||||
@ -83,6 +92,28 @@ self.addEventListener('fetch', event => {
|
||||
"""
|
||||
return Response(content=sw_content, media_type="application/javascript")
|
||||
|
||||
def _setup_exception_handlers(self):
|
||||
@self.app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
logger.error(f"Global exception: {exc}", exc_info=True)
|
||||
|
||||
# Check if request expects JSON
|
||||
if request.url.path.startswith("/api") or "application/json" in request.headers.get("accept", ""):
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": "Internal Server Error", "detail": "An unexpected error occurred."}
|
||||
)
|
||||
|
||||
# Default to HTML error page
|
||||
return self.templates.TemplateResponse(
|
||||
"home.html",
|
||||
{
|
||||
"request": request,
|
||||
"error": "An unexpected error occurred. We have logged this issue and will look into it."
|
||||
},
|
||||
status_code=500
|
||||
)
|
||||
|
||||
def run(self):
|
||||
uvicorn.run(
|
||||
self.app,
|
||||
|
||||
@ -219,6 +219,14 @@ a:visited {
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.favicon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.result-item h3 {
|
||||
margin: 0 0 2px 0;
|
||||
font-size: 16px;
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<h1>Rexa Search</h1>
|
||||
</div>
|
||||
|
||||
<form action="/ai" method="get" class="search-form" id="searchForm">
|
||||
<div class="search-form">
|
||||
<div class="search-box">
|
||||
<input
|
||||
type="text"
|
||||
@ -17,14 +17,15 @@
|
||||
placeholder="Ask anything..."
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
onkeypress="if(event.key === 'Enter') performSearch('ai')"
|
||||
>
|
||||
</div>
|
||||
<div class="search-buttons">
|
||||
<button type="submit" formaction="/ai">AI Search</button>
|
||||
<button type="submit" formaction="/search">Web Search</button>
|
||||
<button type="submit" formaction="/images">Image Search</button>
|
||||
<button type="button" onclick="performSearch('ai')">AI Search</button>
|
||||
<button type="button" onclick="performSearch('search')">Web Search</button>
|
||||
<button type="button" onclick="performSearch('images')">Image Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="search-tips">
|
||||
<p>Powered by Exa API</p>
|
||||
@ -32,12 +33,24 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('searchForm').addEventListener('submit', function(e) {
|
||||
var query = document.getElementById('searchInput').value.trim();
|
||||
if (!query) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
function performSearch(type) {
|
||||
const queryInput = document.getElementById('searchInput');
|
||||
const query = queryInput.value.trim();
|
||||
if (query) {
|
||||
// Show loading state
|
||||
const buttons = document.querySelectorAll('.search-buttons button');
|
||||
buttons.forEach(btn => {
|
||||
btn.disabled = true;
|
||||
if (btn.onclick.toString().includes(type)) {
|
||||
btn.innerText = 'Searching...';
|
||||
}
|
||||
});
|
||||
queryInput.disabled = true;
|
||||
|
||||
window.location.href = `/${type}?q=${encodeURIComponent(query)}`;
|
||||
} else {
|
||||
queryInput.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -49,6 +49,9 @@
|
||||
{% for result in search_results %}
|
||||
<div class="result-item">
|
||||
<h4>
|
||||
{% if result.favicon %}
|
||||
<img src="{{ result.favicon }}" alt="" class="favicon">
|
||||
{% endif %}
|
||||
<a href="{{ result.url }}" target="_blank" rel="noopener noreferrer">
|
||||
{{ result.title }}
|
||||
</a>
|
||||
@ -75,3 +78,19 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.querySelector('.search-form-inline').addEventListener('submit', function(e) {
|
||||
const input = this.querySelector('input[name="q"]');
|
||||
const button = this.querySelector('button');
|
||||
if (input.value.trim()) {
|
||||
input.disabled = true;
|
||||
button.disabled = true;
|
||||
button.innerText = 'Searching...';
|
||||
} else {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -55,3 +55,19 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.querySelector('.search-form-inline').addEventListener('submit', function(e) {
|
||||
const input = this.querySelector('input[name="q"]');
|
||||
const button = this.querySelector('button');
|
||||
if (input.value.trim()) {
|
||||
input.disabled = true;
|
||||
button.disabled = true;
|
||||
button.innerText = 'Searching...';
|
||||
} else {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -35,6 +35,9 @@
|
||||
{% for result in results %}
|
||||
<div class="result-item">
|
||||
<h3>
|
||||
{% if result.favicon %}
|
||||
<img src="{{ result.favicon }}" alt="" class="favicon">
|
||||
{% endif %}
|
||||
<a href="{{ result.url }}" target="_blank" rel="noopener noreferrer">{{ result.title }}</a>
|
||||
</h3>
|
||||
<div class="result-url">{{ result.url }}</div>
|
||||
@ -58,43 +61,18 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% if not query %}
|
||||
<div class="home-container">
|
||||
<div class="logo">
|
||||
<h1>Rexa Search</h1>
|
||||
</div>
|
||||
<div class="search-tips">
|
||||
<p>Please enter a search query.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="results-stats">
|
||||
<p>About {{ results|length }} results found</p>
|
||||
</div>
|
||||
|
||||
<div class="results-container">
|
||||
{% for result in results %}
|
||||
<div class="result-item">
|
||||
<h3>
|
||||
<a href="{{ result.url }}" target="_blank" rel="noopener noreferrer">{{ result.title }}</a>
|
||||
</h3>
|
||||
<div class="result-url">{{ result.url }}</div>
|
||||
<div class="result-snippet">
|
||||
{% if result.highlights %}
|
||||
{{ result.highlights[0]|safe }}
|
||||
{% else %}
|
||||
{{ result.text }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if not results %}
|
||||
<div class="no-results">
|
||||
<p>No results found for "{{ query }}"</p>
|
||||
<p>Try different keywords or check your spelling.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.querySelector('.search-form-inline').addEventListener('submit', function(e) {
|
||||
const input = this.querySelector('input[name="q"]');
|
||||
const button = this.querySelector('button');
|
||||
if (input.value.trim()) {
|
||||
input.disabled = true;
|
||||
button.disabled = true;
|
||||
button.innerText = 'Searching...';
|
||||
} else {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user