Skip to content

Writing your own plug-in

Built and verified

Two ways to make a plug-in available are built and tested: binding by class path (through the Container) and registering a named kind through the coniti.plugins entry point (see “Register a named kind” below). The Embedder, Structure, Strategy and MemoryBackend base classes are all built; for memory backends see memory-backends.

A plug-in lives in your own package. You do not edit coniti. Either bind it by class path, or register a short name for it.

1. Write the class

Subclass the ABC and give it a params model. Params models are frozen and reject unknown fields.

# my_pkg/embedders.py
from coniti import Embedder, EmbedOutput, Params
from coniti.embedding import EmbedInput


class TinyParams(Params):
    dim: int = 16


class TinyEmbedder(Embedder):
    params_model = TinyParams

    def __init__(self, params: TinyParams | None = None, **deps) -> None:
        super().__init__(params, **deps)
        self._dim = (params or TinyParams()).dim

    @property
    def dim(self) -> int:
        return self._dim

    def _embed(self, inp: EmbedInput) -> EmbedOutput:
        ...  # return EmbedOutput(vector=<unit vector of length dim>)

The public embed checks your input and output contract and the vector length.

2. Bind it by class path

[embedding]
kind = "my_pkg.embedders:TinyEmbedder"
params = { dim = 64 }

or in code:

from coniti import Container, ContinuityEngine, Embedder, use

c = Container()
with use(c), c.override(Embedder, {"kind": "my_pkg.embedders:TinyEmbedder", "params": {"dim": 64}}):
    engine = ContinuityEngine()

or pass an instance directly, which wins over every configured layer:

from my_pkg.embedders import TinyEmbedder, TinyParams

engine = ContinuityEngine(embedder=TinyEmbedder(TinyParams(dim=64)))

The class is imported, checked to subclass the interface, and its params validated.

Register a named kind (entry point)

To let people write kind = "tiny" instead of a class path, register the class and point the coniti.plugins entry-point group at the module that does it:

# my_pkg/coniti_plugins.py
from coniti import Params, register
from my_pkg.backend import MyBackend

register("memory", "my_store", Params)(MyBackend)  # or your own Params subclass
# your package's pyproject.toml
[project.entry-points."coniti.plugins"]
my_plugins = "my_pkg.coniti_plugins"

The default registry loads every entry point in that group the first time an unknown kind is looked up or kinds are listed. coniti’s own implementations are registered the same way (builtin = "coniti.impl"), so the framework never imports them directly and your package is treated exactly like coniti’s. A plug-in that fails to import does not break other lookups: the failure is recorded in default_registry.plugin_errors and mentioned in the unknown-kind error.

Needing another part

Declare it with Depends, so you never construct it yourself:

from coniti import Depends, MemoryBackend, Strategy, inject

class MyStrategy(Strategy):
    @inject
    def __init__(self, params=None, memory: MemoryBackend = Depends(MemoryBackend)):
        ...

Extending a contract

To return more than the base output, subclass the output model and set it on your class (see contracts). Added input fields need defaults.

Trusted code only

Class paths execute code on import. Only bind classes you trust.