Privacy-first local document intelligence for macOS. Informity scans and indexes local files, then answers questions with a local RAG pipeline backed by local embeddings and local LLM inference. Your documents and vectors stay on your machine.
- Highlights
- Quick Start
- Installation (optional one-time setup)
- Data Location
- PDF Processing
- Offline Mode
- MCP Server (Read-only)
- Chat Scope Contract
- Tech Stack
- Project Structure
- Release Scripts
- Development
- License
- Local-first RAG over local files
- Offline-first runtime (full privacy mode)
- Temporary per-chat uploaded files in Researcher mode (
+attach,xremove) - SQLite + sqlite-vec for metadata + embeddings
- Local LLM inference via
xllamacpp(Metal/GPU on macOS) - Optional Ollama runtime provider (localhost daemon)
Informity uses two separate extension concepts:
- Plugins are internal behavior extensions that shape how the app routes, formats, ranks, and specializes answers.
- Integrations are external-system bridges that connect Informity to other services or data sources.
Current roadmap focus is on the plugin architecture first; integration runtime work is tracked separately when external actions or external-source bridges are needed.
# Install dependencies
uv sync --all-extras
# Build frontend and run the app (single command — recommended for testing)
make app
# Or run in two steps:
make frontend-build # Build React UI → src/frontend/dist/
make run # Start backend on http://127.0.0.1:8420
# Development with auto-reload (code changes restart the server)
make devNote: The app serves the React frontend from src/frontend/dist/. Run make frontend-build before make run or make dev.
You can either run the app and let it download models on first use (see Offline mode below), or run a one-time install so everything is downloaded up front and the app always uses cached models.
Option A — Install script (recommended for clean setup)
Run once to install Python deps and download the embedding model, reranker (cross-encoder), and optional LLM into app data, then lock the app to cached-only:
./scripts/install/install_app.shFor first-run setup testing (install app/runtime dependencies only, no models preinstalled):
INFORMITY_INSTALL_PROFILE=dev INFORMITY_INSTALL_SKIP_MODELS=1 ./scripts/install/install_app.sh- Uses
scripts/install/install.conf.jsonfor model IDs:embedding_model,reranker_model(default:cross-encoder/ms-marco-MiniLM-L-6-v2), and optional LLM (default: Qwen3.6 35B A3B Q4_K_M viarepo_id/filename); the classifier GGUF is installed from the built-in bootstrap spec intomodels/classifier/. - Downloads all models to
~/.informity/by default (override withINFORMITY_APP_DATA_DIR) and writesconfig.jsonwithfull_privacy=true(no network after install). - After this, the app will never auto-download; it only uses what’s already in app data. With those settings enabled, the app makes no network requests after install (no Hugging Face or internet contact).
Uninstall
To remove all user data and downloaded content and return to a fresh distribution state (as after cloning), run from repo root: ./scripts/install/install_uninstall_app.sh or make uninstall. This removes the app data directory (config, database, embedding cache, LLM models, vectors, logs), the virtualenv (.venv), and local caches. Run ./scripts/install/install_app.sh again to reinstall.
Reset (in-app)
Settings → Reset restores all settings to factory defaults (including default LLM: Qwen3.6 35B A3B). Index → Reset deletes all indexed data and chat history and also resets settings to the same defaults. For a full local cleanup and reset, run ./scripts/install/install_uninstall_app.sh (or make uninstall). Then run ./scripts/install/install_app.sh to reinstall.
Option B — First-run auto-download
Just run the app. On first search/index/chat it may download the embedding model, reranker, and LLM if not already present. In Settings → Full Privacy Mode you can turn “Enable” on so future runs are fully offline.
You do not need to remove auto-download from the app: the install script is for users who want a single, explicit setup step and then strictly cached-only behaviour.
All application data (database, vectors, LLM and embedding models, logs, config) is stored under a single app data directory:
- Default:
~/.informity/ - Override: Set
INFORMITY_APP_DATA_DIRto use a custom path (e.g. an external drive or CI-isolated./data).
Directory layout:
~/.informity/
config.json # Saved settings
db/ # SQLite DB and WAL files
informity.db
storage/
uploads/ # Temporary chat-scoped uploaded files
logs/ # Runtime log files
models/
llm/ # LLM models (*.gguf files)
classifier/ # Classifier models (*.gguf files)
cache/ # Unified cache (not committed)
huggingface/ # Embedding + reranker (cross-encoder) models
hub/ # Model blobs + snapshots (required)
modules/ # Custom model code, created at first load (required)
docling/ # Docling models for document extraction
One cache only (avoid duplicates)
Informity uses only the app data cache directory (cache/ under app data). It does not use the default Hugging Face cache (~/.cache/huggingface/hub). If you have the same models in both places, you can remove the copy under ~/.cache/huggingface/hub to free space.
If embedding or reranker fails with "cache incomplete" (e.g. missing snapshots/ under the model folder), remove the incomplete model dir and re-download: run ./scripts/install/install_app.sh or set INFORMITY_FULL_PRIVACY=false and run a scan/chat once so the missing model is downloaded.
PDFs are processed using Docling (packaged via docling-slim), which provides superior structure preservation including tables, formulas, reading order detection, and built-in OCR support. Scanned PDFs and supported image uploads are handled automatically without requiring external OCR tools.
The runtime uses a centralized PDF extraction orchestrator with ordered strategies:
docling_full -> docling_fast -> pdf_text_layer. The order is configurable in
Settings (pdf_extraction_strategy_order). Per-file timeout is enforced across the
entire strategy chain, and fallback only advances when the previous strategy fails.
The app is offline-first by default. With Full Privacy Mode on (Settings → Full Privacy Mode), no network is used at runtime; all models are loaded from local storage.
- Two models in the Hugging Face cache (
cache/huggingface/hub/under app data): (1) Embedding model (nomic-ai/nomic-embed-text-v1.5) for document and query vectors; (2) Reranker (cross-encoder/ms-marco-MiniLM-L-6-v2) for re-ranking search results. Settings → System shows both for transparency. - With
full_privacy=true(default after install), embedding and reranker are loaded only from this cache. SetINFORMITY_FULL_PRIVACY=false(or turn off in Settings) once to allow downloads, then turn Full Privacy Mode back on for offline use. - LLM (GGUF): App default model is Qwen3.6 35B A3B (
Qwen3.6-35B-A3B-UD-Q4_K_M.gguf), stored inmodels/llm/under the app data directory. Withllm_local_only=true(default), the app only loads from this directory and never downloads. Place your.gguffile there, or setINFORMITY_LLM_LOCAL_ONLY=falseonce to allow a one-time download, then set it back to true.
Note: the optional installer seed inscripts/install/install.conf.jsonpoints to Qwen3.6 35B A3B.
After models are in place, the app runs fully offline with no internet required.
Informity includes a built-in read-only MCP server so external AI clients (for example Claude Desktop) can query your indexed library.
Enable it in the app at Settings -> System -> MCP Server.
Current read-only tools:
informity_healthinformity_files_listinformity_search_semanticinformity_filter_optionsinformity_index_statusinformity_scan_status
Tool behavior notes:
informity_files_listandinformity_search_semanticdefault tolimit=50and allow explicit values up to200.informity_index_statusreports corpus counts with chat-upload records (upload.local) excluded, matching MCP corpus visibility.informity_filter_optionsreturns currently available categories and file type extensions from the index.- For
informity_search_semantic,categoryandfile_typesare optional filters; start with onlyquery+limitunless you specifically need filtering. categoryvalues are:document,plaintext,data,web,other.file_typesaccepts both dot extensions (.pdf) and common aliases (pdf,docx,md), case-insensitive.- If a filtered semantic search returns zero results, the tool includes
hintswith applied filters and recovery guidance.
Access levels:
metadata_only(recommended default)search_snippetsfull_content
For external MCP clients, search_snippets is generally the best balance of privacy and answer quality.
Security notes:
- Keep MCP disabled unless you need it.
- Informity MCP is strictly read-only: only an allowlisted set of read tools can be executed.
httptransport requires a bearer token.- External clients operate outside Informity's internal Full Privacy controls, so only connect trusted local clients.
- MCP interaction logs are written to
~/.informity/logs/app.mcp.log(or your configuredlogs_dir).
After enabling MCP with http transport in Settings, test from a terminal:
curl -sS http://127.0.0.1:8431/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer imcp_YOUR_TOKEN_HERE" \
-d '{
"jsonrpc":"2.0",
"id":3,
"method":"tools/call",
"params":{
"name":"informity_search_semantic",
"arguments":{
"query":"artificial intelligence",
"limit":3
}
}
}'For local development from this repository, use uv run so Claude starts the MCP server in your project environment:
{
"mcpServers": {
"informity": {
"command": "/Users/<you>/.local/bin/uv",
"args": [
"run",
"--project",
"/absolute/path/to/informity-ai",
"informity-mcp"
]
}
}
}For packaged installs (when informity-mcp is on PATH), use:
{
"mcpServers": {
"informity": {
"command": "informity-mcp"
}
}
}Quick validation prompts in Claude Desktop:
- "Use
informity_healthand return raw JSON." - "Use
informity_index_statusand summarize counts in one sentence." - "Use
informity_search_semanticwith querytermination clauseand limit3."
Informity supports two LLM runtime providers:
local_gguf(default): in-processxllamacppwith GGUF models inmodels/llm/.ollama: uses an Ollama daemon (default URLhttp://127.0.0.1:11434) andllm_model_id(for exampleqwen3.6:35b).
Known Ollama model IDs mapped to Informity tuned profiles:
qwen3.6:35b-> Qwen3.6 35B A3B profileqwen3:14b-> Qwen3 14B profileqwen3.5:9b-> Qwen3.5 9B profile
Unknown model IDs use a conservative Ollama default profile.
Setup/readiness is provider-aware:
- First-run setup remains the existing local-model onboarding flow (
local_ggufpath). - Ollama is an advanced post-setup integration enabled from Settings.
- After setup, choosing
ollamabypasses local GGUF gating and uses:- Ollama daemon reachable
- configured Ollama model available locally
- required dependency caches (embedding, reranker, docling)
POST /api/chat supports optional scoped researcher retrieval with:
scoped_file_ids: one-or-more indexed file IDsscoped_upload_ids: one-or-more chat-scoped upload IDs (Researcher mode only)
Notes:
- Legacy
file_idis no longer accepted. - When
scoped_file_idsis provided, researcher retrieval is constrained to that file set. - Uploaded files are temporary and chat-scoped (
POST /api/chat/uploads,GET /api/chat/chats/{chat_id}/uploads,DELETE /api/chat/uploads/{upload_id}). - When uploads are present and no explicit subset is chosen, retrieval defaults to all ready uploaded files in that chat.
- Removing the last upload auto-falls back to scanned-documents retrieval for subsequent turns.
- Assistant mode remains retrieval-free.
- Python 3.13 — all core logic
- FastAPI + uvicorn — async API server
- React + Vite — frontend (served from
src/frontend/dist/when built) - SQLite via aiosqlite — metadata, config, chat history, vector storage (via sqlite-vec extension)
- sqlite-vec — vector storage extension for SQLite (embeddings stored in
vec_chunkstable) - sentence-transformers (nomic-embed-text-v1.5) — embedding generation; (ms-marco-MiniLM-L-6-v2) — optional cross-encoder re-ranking
- xllamacpp (with Metal/GPU) — local LLM inference (app default: Qwen3.6 35B A3B Q4_K_M)
src/frontend/ # React + Vite UI (build output: dist/)
src/informity/
├── main.py # FastAPI app entry point, lifespan, health
├── config.py # Settings via pydantic-settings (config.json + env)
├── db/
│ ├── models.py # Pydantic models (IndexedFile, Chunk, ScanRecord, ChatMessage, etc.)
│ ├── sqlite.py # SQLite connection, schema, queries (aiosqlite)
│ └── vectors.py # SQLite vector storage via sqlite-vec (ChunkEmbedding, VectorStore)
├── scanner/
│ ├── crawler.py # Filesystem traversal, SHA-256 hashes, compare_with_db
│ ├── watcher.py # watchdog file change monitoring
│ └── extractors/ # Docling extractor (PDF, DOCX, PPTX, XLSX, HTML, CSV) + EPUB extractor + text extractor
├── indexer/
│ ├── chunker.py # Parent-child chunking (child ~150 tokens, parent ~512 tokens)
│ ├── embedder.py # Embedding generation (nomic-embed-text-v1.5)
│ ├── classifier.py # Auto-tagging, categorization, year extraction
│ ├── post_process.py # Hyphenation repair (index-time only)
│ ├── reranker.py # Cross-encoder re-ranking (mandatory for all queries)
│ ├── adaptive_tuning.py # Corpus-aware top-k tuning cache
│ ├── term_dictionary_builder.py # Builds term/acronym dictionary from indexed corpus
│ └── pipeline.py # index_file, reindex_file, remove_file — orchestration
├── llm/
│ ├── engine.py # LLM inference (xllamacpp, Metal)
│ ├── model_adapter.py # Per-model profiles (Qwen3 14B, Qwen3.5 9B, Qwen3.6 35B A3B)
│ ├── rag.py # QueryRouter — dispatches to handlers based on intent
│ ├── query_classifier.py # 5Q classifier mapping + route/classification normalization
│ ├── retrieval.py # Unified retrieval pipeline (vector search → rerank)
│ ├── term_dictionary.py # Runtime query expansion via corpus term dictionary
│ ├── classification_policy.py # Intent routing policy and normalization
│ ├── chat_mode.py # Assistant vs Researcher mode routing policy
│ ├── contract_gate.py # Final closeout contract validation/repair
│ ├── contract_prompt_parser.py # Parses required output section cues from user prompts
│ ├── metrics_payload.py # Normalized diagnostics metrics payload helpers
│ ├── five_q_classifier.py # Local 5-question classifier (GGUF LLM + guardrail)
│ ├── nlp_heuristics.py # Minimal deterministic lexical cues
│ ├── prompt_builder.py # Prompt construction and budget management
│ ├── streaming.py # LLM stream wrapper
│ ├── metadata_filters.py # Unified metadata filter extraction (year, category, extension)
│ ├── system_prompts.py # Centralized system prompt templates
│ ├── timeout_policy.py # Request timeout policy mapping by mode/intent
│ ├── user_messages.py # Centralized user-facing message strings
│ ├── web_search.py # Tavily/Linkup-backed web search adapter and status handling
│ ├── rag_runtime/ # RAG execution sub-pipeline (retrieval + generation phases)
│ └── handlers/ # Query handlers (metadata, rag, simple)
└── api/
├── schemas.py # Request/response Pydantic models
├── operation_state.py # Long-running operation flags (scan, reset)
├── setup_state.py # First-run setup state management
├── chat_orchestrator.py # Chat request orchestration entry point
├── chat_continuation.py # Continuation/duplicate detection helpers
├── chat_sse.py # SSE event formatting for chat streams
├── chat_closeout.py # Post-generation chat record finalization
├── chat_stream_registry.py # Active stream registry (cancel support)
├── routes_scan.py # POST /api/scan, GET /api/scan/status, GET /api/scan/errors, GET /api/files, POST /api/files/open
├── routes_index.py # POST /api/index/rebuild, GET /api/index/status, POST /api/index/reset
│ # GET|POST /api/index/term-dictionary/status|rebuild|purge
├── routes_search.py # POST /api/search
├── routes_chat.py # POST /api/chat (SSE), chat uploads, exports, GET/PUT/DELETE conversations
├── routes_settings.py # GET/PUT /api/settings, POST /api/settings/reset, env-vars, file-types
├── routes_system.py # GET /api/diagnostics, GET /api/diagnostics/summary, POST /api/shutdown
└── env_vars_metadata.py # INFORMITY_* env var groups for Configuration page
src/informity/diagnostics/ # Diagnostics package
├── issue_types.py # IssueType enum
├── observer.py # EvalMetrics dataclass, detect_issues()
└── resource_snapshot.py # System resource snapshot at trace time
scripts/ is organized by purpose:
scripts/install/— environment setup, model bootstrap, uninstall cleanupscripts/build/— Tauri/build packaging helpersscripts/release/— release publishing helpersscripts/dev/— developer-only local workflow helpersscripts/diagnostics/— read-only diagnostics utilities
For a read-only view of what the live indexing pipeline does on a single file, use:
make index-quality FILE=path/to/document
# add VERBOSE=1 to print full extracted text and chunk contentsThe diagnostic writes a JSON report to ~/.informity/diagnostics/ and never modifies the index.
Common entry points:
make install
make install-dev
make uninstall
make tauri-icons
make tauri-backend
make tauri-build-mac
make tauri-build-linux-deb
make tauri-build-linux-rpm
make tauri-build-appstoreRuntime chat diagnostics metrics are also persisted in SQLite (response_diagnostics_metrics) for operational observability. Use GET /api/diagnostics/summary for aggregated counts/rates/query-type breakdowns over a time window.
# Run tests
uv run pytest
# Lint
uv run ruff check src/
# Format
uv run ruff format src/
# Frontend development (hot reload)
make frontend # Vite dev server on port 5173 — run backend separately (make run or make dev)
make frontend-build # Build React for production
make app # Build frontend + run backend (single command for testing)Informity AI is licensed under the MIT License.
