Skip to content

Commit de30663

Browse files
committed
Initial public release
RECAP: local-first browsing-history search. Chrome MV3 extension plus a private FastAPI RAG backend (SQLite FTS5 + LanceDB + knowledge graph).
0 parents  commit de30663

72 files changed

Lines changed: 20638 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Keep the build context small and never bake secrets or runtime state into the image.
2+
.git/
3+
.gitignore
4+
.venv/
5+
venv/
6+
__pycache__/
7+
**/__pycache__/
8+
*.pyc
9+
*.pyo
10+
11+
# Secrets + per-user runtime state (provided at runtime via env_file / volumes).
12+
.env
13+
data/
14+
15+
# Not needed in the backend image.
16+
extension/
17+
tests/
18+
*.md
19+
LICENSE
20+
.claude/
21+
.vscode/
22+
categories/
23+
tools/

.env.example

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# RECAP - Environment Variables
2+
# Copy this file to .env and fill in your values
3+
4+
# =============================================================================
5+
# LLM Provider (any OpenAI-compatible endpoint - at least one key required)
6+
# =============================================================================
7+
# Named providers: just supply the key (base URLs are built in).
8+
GROQ_API_KEY=
9+
OPENAI_API_KEY=
10+
ANTHROPIC_API_KEY=
11+
GOOGLE_API_KEY=
12+
OPENROUTER_API_KEY=
13+
14+
# Custom / self-hosted OpenAI-compatible endpoint (Ollama, vLLM, LM Studio, ...).
15+
# Select provider "custom" (or point any provider here). Leave the key blank for
16+
# local servers that don't require one.
17+
# In Docker, a host Ollama is at http://host.docker.internal:11434/v1 - NOT
18+
# localhost (that's the container itself). Compose already maps that hostname.
19+
LLM_BASE_URL=
20+
LLM_API_KEY=
21+
# Optional model override applied to whichever provider is selected.
22+
LLM_MODEL=
23+
# Preferred provider (groq/openai/anthropic/google/openrouter/ollama/custom).
24+
# Leave blank to auto-pick the first provider that has a key.
25+
DEFAULT_PROVIDER=
26+
27+
# =============================================================================
28+
# Embedding Configuration
29+
# =============================================================================
30+
# Embeddings define the vector index (a STRUCTURAL, set-once choice - changing
31+
# the model triggers an automatic re-index from stored text on next start).
32+
# Provider: "local" (on-device, private default) or an OpenAI-compatible endpoint.
33+
EMBEDDING_PROVIDER=local
34+
EMBEDDING_MODEL=BAAI/bge-base-en-v1.5
35+
# Curated on-device models (keep EMBEDDING_PROVIDER=local and pick one):
36+
# BAAI/bge-base-en-v1.5 768-dim default; best size/quality balance
37+
# BAAI/bge-large-en-v1.5 1024-dim higher quality, ~3x slower/larger
38+
# sentence-transformers/all-MiniLM-L6-v2 384-dim fastest & smallest
39+
# google/embeddinggemma-300m 768-dim Google's on-device model (2025)
40+
# GATED: accept the license at huggingface.co/google/embeddinggemma-300m and run
41+
# `huggingface-cli login` first; needs sentence-transformers>=5.1 (bump the pin).
42+
#
43+
# Easiest way to run EmbeddingGemma (ungated, no HF login, no pin bump) is via Ollama -
44+
# `ollama pull embeddinggemma`, then point RECAP at Ollama's OpenAI-compatible endpoint:
45+
# EMBEDDING_PROVIDER=ollama
46+
# EMBEDDING_MODEL=embeddinggemma
47+
# Or any other OpenAI-compatible endpoint (OpenAI, vLLM, LM Studio, ...):
48+
# EMBEDDING_PROVIDER=custom
49+
# EMBEDDING_BASE_URL=http://localhost:11434/v1
50+
# EMBEDDING_API_KEY= # blank for local servers
51+
# NOTE: a REMOTE embedding endpoint receives the text of every indexed page. A loopback
52+
# address (127.0.0.1 / localhost) stays on your machine; anything else does not.
53+
EMBEDDING_BASE_URL=
54+
EMBEDDING_API_KEY=
55+
# Dimension is derived from the model at load; this is only a fallback hint.
56+
EMBEDDING_DIMENSION=768
57+
58+
# =============================================================================
59+
# Server Configuration
60+
# =============================================================================
61+
# Bind to loopback only so no other device on the network can reach the backend
62+
HOST=127.0.0.1
63+
PORT=8000
64+
LOG_LEVEL=info
65+
66+
# =============================================================================
67+
# Storage Paths
68+
# =============================================================================
69+
# Base directory for all data (default: ./data)
70+
DATA_DIR=./data
71+
# SQLite database filename
72+
DB_FILENAME=recap.db
73+
# LanceDB directory name
74+
VECTOR_STORE_DIR=vector_store
75+
# Knowledge graph directory name
76+
KG_DIR=knowledge_graph
77+
78+
# =============================================================================
79+
# RAG Configuration
80+
# =============================================================================
81+
# Minimum content quality score (0.0-1.0) to index a page
82+
MIN_CONTENT_QUALITY=0.3
83+
# Maximum chunk size in tokens for semantic chunking
84+
MAX_CHUNK_TOKENS=512
85+
# Minimum chunk size in tokens
86+
MIN_CHUNK_TOKENS=50
87+
# Number of results to retrieve per search method
88+
RETRIEVAL_TOP_K=10
89+
# Per-day decay applied to fused scores so recently-visited pages rank higher
90+
# (~35-day half-life). Set to 1.0 to disable recency weighting.
91+
RECENCY_DECAY=0.98
92+
# Final number of results after re-ranking
93+
RERANK_TOP_K=5
94+
# Cross-encoder model for re-ranking
95+
RERANK_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
96+
97+
# =============================================================================
98+
# Content Processing
99+
# =============================================================================
100+
# Minimum visit duration (seconds) before indexing a page
101+
MIN_VISIT_DURATION=10
102+
# Retention: delete pages not visited within this many days (0 = keep forever).
103+
# Default 120 (~4 months) keeps the local index bounded instead of growing forever.
104+
RETENTION_DAYS=120
105+
# Enable/disable entity extraction for knowledge graph
106+
ENABLE_KG=true
107+
# SpaCy model for sentence tokenization
108+
SPACY_MODEL=en_core_web_sm
109+
# Embedding-prototype gate that skips login/account/checkout-like pages the URL
110+
# rules miss (fails open if the embedder is unavailable).
111+
SEMANTIC_GATE_ENABLED=true
112+
# How decisively a page must match a sensitive prototype to be skipped (0.0-0.5)
113+
SEMANTIC_GATE_MARGIN=0.02

