Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 29 additions & 86 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,93 +1,36 @@
# Copy to .env and fill in. Defaults are dev-only.
#
# --- Required secrets (compose refuses to start while these are empty) ---
# Generate each value with: openssl rand -hex 32
# Never commit real values; .env is gitignored.
# SearXNG server secret (signs its own cookies/CSRF tokens).
# Several engines by design: running on one meant a single CAPTCHA blocked
# all acquisition (HUB-042). Keyless engines only; google needs an API key.
SEARXNG_ENGINES=duckduckgo,bing,brave,startpage,mojeek,qwant
# Delay between a plan's searches. Bursts are what trigger CAPTCHAs.
SEARCH_PACING_SECONDS=2.0
# Optional keyed search fallback, used only when every SearXNG engine returns
# nothing. Leave blank to run on SearXNG alone. Never commit a real key.
SERPER_API_KEY=
SEARXNG_SECRET=
# Shared bearer token between research-hub/research-worker and the Crawl4AI API.
CRAWL4AI_API_TOKEN=
# MiniMax Token Plan Subscription Key (HUB-035 judge gate). Copy from the
# MiniMax platform account page; never commit the real value.
MINIMAX_SUBSCRIPTION_KEY=
# Optional (webui profile only): leave empty and Open WebUI generates a random
# key on first start, persisted in its data volume. Set only if sessions must
# survive a volume wipe.
WEBUI_SECRET_KEY=
# Copy to .env only if you want to change a default. Every variable below has
# a working default in docker-compose.yml, so the stack runs with no .env at
# all. There are no secrets left in this stack.

EMBEDDING_MODEL=nomic-embed-text
EMBEDDING_DIMENSION=768
LLM_MODEL=qwen3.5:9b
# --- Ollama runtime -------------------------------------------------------
# How long a model stays resident after its last request.
OLLAMA_KEEP_ALIVE=30m
# Context window offered to models. Larger costs VRAM you may not have.
OLLAMA_CONTEXT_LENGTH=16384
# One model, one request at a time. On a single consumer GPU, concurrency
# trades latency for nothing. Raise deliberately.
OLLAMA_NUM_PARALLEL=1
OLLAMA_MAX_LOADED_MODELS=1
WORKER_LEASE_SECONDS=60
WORKER_HEARTBEAT_SECONDS=15
JOB_TIMEOUT_SECONDS=1800
JOB_MAX_ATTEMPTS=3
EMBEDDING_BATCH_SIZE=16
EMBEDDING_BATCH_CHARS=12000
DEPENDENCY_MAX_ATTEMPTS=3
MODEL_CONTEXT_TOKENS=16384
ANSWER_RESERVE_TOKENS=2048
ALLOW_CUSTOM_SYSTEM_PROMPTS=false
RESPECT_ROBOTS_TXT=true
# Fetched documents larger than this are rejected (SSRF/resource guard).
CRAWL_MAX_MARKDOWN_CHARS=2000000
REPORT_RETRIEVAL_CANDIDATES=120
REPORT_MAX_CHUNKS_PER_SOURCE=3
# Synthesis breadth. With a planned corpus of 30+ sources these caps, not the
# corpus, limit how much a report can say. Each drafted span and pair costs one
# metered judge call, so raising them raises metered cost proportionally.
REPORT_MAX_SPAN_CLAIMS=40
REPORT_MAX_PAIR_CLAIMS=20
REPORT_MAX_FINDINGS=40
REPORT_MAX_DISAGREEMENTS=4
REPORT_MAX_CORRECTIONS=6
# Disabled until retrieval evaluation establishes a justified threshold.
REPORT_RETRIEVAL_MIN_SCORE=
# Adaptive query planning (HUB-024, stage 1). Off until measured against the
# single-query baseline. Breadth is emergent: a candidate facet is admitted
# only while its cosine similarity to the admitted set stays below
# PLAN_FACET_DISTINCT, so a narrow topic collapses to exactly one search.
# PLAN_MAX_FACETS and the budgets are rails, not the breadth mechanism.
REPORT_QUERY_PLANNING=false
PLAN_FACET_DISTINCT=0.85
# The relevance half of the two-sided admission bar: a facet must be both
# distinct from the admitted set AND still about the topic. Calibrated from
# the 2026-08-13 runs: on-topic facets measured 0.61-0.84 while the one facet
# that pulled an off-topic source measured 0.52.
PLAN_FACET_RELEVANCE=0.55
# HUB-038: the facet floor admits queries, not the documents they return.
# Screens retained documents against the topic before ingestion. Calibrated on
# a 494-document labelled reference set: keeps 98.2% of on-topic documents
# while removing 35.5% of off-topic ones.
PLAN_SOURCE_RELEVANCE=0.54
PLAN_MAX_FACETS=12
# Rounds are gap-driven and stop when a round covers no additional facet.
# That rule needs no tuned threshold, so PLAN_NOVELTY_MIN is retired:
# document novelty could never fire as a stop signal, because admitting only
# distinct queries guarantees distinct documents. PLAN_MAX_ROUNDS is a backstop.
PLAN_MAX_ROUNDS=4
PLAN_SEARCH_BUDGET=24
PLAN_CRAWL_BUDGET=150
# Claim gate (HUB-034): the MiniMax M3 LLM-as-judge faithfulness gate,
# validated by the sealed v4 blind evaluation (2026-08-12). The NLI stack is
# retired. Judged evidence spans leave the machine for the MiniMax API.
JUDGE_BASE_URL=https://api.minimax.io/v1
JUDGE_MODEL=MiniMax-M3
JUDGE_TIMEOUT_SECONDS=180
# Host interface for Ollama's published API. Keep localhost unless remote access
# is intentional; use a specific trusted LAN/Tailscale IP instead of 0.0.0.0.

