123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- # 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 informations 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:
- """Informations 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]
|