170 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			170 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 
								 | 
							
								# 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]
							 |