# --- Ports and bind addresses ---------------------------------------------
# Everything binds to localhost. Point a *_BIND_ADDRESS at a Tailscale or
# trusted-LAN address only when remote access is actually intended -- and turn
# WEBUI_AUTH on first.
OLLAMA_BIND_ADDRESS=127.0.0.1
# Docker Desktop's model runner commonly reserves host port 11434.
OLLAMA_HOST_PORT=11435
WEBUI_BIND_ADDRESS=127.0.0.1
WEBUI_HOST_PORT=8080
GRAFANA_BIND_ADDRESS=127.0.0.1
GRAFANA_HOST_PORT=3000
PROMETHEUS_BIND_ADDRESS=127.0.0.1
PROMETHEUS_HOST_PORT=9090
DOZZLE_BIND_ADDRESS=127.0.0.1
DOZZLE_HOST_PORT=9999

# --- Open WebUI -----------------------------------------------------------
# Off by operator decision for a single-user localhost box. Set to true before
# exposing port 8080 anywhere.
WEBUI_AUTH=false

# --- Metrics --------------------------------------------------------------
PROMETHEUS_RETENTION=15d
88 changes: 19 additions & 69 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# CI gate (HUB-012): syntax/lint, the unit+Redis suite, Compose validation,
# and the research-hub image build. The live benchmarks (report retrieval,
# claim drafting, exact-term hybrid) stay local — they need Ollama and the
# retained corpus.
# CI for a Compose-only repository. There is no application source left to
# lint or test after the 2026-08-14 teardown, so this validates the two things
# that can still be wrong: the Compose file and the observability configs.
name: CI

on:
Expand All @@ -10,77 +9,28 @@ on:
pull_request:

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install ruff
run: pip install ruff
- name: Syntax and error lint
run: ruff check --select E9,F research-hub/app research-hub/tests
- name: Byte-compile application
run: python -m compileall -q research-hub/app

test:
runs-on: ubuntu-latest
services:
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: research-hub/requirements.lock.txt
- name: Install locked dependencies
run: pip install --require-hashes -r research-hub/requirements.lock.txt
- name: Install test runner
run: pip install pytest
- name: Run the suite (includes Redis DB 15 integration)
working-directory: research-hub
env:
TEST_REDIS_URL: redis://localhost:6379/15
# Offline placeholder: config requires a key for the judge gate;
# no test makes a metered call.
MINIMAX_SUBSCRIPTION_KEY: ci-placeholder
run: python -m pytest tests -q

compose:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate compose configuration
env:
# Placeholders satisfy the required ${VAR:?} expansions; real values
# live only in the deployment's .env.
SEARXNG_SECRET: ci-placeholder
CRAWL4AI_API_TOKEN: ci-placeholder
MINIMAX_SUBSCRIPTION_KEY: ci-placeholder
- name: Validate Compose
# Never print interpolated output -- it would echo .env values.
run: docker compose config --quiet

build:
observability-config:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build research-hub image
uses: docker/build-push-action@v6
with:
context: research-hub
load: true
tags: research-hub:ci
cache-from: type=gha
cache-to: type=gha,mode=max
- name: pip check in built image
run: docker run --rm --entrypoint pip research-hub:ci check
- name: Check Prometheus config and alert rules
# Mounted at /etc/prometheus, not an arbitrary path: prometheus.yml
# refers to its rule file by absolute path, so promtool only resolves
# it when the layout matches the container's.
run: |
docker run --rm --entrypoint promtool \
-v "$PWD/observability:/etc/prometheus:ro" prom/prometheus:v3.1.0 \
check config /etc/prometheus/prometheus.yml
- name: Check dashboard JSON parses
run: |
for f in observability/grafana/dashboards/*.json; do
python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$f"
done
Loading
Loading