batchCooking/services/tech-step-llm-worker/src/api-client.ts
Nicolas 53d415fddb feat(tech-steps): fiabilise la detection des tech steps (corpus + LLM + corrections utilisateur)
Une seule feature livree en une seule PR, en 5 phases :

- Phase 1 : enrichit le corpus NLP (tech-step-training-data.ts) et ajoute
  un harness d'evaluation (precision/rappel/F1) avec un jeu de test etiquete
  - la premiere metrique objective de qualite pour ce classifieur.
- Phase 2 : schema Prisma (StepTechStepCorrection, TechStepTrainingSuggestion)
  + endpoints utilisateur (POST/GET corrections, ouverts a tout viewer, pas
  seulement l'auteur) + endpoints internes /internal/tech-steps/* proteges
  par secret partage (requireInternalWorker).
- Phase 3 : UI de highlight/correction cote web (selection de texte ->
  association a une technique, ou clic sur un highlight existant pour le
  corriger/supprimer) - verifiee via Cypress (component + e2e, en Chrome
  reel).
- Phase 4 : worker LLM autonome (services/tech-step-llm-worker, hors du
  monorepo pnpm comme experiments/llm-tech-step-poc) qui audite les clauses
  a faible confiance et transforme les corrections utilisateur en
  suggestions d'entrainement, sans jamais toucher le chemin interactif.
- Phase 5 : script retrain-tech-steps.ts (gate de regression F1 + backfill)
  et list-pending-training-suggestions.ts pour la revue humaine avant
  application au corpus.

Verification effectuee cette session : tsc/biome sur l'ensemble du repo,
build complet (pnpm build), suite Cypress complete (component 39/39, e2e
75/76 - le seul echec est preexistant et sans rapport, cote
recipe-form.feature/ingredient-picker), tests unitaires du worker (6/6) et
son install/typecheck reels contre node-llama-cpp. Les tests Mocha
d'apps/api (Phases 1 et 2) n'ont pas pu etre executes dans cette session
(pas de Postgres local disponible) - a lancer avant merge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 09:48:02 +02:00

106 lines
4.4 KiB
TypeScript

import { env } from "./config.js";
/**
* Thin fetch wrapper around `apps/api`'s `/internal/tech-steps/*` and
* `/reference/tech-steps` — the only two surfaces this worker ever talks
* to (see `tech-step-worker.service.ts`/`tech-step-worker.routes.ts` on
* that side). No Prisma client, no direct database access at all: every
* read/write goes through here, over HTTP, authenticated with
* `INTERNAL_WORKER_SECRET` — see `requireInternalWorker`
* (`apps/api/src/middlewares/require-internal-worker.ts`) for why that's a
* separate mechanism from a user session.
*/
/** One reference technique, as returned by the public `GET /reference/tech-steps` — this worker's only source of the taxonomy it audits/labels against, never a hardcoded copy (see `tech-step-taxonomy.ts`). */
export interface TechStepReference {
id: number;
key: string;
}
/** Mirrors `TechStepAuditClauseView` (`packages/shared`) — duplicated here rather than importing from `@batch-cooking/shared`, since this worker deliberately lives outside the pnpm workspace (see `package.json`'s own doc comment) and so can't depend on a workspace package. */
export interface AuditClause {
stepId: number;
recipeId: number;
clauseText: string;
anchorKey: string | null;
intentKey: string | null;
score: number;
locale: string;
}
/** Mirrors `PendingTechStepCorrectionView` (`packages/shared`) — same "duplicated, not imported" reasoning as {@link AuditClause}. */
export interface PendingCorrection {
id: number;
stepId: number;
recipeId: number;
clauseText: string;
start: number;
end: number;
previousTechStepKey: string | null;
correctedTechStepKey: string | null;
}
/** One suggestion `postTrainingSuggestions` submits — mirrors one entry of `SubmitTrainingSuggestionsInput["suggestions"]` (`packages/shared`). */
export interface TrainingSuggestionInput {
techStepKey: string;
locale: string;
suggestedSynonyms: string[];
suggestedUtterances: string[];
sourceType: "correction" | "llm_audit";
sourceCorrectionId?: number | null;
}
async function request<TResponseBody>(
path: string,
init: RequestInit = {},
): Promise<TResponseBody> {
try {
const response = await fetch(`${env.API_BASE_URL}${path}`, {
...init,
headers: {
"Content-Type": "application/json",
"X-Internal-Worker-Secret": env.INTERNAL_WORKER_SECRET,
...init.headers,
},
});
if (!response.ok) {
const body = await response.text().catch(() => "");
throw new Error(`${init.method ?? "GET"} ${path} failed: ${response.status} ${body}`);
}
return (await response.json()) as TResponseBody;
} catch (err) {
// Rethrown as-is — every caller (the scheduler's per-job try/catch,
// see `scheduler.ts`) already decides what to do with a failed run;
// this is just the one place the `await` itself has to sit inside a
// try/catch, same convention `apps/api` follows for the same reason.
throw err;
}
}
/** The full reference technique catalog — `apps/api`'s `TechStep` table, read fresh (never cached beyond one process's lifetime) so a catalog change is picked up on the worker's next restart without a code change here. */
export function getTechStepReference(): Promise<TechStepReference[]> {
return request("/reference/tech-steps");
}
/** Low-confidence clauses sampled from existing recipes — `audit-low-confidence`'s raw material. */
export function getAuditBatch(locale: string, limit: number): Promise<AuditClause[]> {
const params = new URLSearchParams({ locale, limit: String(limit) });
return request(`/internal/tech-steps/audit-batch?${params}`);
}
/** Corrections not yet turned into a suggestion — `transform-corrections`'s raw material. */
export function getPendingCorrections(limit: number): Promise<PendingCorrection[]> {
const params = new URLSearchParams({ limit: String(limit) });
return request(`/internal/tech-steps/pending-corrections?${params}`);
}
/** Submits a batch of suggestions — a no-op (resolves immediately) if `suggestions` is empty, so a job with nothing to report doesn't need its own guard at every call site. */
export function postTrainingSuggestions(
suggestions: TrainingSuggestionInput[],
): Promise<{ created: number }> {
if (suggestions.length === 0) return Promise.resolve({ created: 0 });
return request("/internal/tech-steps/training-suggestions", {
method: "POST",
body: JSON.stringify({ suggestions }),
});
}