Skip to content

Specs and ids

Specs and registry: built and verified. Implementations: built.

Specs and the registry are built and tested. The spec classes, Spec, Params and register are exported from coniti; the rest (load_spec, spec_class_for, create, build, the registry itself) live in the submodules coniti.specs and coniti.registry. The base classes that registered implementations must subclass (Embedder, Structure, Strategy, MemoryBackend), the Container and the built-in kinds (in_memory, local_hash, random_projection, flat, segment_tree, default) are built. Examples below register their own kinds.

A spec is metadata that selects and configures an implementation. Four exist: EmbeddingSpec, StructureSpec, StrategySpec and MemorySpec, all subclasses of Spec in coniti.specs. Each is frozen and has only two fields:

Field Meaning
kind A registered name, or a class path "package.module:ClassName"
params Validated against the params model for that kind; defaults filled in; unknown fields rejected

spec_class_for(category) returns the spec class for "embedding", "structure", "strategy" or "memory".

Kinds: registered names and class paths

A kind is one of:

  • A registered name. Registered with a params model; names may not contain :.
  • A class path such as "my_pkg.my_module:MyClass". The class is imported on demand, with no registration. It must subclass the category’s base class, and its params_model attribute (default Params) validates the params.

Class paths run code when imported, so treat configuration files that contain them as trusted input.

Params

Params are normalised against the implementation’s params model (a subclass of coniti.registry.Params, which is frozen and forbids unknown fields). Defaults are filled in, and unknown fields are rejected. After construction params are deep-frozen: mappings are read-only and lists become tuples, so a spec cannot change after its id is computed. NaN and infinity anywhere in params are rejected, because they cannot be represented faithfully in JSON.

Stable ids

A spec’s id (for example emb-1a2b3c4d5e6f) is <prefix>-<12 hex digits>, a hash of its canonical content: category, kind and normalised params. The prefixes are emb, stc, stg and mem. Two specs with the same content have the same id, regardless of key order or whether defaults are written out. Results from experiments can therefore be keyed by id.

from coniti import Embedder, EmbeddingSpec, EmbedOutput, Params, register
from coniti.embedding import EmbedInput

class HashParams(Params):
    dim: int = 64
    seed: int = 0

@register("embedding", "demo_hash", HashParams)
class DemoHash(Embedder):
    params_model = HashParams

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

    def _embed(self, inp: EmbedInput) -> EmbedOutput:
        raise NotImplementedError   # a spec-id demo; see the plugins guide for a real one

a = EmbeddingSpec(kind="demo_hash", params={"dim": 64})
b = EmbeddingSpec(kind="demo_hash", params={"seed": 0, "dim": 64})  # default written out
assert a.id == b.id               # same content, same id
assert a == b

c = a.with_params(dim=128)        # a new variant spec; a is unchanged
assert c.id != a.id
a.to_dict()                       # {'kind': 'demo_hash', 'params': {'dim': 64, 'seed': 0}}

An invalid spec fails at construction with a pydantic ValidationError (unknown param, non-finite number). An unknown kind fails with UnknownKindError.

Registry

coniti.registry binds each category to its base class (define_category(category, base)) and registers kinds with @register(category, kind, params_model). Registering a class that does not subclass the category base fails, as does registering the same kind twice unless replace=True.

Every implementation, registered or class-path, is built as factory(params, **deps). The result must be an instance of the category base, otherwise ContractError is raised. create(category, kind, params, **deps) builds from parts and build(spec, **deps) builds from a spec.

Error When
UnknownKindError A kind is not registered. The message lists the registered kinds.
ClassPathError A class path is malformed, cannot be imported (including a module that raises while importing), names no class, or names a class that does not subclass the category base. A subclass of UnknownKindError.
ContractError A factory built an object that is not an instance of the category base.

Both lookup errors are LookupErrors, deliberately not ValueErrors: pydantic would re-wrap a ValueError raised during validation into a generic ValidationError and hide the type.

Loading specs

load_spec(source, spec_cls) builds a spec from:

  • a mapping,
  • JSON text, or YAML text (YAML needs the yaml extra),
  • a pathlib.Path to a .toml, .json, .yaml or .yml file, chosen by suffix.

A plain str is always treated as text, never as a path; pass a Path to read a file. load_mapping(source) does the same for a bare mapping.

from pathlib import Path
from coniti import EmbeddingSpec
from coniti.specs import load_spec

spec = load_spec('{"kind": "demo_hash", "params": {"dim": 32}}', EmbeddingSpec)
spec = load_spec(Path("embedding.toml"), EmbeddingSpec)   # reads a file