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.
Choose an extension type
Section titled “Choose an extension type”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.
Create the package boundary
Section titled “Create the package boundary”Keep the package independent from the indx workspace:
acme-indx-capability/├── pyproject.toml└── src/ └── acme_indx/ └── provider.pyRegister 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.
Install and verify
Section titled “Install and verify”During development, overlay the editable extension onto the same environment as indx:
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.
Try a working extension
Section titled “Try a working extension”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.
uv run --with-editable examples/acme-indx-capability \ python examples/04_extend.pyRun 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.
Configuration
Section titled “Configuration”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().dpiConstruct 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.
Contract checklist
Section titled “Contract checklist”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 actionableunavailable_reason. - Keep metadata JSON-compatible and credentials, private endpoints, and secrets out of descriptors.
- Claim an
INDX_<VENDOR>_*namespace and declare its variables as anIndxSettingssubclass, read per call. - Test descriptor/create agreement, every requested page, explicit failures, and entry-point discovery.
- Exercise
capabilities,plan,encode, andembedthrough an installed distribution, not only by constructing the provider directly.