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:
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.