Base classes and contracts¶
Built and verified
The contract machinery (Contract, Vector, Contracted, ContractError)
and serialisation are built and tested. They live in coniti.contracts;
Contract, ContractError and Vector are also exported from coniti.
The four abstract base classes below (Embedder, Structure,
Strategy, MemoryBackend) are built too.
Four abstract base classes make up the pluggable surface.
| ABC | Public operations |
|---|---|
Embedder |
embed, embed_many, dim |
Structure |
insert, query, reset, neighbors (optional), stats, rebuild |
Strategy |
admit (default: admit everything), retrieve |
MemoryBackend |
put, get, delete, scan, count, get_meta, set_meta, clear |
Instantiating a subclass that leaves an abstract method unimplemented fails.
Input and output contracts¶
Each operation takes an input base model and returns an output base model, all
deriving from a common Contract: for example EmbedInput/EmbedOutput,
InsertInput/InsertOutput, QueryInput/QueryOutput.
An implementation may extend a contract with a subclass, never replace it:
- A declared model must be the base or a subclass. An unrelated type fails at class definition with
ContractError. - A returned value that is not an instance of the declared model fails the call with
ContractError. - The engine reads base fields only, and keeps the full output so other plug-ins can read the extras.
- Fields added to an input model need defaults, because the engine builds inputs from base fields alone.
What is built¶
All names live in coniti.contracts.
| Name | Behaviour |
|---|---|
Contract |
Pydantic base model for every input and output. Extra (undeclared) fields are forbidden. |
Vector |
Field type for vectors, see below. |
as_vector(value) |
The function behind Vector: returns a validated float64 array. |
Contracted |
Mixin for base classes. A base sets _contracts = {"slot": BaseModel}; each subclass’s slot attribute must be that model or a subclass, checked at class definition. Contracted._check(slot, value) raises ContractError if value is not an instance of the slot’s model. |
ContractError |
A TypeError raised on any contract violation. |
Vectors¶
A Vector accepts a non-empty, finite, 1-D sequence or array of ints or floats.
It rejects strings, booleans, complex numbers, arbitrary objects, NaN or
infinity, empty input and multi-dimensional input. The value is stored as a
float64 numpy array that is a copy of the input, so changing the original
afterwards does not change the model. In JSON it is serialised as a list of
floats.
import numpy as np
from coniti import Contract, ContractError, Vector
from coniti.contracts import Contracted
class Point(Contract):
vector: Vector
p = Point(vector=[1, 2, 3])
p.vector # array([1., 2., 3.]), float64
p.model_dump(mode="json") # {'vector': [1.0, 2.0, 3.0]}
# Point(vector=[]) # rejected: empty
# Point(vector=[1.0, np.nan]) # rejected: not finite
# Point(vector=[[1, 2]]) # rejected: not 1-D
# Point(vector="abc") # rejected: not numeric
class Extended(Point): # extending a contract is allowed
note: str = ""
class Base(Contracted):
_contracts = {"output_model": Point}
output_model = Point
class Good(Base):
output_model = Extended # a subclass of Point: accepted
# class Bad(Base):
# output_model = int # raises ContractError at class definition
Good._check("output_model", Extended(vector=[1.0])) # passes
# Good._check("output_model", Point(vector=[1.0])) # ContractError: not an Extended
The built Embedder uses the same mechanism. A subclass of DenseEmbedding
(the default output) with a second vector is accepted and its extras are kept (a
sketch; _embed omitted):
from coniti import DenseEmbedding, Embedder, Vector
class ProjectedOutput(DenseEmbedding):
source_vector: Vector # a second, higher-dimensional embedding
class MyEmbedder(Embedder):
output_model = ProjectedOutput # a subclass of the default output: accepted
No experiment knobs in the base
Thresholds, decay, temperature and similar factors never appear on a base class, base spec or base contract. Each plug-in declares its own in its params model.
Serialisation¶
Built and verified
Requirement 15, task 4D. Available on every contract model and spec.
Every contract model and every spec provides these methods, defined once on the
shared bases (Contract, Spec), so every subclass, including plug-in
extensions, inherits them:
| Method | Behaviour |
|---|---|
to_dict() |
A plain dict of JSON-compatible values (vectors become lists of floats). |
Model.from_dict(data) |
Builds and validates a model, exactly like construction. |
to_json(indent=None) |
A JSON string. |
Model.from_json(text) |
Parses a str or bytes and validates. |
Model.json_schema() |
The JSON Schema of the model. |
The serialised form follows common standards so other tools can read it without coniti:
- JSON as defined by RFC 8259, encoded as UTF-8; NaN and infinity are never written or accepted.
- Schemas as JSON Schema draft 2020-12 (produced by pydantic; the
$schemakey says so). - Vectors as arrays of numbers.
- Timestamps stay numeric (seconds since the Unix epoch), so they round-trip exactly. Where a human-readable form is offered (the
created_atof the memory file) it is RFC 3339 with an explicit UTC offset. from_x(to_x(m))gives a model equal tom; invalid input raises pydantic’sValidationError(aValueError).
from coniti import EmbeddingSpec, Note
note = Note(id="n1", text="planning the october product launch", timestamp=100.0)
text = note.to_json()
# '{"id": "n1", "text": "planning the october product launch", "timestamp": 100.0, "metadata": {}}'
Note.from_json(text) == note # True
Note.from_dict(note.to_dict()) == note # True
Note.json_schema()["$schema"] # 'https://json-schema.org/draft/2020-12/schema'
spec = EmbeddingSpec(kind="local_hash", params={"dim": 64})
spec.to_json() # '{"kind": "local_hash", "params": {"dim": 64, "seed": 0}}'
EmbeddingSpec.from_json(spec.to_json()) == spec # True, and the spec id is the same
Embeddings serialise as well. embedder.embed(text).to_dict() includes
"space": {"embedding_id": ..., "dim": ...} next to the vector, and
DenseEmbedding.from_json(e.to_json()) == e. The abstract EmbedOutput cannot
be built from data (TypeError); use a concrete kind.
Runtime-only fields. A field that holds live objects can be declared
runtime-only (excluded from dumps and hidden from the schema): it is left out of to_dict / to_json and of the JSON Schema, and
is None after deserialising. The context of the strategy inputs is the
example, see strategy. Code that needs such a field raises ContractError
if it is missing.
Coverage. A JSON Schema can be produced for every contract model in coniti; a test walks all of them to prove it. These methods are on the base contract layer only and add no runtime dependency. The portable memory file built on them is described in memory.