.github/workflows/tests.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: tests
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
8+
jobs:
9+
integration:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: astral-sh/setup-uv@v5
14+
with:
15+
python-version: "3.12"
16+
- name: Install dependencies
17+
run: uv sync --frozen
18+
- name: Run integration suite
19+
run: uv run python tests/test_integration.py

.gitignore

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Secrets
2+
.env
3+
.env.local
4+
*.key
5+
6+
# Local data (RAG store, indexes, user browsing content)
7+
data/
8+
9+
# Python
10+
__pycache__/
11+
*.py[cod]
12+
*$py.class
13+
*.egg-info/
14+
dist/
15+
build/
16+
*.egg
17+
18+
# Virtual environments
19+
venv/
20+
.venv/
21+
env/
22+
23+
# Model / ML caches
24+
.cache/
25+
26+
# Jupyter
27+
.ipynb_checkpoints/
28+
*.ipynb
29+
30+
# Large media (host demo videos as GitHub release assets, not in git)
31+
*.mp4
32+
*.mov
33+
34+
# Editors / IDE
35+
.vscode/
36+
.idea/
37+
*.swp
38+
*.swo
39+
40+
# Local tooling (not part of the project)
41+
.claude/
42+
.claude-plugin/
43+
.mcp.json
44+
CLAUDE.local.md
45+
46+
# OS
47+
.DS_Store
48+
Thumbs.db
49+
50+
# Logs
51+
*.log
52+
53+
# Tests
54+
.pytest_cache/
55+
htmlcov/
56+
.coverage
57+
58+
.mcp.json
59+
# Local scratch (not part of the project)
60+
categories/
61+
tools/
62+
docs/
63+
Plans/

