Skip to content

Quickstart

Built and verified

Every example on this page was run against the release-candidate code (0.1.0) and the output shown is the real output.

Install first (installation). Everything on this page comes from the top-level coniti package.

Ingest and ask

With no configuration, coniti uses its defaults: in_memory memory, the local_hash embedder, a flat structure and the default strategy.

from coniti import ContinuityEngine, Note, Query, TimeRange

engine = ContinuityEngine()
notes = [
    Note(id="a", text="outlined the sync design", timestamp=1_000.0),
    Note(id="b", text="benchmarked the vector index", timestamp=2_000.0),
    Note(id="c", text="sync design review notes", timestamp=3_000.0),
    Note(id="d", text="baked an apple pie", timestamp=4_000.0),
]
print(engine.ingest_many(notes))       # how many were admitted

# By meaning
print(engine.ask(Query(text="sync design", k=3)).render())

# By anchor: what is around this note? (the note itself is left out)
print(engine.ask(Query(near="a", k=2)).render())

# By meaning, inside a time window (half-open: start <= t < end)
window = TimeRange(start=2_500.0, end=5_000.0)
print(engine.ask(Query(text="sync design", time_range=window, k=3)).render())

Output:

4
Hits (3):
  1. [0.707] outlined the sync design
  2. [0.707] sync design review notes
  3. [0.000] benchmarked the vector index
--
Hits (2):
  1. [0.500] sync design review notes
  2. [0.250] benchmarked the vector index
--
Hits (2):
  1. [0.707] sync design review notes
  2. [0.000] baked an apple pie

(The -- separators are printed by the full script; local_hash matches word overlap, not meaning, so the note about a pie scores 0.)

ask returns an Answer; render() is only one view of it. Use answer.hits[i].note and .score, answer.groups, or answer.to_json().

Two engines created like this never share memory: each gets a fresh container. One caveat: a fresh container reads ./coniti.toml if the working directory has one (configuration).

Configure other pieces, with no code change

A time-only question is answered by the segment_tree structure, which is not a default. Put this in coniti.toml; it swaps in the 64-dimension random_projection embedder over a 512-dimension local_hash, keeps flat and adds segment_tree:

[embedding]
kind = "random_projection"
params = { dim = 64, seed = 3, inner = { kind = "local_hash", params = { dim = 512 } } }

[[structures]]
kind = "flat"

[[structures]]
kind = "segment_tree"
params = { bucket_seconds = 1000.0, branching = 4, levels = 6 }

The same ingest code then runs unchanged:

engine = ContinuityEngine.from_config("coniti.toml")   # or just ContinuityEngine() in this directory
print(engine.embedder.dim, [type(s).__name__ for s in engine.structures])
engine.ingest_many(notes)                              # the four notes above

answer = engine.ask(Query(text="sync design", time_range=TimeRange(start=1_000.0, end=3_500.0), k=2))
print(answer.render())

# By time alone
print(engine.ask(Query(time_range=TimeRange(start=0.0, end=3_500.0))).render())

Output:

64 ['FlatStructure', 'SegmentTreeStructure']
Hits (2):
  1. [0.783] sync design review notes
  2. [0.697] outlined the sync design
Groups (1):
  - [1000.0,3500.0): 3 notes, 1000 .. 3000; representative: outlined the sync design
Groups (1):
  - [0.0,3500.0): 3 notes, 1000 .. 3000; representative: outlined the sync design

One ask combined flat hits and a segment_tree group. The group says how many notes fall in the range, their actual time span (1000 .. 3000, not the range bounds) and a representative: the stored note closest to the group’s centroid (engine). Timestamps for segment_tree must lie between 0 and bucket_seconds * branching**levels (structure).

To see the two embeddings that random_projection produces from one call, see embedding.

Remove, back up and move memory

Continuing with the first engine (the four notes, default configuration):

engine = ContinuityEngine()
engine.ingest_many(notes)

print(engine.remove("d"), engine.remove("d"))   # True False: "d" existed once

# Move memory to a new engine by file...
print(engine.dump("backup.jsonl.gz"))           # 3 records written
copy = ContinuityEngine()
print(copy.load_file("backup.jsonl.gz"))        # 3 records loaded, structures rebuilt

# ...or by snapshot (plain data)
copy2 = ContinuityEngine()
print(copy2.load(engine.snapshot()))            # 3

q = Query(text="sync design", k=3)
print(copy.ask(q).render() == engine.ask(q).render())

Output:

True False
3
3
3
True

Nothing is re-embedded and the copy answers exactly like the original. Loading the same snapshot again raises SnapshotError (the ids already exist) and a memory from another embedder raises EmbeddingMismatch, in both cases without writing anything. See backup-restore and engine.