Skip to content

Extend indx

Add your own readers, specialist parsers, embedding spaces, observers, loaders, chunkers, language detectors, classifiers, and entity extractors as installable Python capabilities.

An indx extension is an ordinary Python distribution. Installing it is the enable switch: indx discovers its provider through a standard Python entry point, adds its lightweight declarations to the capability snapshot, and constructs the implementation only when a plan or execution needs it.

your package → indx.capabilities entry point → CapabilityProvider
  → capability snapshot → planner → executor → blocks or vectors

The router and executor never import your package directly. Your code depends on the stable contracts in indx-interfaces, not on either implementation.

Each type has its own page with a worked implementation. What “selected” means — the routing ladder, nomination, and the other doors into a route — is explained in Routing.

Add Implement How it is selected
A reader, OCR engine, or vision model CapabilityProvider + PageReader Its descriptor kind places it in the generic routing ladder.
A specialist parser CapabilityProvider + PageReader + SignatureDetector Cheap opt-in detection nominates it ahead of the generic ladder.
An embedding space CapabilityProvider + EmbeddingSpaceProvider + VectorEncoder The caller names the space for document or query embedding.
A media type indx cannot observe CapabilityProvider + SourceObserverProvider + SourceObserver Installing it is what makes that media type plannable at all.
A URI scheme indx cannot resolve CapabilityProvider + SourceLoaderProvider + SourceLoader Installing it is what makes that scheme fetchable at all.
A different cut of the embeddable unit CapabilityProvider + ChunkerProvider + Chunker Chunk answers are merged per page after reading; first in chunker order wins.
Naming the language a page is written in CapabilityProvider + LanguageDetectorProvider + LanguageDetector Asked with text a reader produced, not with bytes; first detector with an opinion wins a page.
Saying what a text is CapabilityProvider + DocumentClassifierProvider / PageClassifierProvider / ChunkClassifierProvider Runs only when a request names its ID; asked with the full text of one document, page or chunk, and the first enabled classifier with an opinion wins a facet.
Finding the names in a text CapabilityProvider + PageEntityExtractorProvider / ChunkEntityExtractorProvider Runs only when a request names its ID; asked with the text of one page or chunk, and returns spans into it.
Summarising and tagging a text CapabilityProvider + DocumentEnricherProvider / PageEnricherProvider / ChunkEnricherProvider Runs only when a request names its ID; asked with the text of one document, page or chunk, and returns a summary, tags, or both.

Protocols use structural typing. Your classes need the required method signatures; they do not inherit from indx base classes. A provider that declares no capability of its own may subclass indx_interfaces.Plugin, which supplies the empty descriptors() and the refusing create(); subclassing it declares and registers nothing, and the entry point stays what discovery reads.

Keep the package independent from the indx workspace:

acme-indx-capability/
├── pyproject.toml
└── src/
└── acme_indx/
└── provider.py

Register one provider in pyproject.toml:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "acme-indx-capability"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["indx-interfaces>=0.1,<0.2"]
[project.entry-points."indx.capabilities"]
acme = "acme_indx.provider:Provider"
[tool.hatch.build.targets.wheel]
packages = ["src/acme_indx"]

Use the indx-interfaces release line used by the target indx deployment. Add parser, model, or client libraries to this package, not to indx itself.

One entry-point group carries every extension type: a single provider may declare capabilities through descriptors(), and optionally embedding spaces, source observers, source loaders, chunkers, language detectors, classifiers (document_classifiers(), page_classifiers(), chunk_classifiers()) and entity extractors (page_entity_extractors(), chunk_entity_extractors()) through the hooks each page documents.

During development, overlay the editable extension onto the same environment as indx:

Terminal window
uv run --with-editable ../acme-indx-capability \
indx capabilities
uv run --with-editable ../acme-indx-capability \
indx plan --signatures file://$PWD/tests/fixtures/invoice-anthropic.pdf
uv run --with-editable ../acme-indx-capability \
indx encode --signatures file://$PWD/tests/fixtures/invoice-anthropic.pdf
uv run --with-editable ../acme-indx-capability \
indx embed --space acme-text "amount due"

capabilities must show the new IDs and availability before a route can name them. A new installation creates a new content-addressed capability snapshot; plans made against the previous inventory are intentionally not interchangeable with it.

This repository ships one. examples/acme-indx-capability implements the extension types above as real code: a page reader with its source observer, a purchase-order parser, and an embedding space. It depends on nothing beyond indx-interfaces.

Terminal window
uv run --with-editable examples/acme-indx-capability \
python examples/04_extend.py

Run it without the overlay first and it tells you it is not installed, because nothing in indx imports it by name. With the overlay, all three capabilities appear in the snapshot. The parser recognizes a purchase order. indx embed --space acme-text reaches the new space. And an acme:// source plans, even though a stock install can neither resolve that scheme, nor read or observe its media type.

A distribution that needs settings claims a namespace and declares them, and both halves are a convention rather than a port: configuration in indx is environment variables, and a config port would be a second way to do the same thing.

Claim INDX_<VENDOR>_*. First-party lanes hold INDX_VLM_*, INDX_OCR_*, INDX_EMBED_*, INDX_CHUNK_*, INDX_TEXT_*, INDX_ROUTING_*, INDX_VALIDATION_* and INDX_LOADER_<SCHEME>_*. Yours is your own name, so INDX_ACME_API_KEY collides with nothing indx will ever ship.

Declare them as an IndxSettings subclass in a settings.py, not as os.environ.get plus a parse plus a bound check, and not tucked into the module that happens to read them. Every distribution in this workspace has exactly one settings.py and it is the only place its INDX_* names appear, so an operator asking what a distribution can be configured with opens one file. The names, defaults and constraints then live in one place, a value that cannot be used is refused before anything is built with it, and the message names the variable and never the value — which is what keeps a mistyped key out of a 422 a caller reads.

from pydantic import Field
from indx import IndxSettings, SettingsConfigDict
class AcmeSettings(IndxSettings):
model_config = SettingsConfigDict(env_prefix="INDX_ACME_")
api_key: str | None = None
dpi: int = Field(default=150, gt=0)
def dpi() -> int:
return AcmeSettings.load().dpi

Construct it per call and never cache it, the rule every INDX_* reader in this repository follows: an operator who retuned a variable should not have to restart the process to be believed. An exported-but-blank variable means the default. load() is what turns a validation failure into a ValueError naming the variable.

Say what you are missing in unavailable_reason. It already carries install the 'ocr' extra; missing: rapidocr, and no vision model configured; set INDX_VLM_MODEL is the same kind of actionable sentence — so a capability that is installed but unconfigured is an unavailable descriptor the ladder descends past, not a failure at read time. Name the variable there; never the value, and never a credential.

Each extension page adds its own checks; these hold for every provider.

  • Keep capability and space IDs globally unique and stable across releases.
  • Make descriptors() cheap; report unavailable implementations with an actionable unavailable_reason.
  • Keep metadata JSON-compatible and credentials, private endpoints, and secrets out of descriptors.
  • Claim an INDX_<VENDOR>_* namespace and declare its variables as an IndxSettings subclass, read per call.
  • Test descriptor/create agreement, every requested page, explicit failures, and entry-point discovery.
  • Exercise capabilities, plan, encode, and embed through an installed distribution, not only by constructing the provider directly.