This commit is contained in:
+6
-130
@@ -46,14 +46,7 @@ PLUGIN_NAME = "AI Answers"
|
|||||||
DEFAULT_TABS = "general,science,it,news"
|
DEFAULT_TABS = "general,science,it,news"
|
||||||
|
|
||||||
PROVIDER_PRESETS = {
|
PROVIDER_PRESETS = {
|
||||||
'openai': {'url': 'https://api.openai.com/v1/chat/completions', 'model': 'gpt-4o-mini'},
|
|
||||||
'openrouter': {'url': 'https://openrouter.ai/api/v1/chat/completions', 'model': 'google/gemma-3-27b-it:free'},
|
|
||||||
'ollama': {'url': 'http://localhost:11434/v1/chat/completions', 'model': 'llama3.2'},
|
'ollama': {'url': 'http://localhost:11434/v1/chat/completions', 'model': 'llama3.2'},
|
||||||
'localai': {'url': 'http://localhost:8080/v1/chat/completions', 'model': 'gpt-4'},
|
|
||||||
'lmstudio': {'url': 'http://localhost:1234/v1/chat/completions', 'model': 'local-model'},
|
|
||||||
'gemini': {'url': 'https://generativelanguage.googleapis.com/v1beta/models/{model}:streamGenerateContent', 'model': 'gemma-3-27b-it'},
|
|
||||||
'azure': {'url': None, 'model': 'azure-deployment'},
|
|
||||||
'huggingface': {'url': 'https://api-inference.huggingface.co/models/{model}/v1/chat/completions', 'model': 'meta-llama/Meta-Llama-3-8B-Instruct'}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# UI assets
|
# UI assets
|
||||||
@@ -793,37 +786,25 @@ class SXNGPlugin(Plugin):
|
|||||||
raw_url = os.getenv('LLM_URL', '').strip()
|
raw_url = os.getenv('LLM_URL', '').strip()
|
||||||
if not raw_provider and raw_url:
|
if not raw_provider and raw_url:
|
||||||
url_lower = raw_url.lower()
|
url_lower = raw_url.lower()
|
||||||
if 'openai.com' in url_lower:
|
if ':11434' in url_lower:
|
||||||
raw_provider = 'openai'
|
|
||||||
elif 'openrouter.ai' in url_lower:
|
|
||||||
raw_provider = 'openrouter'
|
|
||||||
elif ':11434' in url_lower:
|
|
||||||
raw_provider = 'ollama'
|
raw_provider = 'ollama'
|
||||||
elif 'generativelanguage.googleapis.com' in url_lower:
|
|
||||||
raw_provider = 'gemini'
|
|
||||||
elif 'openai.azure.com' in url_lower or '.azure.com' in url_lower:
|
|
||||||
raw_provider = 'azure'
|
|
||||||
elif 'huggingface.co' in url_lower:
|
|
||||||
raw_provider = 'huggingface'
|
|
||||||
else:
|
else:
|
||||||
raw_provider = 'openai'
|
raw_provider = 'error'
|
||||||
logger.info(f"{PLUGIN_NAME}: Using OpenAI-compatible mode for custom URL")
|
logger.info(f"{raw_provider}: Ollama not detected")
|
||||||
|
|
||||||
if not raw_provider:
|
if not raw_provider:
|
||||||
self.provider = ''
|
self.provider = ''
|
||||||
self.model = ''
|
self.model = ''
|
||||||
self.is_gemini = False
|
|
||||||
self.api_key = ''
|
self.api_key = ''
|
||||||
return
|
return
|
||||||
|
|
||||||
if raw_provider not in PROVIDER_PRESETS:
|
if raw_provider not in PROVIDER_PRESETS:
|
||||||
logger.warning(f"{PLUGIN_NAME}: Unknown provider '{raw_provider}', falling back to 'openai'")
|
logger.warning(f"{PLUGIN_NAME}: Not Ollama '{raw_provider}', please correct.")
|
||||||
self.provider = raw_provider if raw_provider in PROVIDER_PRESETS else 'openai'
|
self.provider = raw_provider if raw_provider in PROVIDER_PRESETS else 'Error'
|
||||||
self.is_gemini = (self.provider == 'gemini')
|
|
||||||
preset = PROVIDER_PRESETS[self.provider]
|
preset = PROVIDER_PRESETS[self.provider]
|
||||||
|
|
||||||
self.api_key = os.getenv('LLM_KEY', '')
|
self.api_key = os.getenv('LLM_KEY', '')
|
||||||
if not self.api_key and self.provider in ('ollama', 'localai', 'lmstudio'):
|
if not self.api_key and self.provider == 'ollama':
|
||||||
self.api_key = 'none'
|
self.api_key = 'none'
|
||||||
self.api_key = self.api_key.strip()
|
self.api_key = self.api_key.strip()
|
||||||
|
|
||||||
@@ -1122,111 +1103,6 @@ class SXNGPlugin(Plugin):
|
|||||||
{numbered_instructions}
|
{numbered_instructions}
|
||||||
</CORE_DIRECTIVES>"""
|
</CORE_DIRECTIVES>"""
|
||||||
|
|
||||||
def call_gemini():
|
|
||||||
base = self.endpoint_url.replace('streamGenerateContent', 'generateContent')
|
|
||||||
url = f"{base}&key={self.api_key}" if '?' in base else f"{base}?key={self.api_key}"
|
|
||||||
conn = None
|
|
||||||
try:
|
|
||||||
conn, path = _get_streaming_connection(url)
|
|
||||||
payload = json.dumps({
|
|
||||||
"contents": [{"parts": [{"text": prompt}]}],
|
|
||||||
"generationConfig": {"maxOutputTokens": min(self.max_tokens * 4, 8192), "temperature": self.temperature}
|
|
||||||
})
|
|
||||||
conn.request("POST", path, body=payload.encode('utf-8'), headers={"Content-Type": "application/json"})
|
|
||||||
res = conn.getresponse()
|
|
||||||
if res.status != 200:
|
|
||||||
body = res.read(2048).decode('utf-8', errors='replace')[:500]
|
|
||||||
logger.error(f"{PLUGIN_NAME}: Gemini API {res.status}: {body}")
|
|
||||||
return '', f"API error {res.status}. Check server logs."
|
|
||||||
obj = json.loads(res.read().decode('utf-8', errors='replace'))
|
|
||||||
if obj.get('promptFeedback', {}).get('blockReason'):
|
|
||||||
return '', f"Gemini blocked prompt: {obj['promptFeedback']['blockReason']}"
|
|
||||||
candidates = obj.get('candidates', [])
|
|
||||||
if not candidates:
|
|
||||||
return '', "No candidates in Gemini response."
|
|
||||||
first = candidates[0]
|
|
||||||
if first.get('finishReason') == 'SAFETY':
|
|
||||||
return '', "Gemini stopped generation due to safety filters."
|
|
||||||
parts = first.get('content', {}).get('parts', [])
|
|
||||||
text = ''.join(p.get('text', '') for p in parts if isinstance(p, dict))
|
|
||||||
return text, None
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"{PLUGIN_NAME}: Gemini call error: {e}", exc_info=True)
|
|
||||||
return '', f"Connection Error: {e}"
|
|
||||||
finally:
|
|
||||||
if conn: conn.close()
|
|
||||||
|
|
||||||
def call_openai_compatible():
|
|
||||||
conn = None
|
|
||||||
try:
|
|
||||||
conn, path = _get_streaming_connection(self.endpoint_url)
|
|
||||||
payload_dict = {
|
|
||||||
"model": effective_model,
|
|
||||||
"messages": [
|
|
||||||
{"role": "system", "content": SYSTEM},
|
|
||||||
{"role": "user", "content": prompt},
|
|
||||||
{"role": "assistant", "content": ""},
|
|
||||||
],
|
|
||||||
"stream": False,
|
|
||||||
"max_tokens": self.max_tokens,
|
|
||||||
"temperature": self.temperature
|
|
||||||
}
|
|
||||||
payload = json.dumps(payload_dict)
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"HTTP-Referer": "https://github.com/searxng/searxng",
|
|
||||||
"X-Title": "SearXNG"
|
|
||||||
}
|
|
||||||
if self.provider == 'azure':
|
|
||||||
headers['api-key'] = self.api_key
|
|
||||||
else:
|
|
||||||
headers['Authorization'] = f"Bearer {self.api_key}"
|
|
||||||
conn.request("POST", path, body=payload.encode('utf-8'), headers=headers)
|
|
||||||
res = conn.getresponse()
|
|
||||||
if res.status != 200:
|
|
||||||
body = res.read(2048).decode('utf-8', errors='replace')[:500]
|
|
||||||
logger.error(f"{PLUGIN_NAME}: {self.provider} API {res.status}: {body}")
|
|
||||||
return '', f"API error {res.status}. Check server logs."
|
|
||||||
obj = json.loads(res.read().decode('utf-8', errors='replace'))
|
|
||||||
if "error" in obj:
|
|
||||||
err = obj["error"]
|
|
||||||
msg = err.get("message", str(err)) if isinstance(err, dict) else str(err)
|
|
||||||
return '', f"API Error: {msg}"
|
|
||||||
choices = obj.get("choices", [])
|
|
||||||
if not choices:
|
|
||||||
return '', "No choices in API response."
|
|
||||||
message = choices[0].get("message", {})
|
|
||||||
content = re.sub(r'<think>.*?</think>', '', message.get("content") or "", flags=re.DOTALL).strip()
|
|
||||||
reasoning = message.get("reasoning") or message.get("reasoning_content") or ""
|
|
||||||
if not content and reasoning:
|
|
||||||
logger.warning(f"{PLUGIN_NAME}: {self.provider} returned empty content; extracting answer from reasoning field")
|
|
||||||
header_pat = re.compile(r'^\s*\*?\*?[A-Z][^:]{0,40}:\*?\*?\s*$', re.MULTILINE)
|
|
||||||
matches = list(header_pat.finditer(reasoning))
|
|
||||||
if matches:
|
|
||||||
answer = reasoning[matches[-1].end():].strip()
|
|
||||||
else:
|
|
||||||
paras = [p.strip() for p in re.split(r'\n{2,}', reasoning) if p.strip()]
|
|
||||||
answer = paras[-1] if paras else reasoning.strip()
|
|
||||||
full = answer
|
|
||||||
else:
|
|
||||||
full = (f"<think>\n{reasoning}\n</think>\n\n" if reasoning else "") + content
|
|
||||||
full = re.sub(r'<think>.*?</think>', '', full, flags=re.DOTALL).strip()
|
|
||||||
return full, None
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"{PLUGIN_NAME}: {self.provider} call error: {e}", exc_info=True)
|
|
||||||
return '', f"Connection Error: {e}"
|
|
||||||
finally:
|
|
||||||
if conn: conn.close()
|
|
||||||
|
|
||||||
call_fn = call_gemini if self.is_gemini else call_openai_compatible
|
|
||||||
text, error = call_fn()
|
|
||||||
|
|
||||||
if self.provider == 'ollama' and getattr(self, 'ollama_unload_after', False):
|
|
||||||
self._ollama_unload_model()
|
|
||||||
|
|
||||||
return jsonify({"text": text, "error": error})
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _assemble_context(self, clean_results, infoboxes, answers, offset=0) -> tuple[str, list]:
|
def _assemble_context(self, clean_results, infoboxes, answers, offset=0) -> tuple[str, list]:
|
||||||
"""Builds context string from normalized search data. Returns (context_str, urls)."""
|
"""Builds context string from normalized search data. Returns (context_str, urls)."""
|
||||||
context_parts = []
|
context_parts = []
|
||||||
|
|||||||
Reference in New Issue
Block a user