first commit
This commit is contained in:
49
searx/answerers/__init__.py
Normal file
49
searx/answerers/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""The *answerers* give instant answers related to the search query, they
|
||||
usually provide answers of type :py:obj:`Answer <searx.result_types.Answer>`.
|
||||
|
||||
Here is an example of a very simple answerer that adds a "Hello" into the answer
|
||||
area:
|
||||
|
||||
.. code::
|
||||
|
||||
from flask_babel import gettext as _
|
||||
from searx.answerers import Answerer
|
||||
from searx.result_types import Answer
|
||||
|
||||
class MyAnswerer(Answerer):
|
||||
|
||||
keywords = [ "hello", "hello world" ]
|
||||
|
||||
def info(self):
|
||||
return AnswererInfo(name=_("Hello"), description=_("lorem .."), keywords=self.keywords)
|
||||
|
||||
def answer(self, request, search):
|
||||
return [ Answer(answer="Hello") ]
|
||||
|
||||
----
|
||||
|
||||
.. autoclass:: Answerer
|
||||
:members:
|
||||
|
||||
.. autoclass:: AnswererInfo
|
||||
:members:
|
||||
|
||||
.. autoclass:: AnswerStorage
|
||||
:members:
|
||||
|
||||
.. autoclass:: searx.answerers._core.ModuleAnswerer
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["AnswererInfo", "Answerer", "AnswerStorage"]
|
||||
|
||||
|
||||
from ._core import AnswererInfo, Answerer, AnswerStorage
|
||||
|
||||
STORAGE: AnswerStorage = AnswerStorage()
|
||||
STORAGE.load_builtins()
|
||||
169
searx/answerers/_core.py
Normal file
169
searx/answerers/_core.py
Normal file
@@ -0,0 +1,169 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=too-few-public-methods, missing-module-docstring
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import importlib
|
||||
import logging
|
||||
import pathlib
|
||||
import warnings
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from searx.utils import load_module
|
||||
from searx.result_types.answer import BaseAnswer
|
||||
|
||||
|
||||
_default = pathlib.Path(__file__).parent
|
||||
log: logging.Logger = logging.getLogger("searx.answerers")
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnswererInfo:
|
||||
"""Object that holds information about an answerer, these infos are shown
|
||||
to the user in the Preferences menu.
|
||||
|
||||
To be able to translate the information into other languages, the text must
|
||||
be written in English and translated with :py:obj:`flask_babel.gettext`.
|
||||
"""
|
||||
|
||||
name: str
|
||||
"""Name of the *answerer*."""
|
||||
|
||||
description: str
|
||||
"""Short description of the *answerer*."""
|
||||
|
||||
examples: list[str]
|
||||
"""List of short examples of the usage / of query terms."""
|
||||
|
||||
keywords: list[str]
|
||||
"""See :py:obj:`Answerer.keywords`"""
|
||||
|
||||
|
||||
class Answerer(abc.ABC):
|
||||
"""Abstract base class of answerers."""
|
||||
|
||||
keywords: list[str]
|
||||
"""Keywords to which the answerer has *answers*."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def answer(self, query: str) -> list[BaseAnswer]:
|
||||
"""Function that returns a list of answers to the question/query."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def info(self) -> AnswererInfo:
|
||||
"""Information about the *answerer*, see :py:obj:`AnswererInfo`."""
|
||||
|
||||
|
||||
class ModuleAnswerer(Answerer):
|
||||
"""A wrapper class for legacy *answerers* where the names (keywords, answer,
|
||||
info) are implemented on the module level (not in a class).
|
||||
|
||||
.. note::
|
||||
|
||||
For internal use only!
|
||||
"""
|
||||
|
||||
def __init__(self, mod):
|
||||
|
||||
for name in ["keywords", "self_info", "answer"]:
|
||||
if not getattr(mod, name, None):
|
||||
raise SystemExit(2)
|
||||
if not isinstance(mod.keywords, tuple):
|
||||
raise SystemExit(2)
|
||||
|
||||
self.module = mod
|
||||
self.keywords = mod.keywords # type: ignore
|
||||
|
||||
def answer(self, query: str) -> list[BaseAnswer]:
|
||||
return self.module.answer(query)
|
||||
|
||||
def info(self) -> AnswererInfo:
|
||||
kwargs = self.module.self_info()
|
||||
kwargs["keywords"] = self.keywords
|
||||
return AnswererInfo(**kwargs)
|
||||
|
||||
|
||||
class AnswerStorage(dict):
|
||||
"""A storage for managing the *answerers* of SearXNG. With the
|
||||
:py:obj:`AnswerStorage.ask`” method, a caller can ask questions to all
|
||||
*answerers* and receives a list of the results."""
|
||||
|
||||
answerer_list: set[Answerer]
|
||||
"""The list of :py:obj:`Answerer` in this storage."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.answerer_list = set()
|
||||
|
||||
def load_builtins(self):
|
||||
"""Loads ``answerer.py`` modules from the python packages in
|
||||
:origin:`searx/answerers`. The python modules are wrapped by
|
||||
:py:obj:`ModuleAnswerer`."""
|
||||
|
||||
for f in _default.iterdir():
|
||||
if f.name.startswith("_"):
|
||||
continue
|
||||
|
||||
if f.is_file() and f.suffix == ".py":
|
||||
self.register_by_fqn(f"searx.answerers.{f.stem}.SXNGAnswerer")
|
||||
continue
|
||||
|
||||
# for backward compatibility (if a fork has additional answerers)
|
||||
|
||||
if f.is_dir() and (f / "answerer.py").exists():
|
||||
warnings.warn(
|
||||
f"answerer module {f} is deprecated / migrate to searx.answerers.Answerer", DeprecationWarning
|
||||
)
|
||||
mod = load_module("answerer.py", str(f))
|
||||
self.register(ModuleAnswerer(mod))
|
||||
|
||||
def register_by_fqn(self, fqn: str):
|
||||
"""Register a :py:obj:`Answerer` via its fully qualified class namen(FQN)."""
|
||||
|
||||
mod_name, _, obj_name = fqn.rpartition('.')
|
||||
mod = importlib.import_module(mod_name)
|
||||
code_obj = getattr(mod, obj_name, None)
|
||||
|
||||
if code_obj is None:
|
||||
msg = f"answerer {fqn} is not implemented"
|
||||
log.critical(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
self.register(code_obj())
|
||||
|
||||
def register(self, answerer: Answerer):
|
||||
"""Register a :py:obj:`Answerer`."""
|
||||
|
||||
self.answerer_list.add(answerer)
|
||||
for _kw in answerer.keywords:
|
||||
self[_kw] = self.get(_kw, [])
|
||||
self[_kw].append(answerer)
|
||||
|
||||
def ask(self, query: str) -> list[BaseAnswer]:
|
||||
"""An answerer is identified via keywords, if there is a keyword at the
|
||||
first position in the ``query`` for which there is one or more
|
||||
answerers, then these are called, whereby the entire ``query`` is passed
|
||||
as argument to the answerer function."""
|
||||
|
||||
results = []
|
||||
keyword = None
|
||||
for keyword in query.split():
|
||||
if keyword:
|
||||
break
|
||||
|
||||
if not keyword or keyword not in self:
|
||||
return results
|
||||
|
||||
for answerer in self[keyword]:
|
||||
for answer in answerer.answer(query):
|
||||
# In case of *answers* prefix ``answerer:`` is set, see searx.result_types.Result
|
||||
answer.engine = f"answerer: {keyword}"
|
||||
results.append(answer)
|
||||
|
||||
return results
|
||||
|
||||
@property
|
||||
def info(self) -> list[AnswererInfo]:
|
||||
return [a.info() for a in self.answerer_list]
|
||||
80
searx/answerers/random.py
Normal file
80
searx/answerers/random.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
from flask_babel import gettext
|
||||
|
||||
from searx.result_types import Answer
|
||||
from searx.result_types.answer import BaseAnswer
|
||||
|
||||
from . import Answerer, AnswererInfo
|
||||
|
||||
|
||||
def random_characters():
|
||||
random_string_letters = string.ascii_lowercase + string.digits + string.ascii_uppercase
|
||||
return [random.choice(random_string_letters) for _ in range(random.randint(8, 32))]
|
||||
|
||||
|
||||
def random_string():
|
||||
return ''.join(random_characters())
|
||||
|
||||
|
||||
def random_float():
|
||||
return str(random.random())
|
||||
|
||||
|
||||
def random_int():
|
||||
random_int_max = 2**31
|
||||
return str(random.randint(-random_int_max, random_int_max))
|
||||
|
||||
|
||||
def random_sha256():
|
||||
m = hashlib.sha256()
|
||||
m.update(''.join(random_characters()).encode())
|
||||
return str(m.hexdigest())
|
||||
|
||||
|
||||
def random_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def random_color():
|
||||
color = "%06x" % random.randint(0, 0xFFFFFF)
|
||||
return f"#{color.upper()}"
|
||||
|
||||
|
||||
class SXNGAnswerer(Answerer):
|
||||
"""Random value generator"""
|
||||
|
||||
keywords = ["random"]
|
||||
|
||||
random_types = {
|
||||
"string": random_string,
|
||||
"int": random_int,
|
||||
"float": random_float,
|
||||
"sha256": random_sha256,
|
||||
"uuid": random_uuid,
|
||||
"color": random_color,
|
||||
}
|
||||
|
||||
def info(self):
|
||||
|
||||
return AnswererInfo(
|
||||
name=gettext(self.__doc__),
|
||||
description=gettext("Generate different random values"),
|
||||
keywords=self.keywords,
|
||||
examples=[f"random {x}" for x in self.random_types],
|
||||
)
|
||||
|
||||
def answer(self, query: str) -> list[BaseAnswer]:
|
||||
|
||||
parts = query.split()
|
||||
if len(parts) != 2 or parts[1] not in self.random_types:
|
||||
return []
|
||||
|
||||
return [Answer(answer=self.random_types[parts[1]]())]
|
||||
64
searx/answerers/statistics.py
Normal file
64
searx/answerers/statistics.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import reduce
|
||||
from operator import mul
|
||||
|
||||
import babel
|
||||
import babel.numbers
|
||||
from flask_babel import gettext
|
||||
|
||||
from searx.extended_types import sxng_request
|
||||
from searx.result_types import Answer
|
||||
from searx.result_types.answer import BaseAnswer
|
||||
|
||||
from . import Answerer, AnswererInfo
|
||||
|
||||
kw2func = [
|
||||
("min", min),
|
||||
("max", max),
|
||||
("avg", lambda args: sum(args) / len(args)),
|
||||
("sum", sum),
|
||||
("prod", lambda args: reduce(mul, args, 1)),
|
||||
]
|
||||
|
||||
|
||||
class SXNGAnswerer(Answerer):
|
||||
"""Statistics functions"""
|
||||
|
||||
keywords = [kw for kw, _ in kw2func]
|
||||
|
||||
def info(self):
|
||||
|
||||
return AnswererInfo(
|
||||
name=gettext(self.__doc__),
|
||||
description=gettext("Compute {func} of the arguments".format(func='/'.join(self.keywords))),
|
||||
keywords=self.keywords,
|
||||
examples=["avg 123 548 2.04 24.2"],
|
||||
)
|
||||
|
||||
def answer(self, query: str) -> list[BaseAnswer]:
|
||||
|
||||
results = []
|
||||
parts = query.split()
|
||||
if len(parts) < 2:
|
||||
return results
|
||||
|
||||
ui_locale = babel.Locale.parse(sxng_request.preferences.get_value('locale'), sep='-')
|
||||
|
||||
try:
|
||||
args = [babel.numbers.parse_decimal(num, ui_locale, numbering_system="latn") for num in parts[1:]]
|
||||
except: # pylint: disable=bare-except
|
||||
# seems one of the args is not a float type, can't be converted to float
|
||||
return results
|
||||
|
||||
for k, func in kw2func:
|
||||
if k == parts[0]:
|
||||
res = func(args)
|
||||
res = babel.numbers.format_decimal(res, locale=ui_locale)
|
||||
f_str = ', '.join(babel.numbers.format_decimal(arg, locale=ui_locale) for arg in args)
|
||||
results.append(Answer(answer=f"[{ui_locale}] {k}({f_str}) = {res} "))
|
||||
break
|
||||
|
||||
return results
|
||||
Reference in New Issue
Block a user