AODS extracts, clusters, and ranks recurring monetizable problems from public sources using local ML for scale, local-first LLMs for audited semantic enrichment, and deterministic scoring for decisions. The current V1 stack supports Reddit, Hacker News, Indie Hackers, and Product Hunt ingestion with inspectable intermediate artifacts.
The repo now uses a local-first LLM contract for semantic tasks. The default runtime decision is Ollama, with qwen3:8b as the primary model and gemma3:4b retained as the lighter fallback profile.
From the repository root:
npm startThis prepares missing .env files from the checked-in examples, starts Ollama when the local provider is configured and not already reachable, verifies configured Ollama models, starts the Docker Compose stack, builds missing images only when needed, applies Alembic migrations, waits for backend and frontend readiness, and prints the local URLs.
Prerequisites:
- Docker Desktop installed. The start script attempts to launch Docker Desktop when the engine is not already running.
- Node/npm available for the root script runner.
- Ollama installed on the host. The start script attempts to launch
ollama servewhenLLM_PROVIDER=ollamaandhttp://127.0.0.1:11434is not already reachable. Configured models such asqwen3:8bandgemma3:4bmust already be pulled.
The app will be ready at http://127.0.0.1:5173.
Useful root commands:
npm start
npm run rebuild
npm run status
npm stopUse npm run rebuild after Dockerfile, dependency, or Python service changes. Backend, worker, and scheduler all use one shared infra-python-app:latest image so the large Python/ML runtime is stored once. Normal npm start intentionally avoids --build when images already exist so DockerData does not accumulate large BuildKit cache on every app start.
Open http://127.0.0.1:5173 after npm start reports that AODS is ready. The main screen is an operator work surface with source controls on the left and an Opportunity inbox on the Dashboard.
Basic workflow:
- Choose an
Opportunity areafrom the dropdown in Source controls. The presets fill in starter source queries; useCustomonly when you already know the exact scope you want. - Click
Discover seedsto have AODS create and score source-registry seed candidates for the selected area across the supported ingestion sources. - Review discovered seeds in the
registrytab. Suggested seeds stay inspectable before they are used by registry-backed ingestion. - Click a source ingest button:
Ingestfor Reddit with the comma-separated subreddit list.HN ingest,IH ingest, orPH ingestfor Hacker News, Indie Hackers, or Product Hunt.Reddit registry ingestfor Reddit seeds from the active source registry snapshot.
- Wait for the new pipeline run to appear in Dashboard or Admin run history.
- Click
Run M1on the latest run to execute embeddings, dedupe, sampling, clustering, deterministic scoring, and output stages. - Inspect results in the right-side tabs:
dashboardshows the workflow, metrics, and a sortable Opportunity inbox. Click a row to expand AI descriptions, score details, and evidence posts. Rows markedWriteuphave audited final LLM output, rows markedSummaryhave an audited cluster summary but no final writeup yet, and rows markedPendingare deterministically scored clusters waiting for semantic output.clustersshows scored clusters and score breakdowns.raw datapages through persisted source records for the selected opportunity area and supports sorting by source, date, engagement, and duplicate status.registrybootstraps/reviews source seeds, queues registry-backed ingestion, inspects seed/source quality history, and supports sorting seed rows.evaluationscreates and compares persisted evaluation runs and label sets.adminshows runtime config, score distributions, threshold profiles, backtests, and pipeline run history.
The API is available at http://127.0.0.1:8000. Use http://127.0.0.1:8000/health for a quick backend readiness check.
Opportunity rows come from scored clusters first. The frequency value is the count of deduplicated raw posts assigned to that cluster in the selected run; AI writeups and summaries are attached when present through ai_status, but missing AI output no longer hides a scored opportunity from /api/opportunities.
| Command | What it does |
|---|---|
npm start |
Starts Docker Desktop and host Ollama when needed, verifies configured Ollama models, starts the AODS stack, creates missing env files, builds only missing Docker images, applies Alembic migrations, waits for backend/frontend readiness, and prints URLs. |
npm run rebuild |
Forces a Docker Compose rebuild before startup; use after Dockerfile, dependency, or service-image changes. |
npm run status |
Shows current Docker Compose service status for the AODS stack. |
npm run storage |
Reports DockerData VHDX size plus visible Docker image/container/volume/cache usage. |
npm stop |
Stops and removes AODS Compose containers and network while preserving volumes such as infra_postgres_data. |
npm run cleanup |
Stops the AODS stack, prunes Docker build cache while preserving images/volumes, enables sparse DockerData reclamation when supported, trims free blocks inside DockerData, shuts down Docker Desktop/WSL, and compacts Docker's WSL VHDX. |
docker compose -f infra\docker-compose.yml up -d |
Starts Compose services using existing images without forcing a rebuild. |
docker compose -f infra\docker-compose.yml up -d --build |
Starts Compose services and forces image builds; prefer npm run rebuild for the standard workflow. |
docker compose -f infra\docker-compose.yml down |
Stops/removes Compose containers and network; does not delete named volumes. |
docker compose -f infra\docker-compose.yml ps |
Lists AODS service containers and health/status. |
docker compose -f infra\docker-compose.yml exec backend alembic -c alembic.ini upgrade head |
Applies database migrations inside the backend container. |
docker builder prune -af |
Removes Docker BuildKit cache; keeps images, containers, and volumes. Useful after rebuilds if DockerData grew. |
docker image prune -af |
Removes unused images; next start may need to rebuild/pull images. Does not delete volumes. |
.\scripts\compact_dockerdata.ps1 |
Direct script form of npm run cleanup; pass -PruneImages only when intentionally deleting unused images too. |
.\scripts\docker_storage_status.ps1 |
Direct script form of npm run storage. |
- Copy environment examples:
Copy-Item infra\.env.example .env
Copy-Item frontend\.env.example frontend\.env- Start services:
docker compose -f infra\docker-compose.yml up --buildFor routine starts after images already exist, prefer:
docker compose -f infra\docker-compose.yml up -dThe local worker runs Celery with --concurrency=1 so the Docker stack can share host memory with Ollama running qwen3:8b.
- Apply migrations:
docker compose -f infra\docker-compose.yml exec backend alembic upgrade head- Open the app at
http://localhost:5173.
Useful checks:
docker compose -f infra\docker-compose.yml ps
Invoke-RestMethod -Uri http://127.0.0.1:8000/health
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/admin/debug-summary
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/admin/runtime-config
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/admin/score-distribution?niche=founder%20operations"
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/admin/score-distribution?niche=founder%20operations&alert_threshold=2.5&high_saturation_threshold=3.0"
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/admin/threshold-profilesSmall smoke ingestion:
$body = @{
niche = "smoke-test"
subreddits = @("Entrepreneur")
query = "problem"
limit_per_subreddit = 2
} | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/ingestion/reddit -Method Post -Body $body -ContentType "application/json"Hacker News ingestion:
$body = @{
niche = "developer tools"
query = "debugging workflow"
limit = 20
} | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/ingestion/hacker-news -Method Post -Body $body -ContentType "application/json"Registry-backed Hacker News ingestion uses active hacker_news query seeds:
$body = @{
niche = "developer tools"
limit = 20
} | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/ingestion/hacker-news/from-registry -Method Post -Body $body -ContentType "application/json"Indie Hackers ingestion uses the public Indie Hackers RSS feed bridge and accepts either a search query, a group slug, or both:
$body = @{
niche = "founder operations"
query = "workflow"
group = "growth"
limit = 20
} | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/ingestion/indie-hackers -Method Post -Body $body -ContentType "application/json"Registry-backed Indie Hackers ingestion uses active indie_hackers query, tag_query, search_query, or tag seeds:
$body = @{
niche = "founder operations"
limit = 20
} | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/ingestion/indie-hackers/from-registry -Method Post -Body $body -ContentType "application/json"Product Hunt ingestion uses the public Product Hunt Atom/RSS feed and filters feed items locally when a query is supplied:
$body = @{
niche = "founder operations"
query = "workflow automation"
limit = 20
} | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/ingestion/product-hunt -Method Post -Body $body -ContentType "application/json"Registry-backed Product Hunt ingestion uses active product_hunt query or search_query seeds:
$body = @{
niche = "founder operations"
limit = 20
} | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/ingestion/product-hunt/from-registry -Method Post -Body $body -ContentType "application/json"If Docker is unavailable but Python dependencies are installed in .venv, use:
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r .\backend\requirements.txt
.\scripts\run_backend_tests.ps1
.\scripts\run_backend.ps1
.\scripts\run_worker.ps1The local scripts set PYTHONPATH to include both backend/ and the repo root so the FastAPI app and worker package resolve consistently.
The backend still expects PostgreSQL + pgvector and Redis for DB-backed APIs and Celery execution. Use Docker Compose or local services on ports 5432 and 6379.
For local semantic tasks, run Ollama locally and make it available at OLLAMA_BASE_URL, then pull the configured models such as qwen3:8b and gemma3:4b.
Bootstrap and inspect the source registry through the backend API:
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/source-registry/bootstrap -Method Post
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/source-registry/sources
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/source-registry/seeds?niche=founder%20operations"
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/source-registry/quality-summary?niche=founder%20operations"
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/source-registry/quality-trends?niche=founder%20operations"
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/source-registry/seeds/<seed-id>/score-history"
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/source-registry/seeds/<seed-id>/lifecycle-history"
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/source-registry/sources/<source-id>/lifecycle-history"
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/source-registry/sources/<source-id>/reliability-history"The quality summary, trends, and score history are deterministic. They report latest seed score coverage, status/source counts, per-source average latest score, per-source low-quality counts, daily source score trends, low-scoring non-pinned seeds, unscored seeds, and per-seed score trends without using LLM output for scoring or ranking.
Seed lifecycle edits append audit entries under source_seeds.seed_metadata.lifecycle_history and can be inspected through the lifecycle-history endpoint or Registry tab.
Source lifecycle edits append audit entries under source_registry.source_metadata.lifecycle_history and can be inspected through the source lifecycle-history endpoint or Registry tab.
Source reliability edits are persisted on source_registry.reliability_score, append audit entries under source_metadata.reliability_history, and can be inspected through the reliability-history endpoint or Registry tab.
The frontend Registry tab exposes the manual async rescore downgrade threshold before queueing seed rescoring. The Admin tab shows runtime config, including dedupe and clustering thresholds, deterministic scoring weights, local model names, and recurring rescore settings.
The Admin tab also shows deterministic opportunity-score distribution by source for the selected niche, including min, average, p50, p90, max, and alert-threshold counts. The alert and saturation thresholds can be adjusted in the Admin tab or passed as alert_threshold and high_saturation_threshold query parameters to preview threshold impact without changing persisted scores or rankings.
Candidate threshold settings can be saved as inspectable threshold profiles:
$body = @{
profile_name = "founder-ops-sensitive-review"
status = "draft"
niche = "founder operations"
source = $null
alert_threshold = 2.5
high_saturation_threshold = 3.0
min_frequency = 5
min_confidence = 0.6
profile_metadata = @{
reason = "manual admin review"
}
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/admin/threshold-profiles -Method Post -Body $body -ContentType "application/json"Backtest a saved profile against persisted deterministic scores, latest competitor saturation snapshots, and the current default alert predicate:
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/admin/threshold-profiles/<profile-id>/backtest"The backtest response includes source-level count deltas and a bounded changed_clusters list of clusters that would newly trigger or stop triggering under the profile.
Apply an approved threshold profile to a specific pipeline run as separate profile-scoped alert events:
$body = @{
pipeline_run_id = "<pipeline-run-id>"
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/admin/threshold-profiles/<profile-id>/apply-alerts" -Method Post -Body $body -ContentType "application/json"Saved profiles are tuning proposals. Approving, backtesting, or applying a profile records and previews review state but does not silently change deterministic scores, rankings, or default alert execution. Applied profile alerts use alert_type = threshold_profile and retain the profile id in threshold_snapshot.
Run the deterministic fixture harness without a database:
.\scripts\run_evaluation_fixtures.ps1Use fixture-defined quality gates for CI-style regression checks:
.\scripts\run_evaluation_fixtures.ps1 --enforce-thresholdsCompare candidate fixtures against a baseline fixture directory with matching filenames:
.\scripts\run_evaluation_fixtures.ps1 --fixtures .\shared\evals --baseline-fixtures .\shared\evals --max-regression 0.01The command prints metric deltas and exits nonzero when any metric regresses by more than --max-regression.
To persist eval runs and metric rows, apply migrations and pass --persist:
docker compose -f infra\docker-compose.yml exec backend alembic upgrade head
.\scripts\run_evaluation_fixtures.ps1 --persistPersisted eval runs include fixture quality-gate summaries in result_summary, and the frontend Evaluations tab shows whether gates passed, failed, or were not configured.
Persisted eval runs can also be compared through the API:
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/evaluations/runs/<candidate-run-id>/compare/<baseline-run-id>Manual labels can be persisted and reused by later live eval runs:
$body = @{
label_type = "ranking_relevance"
dataset_name = "manual-ranking-review"
pipeline_run_id = "<pipeline-run-id>"
labels = @{
"<cluster-id>" = 2
}
} | ConvertTo-Json -Depth 5
$labelSet = Invoke-RestMethod -Uri http://127.0.0.1:8000/api/evaluations/label-sets -Method Post -Body $body -ContentType "application/json"
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/evaluations/label-sets?label_type=ranking_relevance&pipeline_run_id=<pipeline-run-id>"Export a portable label-set artifact and import it for another run:
$artifact = Invoke-RestMethod -Uri http://127.0.0.1:8000/api/evaluations/label-sets/$($labelSet.id)/export
$body = @{
artifact = $artifact
pipeline_run_id = "<target-pipeline-run-id>"
dataset_name = "imported-ranking-review"
label_metadata = @{
imported_by = "manual-review"
}
} | ConvertTo-Json -Depth 8
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/evaluations/label-sets/import -Method Post -Body $body -ContentType "application/json"Mark a label set as reviewed, approved, rejected, or draft and filter by review status:
$body = @{
review_status = "approved"
reviewed_by = "manual-review"
review_note = "labels checked against source records"
} | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/evaluations/label-sets/$($labelSet.id)/review-status -Method Patch -Body $body -ContentType "application/json"
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/evaluations/label-sets?review_status=approved"Bulk-mark label sets during review:
$body = @{
label_set_ids = @("<left-label-set-id>", "<right-label-set-id>")
review_status = "reviewed"
reviewed_by = "manual-review"
review_note = "pair checked together"
} | ConvertTo-Json
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/evaluations/label-sets/review-status -Method Post -Body $body -ContentType "application/json"Compare two label sets before reusing or approving labels:
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/evaluations/label-sets/<left-label-set-id>/diff/<right-label-set-id>Find conflicts across saved label sets for a pipeline run:
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/evaluations/label-sets/conflicts?label_type=clustering_expected&pipeline_run_id=<pipeline-run-id>&review_status=approved"Resolve conflicts into a new label set by supplying explicit values for disputed label ids:
$body = @{
label_type = "clustering_expected"
dataset_name = "resolved-clustering-review"
pipeline_run_id = "<pipeline-run-id>"
source_label_set_ids = @("<left-label-set-id>", "<right-label-set-id>")
resolutions = @{
"<raw-post-id>" = "resolved-cluster-label"
}
} | ConvertTo-Json -Depth 6
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/evaluations/label-sets/resolve-conflicts -Method Post -Body $body -ContentType "application/json"In the frontend conflict-resolution panel, resolution inputs are parsed as JSON when possible, so values such as 2, true, or {"domain":"ops","pain_level":4} are preserved as typed labels.
Validate that a label set points at artifacts in a pipeline run:
Invoke-RestMethod -Uri "http://127.0.0.1:8000/api/evaluations/label-sets/<label-set-id>/validate?pipeline_run_id=<pipeline-run-id>"Create a ranking-usefulness eval from a completed pipeline run's deterministic score order by supplying explicit relevance labels:
$body = @{
pipeline_run_id = "<pipeline-run-id>"
dataset_name = "manual-ranking-review"
label_set_id = $labelSet.id
top_n = 5
relevance_grades = @{
"<cluster-id>" = 2
}
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/evaluations/runs/from-pipeline-ranking -Method Post -Body $body -ContentType "application/json"Create an extraction field-accuracy eval from persisted extracted_data by supplying expected labels keyed by raw post id:
$body = @{
pipeline_run_id = "<pipeline-run-id>"
dataset_name = "manual-extraction-review"
fields = @("domain", "pain_level")
expected_by_raw_post_id = @{
"<raw-post-id>" = @{
domain = "finance ops"
pain_level = 4
}
}
} | ConvertTo-Json -Depth 6
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/evaluations/runs/from-pipeline-extraction -Method Post -Body $body -ContentType "application/json"Create a clustering-quality eval from persisted cluster_assignments by supplying expected cluster labels keyed by raw post id:
$body = @{
pipeline_run_id = "<pipeline-run-id>"
dataset_name = "manual-clustering-review"
expected_cluster_by_raw_post_id = @{
"<raw-post-id>" = "invoice_reconciliation"
}
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri http://127.0.0.1:8000/api/evaluations/runs/from-pipeline-clustering -Method Post -Body $body -ContentType "application/json"- Backend API:
http://localhost:8000 - Frontend:
http://localhost:5173 - PostgreSQL + pgvector:
localhost:5432 - Redis:
localhost:6379
- Reddit, Hacker News, Indie Hackers, and Product Hunt source ingestion.
- Local sentence-transformer embeddings.
- Cosine-threshold deduplication.
- HDBSCAN with deterministic fallback clustering.
- Deterministic score breakdown persistence.
- Selective local-first LLM extraction, cluster summaries, final writeups, and audited
llm_outputs. - Dashboard, cluster explorer, raw data viewer, evaluations, registry review, and admin/debug summary.
The deterministic Milestone 1 stages remain valid without LLM execution, but the current V1 pipeline also includes local-first semantic stages after scoring. Scoring and ranking remain deterministic.