feats: native searxng networking, code composition, ux polish, follow up querying via internals, config var clarity, readme
This commit is contained in:
@@ -5,3 +5,4 @@ venv/
|
|||||||
.env
|
.env
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.agent/
|
||||||
@@ -9,6 +9,10 @@ Features:
|
|||||||
- clickable inline citations
|
- clickable inline citations
|
||||||
- interactive mode to continue summary, ask follow ups, copy, or regenerate
|
- interactive mode to continue summary, ask follow ups, copy, or regenerate
|
||||||
- simple response mode with no extras
|
- simple response mode with no extras
|
||||||
|
- internally called low-latency RAG for follow ups (bypasses http loopback)
|
||||||
|
- native network integration via `searx.network` (respects proxy/SSL settings)
|
||||||
|
- stateless conversation persistence/sharability via URL
|
||||||
|
- provider detection based on URL
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -23,26 +27,32 @@ plugins:
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Set the following environment variables:
|
Configure via the environment variables:
|
||||||
|
|
||||||
### Required
|
### Required
|
||||||
|
|
||||||
- `LLM_PROVIDER`: openrouter, openai, ollama, localai, lmstudio, gemini, azure, or huggingface
|
- `LLM_PROVIDER`: openrouter, openai, ollama, localai, lmstudio, gemini, azure, or huggingface
|
||||||
- `LLM_KEY`: Your API key
|
- `LLM_KEY`: Provider API key (optional for local providers: ollama, localai, lmstudio)
|
||||||
|
|
||||||
### Optional
|
### Optional
|
||||||
|
|
||||||
- `LLM_MODEL`: Model identifier. Defaults vary by provider.
|
- `LLM_MODEL`: Model identifier. Defaults vary. Recommended: 10-30B dense or 5-15B MoE activated.
|
||||||
- `LLM_URL`: Custom endpoint URL. Overrides provider preset.
|
- `LLM_URL`: Overrides endpoint URL for any provider preset.
|
||||||
- `LLM_MAX_TOKENS`: Defaults to `500`.
|
- `LLM_MAX_TOKENS`: Default `500`.
|
||||||
- `LLM_TEMPERATURE`: Defaults to `0.2`.
|
- `LLM_TEMPERATURE`: Default `0.2`.
|
||||||
- `LLM_CONTEXT_COUNT`: Search results to include. Defaults to `5`.
|
- `LLM_CONTEXT_DEEP_COUNT`: results as context with full snippets. Default `5`.
|
||||||
- `LLM_TABS`: Comma-separated tab whitelist. Defaults to general,science,it,news.
|
- `LLM_CONTEXT_SHALLOW_COUNT`: Results with headlines only (additional breadth). Default `15`.
|
||||||
- `LLM_STYLE`: UI mode. Set to "interactive" for interactive controls (copy, regenerate, follow up, continue). Defaults to "simple".
|
- `LLM_TABS`: Tab whitelist, comma delimiter. Default `general,science,it,news`.
|
||||||
|
- `LLM_INTERACTIVE`: UI mode. Default is `true` (interactive: copy, regenerate, follow up). Set to `false` for simple response only mode.
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
1 user initial search
|
||||||
After search completes, the plugin extracts top search results as context. A client-side script calls the stream endpoint with a signed token. The LLM response streams back token by token.
|
2 results return server side
|
||||||
|
3 `post_search` plugin hook entry
|
||||||
|
4 token optimized context extracted
|
||||||
|
5 inject the ui/logic "shell" into standard results answer object
|
||||||
|
6 client side script calls custom endpoint with signed token
|
||||||
|
7 LLM response streams back token by token
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -92,7 +102,7 @@ LLM_MODEL=meta-llama/Meta-Llama-3-8B-Instruct
|
|||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install flask flask-babel python-dotenv
|
pip install flask flask-babel
|
||||||
python demo.py # Interactive test server on localhost:5000
|
python tests/demo.py # Interactive demo at localhost:5000
|
||||||
python test.py # One-shot test suite
|
python tests/test.py # One-shot test suite
|
||||||
```
|
```
|
||||||
|
|||||||
+878
-476
File diff suppressed because it is too large
Load Diff
@@ -1,153 +0,0 @@
|
|||||||
"""
|
|
||||||
AI Answers Plugin - Interactive Demo Server
|
|
||||||
Simulates SearXNG environment for local development and testing.
|
|
||||||
|
|
||||||
Usage: python demo.py
|
|
||||||
Then visit: http://localhost:5000/?q=your+query+here
|
|
||||||
|
|
||||||
Requires: pip install flask flask-babel python-dotenv
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
from types import ModuleType
|
|
||||||
from flask import Flask, request
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
|
||||||
load_dotenv()
|
|
||||||
os.environ.setdefault('LLM_STYLE', 'interactive')
|
|
||||||
|
|
||||||
# Mock SearXNG modules
|
|
||||||
searx = ModuleType("searx")
|
|
||||||
searx_plugins = ModuleType("searx.plugins")
|
|
||||||
searx_results = ModuleType("searx.result_types")
|
|
||||||
|
|
||||||
class MockPlugin:
|
|
||||||
def __init__(self, cfg):
|
|
||||||
self.active = getattr(cfg, 'active', True)
|
|
||||||
|
|
||||||
class MockPluginInfo:
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
self.meta = kwargs
|
|
||||||
|
|
||||||
class MockEngineResults:
|
|
||||||
def __init__(self):
|
|
||||||
self.types = ModuleType("types")
|
|
||||||
self.types.Answer = lambda *args, **kwargs: kwargs.get('answer', args[0] if args else "")
|
|
||||||
self._results = []
|
|
||||||
|
|
||||||
def add(self, res):
|
|
||||||
self._results.append(res)
|
|
||||||
|
|
||||||
searx_plugins.Plugin = MockPlugin
|
|
||||||
searx_plugins.PluginInfo = MockPluginInfo
|
|
||||||
searx_results.EngineResults = MockEngineResults
|
|
||||||
|
|
||||||
sys.modules["searx"] = searx
|
|
||||||
sys.modules["searx.plugins"] = searx_plugins
|
|
||||||
sys.modules["searx.result_types"] = searx_results
|
|
||||||
|
|
||||||
from ai_answers import SXNGPlugin
|
|
||||||
from flask_babel import Babel
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
babel = Babel(app)
|
|
||||||
|
|
||||||
class MockConfig:
|
|
||||||
active = True
|
|
||||||
|
|
||||||
plugin = SXNGPlugin(MockConfig())
|
|
||||||
plugin.init(app)
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
def index():
|
|
||||||
query = request.args.get("q", "why is the sky blue")
|
|
||||||
|
|
||||||
class MockSearchQuery:
|
|
||||||
pageno = 1
|
|
||||||
lang = 'en'
|
|
||||||
categories = ['general']
|
|
||||||
MockSearchQuery.query = query
|
|
||||||
|
|
||||||
class MockSearch:
|
|
||||||
search_query = MockSearchQuery()
|
|
||||||
class MockResultContainer:
|
|
||||||
def __init__(self):
|
|
||||||
self.answers = set()
|
|
||||||
|
|
||||||
def get_ordered_results(self):
|
|
||||||
if 'quantum' in query.lower():
|
|
||||||
return [
|
|
||||||
{"title": "IBM Quantum", "content": "Quantum computers rely on qubits, which can represent 0, 1, or both via superposition. They solve complex problems faster.", "url": "https://www.ibm.com/quantum", "publishedDate": "2026-01-15"},
|
|
||||||
{"title": "Nature Physics", "content": "Entanglement allows qubits to be correlated instantly across distances. This is key for quantum cryptography and teleportation.", "url": "https://nature.com/articles/quantum", "publishedDate": "2026-01-10"},
|
|
||||||
{"title": "Wikipedia", "content": "Quantum computing uses quantum mechanics. Major applications include drug discovery and materials science.", "url": "https://en.wikipedia.org/wiki/Quantum_computing", "publishedDate": "2025-12-01"}
|
|
||||||
]
|
|
||||||
return [
|
|
||||||
{"title": "Wikipedia", "content": "The sky appears blue due to Rayleigh scattering of sunlight.", "url": "https://en.wikipedia.org/wiki/Rayleigh_scattering", "publishedDate": "2026-01-15"},
|
|
||||||
{"title": "NASA Science", "content": "Shorter blue wavelengths scatter more than longer red wavelengths.", "url": "https://science.nasa.gov/blue-sky", "publishedDate": "2026-01-10"},
|
|
||||||
{"title": "Physics Today", "content": "The atmosphere acts as a filter, scattering blue light in all directions.", "url": "https://physicstoday.org/atmosphere", "publishedDate": "2026-01-01"}
|
|
||||||
]
|
|
||||||
result_container = MockResultContainer()
|
|
||||||
|
|
||||||
search = MockSearch()
|
|
||||||
plugin.post_search(None, search)
|
|
||||||
|
|
||||||
injection_html = ""
|
|
||||||
if search.result_container.answers:
|
|
||||||
injection_html = list(search.result_container.answers)[0]
|
|
||||||
|
|
||||||
return f"""
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>AI Answers Demo</title>
|
|
||||||
<style>
|
|
||||||
body {{
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
||||||
padding: 2rem;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: #2e3440;
|
|
||||||
color: #eceff4;
|
|
||||||
}}
|
|
||||||
:root {{
|
|
||||||
--color-result-border: #3b4252;
|
|
||||||
--color-result-description: #d8dee9;
|
|
||||||
--color-base-font: #88c0d0;
|
|
||||||
--color-result-link: #81a1c1;
|
|
||||||
}}
|
|
||||||
h1 {{ color: #88c0d0; }}
|
|
||||||
.meta {{ color: #81a1c1; font-size: 0.9rem; }}
|
|
||||||
hr {{ border-color: #4c566a; }}
|
|
||||||
a {{ color: #88c0d0; }}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div style="margin-top: 2rem;"></div>
|
|
||||||
<p class="meta">Provider: <strong>{plugin.provider or 'Not configured'}</strong> | Model: <strong>{plugin.model or 'N/A'}</strong></p>
|
|
||||||
<p>Query: <strong>{query}</strong></p>
|
|
||||||
<hr>
|
|
||||||
{injection_html if injection_html else '<p style="color:#f66;">Plugin inactive. Set LLM_PROVIDER and LLM_KEY in .env</p>'}
|
|
||||||
<hr>
|
|
||||||
<p class="meta">Try: <a href="/?q=what+is+quantum+computing">/?q=what+is+quantum+computing</a></p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print()
|
|
||||||
print("=" * 50)
|
|
||||||
print(" AI Answers Plugin - Demo Server")
|
|
||||||
print("=" * 50)
|
|
||||||
print(f" Provider: {plugin.provider or 'NOT SET'}")
|
|
||||||
print(f" Model: {plugin.model or 'N/A'}")
|
|
||||||
print(f" Style: {plugin.style}")
|
|
||||||
print(f" Status: {'Active' if plugin.api_key else 'Inactive (no LLM_KEY)'}")
|
|
||||||
print("=" * 50)
|
|
||||||
print(" http://localhost:5000/?q=your+query+here")
|
|
||||||
print("=" * 50)
|
|
||||||
print()
|
|
||||||
app.run(debug=True, port=5000)
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
"""
|
|
||||||
AI Answers Plugin - One-Shot Test
|
|
||||||
Comprehensive test that outputs everything: config, injection, LLM response.
|
|
||||||
|
|
||||||
Usage: python test.py
|
|
||||||
Requires: pip install flask flask-babel python-dotenv
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
from types import ModuleType
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# Suppress Flask noise during test
|
|
||||||
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
|
||||||
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
|
||||||
|
|
||||||
# Mock SearXNG modules
|
|
||||||
searx = ModuleType("searx")
|
|
||||||
searx_plugins = ModuleType("searx.plugins")
|
|
||||||
searx_results = ModuleType("searx.result_types")
|
|
||||||
|
|
||||||
class MockPlugin:
|
|
||||||
def __init__(self, cfg):
|
|
||||||
self.active = getattr(cfg, 'active', True)
|
|
||||||
|
|
||||||
class MockPluginInfo:
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
self.meta = kwargs
|
|
||||||
|
|
||||||
class MockEngineResults:
|
|
||||||
def __init__(self):
|
|
||||||
self.types = ModuleType("types")
|
|
||||||
self.types.Answer = lambda *args, **kwargs: kwargs.get('answer', args[0] if args else "")
|
|
||||||
self._results = []
|
|
||||||
|
|
||||||
def add(self, res):
|
|
||||||
self._results.append(res)
|
|
||||||
|
|
||||||
searx_plugins.Plugin = MockPlugin
|
|
||||||
searx_plugins.PluginInfo = MockPluginInfo
|
|
||||||
searx_results.EngineResults = MockEngineResults
|
|
||||||
|
|
||||||
sys.modules["searx"] = searx
|
|
||||||
sys.modules["searx.plugins"] = searx_plugins
|
|
||||||
sys.modules["searx.result_types"] = searx_results
|
|
||||||
|
|
||||||
from flask import Flask
|
|
||||||
from flask_babel import Babel
|
|
||||||
from ai_answers import SXNGPlugin
|
|
||||||
|
|
||||||
def run_tests():
|
|
||||||
print()
|
|
||||||
print("=" * 60)
|
|
||||||
print(" AI Answers Plugin - Comprehensive Test")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# === CONFIG TEST ===
|
|
||||||
print("\n[1/4] Configuration")
|
|
||||||
print("-" * 40)
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
Babel(app)
|
|
||||||
|
|
||||||
class MockConfig:
|
|
||||||
active = True
|
|
||||||
|
|
||||||
plugin = SXNGPlugin(MockConfig())
|
|
||||||
plugin.init(app)
|
|
||||||
|
|
||||||
print(f" Provider: {plugin.provider or 'NOT SET'}")
|
|
||||||
print(f" Model: {plugin.model or 'N/A'}")
|
|
||||||
print(f" API Key: {'[OK]' if plugin.api_key else '[MISSING]'}")
|
|
||||||
print(f" Max Tokens: {getattr(plugin, 'max_tokens', 'N/A')}")
|
|
||||||
print(f" Temperature: {getattr(plugin, 'temperature', 'N/A')}")
|
|
||||||
print(f" Context Count: {getattr(plugin, 'context_count', 'N/A')}")
|
|
||||||
print(f" Allowed Tabs: {getattr(plugin, 'allowed_tabs', 'N/A')}")
|
|
||||||
|
|
||||||
if not plugin.api_key:
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print(" SKIPPED: No LLM_KEY configured")
|
|
||||||
print(" Set LLM_PROVIDER and LLM_KEY in .env to run full test")
|
|
||||||
print("=" * 60)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# === INJECTION TEST ===
|
|
||||||
print("\n[2/4] HTML Injection")
|
|
||||||
print("-" * 40)
|
|
||||||
|
|
||||||
class MockSearchQuery:
|
|
||||||
pageno = 1
|
|
||||||
query = "why is the sky blue"
|
|
||||||
lang = 'en'
|
|
||||||
categories = ['general']
|
|
||||||
|
|
||||||
class MockSearch:
|
|
||||||
search_query = MockSearchQuery()
|
|
||||||
class MockResultContainer:
|
|
||||||
def __init__(self):
|
|
||||||
self.answers = set()
|
|
||||||
def get_ordered_results(self):
|
|
||||||
return [
|
|
||||||
{"title": "Wikipedia", "content": "The sky appears blue due to Rayleigh scattering.", "url": "https://example.com/1", "publishedDate": "2026-01-15"},
|
|
||||||
{"title": "NASA", "content": "Blue wavelengths scatter more than red.", "url": "https://example.com/2", "publishedDate": "2026-01-10"},
|
|
||||||
]
|
|
||||||
result_container = MockResultContainer()
|
|
||||||
|
|
||||||
search = MockSearch()
|
|
||||||
plugin.post_search(None, search)
|
|
||||||
|
|
||||||
if not search.result_container.answers:
|
|
||||||
print(" FAIL: No HTML injected")
|
|
||||||
return False
|
|
||||||
|
|
||||||
html = str(list(search.result_container.answers)[0])
|
|
||||||
|
|
||||||
has_box = 'id="sxng-stream-box"' in html
|
|
||||||
has_endpoint = '/ai-stream' in html
|
|
||||||
|
|
||||||
token_match = re.search(r'const tk = "(.*?)";', html)
|
|
||||||
has_token = bool(token_match)
|
|
||||||
|
|
||||||
print(f" Stream box: {'[OK]' if has_box else '[FAIL]'}")
|
|
||||||
print(f" Endpoint ref: {'[OK]' if has_endpoint else '[FAIL]'}")
|
|
||||||
print(f" Auth token: {'[OK]' if has_token else '[FAIL]'}")
|
|
||||||
print(f" HTML size: {len(html):,} bytes")
|
|
||||||
|
|
||||||
if not (has_box and has_endpoint and has_token):
|
|
||||||
print(" FAIL: Missing required elements")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# === STREAM ENDPOINT TEST ===
|
|
||||||
print("\n[3/4] Stream Endpoint")
|
|
||||||
print("-" * 40)
|
|
||||||
|
|
||||||
with app.test_client() as client:
|
|
||||||
payload = {
|
|
||||||
"q": "why is the sky blue",
|
|
||||||
"context": "[1] Wikipedia: The sky appears blue due to Rayleigh scattering.",
|
|
||||||
"lang": "en",
|
|
||||||
"tk": token_match.group(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
start = time.time()
|
|
||||||
response = client.post('/ai-stream', json=payload)
|
|
||||||
elapsed = time.time() - start
|
|
||||||
|
|
||||||
print(f" Status: {response.status_code}")
|
|
||||||
print(f" Time: {elapsed:.2f}s")
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
print(f" FAIL: Expected 200, got {response.status_code}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# === LLM RESPONSE TEST ===
|
|
||||||
print("\n[4/4] LLM Response")
|
|
||||||
print("-" * 40)
|
|
||||||
|
|
||||||
data = response.data.decode('utf-8')
|
|
||||||
print(f" Bytes: {len(data):,}")
|
|
||||||
print(f" Words: ~{len(data.split())}")
|
|
||||||
|
|
||||||
if len(data) < 10:
|
|
||||||
print(" FAIL: Response too short (API error?)")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print("\n --- Response Preview ---")
|
|
||||||
preview = data[:500] + ("..." if len(data) > 500 else "")
|
|
||||||
for line in preview.split('\n'):
|
|
||||||
print(f" {line}")
|
|
||||||
print(" --- End Preview ---")
|
|
||||||
|
|
||||||
# === SUMMARY ===
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print(" ALL TESTS PASSED")
|
|
||||||
print("=" * 60)
|
|
||||||
return True
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
success = run_tests()
|
|
||||||
sys.exit(0 if success else 1)
|
|
||||||
+241
@@ -0,0 +1,241 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from types import ModuleType
|
||||||
|
from flask import Flask, request
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
||||||
|
# os.environ.setdefault('LLM_STYLE', 'interactive') # Removed to let plugin config decide defaults
|
||||||
|
|
||||||
|
# SearXNG module mocks
|
||||||
|
searx = ModuleType("searx")
|
||||||
|
searx_plugins = ModuleType("searx.plugins")
|
||||||
|
searx_results = ModuleType("searx.result_types")
|
||||||
|
|
||||||
|
class MockPlugin:
|
||||||
|
def __init__(self, cfg):
|
||||||
|
self.active = getattr(cfg, 'active', True)
|
||||||
|
|
||||||
|
class MockPluginInfo:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.meta = kwargs
|
||||||
|
|
||||||
|
class MockEngineResults:
|
||||||
|
def __init__(self):
|
||||||
|
self.types = ModuleType("types")
|
||||||
|
self.types.Answer = lambda *args, **kwargs: kwargs.get('answer', args[0] if args else "")
|
||||||
|
self._results = []
|
||||||
|
|
||||||
|
def add(self, res):
|
||||||
|
self._results.append(res)
|
||||||
|
|
||||||
|
searx_plugins.Plugin = MockPlugin
|
||||||
|
searx_plugins.PluginInfo = MockPluginInfo
|
||||||
|
searx_results.EngineResults = MockEngineResults
|
||||||
|
|
||||||
|
sys.modules["searx"] = searx
|
||||||
|
sys.modules["searx.plugins"] = searx_plugins
|
||||||
|
sys.modules["searx.result_types"] = searx_results
|
||||||
|
|
||||||
|
# Network module mock
|
||||||
|
searx_network = ModuleType("searx.network")
|
||||||
|
def mock_network_call(method, url, **kwargs):
|
||||||
|
import http.client, ssl, json
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
parsed = urlparse(url)
|
||||||
|
port = parsed.port or (443 if parsed.scheme=='https' else 80)
|
||||||
|
target = f"{parsed.hostname}:{port}"
|
||||||
|
|
||||||
|
if parsed.scheme == 'https':
|
||||||
|
conn = http.client.HTTPSConnection(target, timeout=30, context=ssl.create_default_context())
|
||||||
|
else:
|
||||||
|
conn = http.client.HTTPConnection(target, timeout=30)
|
||||||
|
|
||||||
|
headers = kwargs.get('headers', {})
|
||||||
|
body = None
|
||||||
|
if kwargs.get('json'):
|
||||||
|
body = json.dumps(kwargs['json'])
|
||||||
|
elif kwargs.get('data'):
|
||||||
|
body = kwargs['data']
|
||||||
|
|
||||||
|
path = parsed.path
|
||||||
|
if parsed.query:
|
||||||
|
path += f"?{parsed.query}"
|
||||||
|
|
||||||
|
if kwargs.get('params'):
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
query_str = urlencode(kwargs['params'])
|
||||||
|
if '?' in path:
|
||||||
|
path += f"&{query_str}"
|
||||||
|
else:
|
||||||
|
path += f"?{query_str}"
|
||||||
|
|
||||||
|
conn.request(method, path, body=body, headers=headers)
|
||||||
|
return conn.getresponse()
|
||||||
|
|
||||||
|
def mock_stream(method, url, **kwargs):
|
||||||
|
res = mock_network_call(method, url, **kwargs)
|
||||||
|
|
||||||
|
class MockResponse:
|
||||||
|
def __init__(self, r):
|
||||||
|
self.status_code = r.status
|
||||||
|
self.text = "Mock Response" # Stub
|
||||||
|
self._r = r
|
||||||
|
|
||||||
|
def generator():
|
||||||
|
while True:
|
||||||
|
chunk = res.read(128)
|
||||||
|
if not chunk: break
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
return MockResponse(res), generator()
|
||||||
|
|
||||||
|
def mock_get(url, **kwargs):
|
||||||
|
import json
|
||||||
|
res = mock_network_call('GET', url, **kwargs)
|
||||||
|
|
||||||
|
class MockResponse:
|
||||||
|
def __init__(self, r):
|
||||||
|
self.status_code = r.status
|
||||||
|
self._content = r.read()
|
||||||
|
self.text = self._content.decode('utf-8')
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return json.loads(self.text)
|
||||||
|
|
||||||
|
return MockResponse(res)
|
||||||
|
|
||||||
|
searx_network.stream = mock_stream
|
||||||
|
searx_network.get = mock_get
|
||||||
|
sys.modules["searx.network"] = searx_network
|
||||||
|
|
||||||
|
from ai_answers import SXNGPlugin
|
||||||
|
from flask_babel import Babel
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
babel = Babel(app)
|
||||||
|
|
||||||
|
class MockConfig:
|
||||||
|
active = True
|
||||||
|
|
||||||
|
plugin = SXNGPlugin(MockConfig())
|
||||||
|
plugin.init(app)
|
||||||
|
|
||||||
|
@app.route("/search")
|
||||||
|
def mock_search():
|
||||||
|
query = request.args.get("q", "")
|
||||||
|
format_type = request.args.get("format", "html")
|
||||||
|
|
||||||
|
if format_type != "json":
|
||||||
|
return "Demo only supports JSON format", 400
|
||||||
|
|
||||||
|
results = [
|
||||||
|
{"title": f"Result 1 for: {query}", "content": f"This is simulated content about {query}. It contains relevant information.", "url": f"https://example.com/1/{query.replace(' ', '-')}", "publishedDate": "2026-01-18"},
|
||||||
|
{"title": f"Result 2 for: {query}", "content": f"Additional information regarding {query}. More context and details.", "url": f"https://example.com/2/{query.replace(' ', '-')}", "publishedDate": "2026-01-17"},
|
||||||
|
{"title": f"Result 3 for: {query}", "content": f"Further reading on {query}. Expert analysis.", "url": f"https://example.com/3/{query.replace(' ', '-')}", "publishedDate": "2026-01-16"},
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"results": results,
|
||||||
|
"infoboxes": [],
|
||||||
|
"answers": [],
|
||||||
|
"suggestions": [f"{query} explained", f"{query} tutorial"]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
query = request.args.get("q", "why is the sky blue")
|
||||||
|
|
||||||
|
class MockSearchQuery:
|
||||||
|
pageno = 1
|
||||||
|
lang = 'en'
|
||||||
|
categories = ['general']
|
||||||
|
MockSearchQuery.query = query
|
||||||
|
|
||||||
|
class MockSearch:
|
||||||
|
search_query = MockSearchQuery()
|
||||||
|
class MockResultContainer:
|
||||||
|
def __init__(self):
|
||||||
|
self.answers = set()
|
||||||
|
|
||||||
|
def get_ordered_results(self):
|
||||||
|
base_results = [
|
||||||
|
{"title": "Wikipedia", "content": "The sky appears blue due to Rayleigh scattering of sunlight. When sunlight enters the atmosphere, it collides with gas molecules and scatters in all directions. Blue light scatters more than other colors because it travels in shorter waves.", "url": "https://en.wikipedia.org/wiki/Rayleigh_scattering", "publishedDate": "2026-01-15"},
|
||||||
|
{"title": "NASA Science", "content": "Shorter blue wavelengths scatter more than longer red wavelengths. This phenomenon, discovered by Lord Rayleigh in the 1870s, explains why we see a blue sky during the day.", "url": "https://science.nasa.gov/blue-sky", "publishedDate": "2026-01-10"},
|
||||||
|
{"title": "Physics Today", "content": "The atmosphere acts as a filter, scattering blue light in all directions while letting other colors pass through more directly.", "url": "https://physicstoday.org/atmosphere", "publishedDate": "2026-01-01"},
|
||||||
|
{"title": "Scientific American", "content": "At sunset, light travels through more atmosphere, scattering away the blue and leaving reds and oranges.", "url": "https://scientificamerican.com/sunset", "publishedDate": "2025-12-20"},
|
||||||
|
{"title": "National Geographic", "content": "Ocean color also results from light scattering and absorption by water molecules.", "url": "https://nationalgeographic.com/ocean-blue", "publishedDate": "2025-12-15"},
|
||||||
|
]
|
||||||
|
broad_results = [
|
||||||
|
{"title": "MIT OpenCourseWare: Atmospheric Physics", "content": "Course materials.", "url": "https://ocw.mit.edu/physics"},
|
||||||
|
{"title": "NOAA: Understanding the Atmosphere", "content": "Educational resource.", "url": "https://noaa.gov/atmosphere"},
|
||||||
|
{"title": "BBC Science: Why is the sky blue?", "content": "Explainer article.", "url": "https://bbc.com/science/sky"},
|
||||||
|
{"title": "Khan Academy: Light and Color", "content": "Video lesson.", "url": "https://khanacademy.org/light"},
|
||||||
|
{"title": "HowStuffWorks: Rayleigh Scattering", "content": "Detailed explanation.", "url": "https://howstuffworks.com/rayleigh"},
|
||||||
|
{"title": "Physics Stack Exchange: Sky color discussion", "content": "Q&A thread.", "url": "https://physics.stackexchange.com/sky"},
|
||||||
|
{"title": "Quora: Atmospheric optics explained", "content": "Community answers.", "url": "https://quora.com/atmosphere"},
|
||||||
|
]
|
||||||
|
if 'quantum' in query.lower():
|
||||||
|
return [
|
||||||
|
{"title": "IBM Quantum", "content": "Quantum computers rely on qubits, which can represent 0, 1, or both via superposition. They solve complex problems faster.", "url": "https://www.ibm.com/quantum", "publishedDate": "2026-01-15"},
|
||||||
|
{"title": "Nature Physics", "content": "Entanglement allows qubits to be correlated instantly across distances. This is key for quantum cryptography and teleportation.", "url": "https://nature.com/articles/quantum", "publishedDate": "2026-01-10"},
|
||||||
|
{"title": "Wikipedia", "content": "Quantum computing uses quantum mechanics. Major applications include drug discovery and materials science.", "url": "https://en.wikipedia.org/wiki/Quantum_computing", "publishedDate": "2025-12-01"}
|
||||||
|
] + broad_results
|
||||||
|
return base_results + broad_results
|
||||||
|
result_container = MockResultContainer()
|
||||||
|
|
||||||
|
search = MockSearch()
|
||||||
|
plugin.post_search(None, search)
|
||||||
|
|
||||||
|
injection_html = ""
|
||||||
|
if search.result_container.answers:
|
||||||
|
injection_html = list(search.result_container.answers)[0]
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>AI Answers Demo</title>
|
||||||
|
<style>
|
||||||
|
body {{
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #2e3440;
|
||||||
|
color: #eceff4;
|
||||||
|
}}
|
||||||
|
:root {{
|
||||||
|
--color-result-border: #3b4252;
|
||||||
|
--color-result-description: #d8dee9;
|
||||||
|
--color-base-font: #88c0d0;
|
||||||
|
--color-result-link: #81a1c1;
|
||||||
|
}}
|
||||||
|
h1 {{ color: #88c0d0; }}
|
||||||
|
.meta {{ color: #81a1c1; font-size: 0.9rem; }}
|
||||||
|
hr {{ border-color: #4c566a; }}
|
||||||
|
a {{ color: #88c0d0; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div style="margin-top: 2rem;"></div>
|
||||||
|
<p class="meta">Provider: <strong>{plugin.provider or 'Not configured'}</strong> | Model: <strong>{plugin.model or 'N/A'}</strong></p>
|
||||||
|
<p>Query: <strong>{query}</strong></p>
|
||||||
|
<hr>
|
||||||
|
{injection_html if injection_html else '<p style="color:#f66;">Plugin inactive. Set LLM_PROVIDER and LLM_KEY in .env</p>'}
|
||||||
|
<hr>
|
||||||
|
<p class="meta">Try: <a href="/?q=what+is+quantum+computing">/?q=what+is+quantum+computing</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("AI Answers - Demo\n")
|
||||||
|
print(f" Provider: {plugin.provider or 'NOT SET'}")
|
||||||
|
print(f" Model: {plugin.model or 'N/A'}")
|
||||||
|
print(f" Mode: {'interactive' if plugin.interactive else 'simple'}")
|
||||||
|
print(f" Status: {'active' if plugin.api_key else 'inactive (no LLM_KEY)'}")
|
||||||
|
print(f"\n http://localhost:5000/?q=why+is+the+sky+blue\n")
|
||||||
|
app.run(debug=False, port=5000)
|
||||||
+382
@@ -0,0 +1,382 @@
|
|||||||
|
"""
|
||||||
|
AI Answers Plugin - Comprehensive Test
|
||||||
|
Test suite that verifies both 'interactive' and 'simple' modes,
|
||||||
|
checks configuration, and validates LLM integration.
|
||||||
|
|
||||||
|
Usage: python test.py
|
||||||
|
Requires: pip install flask flask-babel python-dotenv
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add parent directory to path to find ai_answers.py
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
warnings.filterwarnings("ignore", category=SyntaxWarning)
|
||||||
|
|
||||||
|
# Suppress Flask noise during test
|
||||||
|
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||||
|
|
||||||
|
# --- MOCKS START ---
|
||||||
|
|
||||||
|
# SearXNG module mocks
|
||||||
|
searx = ModuleType("searx")
|
||||||
|
searx_plugins = ModuleType("searx.plugins")
|
||||||
|
searx_results = ModuleType("searx.result_types")
|
||||||
|
|
||||||
|
class MockPlugin:
|
||||||
|
def __init__(self, cfg):
|
||||||
|
self.active = getattr(cfg, 'active', True)
|
||||||
|
|
||||||
|
class MockPluginInfo:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.meta = kwargs
|
||||||
|
|
||||||
|
class MockEngineResults:
|
||||||
|
def __init__(self):
|
||||||
|
self.types = ModuleType("types")
|
||||||
|
self.types.Answer = lambda *args, **kwargs: kwargs.get('answer', args[0] if args else "")
|
||||||
|
self._results = []
|
||||||
|
|
||||||
|
def add(self, res):
|
||||||
|
self._results.append(res)
|
||||||
|
|
||||||
|
def get_ordered_results(self):
|
||||||
|
return self._results
|
||||||
|
|
||||||
|
searx_plugins.Plugin = MockPlugin
|
||||||
|
searx_plugins.PluginInfo = MockPluginInfo
|
||||||
|
searx_results.EngineResults = MockEngineResults
|
||||||
|
|
||||||
|
# Internal search API mocks
|
||||||
|
searx_search = ModuleType("searx.search")
|
||||||
|
searx_search_models = ModuleType("searx.search.models")
|
||||||
|
searx_query = ModuleType("searx.query")
|
||||||
|
searx_webadapter = ModuleType("searx.webadapter")
|
||||||
|
|
||||||
|
class MockSearchWithPlugins:
|
||||||
|
def __init__(self, search_query, request, user_plugins):
|
||||||
|
self.search_query = search_query
|
||||||
|
self.result_container = MockEngineResults()
|
||||||
|
# Add some mock results
|
||||||
|
self.result_container.add({"title": "Mock Aux Result", "url": "https://test.com", "content": "Test content", "publishedDate": "2026"})
|
||||||
|
|
||||||
|
# Add mock infoboxes/answers
|
||||||
|
self.result_container.infoboxes = [{"infobox": "Test Box", "content": "Box Content", "attributes": []}]
|
||||||
|
self.result_container.answers = set()
|
||||||
|
self.result_container.answers_list = ["Test Answer"] # Simulating raw answers list if needed
|
||||||
|
|
||||||
|
def search(self):
|
||||||
|
return self.result_container
|
||||||
|
|
||||||
|
class MockSearchQuery:
|
||||||
|
def __init__(self, query, engineref_list, **kwargs):
|
||||||
|
self.query = query
|
||||||
|
|
||||||
|
class MockRawTextQuery:
|
||||||
|
def __init__(self, query, disabled_engines):
|
||||||
|
self.query = query
|
||||||
|
def getQuery(self):
|
||||||
|
return self.query
|
||||||
|
|
||||||
|
searx_search.SearchWithPlugins = MockSearchWithPlugins
|
||||||
|
searx_search.models = searx_search_models
|
||||||
|
searx_search_models.SearchQuery = MockSearchQuery
|
||||||
|
searx_query.RawTextQuery = MockRawTextQuery
|
||||||
|
searx_webadapter.get_engineref_from_category_list = lambda cats, disabled: []
|
||||||
|
|
||||||
|
sys.modules["searx.search"] = searx_search
|
||||||
|
sys.modules["searx.search.models"] = searx_search_models
|
||||||
|
sys.modules["searx.query"] = searx_query
|
||||||
|
sys.modules["searx.webadapter"] = searx_webadapter
|
||||||
|
|
||||||
|
# Network module mock
|
||||||
|
searx_network = ModuleType("searx.network")
|
||||||
|
def mock_network_call(method, url, **kwargs):
|
||||||
|
import http.client, ssl, json
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
parsed = urlparse(url)
|
||||||
|
port = parsed.port or (443 if parsed.scheme=='https' else 80)
|
||||||
|
target = f"{parsed.hostname}:{port}"
|
||||||
|
|
||||||
|
if parsed.scheme == 'https':
|
||||||
|
conn = http.client.HTTPSConnection(target, timeout=30, context=ssl.create_default_context())
|
||||||
|
else:
|
||||||
|
conn = http.client.HTTPConnection(target, timeout=30)
|
||||||
|
|
||||||
|
headers = kwargs.get('headers', {})
|
||||||
|
body = None
|
||||||
|
if kwargs.get('json'):
|
||||||
|
body = json.dumps(kwargs['json'])
|
||||||
|
elif kwargs.get('data'):
|
||||||
|
body = kwargs['data']
|
||||||
|
|
||||||
|
path = parsed.path
|
||||||
|
if parsed.query:
|
||||||
|
path += f"?{parsed.query}"
|
||||||
|
|
||||||
|
if kwargs.get('params'):
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
query_str = urlencode(kwargs['params'])
|
||||||
|
if '?' in path:
|
||||||
|
path += f"&{query_str}"
|
||||||
|
else:
|
||||||
|
path += f"?{query_str}"
|
||||||
|
|
||||||
|
conn.request(method, path, body=body, headers=headers)
|
||||||
|
print(f" [DEBUG] Network Call: {method} {target}{path}")
|
||||||
|
print(f" [DEBUG] Headers: {headers}")
|
||||||
|
# print(f" [DEBUG] Body: {body}")
|
||||||
|
return conn.getresponse()
|
||||||
|
|
||||||
|
def mock_stream(method, url, **kwargs):
|
||||||
|
res = mock_network_call(method, url, **kwargs)
|
||||||
|
|
||||||
|
class MockResponse:
|
||||||
|
def __init__(self, r):
|
||||||
|
self.status_code = r.status
|
||||||
|
self.text = "Mock Response" # Stub
|
||||||
|
self._r = r
|
||||||
|
|
||||||
|
def generator():
|
||||||
|
while True:
|
||||||
|
chunk = res.read(128)
|
||||||
|
if not chunk: break
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
return MockResponse(res), generator()
|
||||||
|
|
||||||
|
def mock_get(url, **kwargs):
|
||||||
|
import json
|
||||||
|
res = mock_network_call('GET', url, **kwargs)
|
||||||
|
|
||||||
|
class MockResponse:
|
||||||
|
def __init__(self, r):
|
||||||
|
self.status_code = r.status
|
||||||
|
self._content = r.read()
|
||||||
|
self.text = self._content.decode('utf-8')
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return json.loads(self.text)
|
||||||
|
|
||||||
|
return MockResponse(res)
|
||||||
|
|
||||||
|
searx_network.stream = mock_stream
|
||||||
|
searx_network.get = mock_get
|
||||||
|
sys.modules["searx.network"] = searx_network
|
||||||
|
|
||||||
|
sys.modules["searx"] = searx
|
||||||
|
sys.modules["searx.plugins"] = searx_plugins
|
||||||
|
sys.modules["searx.result_types"] = searx_results
|
||||||
|
|
||||||
|
# --- MOCKS END ---
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask_babel import Babel
|
||||||
|
from ai_answers import SXNGPlugin
|
||||||
|
|
||||||
|
def check_js_syntax(js_code):
|
||||||
|
"""Returns (valid, error_msg)"""
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False, encoding='utf-8') as f:
|
||||||
|
f.write(js_code)
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
['node', '--check', temp_path],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
os.unlink(temp_path)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
return True, None
|
||||||
|
else:
|
||||||
|
return False, result.stderr.strip()
|
||||||
|
except (FileNotFoundError, subprocess.TimeoutExpired, Exception) as e:
|
||||||
|
return True, f"[SKIP] {e}" # Skip if node not found
|
||||||
|
|
||||||
|
def run_tests():
|
||||||
|
print("AI Answers - Test Suite\n")
|
||||||
|
|
||||||
|
print("[Syntax]")
|
||||||
|
|
||||||
|
import py_compile
|
||||||
|
try:
|
||||||
|
target_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'ai_answers.py')
|
||||||
|
py_compile.compile(target_file, doraise=True)
|
||||||
|
print(" Python: OK")
|
||||||
|
except py_compile.PyCompileError as e:
|
||||||
|
print(f" Syntax: [FAIL] {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
modes = ['interactive', 'simple']
|
||||||
|
|
||||||
|
for mode in modes:
|
||||||
|
app = Flask(__name__)
|
||||||
|
Babel(app)
|
||||||
|
|
||||||
|
# Set LLM_INTERACTIVE based on mode
|
||||||
|
os.environ['LLM_INTERACTIVE'] = 'true' if mode == 'interactive' else 'false'
|
||||||
|
|
||||||
|
# Override env var for this iteration
|
||||||
|
# os.environ['LLM_STYLE'] = mode # Legacy
|
||||||
|
os.environ['LLM_INTERACTIVE'] = 'true' if mode == 'interactive' else 'false'
|
||||||
|
|
||||||
|
class MockConfig:
|
||||||
|
active = True
|
||||||
|
|
||||||
|
# Re-init plugin with new env var in effect
|
||||||
|
plugin = SXNGPlugin(MockConfig())
|
||||||
|
plugin.init(app)
|
||||||
|
|
||||||
|
if mode == 'interactive':
|
||||||
|
print(f"\n[Config]")
|
||||||
|
print(f" Provider: {plugin.provider or 'NOT SET'}")
|
||||||
|
print(f" API Key: {'OK' if plugin.api_key else 'MISSING'}")
|
||||||
|
|
||||||
|
# Construct Search
|
||||||
|
class MockSearchQuery:
|
||||||
|
pageno = 1
|
||||||
|
query = "test query"
|
||||||
|
lang = 'en'
|
||||||
|
categories = ['general']
|
||||||
|
|
||||||
|
class MockSearch:
|
||||||
|
search_query = MockSearchQuery()
|
||||||
|
class MockResultContainer:
|
||||||
|
def __init__(self):
|
||||||
|
self.answers = set()
|
||||||
|
def get_ordered_results(self):
|
||||||
|
return [
|
||||||
|
{"title": "T1", "content": "C1", "url": "https://a.com/1", "publishedDate": "2026-01-15"},
|
||||||
|
{"title": "T2", "content": "C2", "url": "https://a.com/2", "publishedDate": "2026-01-10"},
|
||||||
|
]
|
||||||
|
result_container = MockResultContainer()
|
||||||
|
|
||||||
|
search = MockSearch()
|
||||||
|
plugin.post_search(None, search)
|
||||||
|
|
||||||
|
if not search.result_container.answers:
|
||||||
|
print(" FAIL: No HTML injected")
|
||||||
|
return False
|
||||||
|
|
||||||
|
html = str(list(search.result_container.answers)[0])
|
||||||
|
|
||||||
|
# Mode-specific basic validations
|
||||||
|
has_box = 'id="sxng-stream-box"' in html
|
||||||
|
has_footer = 'id="sxng-footer"' in html
|
||||||
|
|
||||||
|
if mode == 'interactive':
|
||||||
|
if has_box and has_footer:
|
||||||
|
print("\n[Render: interactive]")
|
||||||
|
print(" UI: OK")
|
||||||
|
else:
|
||||||
|
print(f" FAIL: Box={has_box}, Footer={has_footer}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if has_box and not has_footer:
|
||||||
|
print("\n[Render: simple]")
|
||||||
|
print(" UI: OK")
|
||||||
|
else:
|
||||||
|
print(f" FAIL: Box={has_box}, Footer={has_footer}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# JS Verification
|
||||||
|
js_match = re.search(r'<script>(.*?)</script>', html, re.DOTALL)
|
||||||
|
if not js_match:
|
||||||
|
print(" FAIL: No script tag found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
js_code = js_match.group(1).strip()
|
||||||
|
valid, err = check_js_syntax(js_code)
|
||||||
|
|
||||||
|
if valid:
|
||||||
|
print(" JS: OK")
|
||||||
|
else:
|
||||||
|
print(" JS Syntax: [FAIL]")
|
||||||
|
print(f" Error: {err.splitlines()[0][:80]}...")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f" Size: {len(html):,} bytes")
|
||||||
|
|
||||||
|
# Verify Critical Fix: Function Signature
|
||||||
|
# simple mode caused reference error if signature wasn't unified
|
||||||
|
if 'async function startStream(overrideQ = null, prevAnswer = null, auxContext = null)' in js_code:
|
||||||
|
print(" Signature: OK")
|
||||||
|
else:
|
||||||
|
print(" Signature Fix: [FAIL] Unified startStream signature MISSING")
|
||||||
|
# Not fatal for interactive per se, but fatal if consistent code is desired
|
||||||
|
# For simple mode it IS fatal in runtime.
|
||||||
|
if mode == 'simple': return False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# GLOBAL ENDPOINT / INTEGRATION TESTS (Using last plugin init)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
if not plugin.api_key:
|
||||||
|
print("\n[Skip integration: no LLM_KEY]")
|
||||||
|
return True
|
||||||
|
|
||||||
|
print(f"\n[Stream]")
|
||||||
|
print(f" Provider: {plugin.provider}")
|
||||||
|
print(f" Model: {plugin.model}")
|
||||||
|
|
||||||
|
# Needs a token from the last run to pass auth
|
||||||
|
token_match = re.search(r'tk_init = "(.*?)";', html)
|
||||||
|
if not token_match:
|
||||||
|
print(" FAIL: Could not extract token for stream test")
|
||||||
|
return False
|
||||||
|
|
||||||
|
with app.test_client() as client:
|
||||||
|
payload = {
|
||||||
|
"q": "why is the sky blue",
|
||||||
|
"context": "[1] Wikipedia: The sky appears blue.",
|
||||||
|
"lang": "en",
|
||||||
|
"tk": token_match.group(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
response = client.post('/ai-stream', json=payload)
|
||||||
|
elapsed = time.time() - start
|
||||||
|
|
||||||
|
print(f" Status: {response.status_code}")
|
||||||
|
print(f" Time: {elapsed:.2f}s")
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(f" FAIL: Expected 200, got {response.status_code}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
data = response.data.decode('utf-8')
|
||||||
|
if len(data) < 5:
|
||||||
|
print(" FAIL: Empty or too short response")
|
||||||
|
return False
|
||||||
|
print(" Result: OK")
|
||||||
|
|
||||||
|
print("\n[Aux Search]")
|
||||||
|
with app.test_client() as client:
|
||||||
|
aux_response = client.post('/ai-auxiliary-search', json={'query': 'test'})
|
||||||
|
if aux_response.status_code == 200 and 'results' in aux_response.get_json():
|
||||||
|
print(" Result: OK")
|
||||||
|
else:
|
||||||
|
print(" Aux Endpoint: [FAIL]")
|
||||||
|
|
||||||
|
print("\nPASS")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = run_tests()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
Reference in New Issue
Block a user