Source code for searx.engines.exaapi

# SPDX-License-Identifier: AGPL-3.0-or-later
"""Engine to search using the official `Exa Search API`_. Exa is a search engine for AI agents.

.. _Exa Search API: https://exa.ai/docs/reference/search

Configuration
=============

The engine has the following mandatory setting:

- :py:obj:`api_key`

You can obtain an API key from the `API Key section <https://dashboard.exa.ai/api-keys>`_ in the Exa dashboard.

Optional settings are:

- :py:obj:`results_per_page`
- :py:obj:`search_type`
- :py:obj:`content_mode`
- :py:obj:`content_max_characters`

.. code:: yaml

  - name: exaapi
    engine: exaapi
    shortcut: exa
    api_key: "..."
    results_per_page: 10
    search_type: auto
    content_mode: highlights
    inactive: false

The API supports SafeSearch and region-aware results.
"""

import typing as t

from dateutil import parser

from searx.exceptions import SearxEngineAPIException
from searx.result_types import EngineResults
from searx.utils import html_to_text

if t.TYPE_CHECKING:
    from searx.extended_types import SXNG_Response
    from searx.search.processors import OnlineParams


SearchType = t.Literal["fast", "auto", "instant", "deep", "deep-lite", "deep-reasoning"]
ContentMode = t.Literal["highlights", "text"]

about = {
    "website": "https://exa.ai",
    "wikidata_id": None,
    "official_api_documentation": "https://exa.ai/docs/reference/search",
    "use_official_api": True,
    "require_api_key": True,
    "results": "JSON",
}

api_key: str = ""
"""API key for Exa Search API (required)."""

categories = ["general", "web"]
safesearch = True

base_url = "https://api.exa.ai/search"
results_per_page: int = 10
"""Maximum number of results per request. Value must be between 1 and 100, default is 10."""

search_type: SearchType = "auto"
"""Search type. Default is auto, see documentation for more information."""

content_mode: ContentMode = "highlights"
"""Content to request from the API: ``highlights`` (excerpts) or ``text`` (page text)."""

content_max_characters: int = 500
"""Maximum characters for the requested content."""


def init(_):
    if not api_key:
        raise SearxEngineAPIException("No API key provided")
    if not 1 <= results_per_page <= 100:
        raise ValueError("results_per_page must be between 1 and 100")
    if search_type not in t.get_args(SearchType):
        raise ValueError(f"Unsupported search type: {search_type}")
    if content_mode not in t.get_args(ContentMode):
        raise ValueError(f"Unsupported content mode: {content_mode}")
    if content_max_characters < 1:
        raise ValueError("content_max_characters must be at least 1")


def _contents_payload() -> dict[str, t.Any]:
    if content_mode == "text":
        return {"text": {"maxCharacters": content_max_characters, "stripLinks": True}}
    return {"highlights": {"maxCharacters": content_max_characters}}


def _extract_content(result: dict[str, t.Any]) -> str:
    if content_mode == "text":
        return html_to_text(result.get("text") or "")
    return html_to_text(" ".join(result.get("highlights") or []))


[docs] def request(query: str, params: "OnlineParams") -> None: """Create the API request.""" body: dict[str, t.Any] = { "query": query, "type": search_type, "numResults": results_per_page, "contents": _contents_payload(), } # Apply SafeSearch if enabled if params["safesearch"]: body["moderation"] = True # Apply region-aware results if specified locale_parts = params["searxng_locale"].split("-") region = locale_parts[-1] if len(locale_parts) > 1: body["userLocation"] = region.upper() params["url"] = base_url params["method"] = "POST" params["headers"]["x-api-key"] = api_key params["json"] = body
def _extract_published_date(value: str | None): """Extract and parse the published date from the API response. Args: value: Raw date string from the API Returns: Parsed datetime object or None if parsing fails """ if not value: return None try: return parser.parse(value) except (parser.ParserError, TypeError, OverflowError): return None
[docs] def response(resp: "SXNG_Response") -> EngineResults: """Process the API response and return results.""" res = EngineResults() for result in resp.json().get("results", []): url = result.get("url") if not url: continue res.add( res.types.MainResult( url=url, title=html_to_text(result.get("title") or url), content=_extract_content(result), thumbnail=result.get("image") or "", publishedDate=_extract_published_date(result.get("publishedDate")), author=result.get("author") or "", ) ) return res