AGENTS.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# AGENTS.md
2+
3+
Guidance for AI coding agents - and humans - working in the RECAP repository. This is the **single source of truth** for how to set up, run, test, and change this project; tool-specific files (`CLAUDE.md`, etc.) import it. The human-facing overview is in [`README.md`](README.md); the security policy is in [`SECURITY.md`](SECURITY.md).
4+
5+
## What RECAP is
6+
7+
RECAP is a **local-first** Chrome extension (Manifest V3) plus a local Python (FastAPI) RAG backend. It passively indexes the pages a user actually reads and lets them search their browsing history in natural language. Everything runs on the user's machine; page content only leaves the device when the user asks a question - and only the retrieved snippets, sent to the LLM provider the user configured.
8+
9+
## Setup & run
10+
11+
Requires **Docker** (recommended) or **Python 3.11+**, plus Chrome. Three paths, all of
12+
which install spaCy + the `en_core_web_sm` model (so the knowledge graph works out of the box):
13+
14+
```bash
15+
# Docker - recommended: reproducible, one command. Serves http://127.0.0.1:8000 (loopback only).
16+
docker compose up --build
17+
18+
# uv - fast, reproducible local dev. `uv sync` installs deps + the spaCy model.
19+
uv sync && uv run python main.py
20+
21+
# pip - simple fallback.
22+
pip install -r requirements.txt && python main.py
23+
```
24+
25+
`pyproject.toml` (+ `uv.lock`) is the canonical dependency set for uv/Docker; `requirements.txt`
26+
mirrors it for the pip path - **keep the two in sync** when changing versions. torch is pinned to
27+
the CPU wheel (`tool.uv.sources`) so images stay small and it runs anywhere; GPU users override it.
28+
29+
- Copy `.env.example``.env` and set at least one LLM key (or point at a local Ollama). See **LLM & embeddings** below.
30+
- **Docker + a host Ollama:** the container can't see the host's `localhost` - set `LLM_BASE_URL`/`EMBEDDING_BASE_URL` to `http://host.docker.internal:11434/v1` (compose already maps `host.docker.internal`).
31+
- Load the extension: `chrome://extensions` → enable **Developer mode****Load unpacked** → select the `extension/` folder.
32+
- **Knowledge graph:** entities need spaCy (now installed by default). If you enabled it *after* indexing pages, backfill them without re-browsing via `POST /maintenance/rebuild_kg` (re-runs NER over stored text; SQLite is the source of truth).
33+
34+
## Testing
35+
36+
```bash
37+
python tests/test_integration.py
38+
```
39+
40+
Ten checks (config, models, SQLite/FTS5, LanceDB, knowledge graph, classifier, chunker, reranker, end-to-end pipeline, semantic gate). No pytest needed. On a Windows console, prefix `PYTHONUTF8=1` if you hit a Unicode error. **Run this before opening a PR and keep it green.**
41+
42+
## Architecture - the one rule that matters
43+
44+
**SQLite is the single source of truth; the keyword index and vector index are derived and reconstructible.**
45+
46+
- `data/recap.db` (SQLite) holds the canonical chunk **text** + all page/chunk metadata - stored exactly once.
47+
- **FTS5** keyword search is an *external-content* table synced by triggers - it holds the inverted index only, no second copy of the text.
48+
- **LanceDB** (`data/vector_store/`) stores only `chunk_id + vector` - no text, no metadata.
49+
- Retrieval fuses BM25 + dense + KG with **weighted Reciprocal Rank Fusion** (k=60) → cross-encoder rerank → **time-decay recency** → then **hydrates** text/metadata from SQLite. Hydration is the single place chunk text is read and the single place the date filter is applied.
50+
- Because the indexes are derived, changing the embedding model **rebuilds vectors from SQLite** (`POST /maintenance/reindex`) without losing any pages.
51+
52+
**Ingestion order:** `content_classifier` → semantic `chunker` (token-aware) → spaCy NER (optional; degrades gracefully) → SQLite → FTS (via triggers) → LanceDB → knowledge graph. Write SQLite (truth) **first**, derived indexes after, and commit the page's `content_hash` **last** - so a failure mid-pipeline re-indexes cleanly on the next visit instead of leaving a half-indexed page.
53+
54+
## Directory map
55+
56+
```
57+
main.py entry point (uvicorn -> backend.app:app)
58+
backend/
59+
app.py thin FastAPI layer - NO business logic here
60+
config.py pydantic-settings Settings (env / .env)
61+
models.py all Pydantic request/response models (bounded)
62+
bootstrap.py schema-version clean-rebuild of local data/
63+
prompts.py ALL LLM prompt templates + untrusted-content sanitizer
64+
embeddings.py pluggable embedding fn (local OR OpenAI-compatible)
65+
ingestion/ processor, chunker, content_classifier, entity_extractor, semantic_gate
66+
retrieval/ hybrid_retriever (RRF + recency), reranker, answer_generator
67+
storage/ database (SQLite/FTS5), vector_store (LanceDB), knowledge_graph
68+
extension/ MV3 extension
69+
theme.css shared "Index Catalog" design system (every page imports it)
70+
background.js service worker: tab tracking, domain/path blocklist, gating
71+
content.js extraction, Shadow-DOM omnibar (Ctrl+Shift+K), highlight (Ctrl+Shift+S)
72+
newtab/popup/options/onboarding/digest/flashcards/graph
73+
tests/test_integration.py plain-python test runner
74+
```
75+
76+
## LLM & embeddings
77+
78+
- **One OpenAI-compatible client** (the `openai` SDK) serves every LLM provider via a `(base_url, api_key, model)` triple. Presets: `openai`, `groq`, `openrouter`, `google`, `anthropic`, `ollama`; `custom` uses `llm_base_url`. **Do not add per-provider SDKs** (`groq`, `anthropic`, `google-generativeai` are intentionally not dependencies).
79+
- **Embeddings are pluggable the same way**: `local` sentence-transformers (default `BAAI/bge-base-en-v1.5`) or any OpenAI-compatible `/v1/embeddings`. The true vector **dimension is derived from the model at load - never hard-code it**.
80+
- Keys come from `.env` or the extension Options page (`POST /update_api_keys`, session-only).
81+
- LLM calls have a hard timeout; the system prompt includes an injection guard treating retrieved page text as untrusted data.
82+
83+
## Conventions
84+
85+
**Python**
86+
- `from __future__ import annotations`; full type hints; small, single-purpose functions; docstrings on public methods.
87+
- All request/response shapes live in `backend/models.py` as Pydantic models **with bounds** (`max_length`, numeric ranges, ISO-date validators).
88+
- Keep `app.py` a **thin API layer** - delegate to `IngestionProcessor` / `HybridRetriever` / `AnswerGenerator` / storage classes.
89+
- Blocking work (embedding, DB, vector, LLM) inside an `async` handler must run via `asyncio.to_thread` (or be a sync `def` handler) so it doesn't block the event loop.
90+
91+
**Extension (vanilla JS, no build step)**
92+
- **IMPORTANT: MV3's CSP forbids inline `<script>` blocks and inline `on*=` handlers.** Put JS in an external `.js` file and wire events with `addEventListener`.
93+
- **NEVER load remote resources** (fonts, scripts, styles, analytics). The extension must work fully offline for privacy - use system-font stacks, not font CDNs.
94+
- **Escape all untrusted text** - page titles, URLs, and LLM output (which is derived from indexed pages) - before assigning to `innerHTML`; route link `href`s through a `safeUrl()` that allows only `http(s):`. This prevents XSS from a malicious indexed page.
95+
- Every page imports `extension/theme.css` first and uses its tokens/classes. Don't reintroduce a per-page palette or a dark theme.
96+
97+
## Security & privacy - non-negotiable
98+
99+
- **NEVER commit API keys or the `data/` directory.** Both are in `.gitignore`; secrets belong only in `.env`. See [`SECURITY.md`](SECURITY.md).
100+
- The backend **binds `127.0.0.1`** and restricts CORS to the extension origin. **Do not** change the default bind to `0.0.0.0`.
101+
- Local embeddings + a local LLM (Ollama) is a fully private setup. A **remote** embedding provider sends the text of *every indexed page* off-device - keep it opt-in and keep the warning.
102+
- Sensitive domains (banking, email, auth, password managers) are excluded from both tracking and content capture - keep that gating intact.
103+
- Sensitive-page detection is **layered**; keep every layer intact: (1) URL blocklists in the extension + backend (defense in depth for known-bad domains); (2) a **live-DOM structural gate in `content.js`** (`detectSensitivePage`: visible password/payment fields, `noindex`, form dominance, auth-phrase walls) that refuses extraction so sensitive text **never leaves the tab**; (3) a backend PII/auth-text backstop (`detect_sensitive_content`: Luhn-valid cards, SSN, IBAN, contact density); (4) an embedding-prototype `SemanticGate` catching login/account/checkout-like pages on unlisted domains (`semantic_gate_enabled`; fails open). Layers 3-4 run **before** any SQLite write. The DOM check must stay client-side - moving it to the backend would send the text over the wire first. Skip telemetry lands in the `skip_stats` table and surfaces as `top_skipped_domains` in `/stats` (ignore-domain candidates).
104+
105+
## Adding things (quick pointers)
106+
107+
- **New LLM/embedding provider** - add a preset base URL in `retrieval/answer_generator.py` / `embeddings.py`; no new dependency if it's OpenAI-compatible.
108+
- **New endpoint** - define models in `models.py`, add the route in `app.py`, delegate the logic to a service, wrap blocking work in `to_thread`.
109+
- **Storage/schema change** - bump `SCHEMA_VERSION` in `bootstrap.py` (triggers a clean local rebuild on next start); keep SQLite the source of truth.
110+
111+
## Gotchas
112+
113+
- Changing `embedding_model` / provider / dimension invalidates the vector space → the app **auto-reindexes from SQLite on next start**; expect a one-time rebuild.
114+
- `data/` is per-user runtime state; deleting it is safe (it re-indexes as you browse). It is intentionally wiped on a `SCHEMA_VERSION` bump.
115+
- The knowledge graph needs spaCy (`en_core_web_sm`); without it, entities/graph are empty but BM25 + dense retrieval still work.
116+
- With a **remote** embedding provider, the `SemanticGate` embeds the head of pages it may then reject - one more reason remote embeddings are opt-in. Fully-local setups are unaffected.
117+
- Commit conventions: run the test suite first; never bypass hooks; keep secrets and `data/` out of every commit.

CLAUDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# CLAUDE.md
2+
3+
Project instructions for this repo are maintained in **[AGENTS.md](AGENTS.md)** - the cross-tool standard (setup, run, testing, architecture, conventions, and security rules). It is imported below so Claude Code loads it automatically. Edit **AGENTS.md**, not this file, so every agent (Claude, Codex, Cursor, Copilot, Gemini, …) stays in sync from one source of truth.
4+
5+
@AGENTS.md
6+
7+
## Claude Code notes
8+
9+
- Run `/memory` to see which memory files are loaded in the current session.
10+
- Keep this file a thin pointer; all durable guidance lives in `AGENTS.md`.

CONTRIBUTING.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Contributing to RECAP
2+
3+
Thanks for helping! The full contributor guide - setup, architecture, conventions,
4+
and security rules - lives in **[AGENTS.md](AGENTS.md)**. The short version:
5+
6+
1. Set up with `uv sync` (or Docker / pip - see [README](README.md#setup)).
7+
2. Make your change. Keep `backend/app.py` a thin API layer; keep SQLite the
8+
single source of truth; never load remote resources in the extension.
9+
3. Run the test suite and keep it green:
10+
11+
```bash
12+
python tests/test_integration.py
13+
```
14+
15+
4. Never commit API keys, `.env`, or the `data/` directory.
16+
5. Open a PR against `main` with a short description of what and why.
17+
18+
Security issues: please report privately per [SECURITY.md](SECURITY.md), not as
19+
public issues.

0 commit comments

Comments
 (0)