Import documentation: README.md
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,330 @@
|
|||||||
|
# Axon Search API
|
||||||
|
|
||||||
|
Axon is a private, self-hosted AI search API for Linux servers. It aggregates search results from Brave Search, DuckDuckGo, and Serper.dev, optionally scrapes full page content, reranks results with a local BGE cross-encoder, and can synthesize answers or research reports through any OpenAI-compatible LLM endpoint.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- `POST /search` for multi-source search with optional scraping, reranking, and answer synthesis
|
||||||
|
- `POST /extract` for clean content extraction from one or more URLs
|
||||||
|
- `POST /research` for multi-query research reports
|
||||||
|
- `POST /crawl` for same-site traversal with extraction
|
||||||
|
- `POST /map` for same-site URL discovery
|
||||||
|
- `GET /health` for cache and reranker status
|
||||||
|
- `GET /metrics` for lightweight request counters and latency
|
||||||
|
- `POST /ingest` and `POST /index/search` for private local document retrieval
|
||||||
|
- `/research/jobs`, `/crawl/jobs`, `/map/jobs`, and `GET /jobs/{id}` for async long-running work
|
||||||
|
- `/tavily/*` compatibility routes for easier migration
|
||||||
|
- `POST /validate/citations` for basic citation coverage checks
|
||||||
|
- API key auth via `X-API-Key`
|
||||||
|
- Per-key/IP rate limiting
|
||||||
|
- Redis cache with in-memory fallback
|
||||||
|
- CORS enabled for all origins
|
||||||
|
- SSRF protection for URL scraping and extraction
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Axon requires Python 3.11 or newer.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env` with any API keys you want to enable. At least DuckDuckGo works without a key, but Brave and Serper improve coverage and reliability.
|
||||||
|
|
||||||
|
Start the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn axon.main:app --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
The reranker model is loaded at startup. The first run may download the configured HuggingFace model and can take longer than later starts.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Variable | Required | Default | Purpose |
|
||||||
|
|---|---:|---|---|
|
||||||
|
| `BRAVE_API_KEY` | No | empty | Enables Brave Search results |
|
||||||
|
| `SERPER_API_KEY` | No | empty | Enables Serper.dev fallback results |
|
||||||
|
| `REDIS_URL` | No | `redis://localhost:6379` | Redis cache URL; falls back to memory if unavailable |
|
||||||
|
| `API_KEYS` | No | empty | Comma-separated accepted API keys; empty disables auth |
|
||||||
|
| `LITELLM_BASE_URL` | No | `http://localhost:4000` | OpenAI-compatible chat completions base URL |
|
||||||
|
| `LITELLM_API_KEY` | No | empty | Bearer token for synthesis requests |
|
||||||
|
| `SYNTHESIS_MODEL` | No | `claude-sonnet-4-6` | Model sent to the LLM endpoint |
|
||||||
|
| `RERANKER_MODEL` | No | `BAAI/bge-reranker-v2-m3` | Sentence Transformers cross-encoder model |
|
||||||
|
| `PORT` | No | `8000` | Preferred server port |
|
||||||
|
| `MAX_CONCURRENT_SCRAPES` | No | `10` | Concurrent scrape limit for advanced search |
|
||||||
|
| `SCRAPE_TIMEOUT` | No | `5` | Per-URL scrape timeout in seconds |
|
||||||
|
| `EXTRACT_TIMEOUT` | No | `10` | Per-URL extraction timeout in seconds |
|
||||||
|
| `RATE_LIMIT_PER_MINUTE` | No | `60` | Per API key or client IP limit; `0` disables |
|
||||||
|
| `CRAWL_MAX_PAGES` | No | `50` | Server-side cap for crawl/map pages |
|
||||||
|
| `CRAWL_MAX_DEPTH` | No | `3` | Server-side cap for crawl/map depth |
|
||||||
|
| `INDEX_DB_PATH` | No | `axon_index.sqlite3` | SQLite FTS database path for private indexed docs |
|
||||||
|
| `ENABLE_BROWSER_FALLBACK` | No | `false` | Enables optional Playwright rendering fallback when installed |
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
If `API_KEYS` is unset or empty, Axon runs in development mode and does not require authentication.
|
||||||
|
|
||||||
|
If `API_KEYS` is set, send one configured key with each protected request:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "X-API-Key: your-secret-key" http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
`/health`, `/docs`, `/openapi.json`, and `/redoc` are exempt.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### Health
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Example response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"redis": "in-memory fallback",
|
||||||
|
"reranker": "loaded"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metrics
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
Example response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"requests_total": 42,
|
||||||
|
"requests_by_path": {
|
||||||
|
"/search": 10,
|
||||||
|
"/extract": 3
|
||||||
|
},
|
||||||
|
"errors_total": 0,
|
||||||
|
"average_latency_ms": 1234.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search
|
||||||
|
|
||||||
|
Basic search uses search API snippets and reranks them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/search \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-secret-key" \
|
||||||
|
-d '{
|
||||||
|
"query": "latest PostgreSQL indexing improvements",
|
||||||
|
"max_results": 10,
|
||||||
|
"search_depth": "basic",
|
||||||
|
"include_answer": false
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Advanced search scrapes result pages before reranking:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/search \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-secret-key" \
|
||||||
|
-d '{
|
||||||
|
"query": "private AI search APIs",
|
||||||
|
"max_results": 10,
|
||||||
|
"search_depth": "advanced",
|
||||||
|
"include_answer": true,
|
||||||
|
"include_raw_content": false,
|
||||||
|
"include_private": true,
|
||||||
|
"domains_blacklist": ["reddit.com"],
|
||||||
|
"topic": "general",
|
||||||
|
"time_range": "month"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extract
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/extract \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-secret-key" \
|
||||||
|
-d '{
|
||||||
|
"urls": [
|
||||||
|
"https://example.com/article"
|
||||||
|
],
|
||||||
|
"include_images": false
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Each URL is processed independently. Failed URLs return `success: false` with an error while the rest of the request continues.
|
||||||
|
|
||||||
|
### Research
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/research \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-secret-key" \
|
||||||
|
-d '{
|
||||||
|
"query": "state of open source vector databases",
|
||||||
|
"depth": "deep",
|
||||||
|
"search_depth": "basic",
|
||||||
|
"max_results_per_query": 5,
|
||||||
|
"output_format": "markdown"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
`depth: "basic"` generates 3 sub-queries. `depth: "deep"` generates 5 sub-queries. Sources are deduplicated by URL, and the final report is synthesized from the combined results. Use `search_depth: "basic"` for faster research, or `advanced` when scraped page content is worth the extra latency.
|
||||||
|
|
||||||
|
For long-running work, create a background job:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/research/jobs \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-secret-key" \
|
||||||
|
-d '{"query": "OpenAI", "search_depth": "basic"}'
|
||||||
|
|
||||||
|
curl http://localhost:8000/jobs/<job_id> \
|
||||||
|
-H "X-API-Key: your-secret-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Crawl
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/crawl \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-secret-key" \
|
||||||
|
-d '{
|
||||||
|
"url": "https://docs.example.com",
|
||||||
|
"max_depth": 1,
|
||||||
|
"max_pages": 20,
|
||||||
|
"include_raw_content": false,
|
||||||
|
"instructions": "python sdk"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
`/crawl` stays on the same hostname as the start URL, extracts each matching page, and returns discovered links. It is intentionally bounded by `CRAWL_MAX_PAGES` and `CRAWL_MAX_DEPTH`.
|
||||||
|
|
||||||
|
### Map
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/map \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-secret-key" \
|
||||||
|
-d '{
|
||||||
|
"url": "https://docs.example.com",
|
||||||
|
"max_depth": 2,
|
||||||
|
"max_pages": 50,
|
||||||
|
"instructions": "api reference"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
`/map` returns same-site URLs without extracting full content.
|
||||||
|
|
||||||
|
### Private Index
|
||||||
|
|
||||||
|
Ingest private documents:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/ingest \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-secret-key" \
|
||||||
|
-d '{
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"id": "runbook-1",
|
||||||
|
"title": "Production Runbook",
|
||||||
|
"content": "Internal deployment and rollback notes.",
|
||||||
|
"url": "private://runbook-1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Search only the private index:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/index/search \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-secret-key" \
|
||||||
|
-d '{"query": "rollback notes"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Normal `/search` includes private index hits by default. Set `"include_private": false` to use only web sources.
|
||||||
|
|
||||||
|
### Tavily-Compatible Routes
|
||||||
|
|
||||||
|
Axon exposes compatibility routes under `/tavily`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/tavily/search \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-secret-key" \
|
||||||
|
-d '{"query": "OpenAI", "max_results": 5}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Available compatibility routes:
|
||||||
|
- `/tavily/search`
|
||||||
|
- `/tavily/extract`
|
||||||
|
- `/tavily/crawl`
|
||||||
|
- `/tavily/map`
|
||||||
|
|
||||||
|
### Citation Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/validate/citations \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: your-secret-key" \
|
||||||
|
-d '{
|
||||||
|
"report": "OpenAI was founded in 2015 [1].",
|
||||||
|
"sources": [
|
||||||
|
{"title": "Source", "url": "https://example.com", "content": "OpenAI was founded in 2015."}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Search Source Logic
|
||||||
|
|
||||||
|
Axon fetches Brave and DuckDuckGo concurrently. Serper.dev is called only when Brave returns fewer than 5 results. All results are merged and deduplicated by URL before filtering, scraping, reranking, and response truncation.
|
||||||
|
|
||||||
|
If all enabled sources return no results, `/search` returns HTTP 503. Individual source failures are logged and treated as empty result sets.
|
||||||
|
|
||||||
|
DuckDuckGo uses the current `ddgs` package when installed and falls back to `duckduckgo-search` for compatibility.
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
- Search responses are cached for 10 minutes.
|
||||||
|
- Successful extracted URL content is cached for 1 hour.
|
||||||
|
- Redis is used when `REDIS_URL` is configured and reachable.
|
||||||
|
- If Redis is unavailable, Axon logs a warning and uses in-memory cache for the current process.
|
||||||
|
|
||||||
|
## Performance Notes
|
||||||
|
|
||||||
|
Advanced search is slower than basic search because it fetches and extracts each result page. Tune `MAX_CONCURRENT_SCRAPES` and `SCRAPE_TIMEOUT` for your server and network limits.
|
||||||
|
|
||||||
|
The reranker model is loaded once during FastAPI startup and reused for requests. Use a smaller cross-encoder model if startup time, memory use, or CPU latency is too high for your deployment.
|
||||||
|
|
||||||
|
Reranking is serialized through a process-local lock and capped to a small number of chunks per document. This keeps the self-hosted service stable on modest servers, at the cost of some recall on very long pages.
|
||||||
|
|
||||||
|
Optional browser rendering requires installing Playwright and browser assets separately, then setting `ENABLE_BROWSER_FALLBACK=true`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Run tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source venv/bin/activate
|
||||||
|
python -m pytest tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a focused test file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pytest tests/test_search_router.py -v
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user