A local, privacy-focused desktop chat application powered by
Qwen/Qwen2.5-1.5B-Instruct-AWQ, vLLM, PyQt6, and a local Qdrant-based RAG
pipeline.
Blog: https://medium.com/@harshitweb3/building-a-fully-private-gpt-549c0935d307
- 100% local chat: conversations and documents stay on your machine.
- PyQt6 desktop UI with sessions, searchable history, settings, and a knowledge base.
- Local vLLM inference with AWQ Marlin quantization.
- RAG over PDF, DOCX, TXT, and Markdown files.
- Qdrant local vector store plus BM25 hybrid reranking.
- CPU embeddings to keep GPU VRAM available for the LLM.
- Crash recovery, VRAM monitoring, and performance stats.
- Mock mode for UI testing without loading the main LLM.
| Purpose | Model | Where It Runs |
|---|---|---|
| Chat / generation | Qwen/Qwen2.5-1.5B-Instruct-AWQ |
NVIDIA GPU through vLLM |
| Embeddings / RAG | sentence-transformers/all-MiniLM-L6-v2 |
CPU |
The embedding model may download on first use even in --mock mode, because the
RAG stack is still initialized. Mock mode skips the main Qwen/vLLM model.
- Linux desktop environment, tested on Ubuntu-style systems.
- Python 3.10+.
- NVIDIA GPU with CUDA support for real model mode.
- 4GB+ VRAM is the current runtime validation threshold for the 1.5B AWQ model.
- 6GB+ VRAM is recommended for a smoother 3072-token default context.
- Docker is not required. The app uses your NVIDIA GPU directly through PyTorch/vLLM.
The code is now tuned for the lighter 1.5B model by default.
From this repo:
cd ~/coding/private-gpt/privateGPT
uv syncIf uv is not installed:
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"Start with mock mode:
uv run python run.py --mock --devRun the real local model:
uv run python run.py --devYou can monitor GPU usage in another terminal:
watch -n 1 nvidia-smiOn startup the app:
- Cleans stale vLLM GPU processes from previous crashes.
- Creates a PyQt6 + qasync event loop.
- Loads
styles_modern.qsswhen available. - Creates the main window and session sidebar.
- Checks GPU requirements in real model mode.
- Loads Qwen through vLLM after the UI appears.
- Initializes local RAG components lazily as needed.
The UI displays streamed chunks, but vLLM generation is currently performed as a single synchronous call in an executor and then split into small chunks for the chat display. So it behaves like streaming in the UI, but it is not true token streaming from vLLM yet.
# UI smoke test, no main LLM load
uv run python run.py --mock --dev
# Real model mode
uv run python run.py --dev
# Run tests
uv run pytest
# Build packaged app with PyInstaller
uv run python build.pyprivateGPT/
├── run.py # Dev-friendly entrypoint
├── build.py # PyInstaller build script
├── src/private_gpt_app/
│ ├── main.py # Real application entrypoint
│ ├── ui/ # PyQt6 windows, dialogs, widgets, styles
│ ├── backend/ # vLLM, sessions, retrieval router, document DB
│ ├── rag/ # Qdrant, embeddings, ingestion, hybrid search
│ └── utils/ # GPU, paths, setup, performance, crash recovery
├── data/
│ ├── qdrant_db/ # Local Qdrant vector database
│ ├── documents.db # Document registry
│ ├── sessions.db # Chat sessions, created at runtime
│ └── crash_recovery/ # Recovery files
├── models/
│ └── Qwen2.5-1.5B-Instruct-AWQ/ # Optional bundled/local model directory
└── docs/
- Add files through
Tools > Knowledge Base. IngestionWorkerextracts text from PDF, DOCX, TXT, or MD.- Text is split with the current character-based
TextSplitter. - Chunks are embedded on CPU with
all-MiniLM-L6-v2. - Chunks are stored in local Qdrant with source metadata.
- The document registry is stored in SQLite.
- On a query,
RetrievalServicesearches Qdrant, optionally reranks with BM25, truncates context by token budget, and injects the context into the latest user message before vLLM generation.
Note: token-based utilities exist in rag/chunking.py and are used for context
counting/truncation. Ingestion still uses the character-based splitter.
Default runtime settings are in MainWindow.current_settings:
{
"gpu_memory_utilization": 0.55,
"max_model_len": 3072,
"cpu_offload_gb": 1.0,
"temperature": 0.7,
"top_p": 0.95,
"max_tokens": 768,
"rag_strategy": "always",
"relevance_threshold": 0.5,
}The settings dialog can adjust generation and RAG settings. Model memory settings are applied when the model service is created, so restart the app after changing them if you need a clean model reload.
Install uv:
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"nvidia-smi
uv run python -c "import torch; print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))"Try lowering:
gpu_memory_utilizationmax_model_lenmax_tokens
Also close other GPU-heavy applications.
If no bundled model exists under models/Qwen2.5-1.5B-Instruct-AWQ/, vLLM will use
the HuggingFace model ID and download/cache through the normal HuggingFace cache.
The local vector database lives in data/qdrant_db. The document list is tracked
separately in data/documents.db.
This repo currently packages with PyInstaller:
uv run python build.pyThe build expects a local model at:
models/Qwen2.5-1.5B-Instruct-AWQ/
with required files such as config.json, model.safetensors, and
tokenizer.json.
src/private_gpt_app/main.pyis the real app entrypoint.- The root
main.pyis not the desktop app entrypoint. main_broken.py, if present in older checkouts, is a stale backup and is not referenced by the current run/build path.