Source code for smarter.apps.plugin.manifest.models.image_search_plugin.spec

"""
Smarter API Manifest - ImageSearchPlugin.spec.

An ImageSearchPlugin gives the LLM an image search, powered by the
`Brave Image Search API <https://api-dashboard.search.brave.com/app/documentation/image-search/get-started>`__.
Its tool returns a JSON list of strings: the https urls of the images that it found.

The LLM supplies the search query, e.g. ``formula 1 race cars``, in each tool call.
``spec.imageSearchData.queryTerms``, if set, are appended to it, e.g. ``black and white
photograph``, so that Brave searches for ``formula 1 race cars black and white photograph``.
``spec.imageSearchData.searchParams`` map 1:1 to the Brave Image Search API's other query
parameters. ``spec.imageSearchData.filters`` are applied by Smarter to Brave's results, for
what the API itself cannot filter: file type, minimum size, and the websites that images
come from.

.. code-block:: yaml

    imageSearchData:
      apiKey: brave_search_api_key   # the name of a Smarter Secret
      queryTerms: null               # terms appended to every query that the LLM sends
      llmSearchParams: false         # whether the LLM may set every search parameter in each call
      searchParams:
        count: 10                    # 1 to 20
        country: us                  # a two letter country code, or all
        searchLang: en               # a language code, e.g. en, pt-br
        safesearch: strict           # strict or off
        spellcheck: true
      filters:
        fileType: jpg|png            # any of jpg, png, gif, svg, webp, avif, bmp, ico
        minWidth: 800                # pixels. Images of unknown width are skipped
        minHeight: null
        allowedDomains: []           # if not empty, only images from web pages on these domains
        blockedDomains: []           # never images on, or from web pages on, these domains
      validateUrls: true             # only return urls that respond with HTTP 200
      timeout: 10
      cacheTtl: 604800

.. note::

    **Experimental.** The ImageSearchPlugin was designed and coded by Claude Code (Anthropic's
    Claude Opus 5.5), with Lawrence McDaniel as co-author. It is experimental.
"""

import os
import re
from typing import Any, ClassVar, Literal, Optional

from pydantic import Field, field_validator

from smarter.apps.plugin.const import PluginCacheTtl
from smarter.apps.plugin.manifest.models.common.plugin.spec import SAMPluginCommonSpec
from smarter.apps.plugin.manifest.models.websearch_plugin.policy import (
    WebsearchPolicyError,
    normalize_domains,
)
from smarter.lib.manifest.exceptions import SAMValidationError
from smarter.lib.manifest.models import SmarterBasePydanticModel

from .const import (
    DEFAULT_API_KEY_SECRET_NAME,
    DEFAULT_COUNT,
    DEFAULT_TIMEOUT,
    MANIFEST_KIND,
    MAX_COUNT,
    MAX_DIMENSION,
    MAX_QUERY_LENGTH,
    MAX_TIMEOUT,
)
from .enum import ImageSearchFileType, ImageSearchSafeSearch

filename = os.path.splitext(os.path.basename(__file__))[0]
MODULE_IDENTIFIER = f"{MANIFEST_KIND}.{filename}"

COUNTRY_PATTERN = re.compile(r"^([a-z]{2}|all)$")
SEARCH_LANG_PATTERN = re.compile(r"^[a-z]{2}(-[a-z]{2,4})?$")


