Enrichment configs (the enrich-worker harness)
The corpus is decorated by two independent enrichment harnesses. This page
documents the second one — enrich-worker — which the older
enrichment.md (the queue / fraud-enricher-v2 system)
does not cover.
Read
enrichment.mdfirst for the queue pipeline (fraud-publisher→fraud.rows→fraud-enricher-v2). This page is the companion for the config-loop pipeline.
Two harnesses, one corpus
Both write through fraud-db-broker into the single corpus.db on
node-eighteen. They are wired completely differently:
Queue harness (fraud-enricher-v2) |
Config-loop harness (enrich-worker) |
|
|---|---|---|
| Source | services/fraud-enricher-v2/ + services/fraud-publisher/ |
enrich-worker/src/ |
| Unit of work | a workflow (WorkflowDef) |
an EnrichConfig |
| Trigger | RabbitMQ envelope popped off fraud.rows |
in-process for(;;) loop over ENRICH_CONFIGS |
| Producer | fraud-publisher sweeps + publishes |
none — the worker self-selects via selectorSql |
| Concurrency model | N stateless consumers share one queue | one daemon, per-config concurrency cap |
| Scales by | adding consumer replicas | tuning concurrency / batchSize per config |
| Writes via | broker /update or /apply/* |
broker /apply/*, or the enrichments table |
| Idempotency | workflow_runs terminal-status join |
selectorSql guards on the target column / enrichments NOT EXISTS |
| Service | fraud_fraud-enricher-v2 (many replicas) |
fraud_fraud-enrich-worker (1 replica) |
Rule of thumb: if the output is a deterministic resolve (CIK lookup,
matter grouping), a network call (CourtListener), or a bespoke LLM prompt that
needs its own pool/parse/apply shape, it belongs in enrich-worker. If it is a
per-document regex/LLM decorate that fits the WorkflowDef contract, it belongs
in the queue harness.
Anatomy of an EnrichConfig
Defined in enrich-worker/src/config.ts. Every config implements:
export interface EnrichConfig {
name: string; // enrichment_name — log label; not written when apply is set
promptVersion: number; // bump to re-enrich every row
pool: string; // gpumon pool to route the LLM call to
parse: "text" | "json";// how to interpret the LLM content
concurrency: number; // in-flight cap (shares the NIM keypool)
batchSize: number; // rows fetched per cycle
selectorSql: string; // SELECT that returns rows to enrich — MUST select id
buildBody(row): Record<string, unknown>; // OpenAI chat body for one row
attribution(row): Attribution; // envelope attribution headers
apply?: ApplyHook; // POST result to a broker /apply/* endpoint
resolveLocal?(row): ...; // LOCAL resolver — skip gpumon dispatch entirely
}
Three config flavors
The worker (enrich-worker/src/index.ts) branches on which optional hooks are
set:
Deterministic (
resolveLocal) — no GPU, no network LLM.processRowcallscfg.resolveLocal(row)and POSTs the returned{endpoint, apply}to the broker. Returnnullto mark the row processed without writing. Examples:cik_resolve(flat-file CIK lookup),matters_build(group docs bycase_number).Network / non-LLM apply (
apply+resolveLocal) — a deterministic call that hits an external service. Example:courtlistener_caption(case_number→ caption via the CourtListener API).LLM (
pool+buildBody+parse) — dispatched to agpumon-ingresspool. Withapplyset, the parsed result is POSTed to/apply/*; without it, the result is upserted into theenrichmentstable. Examples:scheme_classify,bluebook_citation,matter_title,caption_body_extract.
The selectorSql bind-param contract
runConfigCycle binds parameters based on the config flavor — get this wrong
and the worker either re-processes every row forever or selects nothing:
| Flavor | Bind params | selectorSql must guard on |
|---|---|---|
resolveLocal set |
[batchSize] |
target column(s) IS NULL |
apply set (no resolveLocal) |
[batchSize] |
target column IS NULL |
| classic (neither) | [name, promptVersion, batchSize] |
enrichments NOT EXISTS sub-select |
The worker opens a fresh read-only corpus.db handle each loop iteration so
it sees the current WAL view of broker writes (cross-container WAL gotcha — a
long-lived handle goes stale).
Registered configs
From ENRICH_CONFIGS in enrich-worker/src/config.ts (execution order):
| Config | Flavor | Pool / source | Writes |
|---|---|---|---|
matters_build |
deterministic | — | matters (groups by case_number) |
cik_resolve |
deterministic | flat-file CIK index | documents.cik* |
courtlistener_caption |
network | CourtListener API | caption, case metadata |
caption_body_extract |
LLM | pool-postcrime-triples |
caption (CL-miss fallback) |
anomaly_explanation |
LLM (classic) | gpumon pool | enrichments |
title_cased |
LLM | gpumon pool | title casing |
scheme_classify |
LLM | pool-postcrime-triples |
scheme* |
bluebook_citation |
LLM | gpumon pool | bluebook cite |
entity_ticker_cik |
deterministic | ticker/name → CIK | entity CIK |
matter_title |
LLM | gpumon pool | matter title (≤10 words) |
scheme_classifyexists in both harnesses. The queuescheme-classifyworkflow is the primary path; theenrich-workervariant is the older config-loop implementation. Avoid running both against the same rows.
Adding a new enrichment config
- Create
enrich-worker/src/configs/<name>.tsexporting oneEnrichConfig. Pick the flavor (resolveLocal / apply / classic LLM) and write aselectorSqlthat follows the bind-param contract above. - If it writes a dedicated column (not
enrichments), add a broker/apply/<name>endpoint inbroker/src/handlers.tsand whitelist the target columns. - Register it: import in
enrich-worker/src/config.tsand add to theENRICH_CONFIGSarray in the desired execution slot (deterministic configs first — they are free and unblock LLM configs that depend on their output). - Test the bind count (
enrich-worker/src/configs/apply-hook.test.tsasserts?-placeholder counts) and runbun testinenrich-worker/. - Migrate any new columns into
corpus.dbvia a broker migration (broker/src/migrations/). - Deploy — see below.
Deploy mechanics
enrich-worker runs as fraud_fraud-enrich-worker, pinned to node-eighteen,
1 replica, mem limit 1.5 GB (the CIK flat-file map is memory-heavy — do not
lower). There is no Docker registry; after rebuild you must
docker save | ssh ... docker load to node-eighteen, then force-update:
docker save fraud-processor:latest | ssh rooot@node-eighteen docker load
ssh rooot@node-eleven 'docker service update --force fraud_fraud-enrich-worker'
Confirm by grepping a marker string from the new code inside the running
container — :latest is mutable and a stale image will look successful.
Throughput / gentleness
Per-config concurrency and batchSize are the throttle. LLM configs share the
pool-postcrime-triples NIM keypool with the queue harness, so keep concurrency
modest (history: concurrency ≤ 8 to avoid broker flooding). Deterministic
configs (resolveLocal) cost nothing but broker write bandwidth. When the loop
finds zero rows across all configs it sleeps IDLE_SLEEP_MS — so a fully-drained
worker idles cheaply at 1 replica.