Merge bcd9476a6dee087aeb4b2c17b43d0c6189d1b3f4 into 4dfc47584d7c946b9682dc1e4858fae003b16d1f
This commit is contained in:
commit
347535abda
38
client/simple/package-lock.json
generated
38
client/simple/package-lock.json
generated
@ -8,7 +8,9 @@
|
||||
"name": "simple",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"autocomplete-js": "^2.7.1"
|
||||
"autocomplete-js": "^2.7.1",
|
||||
"katex": "^0.16.19",
|
||||
"marked": "^15.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.22.0",
|
||||
@ -4433,6 +4435,29 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/katex": {
|
||||
"version": "0.16.21",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz",
|
||||
"integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==",
|
||||
"funding": [
|
||||
"https://opencollective.com/katex",
|
||||
"https://github.com/sponsors/katex"
|
||||
],
|
||||
"dependencies": {
|
||||
"commander": "^8.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"katex": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/katex/node_modules/commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@ -4689,6 +4714,17 @@
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "15.0.7",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.7.tgz",
|
||||
"integrity": "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/mathml-tag-names": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz",
|
||||
|
@ -35,6 +35,8 @@
|
||||
"webpack-cli": "^6.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"autocomplete-js": "^2.7.1"
|
||||
"autocomplete-js": "^2.7.1",
|
||||
"katex": "^0.16.19",
|
||||
"marked": "^15.0.6"
|
||||
}
|
||||
}
|
||||
|
280
client/simple/src/js/main/quick-answer.js
Normal file
280
client/simple/src/js/main/quick-answer.js
Normal file
@ -0,0 +1,280 @@
|
||||
import { marked } from "../../../node_modules/marked/lib/marked.esm.js";
|
||||
import renderMathInElement from "../../../node_modules/katex/dist/contrib/auto-render.js";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
if (typeof window.referenceMap === "undefined") {
|
||||
console.error("referenceMap is not defined");
|
||||
return;
|
||||
}
|
||||
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
highlight: function (code, language) {
|
||||
if (language) {
|
||||
return (
|
||||
'<pre><code class="language-' +
|
||||
language +
|
||||
'">' +
|
||||
code.replace(
|
||||
/[&<>'"]/g,
|
||||
(c) =>
|
||||
({
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"'": "'",
|
||||
'"': """,
|
||||
})[c],
|
||||
) +
|
||||
"</code></pre>"
|
||||
);
|
||||
}
|
||||
return code;
|
||||
},
|
||||
});
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.link = (token) => {
|
||||
const href = token.href;
|
||||
const title = token.title || "";
|
||||
const text = token.text;
|
||||
return `<a href="${href}" title="${title}" rel="noreferrer">${text}</a>`;
|
||||
};
|
||||
marked.use({ renderer });
|
||||
|
||||
// Custom math handling; we roll our own because LLMs are inconsistent(!)
|
||||
const mathExtension = {
|
||||
name: "math",
|
||||
level: "block",
|
||||
start(src) {
|
||||
return src.match(/\$\$/)?.index;
|
||||
},
|
||||
tokenizer(src) {
|
||||
const rule = /^\$\$([\s\S]+?)\$\$/;
|
||||
const match = rule.exec(src);
|
||||
if (match) {
|
||||
return {
|
||||
type: "math",
|
||||
raw: match[0],
|
||||
text: match[1].trim(),
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return `$$${token.text}$$`;
|
||||
},
|
||||
};
|
||||
|
||||
const inlineMathExtension = {
|
||||
name: "inlineMath",
|
||||
level: "inline",
|
||||
start(src) {
|
||||
return src.match(/\$/)?.index;
|
||||
},
|
||||
tokenizer(src) {
|
||||
const rule = /^\$([^$\n]+?)\$/;
|
||||
const match = rule.exec(src);
|
||||
if (match) {
|
||||
return {
|
||||
type: "inlineMath",
|
||||
raw: match[0],
|
||||
text: match[1].trim(),
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return `$${token.text}$`;
|
||||
},
|
||||
};
|
||||
|
||||
marked.use({ extensions: [mathExtension, inlineMathExtension] });
|
||||
|
||||
// Answer
|
||||
const qa = document.querySelector(".infobox p");
|
||||
if (!qa) {
|
||||
console.error("Quick answer container not found");
|
||||
return;
|
||||
}
|
||||
qa.id = "quick-answer";
|
||||
qa.className = "markdown-content";
|
||||
|
||||
// References
|
||||
const refContainer = document.createElement("div");
|
||||
refContainer.className = "references";
|
||||
const refHeading = document.createElement("h4");
|
||||
refHeading.textContent = "References";
|
||||
refContainer.appendChild(refHeading);
|
||||
const refList = document.createElement("ol");
|
||||
refContainer.appendChild(refList);
|
||||
qa.after(refContainer);
|
||||
|
||||
let accumulatedText = "";
|
||||
let lastProcessedLength = 0;
|
||||
let references = {};
|
||||
let referenceCounter = 1;
|
||||
let referenceMap = window.referenceMap;
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
return unsafe.replace(/[&<>"']/g, function (m) {
|
||||
switch (m) {
|
||||
case "&":
|
||||
return "&";
|
||||
case "<":
|
||||
return "<";
|
||||
case ">":
|
||||
return ">";
|
||||
case '"':
|
||||
return """;
|
||||
case "'":
|
||||
return "'";
|
||||
default:
|
||||
return m;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function replaceCitations(text) {
|
||||
// First pass: replace citations with temporary markers to be replaced by actual spaces in the second pass
|
||||
// LLMs do not consistently follow prompted formatting, and inline citations *need* to be space-delimited
|
||||
let processedText = text.replace(
|
||||
/【(\d+)】/g,
|
||||
(match, citationIndex, offset) => {
|
||||
const isFollowedByCitation = text
|
||||
.slice(offset + match.length)
|
||||
.match(/^【\d+】/);
|
||||
const source = referenceMap[citationIndex];
|
||||
|
||||
if (source) {
|
||||
const [url, title] = source;
|
||||
let refNumber = references[citationIndex];
|
||||
const escapedTitle = escapeHtml(title);
|
||||
|
||||
if (!refNumber) {
|
||||
const refItem = document.createElement("li");
|
||||
const refLink = document.createElement("a");
|
||||
refLink.href = url;
|
||||
refLink.textContent = title;
|
||||
refLink.rel = "noreferrer";
|
||||
refItem.appendChild(refLink);
|
||||
refList.appendChild(refItem);
|
||||
references[citationIndex] = referenceCounter;
|
||||
refNumber = referenceCounter;
|
||||
referenceCounter += 1;
|
||||
}
|
||||
|
||||
// Add look-ahead marker |||CITATION_SPACE||| if followed by another citation
|
||||
return `<a href="${url}" class="inline-reference" title="${escapedTitle}">${refNumber}</a>${
|
||||
isFollowedByCitation ? "|||CITATION_SPACE|||" : ""
|
||||
}`;
|
||||
}
|
||||
return match;
|
||||
},
|
||||
);
|
||||
|
||||
// Second pass: replace temporary markers with spaces
|
||||
return processedText.replace(/\|\|\|CITATION_SPACE\|\|\|/g, " ");
|
||||
}
|
||||
|
||||
fetch("/quick_answer", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
system: window.systemPrompt,
|
||||
user: window.userPrompt,
|
||||
token: window.userToken,
|
||||
model: window.userModel,
|
||||
providers: window.userProviders,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
function processMarkdownChunk(text) {
|
||||
accumulatedText += text;
|
||||
|
||||
const markdownElements = {
|
||||
codeBlock: { start: "```", end: "```" },
|
||||
bold: { start: "**", end: "**" },
|
||||
italic: { start: "_", end: "_" },
|
||||
link: { start: "[", end: ")" },
|
||||
mathDisplay: { start: "$$", end: "$$" },
|
||||
mathInline: { start: "$", end: "$" },
|
||||
};
|
||||
|
||||
let processUpTo = accumulatedText.length;
|
||||
|
||||
// Find last complete element
|
||||
for (const element of Object.values(markdownElements)) {
|
||||
const lastStart = accumulatedText.lastIndexOf(element.start);
|
||||
if (lastStart > lastProcessedLength) {
|
||||
const nextEnd = accumulatedText.indexOf(
|
||||
element.end,
|
||||
lastStart + element.start.length,
|
||||
);
|
||||
if (nextEnd === -1) {
|
||||
processUpTo = Math.min(processUpTo, lastStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process complete portion
|
||||
if (processUpTo > lastProcessedLength) {
|
||||
const processedText = replaceCitations(
|
||||
accumulatedText.substring(0, processUpTo),
|
||||
);
|
||||
qa.innerHTML = marked.parse(processedText);
|
||||
|
||||
renderMathInElement(qa, {
|
||||
delimiters: [
|
||||
{ left: "$$", right: "$$", display: true },
|
||||
{ left: "$", right: "$", display: false },
|
||||
],
|
||||
throwOnError: false,
|
||||
});
|
||||
|
||||
lastProcessedLength = processUpTo;
|
||||
}
|
||||
}
|
||||
|
||||
function readStream() {
|
||||
reader
|
||||
.read()
|
||||
.then(({ done, value }) => {
|
||||
if (done) {
|
||||
// Process any remaining text
|
||||
if (accumulatedText.length > lastProcessedLength) {
|
||||
const processedText = replaceCitations(accumulatedText);
|
||||
qa.innerHTML = marked.parse(processedText);
|
||||
renderMathInElement(qa, {
|
||||
delimiters: [
|
||||
{ left: "$$", right: "$$", display: true },
|
||||
{ left: "$", right: "$", display: false },
|
||||
],
|
||||
throwOnError: false,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const text = decoder.decode(value, { stream: true });
|
||||
processMarkdownChunk(text);
|
||||
|
||||
// Scroll to bottom of the div to show new content
|
||||
qa.scrollTop = qa.scrollHeight;
|
||||
|
||||
// Continue reading
|
||||
readStream();
|
||||
})
|
||||
.catch((error) => console.error("Error:", error));
|
||||
}
|
||||
readStream();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
qa.innerHTML = marked.parse(`**Error**: ${error.message}`);
|
||||
});
|
||||
});
|
@ -5,3 +5,4 @@ import "./main/mapresult.js";
|
||||
import "./main/preferences.js";
|
||||
import "./main/results.js";
|
||||
import "./main/search.js";
|
||||
import "./main/quick-answer.js";
|
||||
|
@ -130,6 +130,14 @@
|
||||
// Favicons Colors
|
||||
--color-favicon-background-color: #ddd;
|
||||
--color-favicon-border-color: #ccc;
|
||||
|
||||
/// Quick Answer Colors
|
||||
--color-quick-answer-code-background: rgb(27 31 35 / 5%);
|
||||
--color-quick-answer-pre-background: #f6f8fa;
|
||||
--color-quick-answer-blockquote-border: #dfe2e5;
|
||||
--color-quick-answer-blockquote-font: #6a737d;
|
||||
--color-quick-answer-table-border: #dfe2e5;
|
||||
--color-quick-answer-table-tr-background: #f6f8fa;
|
||||
}
|
||||
|
||||
.dark-themes() {
|
||||
@ -249,6 +257,14 @@
|
||||
// Favicons Colors
|
||||
--color-favicon-background-color: #ddd;
|
||||
--color-favicon-border-color: #ccc;
|
||||
|
||||
/// Quick Answer Colors
|
||||
--color-quick-answer-code-background: #4d5a6f;
|
||||
--color-quick-answer-pre-background: #4d5a6f;
|
||||
--color-quick-answer-blockquote-border: #555;
|
||||
--color-quick-answer-blockquote-font: #bbb;
|
||||
--color-quick-answer-table-border: #555;
|
||||
--color-quick-answer-table-tr-background: #4d5a6f;
|
||||
}
|
||||
|
||||
.black-themes() {
|
||||
|
83
client/simple/src/less/quick-answer.less
Normal file
83
client/simple/src/less/quick-answer.less
Normal file
@ -0,0 +1,83 @@
|
||||
#sidebar .infobox .markdown-content code {
|
||||
background-color: var(--color-quick-answer-code-background);
|
||||
border-radius: 3px;
|
||||
font-family:
|
||||
SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
font-size: 85%;
|
||||
margin: 0;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
|
||||
#sidebar .infobox .markdown-content pre {
|
||||
background-color: var(--color-quick-answer-pre-background);
|
||||
border-radius: 3px;
|
||||
font-size: 85%;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
#sidebar .infobox .markdown-content pre > code {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
white-space: pre;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
#sidebar .infobox .markdown-content blockquote {
|
||||
border-left: 0.25em solid var(--color-quick-answer-blockquote-border);
|
||||
color: var(--color-quick-answer-blockquote-font);
|
||||
margin: 0;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
#sidebar .infobox .markdown-content a.inline-reference {
|
||||
color: var(--color-result-link-font);
|
||||
line-height: 1.4;
|
||||
position: relative;
|
||||
top: -0.2em;
|
||||
vertical-align: top;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
#sidebar .infobox .markdown-content table {
|
||||
border-collapse: collapse;
|
||||
margin: 1em 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#sidebar .infobox .markdown-content table th,
|
||||
#sidebar .infobox .markdown-content table td {
|
||||
border: 1px solid var(--color-quick-answer-table-border);
|
||||
padding: 6px 13px;
|
||||
}
|
||||
|
||||
#sidebar .infobox .markdown-content table tr:nth-child(2n) {
|
||||
background-color: var(--color-quick-answer-table-tr-background);
|
||||
}
|
||||
|
||||
#sidebar .infobox .markdown-content h1,
|
||||
#sidebar .infobox .markdown-content h2,
|
||||
#sidebar .infobox .markdown-content h3,
|
||||
#sidebar .infobox .markdown-content h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
#sidebar .infobox .references a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#sidebar .infobox .references ol {
|
||||
list-style-type: decimal;
|
||||
list-style-position: inside;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
#sidebar .infobox .references ol li {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
@ -32,6 +32,9 @@
|
||||
// to center the results
|
||||
@import "style-center.less";
|
||||
|
||||
// quick-answer plugin
|
||||
@import "quick-answer.less";
|
||||
|
||||
// sxng-icon-set
|
||||
.sxng-icon-set {
|
||||
display: inline-block;
|
||||
|
@ -22,6 +22,8 @@ const PATH = {
|
||||
brand: "src/brand",
|
||||
static: resolve(ROOT, "client/simple/static"),
|
||||
leaflet: resolve(ROOT, "client/simple/node_modules/leaflet/dist"),
|
||||
katex: resolve(ROOT, "client/simple/node_modules/katex"),
|
||||
marked: resolve(ROOT, "client/simple/node_modules/marked"),
|
||||
templates: resolve(ROOT, "searx/templates/simple"),
|
||||
};
|
||||
|
||||
@ -133,6 +135,18 @@ export default defineConfig({
|
||||
{ src: PATH.leaflet + "/leaflet.{js,js.map}", dest: PATH.dist + "/js" },
|
||||
{ src: PATH.leaflet + "/images/*.png", dest: PATH.dist + "/css/images/" },
|
||||
{ src: PATH.leaflet + "/*.{css,css.map}", dest: PATH.dist + "/css" },
|
||||
]
|
||||
}),
|
||||
|
||||
// Quick Answer (KaTeX + Marked)
|
||||
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{ src: PATH.katex + "/dist/katex.js", dest: PATH.dist + "/js" },
|
||||
{ src: PATH.katex + "/contrib/auto-render/auto-render.js", dest: PATH.dist + "/js" },
|
||||
{ src: PATH.katex + "/dist/katex.css", dest: PATH.dist + "/css" },
|
||||
{ src: PATH.katex + "/dist/fonts/*.{ttf,woff,woff2}", dest: PATH.dist + "/css/fonts/" },
|
||||
{ src: PATH.marked + "/lib/marked.esm.js", dest: PATH.dist + "/js" },
|
||||
{ src: PATH.static + "/**/*", dest: PATH.dist },
|
||||
]
|
||||
}),
|
||||
|
@ -72,3 +72,6 @@
|
||||
|
||||
``url_formatting``:
|
||||
Formatting type to use for result URLs: ``pretty``, ``full`` or ``host``.
|
||||
|
||||
``quick_answer_api``:
|
||||
Quick Answer OpenAI-compatible API endpoint to query for search-supported LLM responses.
|
||||
|
108
searx/plugins/quick_answer.py
Normal file
108
searx/plugins/quick_answer.py
Normal file
@ -0,0 +1,108 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring, missing-class-docstring
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from flask_babel import gettext
|
||||
|
||||
from searx.plugins import Plugin, PluginInfo
|
||||
|
||||
|
||||
class SXNGPlugin(Plugin):
|
||||
id = "quick_answer"
|
||||
default_on = False
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.info = PluginInfo(
|
||||
id=self.id,
|
||||
name=gettext("Quick Answer"),
|
||||
description=gettext("Use search results to obtain cited answers from LLMs by appending '?' to queries"),
|
||||
examples=["Linear congruential generator?"],
|
||||
preference_section="general/quick_answer",
|
||||
)
|
||||
|
||||
def get_sys_prompt(self):
|
||||
now = datetime.now()
|
||||
return f"""
|
||||
The current date is {now:%Y-%m-%d}
|
||||
|
||||
You ALWAYS follow these guidelines when writing your response:
|
||||
- Use markdown formatting to enhance clarity and readability of your response.
|
||||
- If you need to include mathematical expressions, use LaTeX to format them properly. Only use LaTeX when necessary for math.
|
||||
- Delimit inline mathematical expressions with '$', for example: $y = mx + b$.
|
||||
- Delimit block mathematical expressions with '$$', for example: $$F = ma$$.
|
||||
- If you need to write code or program commands, format them as markdown code blocks.
|
||||
- For all other output, use plain text formatting unless the user specifically requests otherwise.
|
||||
- DO NOT include headers which only describe or rephrase the query before beginning your response.
|
||||
- DO NOT include URLs or links in your response.
|
||||
- ALWAYS enclose currency and price values in '**', for example: **$5.99**, to ensure they are formatted correctly.
|
||||
|
||||
The relevant available information is contained within the <information></information> tags. When a user asks a question, perform the following tasks:
|
||||
0. Examine the available information and assess whether you can answer the question based on it, even if the answer is not explicitly stated. For example, if the question asks about a specific feature of a product and the available information discusses the product's features without mentioning the specific feature, you can infer that the product likely does not have that feature.
|
||||
1. Use the available information to inform your answer.
|
||||
2. When answering questions, provide inline citation references by putting their citation index delimited by 【 and 】 at end of sentence, example: This is a claim【1】."
|
||||
3. If you need to cite multiple pieces of information inline, use separate 【 and 】 for each citation, example: "This is a claim【1】【2】."
|
||||
4. Use citations most relevant to the query to augment your answer with informative supportive resources; do not create unhelpful, extended chains of citations.
|
||||
5. DO NOT list URLs/links of the citation source or an aggregate list of citations at the end of the response. They would be automatically added by the system based on citation indices.
|
||||
6. DO NOT provide inline citations inside or around code blocks, as they break formatting of output, only provide them to augment plaintext.
|
||||
7. DO NOT use markdown to format your citations, always provide them in plaintext.
|
||||
|
||||
A few guidelines for you when answering questions:
|
||||
- Highlight relevant entities/phrases with **, for example: "**Neil Armstrong** is known as the first person to land on the moon." (Do not apply this guideline to citations or in code blocks.)
|
||||
- DO NOT talk about how you based your answer on the information provided to you as it may confuse the user.
|
||||
- Don't copy-paste the information from the available information directly. Paraphrase the information in your own words.
|
||||
- Even if the information is in another format, your output MUST follow the guidelines. for example: output O₁ instead of O<sub>1</sub>, output R⁷ instead of R<sup>7</sup>, etc.
|
||||
- Be concise and informative in your answers.
|
||||
"""
|
||||
|
||||
def format_sources(self, sources):
|
||||
ret = "<available_information>\n"
|
||||
for pos, source in enumerate(sources):
|
||||
ret += "<datum>\n"
|
||||
ret += f'<citation index="{pos}">\n'
|
||||
ret += f"<source>\n{source.get('url', '')}\n</source>\n"
|
||||
ret += f"<title>\n{source.get('title', '')}\n</title>\n"
|
||||
ret += f"<content>\n{source.get('content', '')}\n</content>\n"
|
||||
ret += "</datum>\n"
|
||||
|
||||
return ret + "</available_information>"
|
||||
|
||||
def post_search(self, request, search):
|
||||
query = search.search_query
|
||||
if query.pageno > 1 or not query.query.endswith("?"):
|
||||
return
|
||||
|
||||
token = request.preferences.get_value("quick_answer_token")
|
||||
if not token:
|
||||
return
|
||||
|
||||
model = request.preferences.get_value("quick_answer_model")
|
||||
providers = request.preferences.get_value("quick_answer_providers")
|
||||
if providers:
|
||||
providers = [provider.strip() for provider in providers.split(",")]
|
||||
|
||||
sources = search.result_container.get_ordered_results()
|
||||
formatted_sources = self.format_sources(sources)
|
||||
user = formatted_sources + f"\n\nUser query: {query.query}"
|
||||
system = self.get_sys_prompt()
|
||||
|
||||
reference_map = {str(i): (source.get("url"), source.get("title")) for i, source in enumerate(sources)}
|
||||
|
||||
search.result_container.infoboxes.append(
|
||||
{
|
||||
"infobox": "Quick Answer",
|
||||
"id": "quick_answer",
|
||||
"content": f"""
|
||||
<script>
|
||||
window.systemPrompt = {json.dumps(system)};
|
||||
window.userPrompt = {json.dumps(user)};
|
||||
window.userToken = {json.dumps(token)};
|
||||
window.userModel = {json.dumps(model)};
|
||||
window.userProviders = {json.dumps(providers)};
|
||||
window.referenceMap = {json.dumps(reference_map)};
|
||||
</script>
|
||||
""",
|
||||
}
|
||||
)
|
@ -76,6 +76,26 @@ class Setting:
|
||||
class StringSetting(Setting):
|
||||
"""Setting of plain string values"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.value = ""
|
||||
|
||||
def get_value(self):
|
||||
return self.value
|
||||
|
||||
def parse(self, data: str):
|
||||
self.value = data
|
||||
|
||||
def parse_form(self, data: str):
|
||||
if self.locked:
|
||||
return
|
||||
|
||||
self.value = data
|
||||
|
||||
def save(self, name: str, resp: flask.Response):
|
||||
"""Save cookie ``name`` in the HTTP response object"""
|
||||
resp.set_cookie(name, self.value, max_age=COOKIE_MAX_AGE)
|
||||
|
||||
|
||||
class EnumStringSetting(Setting):
|
||||
"""Setting of a value which can only come from the given choices"""
|
||||
@ -132,6 +152,35 @@ class MultipleChoiceSetting(Setting):
|
||||
resp.set_cookie(name, ','.join(self.value), max_age=COOKIE_MAX_AGE)
|
||||
|
||||
|
||||
class ListSetting(Setting):
|
||||
"""Setting of values of type ``list`` (ordered comma separated string)"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.values = []
|
||||
|
||||
def get_value(self):
|
||||
return ",".join(self.values)
|
||||
|
||||
def parse(self, data: str):
|
||||
"""Parse and validate ``data`` and store the result at ``self.value``"""
|
||||
if data == "":
|
||||
self.values = []
|
||||
return
|
||||
|
||||
self.values = data.split(",")
|
||||
|
||||
def parse_form(self, data: str):
|
||||
if self.locked:
|
||||
return
|
||||
|
||||
self.values = data.split(",")
|
||||
|
||||
def save(self, name: str, resp: flask.Response):
|
||||
"""Save cookie ``name`` in the HTTP response object"""
|
||||
resp.set_cookie(name, ",".join(self.values), max_age=COOKIE_MAX_AGE)
|
||||
|
||||
|
||||
class SetSetting(Setting):
|
||||
"""Setting of values of type ``set`` (comma separated string)"""
|
||||
|
||||
@ -479,6 +528,9 @@ class Preferences:
|
||||
settings['ui']['url_formatting'],
|
||||
choices=['pretty', 'full', 'host']
|
||||
),
|
||||
"quick_answer_token": StringSetting("quick_answer_token"),
|
||||
"quick_answer_model": StringSetting("quick_answer_model"),
|
||||
"quick_answer_providers": ListSetting("quick_answer_providers"),
|
||||
# fmt: on
|
||||
}
|
||||
|
||||
|
@ -2673,3 +2673,5 @@ doi_resolvers:
|
||||
sci-hub.ru: 'https://sci-hub.ru/'
|
||||
|
||||
default_doi_resolver: 'oadoi.org'
|
||||
|
||||
quick_answer_api: "https://openrouter.ai/api/v1/chat/completions"
|
||||
|
BIN
searx/static/themes/simple/css/fonts/KaTeX_AMS-Regular.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_AMS-Regular.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_AMS-Regular.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_AMS-Regular.woff
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_AMS-Regular.woff2
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_AMS-Regular.woff2
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Caligraphic-Bold.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Caligraphic-Bold.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Caligraphic-Bold.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Caligraphic-Bold.woff
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Fraktur-Bold.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Fraktur-Bold.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Fraktur-Bold.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Fraktur-Bold.woff
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Fraktur-Bold.woff2
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Fraktur-Bold.woff2
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Fraktur-Regular.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Fraktur-Regular.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Fraktur-Regular.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Fraktur-Regular.woff
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Fraktur-Regular.woff2
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Fraktur-Regular.woff2
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Bold.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Bold.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Bold.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Bold.woff
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Bold.woff2
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Bold.woff2
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-BoldItalic.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-BoldItalic.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-BoldItalic.woff
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-BoldItalic.woff2
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Italic.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Italic.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Italic.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Italic.woff
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Italic.woff2
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Italic.woff2
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Regular.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Regular.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Regular.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Regular.woff
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Regular.woff2
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Main-Regular.woff2
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Math-BoldItalic.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Math-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Math-BoldItalic.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Math-BoldItalic.woff
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Math-BoldItalic.woff2
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Math-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Math-Italic.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Math-Italic.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Math-Italic.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Math-Italic.woff
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Math-Italic.woff2
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Math-Italic.woff2
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_SansSerif-Bold.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_SansSerif-Bold.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_SansSerif-Bold.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_SansSerif-Bold.woff
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_SansSerif-Bold.woff2
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_SansSerif-Bold.woff2
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_SansSerif-Italic.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_SansSerif-Italic.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_SansSerif-Italic.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_SansSerif-Italic.woff
Normal file
Binary file not shown.
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_SansSerif-Regular.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_SansSerif-Regular.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Script-Regular.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Script-Regular.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Script-Regular.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Script-Regular.woff
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Script-Regular.woff2
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Script-Regular.woff2
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size1-Regular.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size1-Regular.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size1-Regular.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size1-Regular.woff
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size1-Regular.woff2
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size1-Regular.woff2
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size2-Regular.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size2-Regular.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size2-Regular.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size2-Regular.woff
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size2-Regular.woff2
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size2-Regular.woff2
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size3-Regular.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size3-Regular.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size3-Regular.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size3-Regular.woff
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size3-Regular.woff2
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size3-Regular.woff2
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size4-Regular.ttf
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size4-Regular.ttf
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size4-Regular.woff
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size4-Regular.woff
Normal file
Binary file not shown.
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size4-Regular.woff2
Normal file
BIN
searx/static/themes/simple/css/fonts/KaTeX_Size4-Regular.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1210
searx/static/themes/simple/css/katex.css
Normal file
1210
searx/static/themes/simple/css/katex.css
Normal file
File diff suppressed because it is too large
Load Diff
2
searx/static/themes/simple/css/rss.min.css
vendored
2
searx/static/themes/simple/css/rss.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
142
searx/static/themes/simple/js/auto-render.js
Normal file
142
searx/static/themes/simple/js/auto-render.js
Normal file
@ -0,0 +1,142 @@
|
||||
/* eslint no-console:0 */
|
||||
|
||||
import katex from "katex";
|
||||
import splitAtDelimiters from "./splitAtDelimiters";
|
||||
|
||||
/* Note: optionsCopy is mutated by this method. If it is ever exposed in the
|
||||
* API, we should copy it before mutating.
|
||||
*/
|
||||
const renderMathInText = function(text, optionsCopy) {
|
||||
const data = splitAtDelimiters(text, optionsCopy.delimiters);
|
||||
if (data.length === 1 && data[0].type === 'text') {
|
||||
// There is no formula in the text.
|
||||
// Let's return null which means there is no need to replace
|
||||
// the current text node with a new one.
|
||||
return null;
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].type === "text") {
|
||||
fragment.appendChild(document.createTextNode(data[i].data));
|
||||
} else {
|
||||
const span = document.createElement("span");
|
||||
let math = data[i].data;
|
||||
// Override any display mode defined in the settings with that
|
||||
// defined by the text itself
|
||||
optionsCopy.displayMode = data[i].display;
|
||||
try {
|
||||
if (optionsCopy.preProcess) {
|
||||
math = optionsCopy.preProcess(math);
|
||||
}
|
||||
katex.render(math, span, optionsCopy);
|
||||
} catch (e) {
|
||||
if (!(e instanceof katex.ParseError)) {
|
||||
throw e;
|
||||
}
|
||||
optionsCopy.errorCallback(
|
||||
"KaTeX auto-render: Failed to parse `" + data[i].data +
|
||||
"` with ",
|
||||
e
|
||||
);
|
||||
fragment.appendChild(document.createTextNode(data[i].rawData));
|
||||
continue;
|
||||
}
|
||||
fragment.appendChild(span);
|
||||
}
|
||||
}
|
||||
|
||||
return fragment;
|
||||
};
|
||||
|
||||
const renderElem = function(elem, optionsCopy) {
|
||||
for (let i = 0; i < elem.childNodes.length; i++) {
|
||||
const childNode = elem.childNodes[i];
|
||||
if (childNode.nodeType === 3) {
|
||||
// Text node
|
||||
// Concatenate all sibling text nodes.
|
||||
// Webkit browsers split very large text nodes into smaller ones,
|
||||
// so the delimiters may be split across different nodes.
|
||||
let textContentConcat = childNode.textContent;
|
||||
let sibling = childNode.nextSibling;
|
||||
let nSiblings = 0;
|
||||
while (sibling && (sibling.nodeType === Node.TEXT_NODE)) {
|
||||
textContentConcat += sibling.textContent;
|
||||
sibling = sibling.nextSibling;
|
||||
nSiblings++;
|
||||
}
|
||||
const frag = renderMathInText(textContentConcat, optionsCopy);
|
||||
if (frag) {
|
||||
// Remove extra text nodes
|
||||
for (let j = 0; j < nSiblings; j++) {
|
||||
childNode.nextSibling.remove();
|
||||
}
|
||||
i += frag.childNodes.length - 1;
|
||||
elem.replaceChild(frag, childNode);
|
||||
} else {
|
||||
// If the concatenated text does not contain math
|
||||
// the siblings will not either
|
||||
i += nSiblings;
|
||||
}
|
||||
} else if (childNode.nodeType === 1) {
|
||||
// Element node
|
||||
const className = ' ' + childNode.className + ' ';
|
||||
const shouldRender = optionsCopy.ignoredTags.indexOf(
|
||||
childNode.nodeName.toLowerCase()) === -1 &&
|
||||
optionsCopy.ignoredClasses.every(
|
||||
x => className.indexOf(' ' + x + ' ') === -1);
|
||||
|
||||
if (shouldRender) {
|
||||
renderElem(childNode, optionsCopy);
|
||||
}
|
||||
}
|
||||
// Otherwise, it's something else, and ignore it.
|
||||
}
|
||||
};
|
||||
|
||||
const renderMathInElement = function(elem, options) {
|
||||
if (!elem) {
|
||||
throw new Error("No element provided to render");
|
||||
}
|
||||
|
||||
const optionsCopy = {};
|
||||
|
||||
// Object.assign(optionsCopy, option)
|
||||
for (const option in options) {
|
||||
if (options.hasOwnProperty(option)) {
|
||||
optionsCopy[option] = options[option];
|
||||
}
|
||||
}
|
||||
|
||||
// default options
|
||||
optionsCopy.delimiters = optionsCopy.delimiters || [
|
||||
{left: "$$", right: "$$", display: true},
|
||||
{left: "\\(", right: "\\)", display: false},
|
||||
// LaTeX uses $…$, but it ruins the display of normal `$` in text:
|
||||
// {left: "$", right: "$", display: false},
|
||||
// $ must come after $$
|
||||
|
||||
// Render AMS environments even if outside $$…$$ delimiters.
|
||||
{left: "\\begin{equation}", right: "\\end{equation}", display: true},
|
||||
{left: "\\begin{align}", right: "\\end{align}", display: true},
|
||||
{left: "\\begin{alignat}", right: "\\end{alignat}", display: true},
|
||||
{left: "\\begin{gather}", right: "\\end{gather}", display: true},
|
||||
{left: "\\begin{CD}", right: "\\end{CD}", display: true},
|
||||
|
||||
{left: "\\[", right: "\\]", display: true},
|
||||
];
|
||||
optionsCopy.ignoredTags = optionsCopy.ignoredTags || [
|
||||
"script", "noscript", "style", "textarea", "pre", "code", "option",
|
||||
];
|
||||
optionsCopy.ignoredClasses = optionsCopy.ignoredClasses || [];
|
||||
optionsCopy.errorCallback = optionsCopy.errorCallback || console.error;
|
||||
|
||||
// Enable sharing of global macros defined via `\gdef` between different
|
||||
// math elements within a single call to `renderMathInElement`.
|
||||
optionsCopy.macros = optionsCopy.macros || {};
|
||||
|
||||
renderElem(elem, optionsCopy);
|
||||
};
|
||||
|
||||
export default renderMathInElement;
|
19077
searx/static/themes/simple/js/katex.js
Normal file
19077
searx/static/themes/simple/js/katex.js
Normal file
File diff suppressed because it is too large
Load Diff
2580
searx/static/themes/simple/js/marked.esm.js
Normal file
2580
searx/static/themes/simple/js/marked.esm.js
Normal file
File diff suppressed because it is too large
Load Diff
319
searx/static/themes/simple/js/searxng.min.js
vendored
319
searx/static/themes/simple/js/searxng.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -20,6 +20,9 @@
|
||||
{% if get_setting('server.limiter') or get_setting('server.public_instance') %}
|
||||
<link rel="stylesheet" href="{{ url_for('client_token', token=link_token) }}" type="text/css">
|
||||
{% endif %}
|
||||
{% if 'Quick Answer' in get_setting('enabled_plugins') %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/katex.css') }}" type="text/css">
|
||||
{% endif %}
|
||||
<!--[if gte IE 9]>-->
|
||||
<script src="{{ url_for('static', filename='js/searxng.head.min.js') }}" client_settings="{{ client_settings }}"></script>
|
||||
<!--<![endif]-->
|
||||
|
@ -180,8 +180,9 @@
|
||||
{% if 'safesearch' not in locked_preferences %}
|
||||
{%- include 'simple/preferences/safesearch.html' -%}
|
||||
{%- endif -%}
|
||||
{%- include 'simple/preferences/tokens.html' -%}
|
||||
{{- plugin_preferences('general') -}}
|
||||
{%- include 'simple/preferences/tokens.html' -%}
|
||||
{%- include 'simple/preferences/quick_answer.html' -%}
|
||||
|
||||
|
||||
{%- if 'doi_resolver' not in locked_preferences %}
|
||||
|
39
searx/templates/simple/preferences/quick_answer.html
Normal file
39
searx/templates/simple/preferences/quick_answer.html
Normal file
@ -0,0 +1,39 @@
|
||||
<div class="pref-group">{{- _('Quick Answer') -}}</div>
|
||||
|
||||
{{- plugin_preferences('general/quick_answer') -}}
|
||||
|
||||
<fieldset>{{- '' -}}
|
||||
<legend id="quick_answer_token">{{- _('Quick Answer token') -}}</legend>{{- '' -}}
|
||||
<div class="value">{{- '' -}}
|
||||
<input name="quick_answer_token" aria-labelledby="quick_answer_token" type="text"
|
||||
autocomplete="off" spellcheck="false" autocorrect="off"
|
||||
value='{{ preferences.get_value("quick_answer_token") }}'>{{- '' -}}
|
||||
</div>{{- '' -}}
|
||||
<div class="description">
|
||||
{{- _('OpenRouter access token used to authenticate Quick Answer API requests') -}}
|
||||
</div>{{- '' -}}
|
||||
</fieldset>{{- '' -}}
|
||||
|
||||
<fieldset>{{- '' -}}
|
||||
<legend id="quick_answer_model">{{- _('Quick Answer model') -}}</legend>{{- '' -}}
|
||||
<div class="value">{{- '' -}}
|
||||
<input name="quick_answer_model" aria-labelledby="quick_answer_model" type="text"
|
||||
autocomplete="off" spellcheck="false" autocorrect="off"
|
||||
value='{{ preferences.get_value("quick_answer_model") }}'>{{- '' -}}
|
||||
</div>{{- '' -}}
|
||||
<div class="description">
|
||||
{{- _('OpenRouter LLM used to provide Quick Answers (e.g. meta-llama/llama-3.3-70b-instruct)') -}}
|
||||
</div>{{- '' -}}
|
||||
</fieldset>{{- '' -}}
|
||||
|
||||
<fieldset>{{- '' -}}
|
||||
<legend id="quick_answer_providers">{{- _('Quick Answer providers') -}}</legend>{{- '' -}}
|
||||
<div class="value">{{- '' -}}
|
||||
<input name="quick_answer_providers" aria-labelledby="quick_answer_providers" type="text"
|
||||
autocomplete="off" spellcheck="false" autocorrect="off"
|
||||
value='{{ preferences.get_value("quick_answer_providers") }}'>{{- '' -}}
|
||||
</div>{{- '' -}}
|
||||
<div class="description">
|
||||
{{- _('List of OpenRouter providers used to supply Quick Answers (e.g. Fireworks,DeepInfra)') -}}
|
||||
</div>{{- '' -}}
|
||||
</fieldset>{{- '' -}}
|
102
searx/webapp.py
102
searx/webapp.py
@ -13,6 +13,7 @@ import os
|
||||
import sys
|
||||
import base64
|
||||
|
||||
from datetime import timedelta, datetime
|
||||
from timeit import default_timer
|
||||
from html import escape
|
||||
from io import StringIO
|
||||
@ -1327,6 +1328,107 @@ def config():
|
||||
)
|
||||
|
||||
|
||||
# User-scoped cache for quick answer responses
|
||||
quick_answer_cache = {}
|
||||
quick_answer_cache_max_keys = 1000
|
||||
quick_answer_cache_expiry = timedelta(minutes=60)
|
||||
|
||||
|
||||
@app.route("/quick_answer", methods=["POST"])
|
||||
def quick_answer():
|
||||
"""Endpoint to handle LLM requests."""
|
||||
data = sxng_request.get_json()
|
||||
if not data:
|
||||
return "Invalid JSON data", 400
|
||||
|
||||
user = data.get("user")
|
||||
system = data.get("system")
|
||||
token = data.get("token")
|
||||
if not all([user, system, token]):
|
||||
return "Missing required fields", 400
|
||||
|
||||
# These can be unproblematically empty; account defaults are OK
|
||||
model = data.get("model")
|
||||
providers = data.get("providers")
|
||||
|
||||
now = datetime.now()
|
||||
expired_keys = [
|
||||
key for key, value in quick_answer_cache.items() if now - value["timestamp"] >= quick_answer_cache_expiry
|
||||
]
|
||||
for key in expired_keys:
|
||||
del quick_answer_cache[key]
|
||||
|
||||
if len(quick_answer_cache) >= quick_answer_cache_max_keys:
|
||||
sorted_keys = sorted(quick_answer_cache.keys(), key=lambda k: quick_answer_cache[k]["timestamp"])
|
||||
for key in sorted_keys[: len(quick_answer_cache) - quick_answer_cache_max_keys + 1]:
|
||||
del quick_answer_cache[key]
|
||||
|
||||
# Prevent re-generation of LLM responses when navigating to/from results pages
|
||||
query_hash = hashlib.sha256((token + model + user + system).encode("utf-8")).hexdigest()
|
||||
cached_response = quick_answer_cache.get(query_hash)
|
||||
if cached_response and datetime.now() - cached_response["timestamp"] < quick_answer_cache_expiry:
|
||||
return Response(cached_response["content"], mimetype="text/html")
|
||||
|
||||
def stream_response():
|
||||
try:
|
||||
with httpx.stream(
|
||||
method="POST",
|
||||
url=settings["quick_answer_api"],
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"model": model,
|
||||
"provider": {"order": providers},
|
||||
"stream": True,
|
||||
"messages": [
|
||||
{"role": "system", "content": system},
|
||||
{
|
||||
"role": "user",
|
||||
"content": user,
|
||||
},
|
||||
],
|
||||
},
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
|
||||
content_buffer = []
|
||||
|
||||
for line in resp.iter_lines():
|
||||
try:
|
||||
if line.startswith("data: "):
|
||||
json_str = line[6:] # Remove 'data: ' prefix
|
||||
if json_str.strip() == "[DONE]":
|
||||
break
|
||||
|
||||
json_data = json.loads(json_str)
|
||||
if "choices" in json_data:
|
||||
content = json_data["choices"][0].get("delta", {}).get("content", "")
|
||||
if content:
|
||||
content_buffer.append(content)
|
||||
yield content
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
yield f"Error processing chunk: {str(e)}"
|
||||
break
|
||||
|
||||
if not any(
|
||||
error in "".join(content_buffer) for error in ["API request failed", "Error processing chunk"]
|
||||
):
|
||||
quick_answer_cache[query_hash] = {
|
||||
"content": "".join(content_buffer),
|
||||
"timestamp": datetime.now(),
|
||||
}
|
||||
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
yield f"API request failed: {str(e)}"
|
||||
|
||||
return Response(stream_response(), mimetype="text/html")
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(_e):
|
||||
return render('404.html'), 404
|
||||
|
Loading…
x
Reference in New Issue
Block a user