_core.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. # pylint: disable=too-few-public-methods, missing-module-docstring
  3. from __future__ import annotations
  4. import abc
  5. import importlib
  6. import logging
  7. import pathlib
  8. import warnings
  9. from dataclasses import dataclass
  10. from searx.utils import load_module
  11. from searx.result_types.answer import BaseAnswer
  12. _default = pathlib.Path(__file__).parent
  13. log: logging.Logger = logging.getLogger("searx.answerers")
  14. @dataclass
  15. class AnswererInfo:
  16. """Object that holds informations about an answerer, these infos are shown
  17. to the user in the Preferences menu.
  18. To be able to translate the information into other languages, the text must
  19. be written in English and translated with :py:obj:`flask_babel.gettext`.
  20. """
  21. name: str
  22. """Name of the *answerer*."""
  23. description: str
  24. """Short description of the *answerer*."""
  25. examples: list[str]
  26. """List of short examples of the usage / of query terms."""
  27. keywords: list[str]
  28. """See :py:obj:`Answerer.keywords`"""
  29. class Answerer(abc.ABC):
  30. """Abstract base class of answerers."""
  31. keywords: list[str]
  32. """Keywords to which the answerer has *answers*."""
  33. @abc.abstractmethod
  34. def answer(self, query: str) -> list[BaseAnswer]:
  35. """Function that returns a list of answers to the question/query."""
  36. @abc.abstractmethod
  37. def info(self) -> AnswererInfo:
  38. """Informations about the *answerer*, see :py:obj:`AnswererInfo`."""
  39. class ModuleAnswerer(Answerer):
  40. """A wrapper class for legacy *answerers* where the names (keywords, answer,
  41. info) are implemented on the module level (not in a class).
  42. .. note::
  43. For internal use only!
  44. """
  45. def __init__(self, mod):
  46. for name in ["keywords", "self_info", "answer"]:
  47. if not getattr(mod, name, None):
  48. raise SystemExit(2)
  49. if not isinstance(mod.keywords, tuple):
  50. raise SystemExit(2)
  51. self.module = mod
  52. self.keywords = mod.keywords # type: ignore
  53. def answer(self, query: str) -> list[BaseAnswer]:
  54. return self.module.answer(query)
  55. def info(self) -> AnswererInfo:
  56. kwargs = self.module.self_info()
  57. kwargs["keywords"] = self.keywords
  58. return AnswererInfo(**kwargs)
  59. class AnswerStorage(dict):
  60. """A storage for managing the *answerers* of SearXNG. With the
  61. :py:obj:`AnswerStorage.ask`” method, a caller can ask questions to all
  62. *answerers* and receives a list of the results."""
  63. answerer_list: set[Answerer]
  64. """The list of :py:obj:`Answerer` in this storage."""
  65. def __init__(self):
  66. super().__init__()
  67. self.answerer_list = set()
  68. def load_builtins(self):
  69. """Loads ``answerer.py`` modules from the python packages in
  70. :origin:`searx/answerers`. The python modules are wrapped by
  71. :py:obj:`ModuleAnswerer`."""
  72. for f in _default.iterdir():
  73. if f.name.startswith("_"):
  74. continue
  75. if f.is_file() and f.suffix == ".py":
  76. self.register_by_fqn(f"searx.answerers.{f.stem}.SXNGAnswerer")
  77. continue
  78. # for backward compatibility (if a fork has additional answerers)
  79. if f.is_dir() and (f / "answerer.py").exists():
  80. warnings.warn(
  81. f"answerer module {f} is deprecated / migrate to searx.answerers.Answerer", DeprecationWarning
  82. )
  83. mod = load_module("answerer.py", str(f))
  84. self.register(ModuleAnswerer(mod))
  85. def register_by_fqn(self, fqn: str):
  86. """Register a :py:obj:`Answerer` via its fully qualified class namen(FQN)."""
  87. mod_name, _, obj_name = fqn.rpartition('.')
  88. mod = importlib.import_module(mod_name)
  89. code_obj = getattr(mod, obj_name, None)
  90. if code_obj is None:
  91. msg = f"answerer {fqn} is not implemented"
  92. log.critical(msg)
  93. raise ValueError(msg)
  94. self.register(code_obj())
  95. def register(self, answerer: Answerer):
  96. """Register a :py:obj:`Answerer`."""
  97. self.answerer_list.add(answerer)
  98. for _kw in answerer.keywords:
  99. self[_kw] = self.get(_kw, [])
  100. self[_kw].append(answerer)
  101. def ask(self, query: str) -> list[BaseAnswer]:
  102. """An answerer is identified via keywords, if there is a keyword at the
  103. first position in the ``query`` for which there is one or more
  104. answerers, then these are called, whereby the entire ``query`` is passed
  105. as argument to the answerer function."""
  106. results = []
  107. keyword = None
  108. for keyword in query.split():
  109. if keyword:
  110. break
  111. if not keyword or keyword not in self:
  112. return results
  113. for answerer in self[keyword]:
  114. for answer in answerer.answer(query):
  115. # In case of *answers* prefix ``answerer:`` is set, see searx.result_types.Result
  116. answer.engine = f"answerer: {keyword}"
  117. results.append(answer)
  118. return results
  119. @property
  120. def info(self) -> list[AnswererInfo]:
  121. return [a.info() for a in self.answerer_list]