Self-hosted Docker Compose stack for DGX Spark (ARM64/aarch64, CPU-only): - SearXNG on port 8889 with JSON API enabled, rate limiting off - Firecrawl (built from local source) on port 3002, no API key required - HHEM API (FastAPI + vectara/hallucination_evaluation_model) on port 8881 - Portainer stack YAML using dgx_net external network - build-images.sh to pre-build local images before Portainer deploy - HF model cache bind-mounted from /home/sparky/LLMs/huggingface Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
242 lines
5.5 KiB
Markdown
242 lines
5.5 KiB
Markdown
# AI Tools Stack — SearXNG + Firecrawl + HHEM
|
||
|
||
Self-hosted Docker Compose stack for the DGX Spark (ARM64/aarch64, CPU-only).
|
||
Runs alongside the existing n8n automation stack and exposes three services for agent use.
|
||
|
||
## Quick Start
|
||
|
||
```bash
|
||
cd ~/Docker/searXNG\ Firecrawl\ HHEM\ \
|
||
|
||
# First run: build Firecrawl from source (takes ~5–10 min)
|
||
docker compose build
|
||
|
||
# Start everything
|
||
docker compose up -d
|
||
|
||
# Check status
|
||
docker compose ps
|
||
```
|
||
|
||
## Services & Ports
|
||
|
||
| Service | Port | Container name | Purpose |
|
||
|------------|------|-------------------|---------|
|
||
| SearXNG | 8888 | `searxng` | Meta-search (JSON API) |
|
||
| Firecrawl | 3002 | `firecrawl-api` | Web scraper / crawler |
|
||
| HHEM API | 8881 | `hhem-api` | Hallucination evaluator |
|
||
|
||
All services share the `ai-tools` Docker network.
|
||
n8n reaches them at `http://host.docker.internal:<port>` (or `http://<DGX-IP>:<port>`).
|
||
|
||
---
|
||
|
||
## 1. SearXNG — Meta-Search
|
||
|
||
**Base URL:** `http://localhost:8888`
|
||
|
||
### Search (JSON)
|
||
|
||
```
|
||
GET /search?q=<query>&format=json&categories=general
|
||
```
|
||
|
||
**Example curl:**
|
||
```bash
|
||
curl "http://localhost:8888/search?q=Claude+AI&format=json" | jq '.results[0]'
|
||
```
|
||
|
||
**Response shape:**
|
||
```json
|
||
{
|
||
"query": "Claude AI",
|
||
"results": [
|
||
{
|
||
"title": "...",
|
||
"url": "https://...",
|
||
"content": "...",
|
||
"engine": "google",
|
||
"score": 1.0
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### n8n HTTP Request node
|
||
|
||
| Field | Value |
|
||
|-------|-------|
|
||
| Method | GET |
|
||
| URL | `http://host.docker.internal:8888/search` |
|
||
| Query params | `q` = `{{ $json.query }}`, `format` = `json`, `categories` = `general` |
|
||
| Response | JSON |
|
||
|
||
---
|
||
|
||
## 2. Firecrawl — Web Scraper
|
||
|
||
**Base URL:** `http://localhost:3002`
|
||
**No API key required** (self-hosted, `USE_DB_AUTHENTICATION=false`).
|
||
|
||
### Scrape a single URL
|
||
|
||
```
|
||
POST /v1/scrape
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"url": "https://example.com",
|
||
"formats": ["markdown"]
|
||
}
|
||
```
|
||
|
||
**Example curl:**
|
||
```bash
|
||
curl -X POST http://localhost:3002/v1/scrape \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"url": "https://example.com", "formats": ["markdown"]}'
|
||
```
|
||
|
||
**Response shape:**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"markdown": "# Example Domain\n\n...",
|
||
"metadata": {
|
||
"title": "Example Domain",
|
||
"sourceURL": "https://example.com"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### Crawl a site (async)
|
||
|
||
```
|
||
POST /v1/crawl
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"url": "https://example.com",
|
||
"limit": 10,
|
||
"scrapeOptions": { "formats": ["markdown"] }
|
||
}
|
||
```
|
||
|
||
Returns a `jobId`. Poll `GET /v1/crawl/<jobId>` for results.
|
||
|
||
### n8n HTTP Request node (scrape)
|
||
|
||
| Field | Value |
|
||
|-------|-------|
|
||
| Method | POST |
|
||
| URL | `http://host.docker.internal:3002/v1/scrape` |
|
||
| Body (JSON) | `{"url": "{{ $json.url }}", "formats": ["markdown"]}` |
|
||
| Response | JSON |
|
||
|
||
### Queue admin UI
|
||
|
||
`http://localhost:3002/admin/aitools-bull-changeme/queues`
|
||
|
||
---
|
||
|
||
## 3. HHEM API — Hallucination Evaluator
|
||
|
||
**Base URL:** `http://localhost:8881`
|
||
**Model:** `vectara/hallucination_evaluation_model` (183 M BERT-based, CPU-only)
|
||
|
||
> On first start the model is downloaded from HuggingFace (~700 MB).
|
||
> Cached in the `hhem-model-cache` volume for subsequent restarts.
|
||
|
||
### Score
|
||
|
||
```
|
||
POST /score
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"source": "The Eiffel Tower is in Paris, France.",
|
||
"generated": "The Eiffel Tower is located in Paris."
|
||
}
|
||
```
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"score": 0.9741,
|
||
"label": "grounded"
|
||
}
|
||
```
|
||
|
||
| Score range | Meaning |
|
||
|-------------|---------|
|
||
| > 0.5 | **Grounded** — generated text is faithful to source |
|
||
| ≤ 0.5 | **Hallucinated** — generated text contradicts or invents facts |
|
||
|
||
**Health check:**
|
||
```bash
|
||
curl http://localhost:8881/health
|
||
# {"status":"ok","model":"vectara/hallucination_evaluation_model","loaded":true}
|
||
```
|
||
|
||
### n8n HTTP Request node
|
||
|
||
| Field | Value |
|
||
|-------|-------|
|
||
| Method | POST |
|
||
| URL | `http://host.docker.internal:8881/score` |
|
||
| Body (JSON) | `{"source": "{{ $json.scrapedContent }}", "generated": "{{ $json.llmAnswer }}"}` |
|
||
| Response | JSON |
|
||
|
||
---
|
||
|
||
## n8n Agentic Pipeline Example
|
||
|
||
This stack is designed for a **Search → Scrape → Validate** pipeline:
|
||
|
||
```
|
||
1. [HTTP Request] → SearXNG: search for the user's query, extract top URL
|
||
2. [HTTP Request] → Firecrawl /v1/scrape: scrape the top result into markdown
|
||
3. [LLM Node] → Generate an answer grounded in the scraped content
|
||
4. [HTTP Request] → HHEM /score: validate the answer against the scraped source
|
||
5. [If] → Route: score > 0.5 → return answer | score ≤ 0.5 → flag/retry
|
||
```
|
||
|
||
### n8n expression for step 4 body:
|
||
```json
|
||
{
|
||
"source": "{{ $('Firecrawl Scrape').item.json.data.markdown }}",
|
||
"generated": "{{ $('LLM').item.json.text }}"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Build Notes (ARM64 / aarch64)
|
||
|
||
- **SearXNG**: official image is multi-arch (ARM64 supported natively).
|
||
- **Redis**: `redis:7-alpine` is multi-arch.
|
||
- **Firecrawl**: built from source at `/home/sparky/Docker/firecrawl/git/firecrawl/`.
|
||
Dockerfile targets Node 22 slim which supports ARM64.
|
||
- **HHEM API**: `python:3.11-slim` + PyTorch CPU. PyPI provides `linux_aarch64` wheels for torch 2.x.
|
||
|
||
## Rebuilding after Firecrawl updates
|
||
|
||
```bash
|
||
git -C ~/Docker/firecrawl/git/firecrawl pull
|
||
docker compose build firecrawl-playwright firecrawl-api
|
||
docker compose up -d firecrawl-playwright firecrawl-api
|
||
```
|
||
|
||
## Connecting n8n to this stack
|
||
|
||
If n8n does not already have `host.docker.internal` available, add to the n8n service in its compose file:
|
||
|
||
```yaml
|
||
extra_hosts:
|
||
- "host.docker.internal:host-gateway"
|
||
```
|
||
|
||
Then use `http://host.docker.internal:<port>` in all n8n HTTP Request nodes.
|