[docs] def normalize_file_types(value: Any) -> Optional[str]: """ Validate and normalize a ``fileType`` filter. :param value: File types separated by ``|``, e.g. ``jpg|png``. ``jpeg`` is read as ``jpg``. :return: The normalized value, e.g. ``jpg|png``, or None if the value is empty. :raises ValueError: If a file type is not permitted. """ if value is None or (isinstance(value, str) and not value.strip()): return None if not isinstance(value, str): raise ValueError(f"fileType must be a string: {value}") permitted = ImageSearchFileType.all() values = [item.strip().lower() for item in value.split("|") if item.strip()] values = ["jpg" if item == "jpeg" else item for item in values] for item in values: if item not in permitted: raise ValueError(f"fileType must be any of {permitted}, separated by '|': {item}") return "|".join(dict.fromkeys(values)) or None
[docs] def normalize_country(value: Any) -> Optional[str]: """ Validate and normalize a ``country`` search parameter. :param value: A two letter country code, or ``all`` for worldwide results. :return: The lower case value, or None if the value is empty. :raises ValueError: If the value is not a two letter country code, nor ``all``. """ if value is None or not str(value).strip(): return None value = str(value).strip().lower() if not COUNTRY_PATTERN.match(value): raise ValueError(f"country must be a two letter country code, or all: {value}") return value
[docs] def normalize_search_lang(value: Any) -> Optional[str]: """ Validate and normalize a ``searchLang`` search parameter. :param value: A language code, e.g. ``en`` or ``pt-br``. :return: The lower case value, or None if the value is empty. :raises ValueError: If the value is not a language code. """ if value is None or not str(value).strip(): return None value = str(value).strip().lower() if not SEARCH_LANG_PATTERN.match(value): raise ValueError(f"searchLang must be a language code, e.g. en or pt-br: {value}") return value
[docs] class ImageSearchParams(SmarterBasePydanticModel): """ Smarter API - ImageSearchPlugin.spec.imageSearchData.searchParams. These map 1:1 to the query parameters of the Brave Image Search API, except for ``q``, the query itself, which the LLM supplies. See ``imageSearchData.queryTerms``. """ class_identifier: ClassVar[str] = f"{MODULE_IDENTIFIER}.searchParams" count: int = Field( default=DEFAULT_COUNT, ge=1, le=MAX_COUNT, description=f"{class_identifier}.count[int]: the maximum number of images per search, 1 to {MAX_COUNT}.", ) country: Optional[str] = Field( default=None, description=f"{class_identifier}.country[str]: a two letter country code, e.g. us, or all for worldwide results.", ) searchLang: Optional[str] = Field( default=None, description=f"{class_identifier}.searchLang[str]: prefer images from content in this language, e.g. en or pt-br.", ) safesearch: Literal[tuple(ImageSearchSafeSearch.all())] = Field( # type: ignore[valid-type] default=ImageSearchSafeSearch.STRICT.value, description=f"{class_identifier}.safesearch[str]: adult content filtering. One of: {ImageSearchSafeSearch.all()}", ) spellcheck: bool = Field( default=True, description=f"{class_identifier}.spellcheck[bool]: whether Brave may correct the spelling of the query.", )
[docs] @field_validator("country") @classmethod def validate_country(cls, v: Optional[str]) -> Optional[str]: """Validate the country code.""" try: return normalize_country(v) except ValueError as e: raise SAMValidationError(f"{cls.class_identifier}: {e}") from e
[docs] @field_validator("searchLang") @classmethod def validate_search_lang(cls, v: Optional[str]) -> Optional[str]: """Validate the language code.""" try: return normalize_search_lang(v) except ValueError as e: raise SAMValidationError(f"{cls.class_identifier}: {e}") from e
[docs] @field_validator("safesearch", mode="before") @classmethod def validate_safesearch(cls, v: str) -> str: """Validate the safe search level.""" v = str(v).strip().lower() if v not in ImageSearchSafeSearch.all(): raise SAMValidationError( f"{cls.class_identifier}.safesearch must be one of {ImageSearchSafeSearch.all()}: {v}" ) return v
[docs] class ImageSearchFilters(SmarterBasePydanticModel): """ Smarter API - ImageSearchPlugin.spec.imageSearchData.filters. Smarter applies these to the Brave Image Search API's results. """ class_identifier: ClassVar[str] = f"{MODULE_IDENTIFIER}.filters" fileType: Optional[str] = Field( default=None, description=( f"{class_identifier}.fileType[str]: only images of these file types, separated by '|', e.g. jpg|png. " f"Any of: {ImageSearchFileType.all()}" ), ) minWidth: Optional[int] = Field( default=None, ge=1, le=MAX_DIMENSION, description=( f"{class_identifier}.minWidth[int]: only images at least this many pixels wide. Images whose width " "Brave does not report are skipped." ), ) minHeight: Optional[int] = Field( default=None, ge=1, le=MAX_DIMENSION, description=( f"{class_identifier}.minHeight[int]: only images at least this many pixels high. Images whose height " "Brave does not report are skipped." ), ) allowedDomains: Optional[list[str]] = Field( default=None, description=( f"{class_identifier}.allowedDomains[list]: if not empty, only images from web pages on these domains, " "and their subdomains." ), ) blockedDomains: Optional[list[str]] = Field( default=None, description=( f"{class_identifier}.blockedDomains[list]: never images on these domains, nor from web pages on them, " "including their subdomains." ), )
[docs] @field_validator("fileType") @classmethod def validate_file_type(cls, v: Optional[str]) -> Optional[str]: """Validate the file types.""" try: return normalize_file_types(v) except ValueError as e: raise SAMValidationError(f"{cls.class_identifier}: {e}") from e
[docs] @field_validator("allowedDomains", "blockedDomains") @classmethod def validate_domains(cls, v: Optional[list[str]]) -> Optional[list[str]]: """Validate and normalize domain names.""" if v is None: return None try: return list(normalize_domains(v)) except WebsearchPolicyError as e: raise SAMValidationError(f"{cls.class_identifier}: {e}") from e
[docs] class ImageSearchData(SmarterBasePydanticModel): """Smarter API - ImageSearchPlugin.spec.imageSearchData.""" class_identifier: ClassVar[str] = MODULE_IDENTIFIER apiKey: str = Field( default=DEFAULT_API_KEY_SECRET_NAME, description=( f"{class_identifier}.apiKey[str]: the name of the Smarter Secret that contains a Brave Search API key. " "The api key itself is never included in the manifest." ), ) queryTerms: Optional[str] = Field( default=None, max_length=MAX_QUERY_LENGTH, description=( f"{class_identifier}.queryTerms[str]: optional search terms appended to every query that the LLM " "sends, e.g. 'black and white photograph'. The LLM always supplies the query itself, e.g. 'formula 1 " "race cars', so that Brave searches for 'formula 1 race cars black and white photograph'." ), ) llmSearchParams: bool = Field( default=False, description=( f"{class_identifier}.llmSearchParams[bool]: whether the LLM may set every Brave Image Search API " f"parameter in each tool call: count (up to {MAX_COUNT}), country, search_lang, safesearch and " "spellcheck. searchParams are then the defaults. If false, the LLM may only lower the count." ), ) searchParams: ImageSearchParams = Field( default_factory=ImageSearchParams, description=f"{class_identifier}.searchParams[obj]: the Brave Image Search API query parameters.", ) filters: ImageSearchFilters = Field( default_factory=ImageSearchFilters, description=f"{class_identifier}.filters[obj]: the filters that Smarter applies to the search results.", ) validateUrls: bool = Field( default=True, description=( f"{class_identifier}.validateUrls[bool]: only return image urls that respond to a request with " "HTTP 200, and not with a web page. Urls are always https." ), ) timeout: int = Field( default=DEFAULT_TIMEOUT, ge=1, le=MAX_TIMEOUT, description=f"{class_identifier}.timeout[int]: seconds to wait for each web request, 1 to {MAX_TIMEOUT}.", ) cacheTtl: int = Field( default=PluginCacheTtl.IMAGE_SEARCH, ge=0, le=PluginCacheTtl.IMAGE_SEARCH, description=f"{class_identifier}.cacheTtl[int]: seconds to cache search results. 0 disables caching.", )
[docs] @field_validator("queryTerms") @classmethod def validate_query_terms(cls, v: Optional[str]) -> Optional[str]: """Normalize the query terms.""" if v is None: return None v = " ".join(str(v).split()) return v or None
[docs] @field_validator("apiKey") @classmethod def validate_api_key(cls, v: str) -> str: """Validate that the api key is the name of a Secret, not empty.""" if not isinstance(v, str) or not v.strip(): raise SAMValidationError(f"{cls.class_identifier}.apiKey must be the name of a Smarter Secret.") return v.strip()
[docs] class SAMImageSearchPluginSpec(SAMPluginCommonSpec): """Smarter API Manifest - ImageSearchPlugin.spec.""" class_identifier: ClassVar[str] = MODULE_IDENTIFIER imageSearchData: ImageSearchData = Field( default_factory=ImageSearchData, description=f"{class_identifier}.imageSearchData[obj]: the image search configuration of the {MANIFEST_KIND}.", )