Spaces:
Running
Running
from flask import Flask, request, render_template_string, Response | |
from openai import OpenAI | |
import os | |
import json | |
from urllib.parse import quote | |
import html | |
app = Flask(__name__) | |
# Initialize OpenAI client with API key and base URL from environment variables | |
client = OpenAI( | |
api_key=os.environ["OPENAI_API_KEY"], | |
base_url=os.environ["OPENAI_BASE_URL"] | |
) | |
# Define constants for pagination | |
RESULTS_PER_PAGE = 10 | |
TOTAL_RESULTS = 30 # Generate 30 results to allow pagination | |
def fetch_search_results(query, stream=False): | |
"""Fetch search results from the LLM, with optional streaming.""" | |
if not query.strip(): | |
return None, "Please enter a search query." | |
prompt = f""" | |
You are a search engine that provides informative and relevant results. For the given query '{query}', | |
generate {TOTAL_RESULTS} search results. Each result should include: | |
- 'title': A concise, descriptive title of the result. | |
- 'snippet': A short summary (2-3 sentences) of the content. | |
- 'url': A plausible, clickable URL where the information might be found (e.g., a real or hypothetical website). | |
Format the response as a JSON array of objects, where each object has 'title', 'snippet', and 'url' fields. | |
Ensure the results are diverse, relevant to the query, and the URLs are realistic (e.g., https://example.com/page). | |
""" | |
try: | |
response = client.chat.completions.create( | |
model="gemini-2.0-flash-lite", # Updated model name | |
messages=[ | |
{"role": "system", "content": "You are a helpful search engine."}, | |
{"role": "user", "content": prompt} | |
], | |
response_format={"type": "json_object"}, | |
stream=stream # Enable streaming if requested | |
) | |
if stream: | |
return response, None # Return raw streaming response | |
else: | |
content = response.choices[0].message.content | |
results = json.loads(content) | |
# Handle different possible JSON structures | |
if isinstance(results, dict) and "results" in results: | |
results = results["results"] | |
elif isinstance(results, list): | |
pass | |
else: | |
return None, "Error: Unexpected JSON structure." | |
return results, None | |
except Exception as e: | |
error_msg = str(e) | |
if "404" in error_msg: | |
return None, f"Error 404: Model or endpoint not found. Check OPENAI_BASE_URL ({os.environ['OPENAI_BASE_URL']}) and model name." | |
elif "401" in error_msg: | |
return None, "Error 401: Invalid API key. Check OPENAI_API_KEY." | |
else: | |
return None, f"Error: {error_msg}" | |
def stream_search_results(query, page): | |
"""Stream search results incrementally.""" | |
stream_response, error = fetch_search_results(query, stream=True) | |
if error: | |
yield f"<p style='color: red; text-align: center;'>{error}</p>" | |
return | |
# Generate header | |
header = """ | |
<html> | |
<head> | |
<title>LLM Search Engine</title> | |
<style> | |
body { font-family: Arial, sans-serif; margin: 0; padding: 0; color: #202124; } | |
.header { text-align: center; padding: 20px 0; } | |
.logo { font-size: 36px; font-weight: bold; } | |
.logo span:nth-child(1) { color: #4285f4; } | |
.logo span:nth-child(2) { color: #ea4335; } | |
.logo span:nth-child(3) { color: #fbbc05; } | |
.logo span:nth-child(4) { color: #4285f4; } | |
.logo span:nth-child(5) { color: #34a853; } | |
.search-box { max-width: 584px; margin: 0 auto 20px; } | |
.search-box input[type="text"] { | |
width: 100%; padding: 12px 20px; font-size: 16px; | |
border: 1px solid #dfe1e5; border-radius: 24px; | |
box-shadow: 0 1px 6px rgba(32,33,36,0.28); outline: none; | |
} | |
.search-box input[type="submit"] { | |
background-color: #f8f9fa; border: 1px solid #f8f9fa; | |
border-radius: 4px; color: #3c4043; font-size: 14px; | |
padding: 10px 16px; margin: 11px 4px; cursor: pointer; | |
} | |
.search-box input[type="submit"]:hover { | |
border: 1px solid #dadce0; box-shadow: 0 1px 2px rgba(0,0,0,0.1); | |
} | |
.search-buttons { text-align: center; } | |
.results { max-width: 652px; margin: 0 auto; } | |
.search-result { margin-bottom: 28px; } | |
.search-result a { color: #1a0dab; font-size: 20px; text-decoration: none; } | |
.search-result a:hover { text-decoration: underline; } | |
.search-result .url { color: #006621; font-size: 14px; line-height: 20px; } | |
.search-result p { color: #4d5156; font-size: 14px; line-height: 22px; margin: 0; } | |
.pagination { text-align: center; margin: 40px 0; } | |
.pagination a, .pagination span { | |
color: #1a0dab; font-size: 14px; margin: 0 8px; text-decoration: none; | |
} | |
.pagination a:hover { text-decoration: underline; } | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
<div class="logo"> | |
<span>L</span><span>L</span><span>M</span><span> </span><span>Search</span> | |
</div> | |
</div> | |
<div class="search-box"> | |
<form method="get" action="/"> | |
<input type="text" name="query" value="{{query}}"> | |
<input type="hidden" name="page" value="1"> | |
<div class="search-buttons"> | |
<input type="submit" name="btn" value="LLM Search"> | |
<input type="submit" name="btn" value="I'm Feeling Lucky"> | |
</div> | |
</form> | |
</div> | |
<div class="results"> | |
<h2 style="font-size: 18px; color: #70757a; margin-bottom: 20px;">Results for '{{query}}' (Page {{page}})</h2> | |
""".replace('{{query}}', html.escape(query)).replace('{{page}}', str(page)) | |
yield header | |
# Stream and parse results | |
buffer = "" | |
results = [] | |
for chunk in stream_response: | |
if chunk.choices[0].delta.content: | |
buffer += chunk.choices[0].delta.content | |
try: | |
# Try to parse the buffer as JSON incrementally | |
temp_results = json.loads(buffer) | |
if isinstance(temp_results, list): | |
results = temp_results | |
elif isinstance(temp_results, dict) and "results" in temp_results: | |
results = temp_results["results"] | |
# Process only new results | |
for i, result in enumerate(results[len(results) - (len(results) % RESULTS_PER_PAGE):]): | |
if i >= start_idx and i < end_idx: | |
title = html.escape(result.get("title", "No title")) | |
snippet = html.escape(result.get("snippet", "No snippet")) | |
url = html.escape(result.get("url", "#")) | |
yield f""" | |
<div class="search-result"> | |
<a href="{url}" target="_blank">{title}</a> | |
<div class="url">{url}</div> | |
<p>{snippet}</p> | |
</div> | |
""" | |
except json.JSONDecodeError: | |
continue # Keep buffering until complete JSON | |
# Calculate pagination | |
start_idx = (page - 1) * RESULTS_PER_PAGE | |
end_idx = start_idx + RESULTS_PER_PAGE | |
total_pages = (len(results) + RESULTS_PER_PAGE - 1) // RESULTS_PER_PAGE | |
# Pagination links | |
encoded_query = quote(query) | |
prev_link = f'<a href="/?query={encoded_query}&page={page-1}&btn=LLM+Search">Previous</a>' if page > 1 else '<span>Previous</span>' | |
next_link = f'<a href="/?query={encoded_query}&page={page+1}&btn=LLM+Search">Next</a>' if page < total_pages else '<span>Next</span>' | |
yield f""" | |
</div> | |
<div class="pagination"> | |
{prev_link} | |
<span>Page {page} of {total_pages}</span> | |
{next_link} | |
</div> | |
</body> | |
</html> | |
""" | |
def search_page(): | |
"""Generate and serve the search results page styled like Google.""" | |
query = request.args.get('query', '') | |
page = request.args.get('page', '1') | |
btn = request.args.get('btn', 'LLM Search') | |
try: | |
page = int(page) | |
except ValueError: | |
page = 1 | |
if not query.strip(): | |
html_content = """ | |
<html> | |
<head> | |
<title>LLM Search Engine</title> | |
<style> | |
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; } | |
.header { text-align: center; margin-bottom: 20px; } | |
.logo { font-size: 36px; font-weight: bold; } | |
.logo span:nth-child(1) { color: #4285f4; } | |
.logo span:nth-child(2) { color: #ea4335; } | |
.logo span:nth-child(3) { color: #fbbc05; } | |
.logo span:nth-child(4) { color: #4285f4; } | |
.logo span:nth-child(5) { color: #34a853; } | |
.search-box { max-width: 584px; margin: 0 auto; } | |
.search-box input[type="text"] { | |
width: 100%; padding: 12px 20px; font-size: 16px; | |
border: 1px solid #dfe1e5; border-radius: 24px; | |
box-shadow: 0 1px 6px rgba(32,33,36,0.28); | |
} | |
.search-box input[type="submit"] { | |
background-color: #f8f9fa; border: 1px solid #f8f9fa; | |
border-radius: 4px; color: #3c4043; font-size: 14px; | |
padding: 10px 16px; margin: 11px 4px; cursor: pointer; | |
} | |
.search-box input[type="submit"]:hover { | |
border: 1px solid #dadce0; box-shadow: 0 1px 2px rgba(0,0,0,0.1); | |
} | |
.search-buttons { text-align: center; } | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
<div class="logo"> | |
<span>L</span><span>L</span><span>M</span><span> </span><span>Search</span> | |
</div> | |
</div> | |
<div class="search-box"> | |
<form method="get" action="/"> | |
<input type="text" name="query" placeholder="Search..."> | |
<input type="hidden" name="page" value="1"> | |
<div class="search-buttons"> | |
<input type="submit" name="btn" value="LLM Search"> | |
<input type="submit" name="btn" value="I'm Feeling Lucky"> | |
</div> | |
</form> | |
</div> | |
</body> | |
</html> | |
""" | |
return render_template_string(html_content) | |
if btn == "I'm Feeling Lucky": | |
results, error = fetch_search_results(query, stream=False) | |
if error: | |
return render_template_string(f""" | |
<html> | |
<head><title>LLM Search Engine</title></head> | |
<body style="font-family: Arial, sans-serif;"> | |
<h1>LLM Search Engine</h1> | |
<form method="get" action="/"> | |
<input type="text" name="query" value="{html.escape(query)}"> | |
<input type="submit" value="Search"> | |
</form> | |
<p style="color: red;">{error}</p> | |
</body> | |
</html> | |
""") | |
first_url = results[0].get("url", "#") if results else "#" | |
return Response(f""" | |
<html> | |
<head> | |
<meta http-equiv="refresh" content="0; url={first_url}"> | |
</head> | |
<body> | |
<p>Redirecting to {first_url}...</p> | |
</body> | |
</html> | |
""", mimetype="text/html") | |
# Stream results for "LLM Search" | |
return Response(stream_search_results(query, page), mimetype="text/html") | |
if __name__ == '__main__': | |
app.run(debug=True, host='0.0.0.0', port=int(os.environ.get("PORT", 5000))) |