SPB Git

spb/airiskindex Public

The most methodologically rigorous, fully transparent AI job-exposure index.

TypeScript 88% Python 6.1% SQL 2.7% CSS 1.2% JavaScript 0.9% Shell 0.8%
4.2 KB · 118 lines typescript
Raw Blame History
1//  File:    rate.ts2//  Path:    apps/worker/src/scripts/rate.ts3//  Project: AI Risk Index — airiskindex.io4//  Author:  Simon-Pierre Boucher5//  Contact: contact@spboucher.ai6//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.7//8//  Description: One-shot parallel rating run with per-model resume via state file.910import { prisma } from "@airiskindex/db";11import { RATER_MODELS, RATER_PROMPT_VERSION } from "../config";12import {13  ingestBatchResults,14  isBatchComplete,15  submitRatingBatch,16  type RatingTask,17} from "../raters/batch";18import { ratingJobId } from "../raters/job-id";1920// One-shot rating run: submit a Message Batch per model for every task that21// lacks ratings under the current prompt version, poll to completion, ingest.22// Models run in PARALLEL (batches are independent server-side queues).23// Usage: tsx src/scripts/rate.ts [--limit N] [--poll-seconds S]24// Idempotent & resumable: already-rated tasks are excluded, and an in-flight25// batch per model is remembered in a state file so a relaunch resumes polling26// instead of resubmitting (a resubmit would double the spend).2728import { readFileSync, writeFileSync } from "node:fs";2930function argValue(flag: string): string | undefined {31  const index = process.argv.indexOf(flag);32  return index >= 0 ? process.argv[index + 1] : undefined;33}3435const limit = Number(argValue("--limit") ?? "0") || undefined;36const pollSeconds = Number(argValue("--poll-seconds") ?? "60") || 60;37const STATE_PATH = process.env.RATE_STATE ?? "/tmp/aix-batch-state.json";3839function readState(): Record<string, string> {40  try {41    return JSON.parse(readFileSync(STATE_PATH, "utf8")) as Record<string, string>;42  } catch {43    return {};44  }45}4647function writeState(state: Record<string, string>): void {48  writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));49}5051const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));5253async function runForModel(model: string): Promise<void> {54  const tasks = await prisma.task.findMany({55    where: {56      ratings: { none: { model, promptVersion: RATER_PROMPT_VERSION } },57      // exclude demo-seeded synthetic tasks (real O*NET task IDs are numeric)58      NOT: { id: { contains: "-T" } },59    },60    include: { occupation: { select: { title: true } } },61    orderBy: { id: "asc" },62    ...(limit ? { take: limit } : {}),63  });64  if (tasks.length === 0) {65    console.log(`[${model}] nothing to rate`);66    return;67  }6869  const ratingTasks: RatingTask[] = tasks.map((task) => ({70    taskId: task.id,71    occupationTitle: task.occupation.title,72    statement: task.statement,73  }));74  const jobIdToTaskId = new Map(75    ratingTasks.map((task) => [ratingJobId(task.taskId, model, RATER_PROMPT_VERSION), task.taskId]),76  );7778  let batchId = readState()[model];79  if (batchId) {80    console.log(`[${model}] resuming in-flight batch ${batchId}; polling every ${pollSeconds}s`);81  } else {82    console.log(`[${model}] submitting batch of ${ratingTasks.length} tasks…`);83    batchId = await submitRatingBatch(ratingTasks, model);84    writeState({ ...readState(), [model]: batchId });85    console.log(`[${model}] batch ${batchId} submitted; polling every ${pollSeconds}s`);86  }8788  for (;;) {89    await sleep(pollSeconds * 1000);90    if (await isBatchComplete(batchId)) break;91    console.log(`[${model}] batch ${batchId} still processing…`);92  }9394  const { ingested, failed } = await ingestBatchResults(batchId, model, jobIdToTaskId);95  const state = readState();96  delete state[model];97  writeState(state);98  console.log(`[${model}] batch ${batchId} done: ${ingested} dimension ratings ingested, ${failed} requests failed`);99}100101async function main(): Promise<void> {102  if (RATER_MODELS.length < 2) {103    throw new Error("RATER_MODELS must list ≥2 models (multi-model rating, CLAUDE.md §6)");104  }105  console.log(106    `rating run — prompt ${RATER_PROMPT_VERSION}, models: ${RATER_MODELS.join(", ")}${limit ? `, limit ${limit}` : " (all unrated tasks)"}`,107  );108  await Promise.all(RATER_MODELS.map((model) => runForModel(model)));109  console.log("rating run complete — next: pnpm score:recompute");110}111112main()113  .catch((error) => {114    console.error(error);115    process.exitCode = 1;116  })117  .finally(() => prisma.$disconnect());118