Skip to content

Commit cba1b7c

Browse files
committed
feat(memory): add livekit-memory package for sub-10ms in-process semantic memory
1 parent 059815f commit cba1b7c

14 files changed

Lines changed: 2391 additions & 30 deletions

File tree

examples/memory.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Semantic memory for an agent: pin fixed facts, store memories, retrieve fast.
2+
3+
Run with the recommended extras for the real static embedder + ANN index:
4+
5+
uv run --with model2vec --with usearch python examples/memory.py
6+
7+
Without those extras it falls back to the dependency-free HashingEmbedder (lexical
8+
only) and the exact brute-force index — still fully functional, just less semantic.
9+
"""
10+
11+
from livekit.memory import MemoryStore
12+
13+
try:
14+
from livekit.memory import Model2VecEmbedder
15+
16+
embedder = Model2VecEmbedder() # static, ~0.03ms/query, no transformer forward pass
17+
except ImportError:
18+
embedder = None # -> HashingEmbedder default
19+
print("(model2vec not installed; using the dependency-free HashingEmbedder)\n")
20+
21+
22+
def main() -> None:
23+
# One store per user/session. `expected_size` lets `auto` pick the HNSW backend.
24+
store = MemoryStore(embedder=embedder, backend="auto", expected_size=1_000_000)
25+
26+
# "Fixed" facts about the user — pinned, always available to context().
27+
store.upsert("name", "The user's name is Ada Lovelace.")
28+
store.upsert("units", "The user prefers metric units and a 24-hour clock.")
29+
30+
# Free-form semantic memories accumulated over the conversation.
31+
store.add("We talked about the analytical engine and Bernoulli numbers.")
32+
store.add("The user is planning a trip to Turin next spring.")
33+
store.add("The user dislikes phone calls and prefers async messages.")
34+
35+
# The latency-critical call an agent makes each turn: one prompt-ready string.
36+
print("=== context() for 'what should I call them and any travel plans?' ===")
37+
print(store.context("what should I call them and any travel plans?", limit=3))
38+
39+
print("\n=== search() ranked hits for 'communication preferences' ===")
40+
for hit in store.search("how does the user like to communicate?", limit=3):
41+
print(f" {hit.score:+.3f} {hit.text}")
42+
43+
44+
if __name__ == "__main__":
45+
main()

livekit-memory/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# LiveKit Memory
2+
3+
In-process, in-memory **semantic memory** for LiveKit agents — sub-10ms end-to-end
4+
retrieval (embed the query *and* search) of a user's fixed context, fully self-hosted.
5+
6+
```shell
7+
pip install livekit-memory[recommended] # static embedder + ANN index
8+
```
9+
10+
## Why it's fast
11+
12+
The hard constraint for a voice agent loop is the *end-to-end* budget: you hand it text,
13+
it must embed and search in under 10ms. A transformer embedding on CPU alone is ~10ms
14+
(p99 ~50ms) and blows that. The route here:
15+
16+
- **Static embeddings (Model2Vec)** — token-lookup + mean-pool, no transformer forward
17+
pass. ~0.03ms per short query on CPU.
18+
- **In-memory index** — exact brute-force cosine (sub-ms to ~100k vectors), automatically
19+
upgrading to a [usearch](https://github.com/unum-cloud/usearch) HNSW graph for large
20+
per-user corpora (~0.27ms search at 1M vectors).
21+
22+
Measured on an Apple M4 Pro: **0.17ms median / 0.31ms p99 end-to-end at 1M vectors**
23+
~30× under budget.
24+
25+
## Usage
26+
27+
```python
28+
from livekit.memory import MemoryStore, Model2VecEmbedder
29+
30+
# one store per user / session
31+
store = MemoryStore(embedder=Model2VecEmbedder(), backend="auto", expected_size=1_000_000)
32+
33+
# "fixed" facts about the user — pinned, always available
34+
store.upsert("name", "The user's name is Ada Lovelace.")
35+
store.upsert("units", "Prefers metric units.")
36+
37+
# free-form semantic memories — ranked by relevance
38+
store.add("Discussed the analytical engine and Bernoulli numbers.", metadata={"session": 42})
39+
40+
# the latency-critical call your agent makes each turn:
41+
context = store.context("what should I call them, and what did we talk about?")
42+
# -> a prompt-ready string with the pinned facts + top relevant memories
43+
44+
# or rank directly:
45+
for hit in store.search("mathematics history", limit=5):
46+
print(hit.score, hit.text)
47+
```
48+
49+
### Bring your own embedder
50+
51+
`embedder` accepts an `Embedder`, any batched `list[str] -> vectors` callable (pass
52+
`dims=`), or `None` for the dependency-free `HashingEmbedder` (deterministic, no model
53+
download — good for tests/offline). For higher recall at the cost of latency, an ONNX
54+
transformer embedder can be wrapped via `CallableEmbedder`.
55+
56+
### Persistence
57+
58+
`store.save(dir)` / `MemoryStore.load(dir, embedder=...)` snapshot items and both indices
59+
to disk (the same embedder must be supplied on load).
60+
61+
## Backends
62+
63+
| Backend | When | Latency (384d) |
64+
|---|---|---|
65+
| `bruteforce` (default) | ≲100k vectors / user | exact, ~1.5ms @ 100k |
66+
| `usearch` | ≳100k vectors / user | HNSW, ~0.27ms @ 1M |
67+
| `auto` | picks per `expected_size` ||
68+
69+
`usearch` is an optional dependency (`pip install livekit-memory[ann]`); without it,
70+
`auto` stays on exact brute force.
71+
72+
See https://docs.livekit.io for more information.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright 2024 LiveKit, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""LiveKit Memory — in-process semantic memory for agents.
16+
17+
`pip install livekit-memory[recommended]`
18+
19+
Sub-10ms end-to-end (embed query + ANN search) retrieval of a user's fixed context,
20+
fully self-hosted. The default route is a static Model2Vec embedder (no transformer
21+
forward pass) plus an in-memory index (exact brute-force, upgrading to a usearch HNSW
22+
graph for large per-user corpora).
23+
24+
from livekit.memory import MemoryStore, Model2VecEmbedder
25+
26+
store = MemoryStore(embedder=Model2VecEmbedder())
27+
store.upsert("name", "The user's name is Ada.") # a fixed fact
28+
store.add("They prefer metric units and dark mode.") # semantic memory
29+
ctx = store.context("what should I call them?") # prompt-ready string
30+
"""
31+
32+
from ._index import ANN_CROSSOVER, BruteForceIndex, UsearchIndex, VectorIndex
33+
from ._types import DEFAULT_NAMESPACE, FACTS_NAMESPACE, MemoryItem
34+
from .embeddings import (
35+
CallableEmbedder,
36+
Embedder,
37+
EmbedFn,
38+
HashingEmbedder,
39+
Model2VecEmbedder,
40+
)
41+
from .store import MemoryStore, QueryLike
42+
from .version import __version__
43+
44+
__all__ = [
45+
"MemoryStore",
46+
"MemoryItem",
47+
"QueryLike",
48+
"Embedder",
49+
"Model2VecEmbedder",
50+
"HashingEmbedder",
51+
"CallableEmbedder",
52+
"EmbedFn",
53+
"VectorIndex",
54+
"BruteForceIndex",
55+
"UsearchIndex",
56+
"ANN_CROSSOVER",
57+
"DEFAULT_NAMESPACE",
58+
"FACTS_NAMESPACE",
59+
"__version__",
60+
]

0 commit comments

Comments
 (0)