Files
skillfactor-pipeline/pipeline/batch_run.py

107 lines
4.1 KiB
Python

"""Full-catalog batch runner — SKELETON (Fable 5, 2026-07-07).
Sonnet: implement the TODO functions, do not redesign the flow. The staging
logic, resume mechanics and budget enforcement are final; see
docs/batch-architecture.md for the design and data contracts.
Stages (per skillfactor_finalize.md / auftrag_sonnet.md):
1. packages: deterministic generator for ALL ESCO occupations (no LLM)
2. evidence: per occupation fetch (JSearch, budget-capped) -> extract
(Ollama, extract_local.py) -> aggregate (market.md)
3. depth: tier 1 = top-200 by evidence volume, tier 3 = night batch
Every step is idempotent and resumable: progress.json + file existence
decide what still needs work; an abort at any point loses at most the
current occupation's in-flight step.
Usage:
python pipeline/batch_run.py --stage packages [--limit N]
python pipeline/batch_run.py --stage evidence [--slugs a,b] [--limit N]
python pipeline/batch_run.py --stage tier # (re)compute tiers only
"""
import argparse
import os
import subprocess
import sys
sys.path.insert(0, os.path.dirname(__file__))
import progress
from db import connect
BASE = os.path.join(os.path.dirname(__file__), "..")
ADS_PER_OCC_TARGET = 60 # ~6 requests/occupation (1 req ~ 10 ads)
COUNTRIES = ("us", "gb")
REQ_RATE_SLEEP = 0.5 # ~2 req/s max (finalize order)
TOP_TIER_SIZE = 200
def esco_occupations(cur):
"""All ESCO occupations with slug + altLabels for query building."""
# TODO(sonnet): return [(slug, preferred_label, [alt_labels...]), ...]
# from esco_occupation; slug generation must match p2_generate.py
raise NotImplementedError
def stage_packages(state, limit=0):
"""Run the existing generator for every occupation without a package.
TODO(sonnet): refactor p2_generate.py so it can be called per-occupation
(it currently generates a fixed balanced set of 100). Reuse, don't fork.
Gitea pushes go through p4_publish.py, throttled (sleep >= 1s/repo,
resumable by checking repo existence via API before create)."""
raise NotImplementedError
def stage_evidence(state, slugs=None, limit=0):
"""Per occupation: fetch (cached) -> extract (Ollama) -> aggregate.
fetch: generalize p3a (queries = preferredLabel + top-3 altLabels x
COUNTRIES, cache data/raw/jobs/<slug>/, budget via
progress.spend_request BEFORE each HTTP call).
< 20 usable ads -> synonym fallback, then Adzuna (free), then
mark evidence="failed", error="thin market" and move on.
extract: subprocess extract_local.py --ads data/raw/jobs/<slug>_ads.json
--out data/evidence/<slug>.jsonl (already implemented + tested)
store: extend p3b: incremental MERGE keyed by (occupation_slug, job_id)
-- needs ALTER TABLE evidence_job ADD occupation_slug (see
docs/batch-architecture.md, backfill 'recruitment-consultant')
aggregate: generalize p3c per occupation (market.md + market-evidence
sections); QA: run qa_sample.py per batch, stop the run if the
harness exits 1 twice in a row (systemic problem, not noise).
"""
raise NotImplementedError
def stage_tier(state):
"""Rank occupations for depth staging: tier 1 = TOP_TIER_SIZE by
(ads collected, crosswalk strength, marketplace demand), rest tier 3.
Pure bookkeeping in progress.json — no API calls."""
raise NotImplementedError
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--stage", required=True,
choices=["packages", "evidence", "tier"])
ap.add_argument("--slugs", default="")
ap.add_argument("--limit", type=int, default=0)
args = ap.parse_args()
state = progress.load()
try:
if args.stage == "packages":
stage_packages(state, args.limit)
elif args.stage == "evidence":
slugs = [s for s in args.slugs.split(",") if s] or None
stage_evidence(state, slugs, args.limit)
else:
stage_tier(state)
except progress.BudgetExhausted as exc:
print(f"STOP: {exc}")
sys.exit(2)
finally:
progress.save(state)
if __name__ == "__main__":
main()