A local-first retrieval-augmented generation (RAG) API for indexing web documents and answering questions from their contents. It combines local sentence-transformers embeddings and a persistent Chroma vector store with an OpenAI-compatible chat model. For complex questions, it can use LangChain Deep Agents (deepagents) to search, delegate chunk analysis, and synthesize an answer.
- Fetches HTTP(S) URLs, extracts text from HTML, and splits content into overlapping chunks.
- Embeds and stores the chunks locally in Chroma, retaining the source URL as metadata.
- Retrieves the closest chunks for a question and returns a grounded answer with citations—or runs a multi-step deep-agent workflow and returns its trace.
Embeddings are computed locally. Chat completions are sent to the configured OpenAI-compatible endpoint. The embedding model is downloaded on first use.
- Python 3.13 or newer
- uv
- An OpenAI-compatible chat endpoint and API key to answer questions. The API can start without a key, but chat and agent requests need one.
git clone <repository-url>
cd rag-deep-agent
uv sync --dev
cp .env.example .envEdit .env with the chat endpoint, model, and key for your provider. The checked-in example is set up for an OpenAI-compatible endpoint at http://localhost:11434/v1; change it if you use another provider.
Start the development server:
uv run fastapi dev app/main.pyOpen http://127.0.0.1:8000/docs for the interactive API reference. The first embedding operation downloads sentence-transformers/all-mpnet-base-v2 into model-home/; indexed data is persisted in chroma_langchain_db/.
Warning
This API has no authentication and allows cross-origin requests. Run it only in a trusted environment; do not expose it to an untrusted network without adding appropriate access controls.
Settings are read from environment variables or the project-root .env file. Environment variables take precedence. Relative storage paths resolve from the project root.
| Variable | Default | Description |
|---|---|---|
OPENAI_API_KEY |
Empty | Chat API key; required for chat and agent requests. The legacy variable name api_key is also accepted. |
CHAT_BASE_URL |
https://llm.wi8.ir/v1 |
OpenAI-compatible chat endpoint. |
CHAT_MODEL |
auto |
Model identifier sent to the endpoint. |
CHAT_TEMPERATURE |
0 |
Chat model temperature. |
CHAT_TIMEOUT_SECONDS |
120 |
Chat request timeout. |
EMBEDDING_MODEL_NAME |
sentence-transformers/all-mpnet-base-v2 |
Local embedding model. |
EMBEDDING_CACHE_FOLDER |
model-home/ |
Embedding model download/cache directory. |
CHROMA_PERSIST_DIRECTORY |
chroma_langchain_db/ |
Persistent vector-store directory. |
CHROMA_COLLECTION_NAME |
rag_documents |
Chroma collection name. |
FETCH_TIMEOUT_SECONDS |
20 |
Timeout for each source request. |
MAX_SOURCE_BYTES |
5000000 |
Maximum response size per source. |
CHUNK_SIZE / CHUNK_OVERLAP |
1000 / 200 |
Text splitting size and overlap. |
DEFAULT_TOP_K / MAX_TOP_K |
4 / 25 |
Default and maximum number of retrieved chunks. |
SNIPPET_LENGTH |
400 |
Citation preview length. |
MAX_CONCURRENT_ANALYSTS |
3 |
Maximum parallel chunk-analyst tasks for the deep agent. |
LOG_LEVEL |
INFO |
Application log level. |
Configuration is validated at startup. Invalid values are reported by /health; the server still starts so the problem can be diagnosed. A missing chat key also leaves the API available, but requests that need the model return 503.
| Method | Path | Description |
|---|---|---|
GET |
/ |
Service banner. |
GET |
/health |
Readiness checks and configuration problems. |
GET |
/api/system/info |
Effective non-secret configuration. |
GET |
/api/corpus/stats |
Source/chunk counts and index settings. |
POST |
/api/documents |
Fetch and index one or more URLs (201). |
GET |
/api/documents |
List indexed sources and chunk counts. |
GET |
/api/documents/search |
Semantic search without a chat completion. |
POST |
/api/documents/remove |
Remove all chunks for one source. |
DELETE |
/api/documents?confirm=true |
Clear the entire index; confirmation is required. |
POST |
/api/chat |
Answer from retrieved chunks with citations. |
POST |
/api/agent/runs |
Run the deep agent and return its steps. |
Ingestion accepts 1–20 URLs per request, removes duplicates, and skips failed URLs when other sources can still be indexed. Each source must be HTTP(S) and is capped at MAX_SOURCE_BYTES.
Index URLs:
curl -X POST http://127.0.0.1:8000/api/documents \
-H 'content-type: application/json' \
-d '{"urls":["https://example.com/guide.html"]}'List indexed sources:
curl http://127.0.0.1:8000/api/documentsSearch without calling the chat model. Lower scores indicate closer matches:
curl 'http://127.0.0.1:8000/api/documents/search?q=streaming&top_k=3'Ask a grounded question:
curl -X POST http://127.0.0.1:8000/api/chat \
-H 'content-type: application/json' \
-d '{"question":"How does this guide configure the service?","top_k":4}'Run the deep agent:
curl -X POST http://127.0.0.1:8000/api/agent/runs \
-H 'content-type: application/json' \
-d '{"question":"Summarize the guide and compare its setup options."}'Remove one source, or clear the full index (the confirmation query is required):
curl -X POST http://127.0.0.1:8000/api/documents/remove \
-H 'content-type: application/json' \
-d '{"source":"https://example.com/guide.html"}'
curl -X DELETE 'http://127.0.0.1:8000/api/documents?confirm=true'top_k defaults to 4 and is limited to 25. The chat and search endpoints also accept max_distance to exclude weak matches. The agent uses the configured default retrieval count.
Chat responses contain answer, unique sources, and citations with a source URL, similarity distance, and snippet. Agent responses contain the final answer and ordered steps. Search responses include ranked chunks and their scores.
Errors use a stable JSON shape:
{"detail":"No indexed documents found. Ingest documents first.","code":"empty_corpus"}| Code | HTTP status | Meaning |
|---|---|---|
not_configured |
503 |
Invalid configuration or missing chat API key. |
source_fetch_failed |
502 |
A source could not be fetched. |
invalid_source |
422 |
Invalid URL or unsupported scheme. |
unsupported_content |
415 |
Source content is unsupported or too large. |
empty_document |
422 |
No indexable text was produced. |
empty_corpus |
409 |
No documents are indexed. |
unknown_source |
404 |
The requested source is not indexed. |
confirmation_required |
400 |
Whole-index deletion needs ?confirm=true. |
validation_error |
422 |
Invalid request; includes an errors array. |
internal_error |
500 |
An unclassified domain error. |
POST /api/ingest and POST /api/query remain available as deprecated aliases for document ingestion and chat.
The rag-demo command uses the same local index:
uv run rag-demo stats
uv run rag-demo ask "How do I stream?"
uv run rag-demo agent "Summarize the indexed guide."Index documents through the API before running ask or agent.
Run the test suite:
uv run pytestRun Ruff:
uvx ruff check app testsTests mock service boundaries and do not require downloading the embedding model or contacting a chat provider.
app/
├── agents/ Deep-agent setup, prompts, and retrieval tool
├── api/ FastAPI routers, request helpers, and error mapping
├── core/ Settings and domain errors
├── services/ Ingestion, vector storage, retrieval, and answering
├── cli.py rag-demo command
├── main.py FastAPI application
└── schemas.py HTTP request and response models
tests/ Unit and API tests