Local-first memory for AI: cited answers over your own documents, 100% on-device
Project description
UPII — your memory, on your machine
Local-first memory for AI. Ingest your notes and documents, ask questions, get answers with citations — and no byte of your corpus ever leaves your device.
$ upii ask "What did we agree with ICEYE on resolution and latency?"
Answer: You agreed on 0.5 m resolution imagery with a 6-hour delivery
SLA, with latency relaxed to 12 hours during the pilot phase.
Sources:
[1] meetings/2026-03-14_iceye_sync.md (chunk a3f2c9)
[2] contracts/iceye_pilot_terms.md (chunk 77d01b)
Status: early preview. The core capture, retrieval and ambient stack works and is tested, but interfaces are still moving.
Known gaps — worth knowing before you judge the claims above:
- The relational retrieval signal is off by default. Ingestion now populates a local knowledge graph (people/orgs/projects) and
upii knowledge --graphrenders it, but on the retrieval eval, letting the relational signal re-rank is not yet net-positive (it can boost a chunk that merely mentions a query entity over a better semantic match), so its fusion weight defaults to 0. Retrieval today is effectively semantic + (recency-weighted) temporal; raise the signal per query withupii ask --w-relational. Tuning it to help is ongoing.
Why UPII
Most RAG and "second brain" tools upload your corpus to someone else's cloud and rent your own memory back to you. UPII inverts that:
- 100% local. Embeddings (sentence-transformers MiniLM), vector store (LanceDB), metadata (SQLite), and reasoning (Ollama) all run on-device. A remote LLM (Gemini) is optional and swappable — never required.
- Every answer is cited. Answers are attributed to source chunks by content-addressed ID. Retrieval is verifiable, not just plausible.
- Deterministic, reproducible memory. Chunk IDs are a pure function of content + config. Re-ingesting the same corpus reproduces 100% of chunk hashes — validated by tests and benchmarks. Citations stay stable across re-indexes; re-embedding is cache-safe.
- More than cosine similarity. The Context Rehydrator is built to fuse three signals — semantic (vectors), temporal ("what was I working on last week"), and relational (a local knowledge graph of people, projects, orgs, populated on ingest) — into one ranked context window, with per-signal contributions visible via
upii ask --debug. Measured, not asserted: Recall@10 = 0.958 on a committed labelled set (reproduce it). The relational signal is live but weight-0 by default until it's tuned to help (see Status); the score above is from semantic + temporal. - Consent-gated ambient capture. A filesystem watcher keeps memory current, but sources are explicitly enabled and ambient events pass through an approval inbox before entering durable memory.
- Graceful degradation. No GPU, no API key, no internet? Retrieval still works and the system still answers.
Quickstart
Requires Python 3.10–3.12.
pip install upii # or: pipx install upii
upii doctor # verify the local stack (db, vectors, model, disk)
upii ingest ./notes --recursive
upii search "satellite latency"
upii ask "What did we agree with ICEYE on resolution and latency?"
First run downloads the embedding model (all-MiniLM-L6-v2, ~90 MB) — the only download UPII ever needs. For local reasoning, install Ollama and ollama pull llama3.2; without it, UPII falls back to retrieval + a deterministic mock, so it always responds.
From source
git clone https://github.com/<you>/upii && cd upii
python3 -m venv venv && source venv/bin/activate # Windows: venv\Scripts\activate
pip install -e ".[dev]"
pytest tests/ -q
Usage
upii ingest ./notes --recursive # build memory from a directory (idempotent)
upii search "satellite latency" # semantic + attributed retrieval
upii ask "…" # cited answer over your corpus
upii write "follow-up" --target email # synthesize in your voice
upii watch ./inbox # ambient capture (consent-gated)
upii inbox # approve/reject ambient captures
upii knowledge --graph # render the local knowledge graph to HTML
upii sources list # manage enabled sources
upii metrics show # observability
upii doctor # health check
Run upii --help for the full command set.
How it works
graph TD
User[User / Ambient Sources] -->|ingest| IngestionEngine
User -->|ask / search| Rehydrator
subgraph "Everything runs locally"
IngestionEngine -->|hash + dedup| MetadataStore[(SQLite — source of truth)]
IngestionEngine -->|deterministic chunking| Chunker
Chunker -->|local embeddings| Embedder
Embedder -->|store vectors| VectorStore[(LanceDB)]
Rehydrator -->|semantic| VectorStore
Rehydrator -->|temporal + relational| MetadataStore
Rehydrator -->|fused context| LLM[Ollama local / optional remote]
LLM -->|attributed answer| User
end
Every write path — ingest, watch-approve, demo seed — flows through a single deterministic pipeline (src/upii/ingestion/pipeline.py):
- Identity is content-addressed.
doc_id = hash(content),chunk_id = hash(chunk_text, config). No random UUIDs, no wall-clock, no path dependence. Same input ⇒ same memory state. - Dedup — re-ingesting unchanged bytes is a no-op.
- Edit — a changed file re-chunks only what changed; unchanged chunk hashes stay stable; the prior version (chunks + vectors + metadata) is purged.
- Delete — removal cleans chunks, vectors, and metadata together.
Reproduce it yourself:
bash scripts/demo/repro_demo.sh # ingest → re-ingest → identical state
python scripts/bench/scale_check.py --docs 500 --paras 60 # scale + hash-reproducibility report
How UPII compares
| UPII | Cloud RAG / memory APIs | Typical local RAG | |
|---|---|---|---|
| Corpus stays on device | ✅ always | ❌ | ✅ |
| Cited, chunk-level attribution | ✅ | varies | rarely |
| Reproducible ingestion (stable hashes) | ✅ 100% | ❌ | ❌ |
| Temporal + relational retrieval signals | ✅ both built; relational off by default (status) | ❌ | ❌ cosine only |
| Works fully offline | ✅ | ❌ | usually |
| Consent-gated ambient capture | ✅ | ❌ | ❌ |
Roadmap
- Tune the relational signal to net-positive — it re-ranks on knowledge-graph entity overlap but currently isn't a clear win on the eval, so it ships weight-0; make it help before enabling by default
- Batched vector writes — remove the per-document LanceDB append that caps ingestion throughput
- MCP server — expose
upii_search/upii_askas local MCP tools so Claude, Cursor, and other agents can use your private, cited memory (scope) - Mail + calendar connectors (consent-gated)
- Answer verification / abstention
- Signed installers for macOS, Windows, Linux
upii forget— user-facing verified deletion
Contributing
Issues and PRs welcome. Start with:
pip install -e ".[dev]" && pytest tests/ -q
Hard constraints for any change: local-first (no required cloud dependency), deterministic ingestion (same input ⇒ same chunk hashes), and consent-gating for any ambient capture path. See tests/test_chunk_determinism.py for the invariants that must hold.
License
MIT © 2026 Maddy
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file upii-0.7.0.tar.gz.
File metadata
- Download URL: upii-0.7.0.tar.gz
- Upload date:
- Size: 92.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3478ad04f3aedebeafe91a4181283f153c65c880c10095f0d076b4fddaf62381
|
|
| MD5 |
bc7c86b2903a30005af9997e8c8cbc22
|
|
| BLAKE2b-256 |
2827adafdcd57c6aeb2949343dd7d094168280ef74644ba958643eaed9698474
|
Provenance
The following attestation bundles were made for upii-0.7.0.tar.gz:
Publisher:
pypi-publish.yml on maddymaster/UPII
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
upii-0.7.0.tar.gz -
Subject digest:
3478ad04f3aedebeafe91a4181283f153c65c880c10095f0d076b4fddaf62381 - Sigstore transparency entry: 2188417656
- Sigstore integration time:
-
Permalink:
maddymaster/UPII@fb4aeba665b0f12041f79768d856dd9b810cde1b -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/maddymaster
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@fb4aeba665b0f12041f79768d856dd9b810cde1b -
Trigger Event:
push
-
Statement type:
File details
Details for the file upii-0.7.0-py3-none-any.whl.
File metadata
- Download URL: upii-0.7.0-py3-none-any.whl
- Upload date:
- Size: 73.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
993fa8db12e634ee461dc6dc3a57896e2d28a7aac3cce442e6fe747cac95ae0e
|
|
| MD5 |
4e1193ac6314b060b823df999c4781c9
|
|
| BLAKE2b-256 |
7e595bd34fa45a83b7f1568ec58895b7f1275fb5ba3e3e209b9eb02759f1c874
|
Provenance
The following attestation bundles were made for upii-0.7.0-py3-none-any.whl:
Publisher:
pypi-publish.yml on maddymaster/UPII
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
upii-0.7.0-py3-none-any.whl -
Subject digest:
993fa8db12e634ee461dc6dc3a57896e2d28a7aac3cce442e6fe747cac95ae0e - Sigstore transparency entry: 2188417727
- Sigstore integration time:
-
Permalink:
maddymaster/UPII@fb4aeba665b0f12041f79768d856dd9b810cde1b -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/maddymaster
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@fb4aeba665b0f12041f79768d856dd9b810cde1b -
Trigger Event:
push
-
Statement type: