182 lines
8.1 KiB
Python
182 lines
8.1 KiB
Python
import json, http.client, ssl, os, logging, base64
|
|
from flask import Response, request
|
|
from searx.plugins import Plugin, PluginInfo
|
|
from searx.result_types import EngineResults
|
|
from flask_babel import gettext
|
|
from markupsafe import Markup
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class SXNGPlugin(Plugin):
|
|
id = "gemini_flash"
|
|
|
|
def __init__(self, plg_cfg):
|
|
super().__init__(plg_cfg)
|
|
self.info = PluginInfo(
|
|
id=self.id,
|
|
name=gettext("Gemini Flash Streaming"),
|
|
description=gettext("Live AI search answers using Google Gemini Flash"),
|
|
preference_section="general",
|
|
)
|
|
self.provider = os.getenv('LLM_PROVIDER', 'openrouter').lower()
|
|
self.api_key = os.getenv('OPENROUTER_API_KEY') if self.provider == 'openrouter' else os.getenv('GEMINI_API_KEY')
|
|
self.model = os.getenv('GEMINI_MODEL', 'gemma-3-27b-it') if self.provider == 'gemini' else os.getenv('OPENROUTER_MODEL', 'google/gemma-3-27b-it:free')
|
|
self.max_tokens = int(os.getenv('GEMINI_MAX_TOKENS', 500))
|
|
self.temperature = float(os.getenv('GEMINI_TEMPERATURE', 0.2))
|
|
self.base_url = os.getenv('OPENROUTER_BASE_URL', 'openrouter.ai')
|
|
|
|
def init(self, app):
|
|
@app.route('/gemini-stream', methods=['POST'])
|
|
def g_stream():
|
|
data = request.json or {}
|
|
context_text = data.get('context', '')
|
|
q = data.get('q', '')
|
|
|
|
if not self.api_key or not q:
|
|
return Response("Error: Missing Key or Query", status=400)
|
|
|
|
prompt = (
|
|
f"Answer concisely (<4 sentences) using the provided search results.\n"
|
|
f"Prioritize results for facts. If results are empty, use general knowledge.\n\n"
|
|
f"Results:\n{context_text}\n\n"
|
|
f"Query: {q}\n\n"
|
|
f"Answer:"
|
|
)
|
|
|
|
def generate_gemini():
|
|
host = "generativelanguage.googleapis.com"
|
|
path = f"/v1/models/{self.model}:streamGenerateContent?key={self.api_key}"
|
|
try:
|
|
conn = http.client.HTTPSConnection(host, context=ssl.create_default_context())
|
|
payload = {"contents": [{"parts": [{"text": prompt}]}], "generationConfig": {"maxOutputTokens": self.max_tokens, "temperature": self.temperature}}
|
|
conn.request("POST", path, body=json.dumps(payload), headers={"Content-Type": "application/json"})
|
|
res = conn.getresponse()
|
|
|
|
if res.status != 200:
|
|
return
|
|
|
|
decoder = json.JSONDecoder()
|
|
buffer = ""
|
|
|
|
for chunk in res:
|
|
if not chunk: continue
|
|
buffer += chunk.decode('utf-8')
|
|
|
|
while buffer:
|
|
buffer = buffer.lstrip()
|
|
if not buffer: break
|
|
|
|
try:
|
|
obj, idx = decoder.raw_decode(buffer)
|
|
candidates = obj.get('candidates', [])
|
|
if candidates:
|
|
content = candidates[0].get('content', {})
|
|
parts = content.get('parts', [])
|
|
if parts:
|
|
text = parts[0].get('text', '')
|
|
if text: yield text
|
|
|
|
buffer = buffer[idx:]
|
|
except json.JSONDecodeError:
|
|
break
|
|
|
|
conn.close()
|
|
except Exception:
|
|
pass
|
|
|
|
def generate_openrouter():
|
|
try:
|
|
conn = http.client.HTTPSConnection(self.base_url, context=ssl.create_default_context())
|
|
payload = {
|
|
"model": self.model,
|
|
"messages": [{"role": "user", "content": prompt}],
|
|
"stream": True,
|
|
"max_tokens": self.max_tokens,
|
|
"temperature": self.temperature
|
|
}
|
|
headers = {
|
|
"Authorization": f"Bearer {self.api_key}",
|
|
"Content-Type": "application/json",
|
|
"HTTP-Referer": "https://github.com/cra88y/searxng-stream-gemini",
|
|
"X-Title": "SearXNG Stream"
|
|
}
|
|
conn.request("POST", "/api/v1/chat/completions", body=json.dumps(payload), headers=headers)
|
|
res = conn.getresponse()
|
|
if res.status != 200: return
|
|
|
|
buffer = ""
|
|
while True:
|
|
chunk = res.read(1024)
|
|
if not chunk: break
|
|
buffer += chunk.decode('utf-8')
|
|
while "\n" in buffer:
|
|
line, buffer = buffer.split("\n", 1)
|
|
line = line.strip()
|
|
if line.startswith("data: "):
|
|
data_str = line[6:].strip()
|
|
if data_str == "[DONE]": return
|
|
try:
|
|
data_json = json.loads(data_str)
|
|
content = data_json.get("choices", [{}])[0].get("delta", {}).get("content", "")
|
|
if content: yield content
|
|
except: pass
|
|
conn.close()
|
|
except Exception: pass
|
|
|
|
generator = generate_openrouter if self.provider == 'openrouter' else generate_gemini
|
|
return Response(generator(), mimetype='text/plain', headers={'X-Accel-Buffering': 'no'})
|
|
return True
|
|
|
|
def post_search(self, request, search) -> EngineResults:
|
|
results = EngineResults()
|
|
if not self.active or not self.api_key or search.search_query.pageno > 1:
|
|
return results
|
|
|
|
raw_results = search.result_container.get_ordered_results()
|
|
context_list = [f"[{i+1}] {r.get('title')}: {r.get('content')}" for i, r in enumerate(raw_results[:6])]
|
|
context_str = "\n".join(context_list)
|
|
|
|
b64_context = base64.b64encode(context_str.encode('utf-8')).decode('utf-8')
|
|
js_q = json.dumps(search.search_query.query)
|
|
|
|
html_payload = f'''
|
|
<article id="sxng-stream-box" class="answer" style="display:none; margin-bottom: 1rem;">
|
|
<p id="sxng-stream-data" style="white-space: pre-wrap;"></p>
|
|
</article>
|
|
<script>
|
|
(async () => {{
|
|
const q = {js_q};
|
|
const b64 = "{b64_context}";
|
|
const shell = document.getElementById('sxng-stream-box');
|
|
const data = document.getElementById('sxng-stream-data');
|
|
|
|
try {{
|
|
const ctx = new TextDecoder().decode(Uint8Array.from(atob(b64), c => c.charCodeAt(0)));
|
|
const res = await fetch('/gemini-stream', {{
|
|
method: 'POST',
|
|
headers: {{ 'Content-Type': 'application/json' }},
|
|
body: JSON.stringify({{ q: q, context: ctx }})
|
|
}});
|
|
|
|
if (!res.ok) return;
|
|
|
|
const reader = res.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
|
|
while (true) {{
|
|
const {{done, value}} = await reader.read();
|
|
if (done) break;
|
|
|
|
const chunk = decoder.decode(value);
|
|
if (chunk) {{
|
|
if (shell.style.display === 'none') shell.style.display = 'block';
|
|
data.innerText += chunk;
|
|
}}
|
|
}}
|
|
}} catch (e) {{ console.error(e); }}
|
|
}})();
|
|
</script>
|
|
'''
|
|
search.result_container.answers.add(results.types.Answer(answer=Markup(html_payload)))
|
|
return results
|